前言
到現在為止,所有的教程項目都沒有使用Effects11框架類來管理資源。因為在D3DCompile API (#47)版本中,如果你嘗試編譯fx_5_0的效果文件,會收到這樣的警告:
X4717: Effects deprecated for D3DCompiler_47
在未來的版本中,D3DCompiler可能會停止對FX11的支持,所以我們需要自行去管理各種特效,並改用HLSL編譯器去編譯每一個着色器。同時,在閱讀本章之前,你需要先學習本系列前面的一些重點章節再繼續:
章節目錄 |
---|
01 DirectX11初始化 |
02 頂點/像素着色器的創建、頂點緩沖區 |
03 索引緩沖區、常量緩沖區 |
09 紋理映射與采樣器狀態 |
11 混合狀態與光柵化狀態 |
12 深度/模板狀態、反射繪制 |
在DirectXTK中的Effects.h
可以看到它實現了一系列Effects管理類,相比Effects11
框架庫,它缺少了反射機制,並且使用的是它內部已經寫好、編譯好的着色器。DirectXTK的Effects也只不過是為了簡化游戲開發流程而設計出來的。當然,里面的一部分源碼實現也值得我們去學習。
注意:這章經歷了一次十分大的改動,原先所使用的BasicEffect類因為在后續的章節中發現很難擴展,所以進行了一次大幅度重構。並會逐漸替換掉后面教程的項目源碼所使用的BasicEffect。
在這一章的學習過后,你將會理解Effects11
的一部分運作機制是怎樣的。如果想更深入了解的話,推薦閱讀下面這篇,內部實現了一個功能和Effects11
相仿的EffectHelper
類,可以更好地幫助你簡化代碼:
章節目錄 |
---|
深入理解Effects11、使用着色器反射機制(Shader Reflection)實現一個復雜Effects框架 |
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
回顧RenderStates類
目前的RenderStates
類存放有比較常用的各種狀態,原來在Effects11
框架下是可以在fx文件初始化各種渲染狀態,並設置到Technique11
中。但現在我們只能在C++代碼層中一次性創建好各種所需的渲染狀態:
class RenderStates
{
public:
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
static bool IsInit();
static void InitAll(ID3D11Device * device);
// 使用ComPtr無需手工釋放
public:
static ComPtr<ID3D11RasterizerState> RSWireframe; // 光柵化器狀態:線框模式
static ComPtr<ID3D11RasterizerState> RSNoCull; // 光柵化器狀態:無背面裁剪模式
static ComPtr<ID3D11RasterizerState> RSCullClockWise; // 光柵化器狀態:順時針裁剪模式
static ComPtr<ID3D11SamplerState> SSLinearWrap; // 采樣器狀態:線性過濾
static ComPtr<ID3D11SamplerState> SSAnistropicWrap; // 采樣器狀態:各項異性過濾
static ComPtr<ID3D11BlendState> BSNoColorWrite; // 混合狀態:不寫入顏色
static ComPtr<ID3D11BlendState> BSTransparent; // 混合狀態:透明混合
static ComPtr<ID3D11BlendState> BSAlphaToCoverage; // 混合狀態:Alpha-To-Coverage
static ComPtr<ID3D11DepthStencilState> DSSWriteStencil; // 深度/模板狀態:寫入模板值
static ComPtr<ID3D11DepthStencilState> DSSDrawWithStencil; // 深度/模板狀態:對指定模板值的區域進行繪制
static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend; // 深度/模板狀態:無二次混合區域
static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest; // 深度/模板狀態:關閉深度測試
static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite; // 深度/模板狀態:僅深度測試,不寫入深度值
};
具體的設置可以參照源碼或者上一章內容。
簡易Effects框架
該Effects框架支持的功能如下:
- 管理/修改常量緩沖區的內容,並應用(Apply)變更
- 編譯HLSL着色器而不是fx文件
- 管理/使用四種渲染狀態
- 切換渲染模式(涉及到渲染管線各種資源的綁定、切換)
- 僅更新修改的變量所對應的常量緩沖區塊
不過它也有這樣的缺陷:
- 一個特效類對應一套着色器和所使用的常量緩沖區,所屬着色器代碼的變動很可能會引起對框架類的修改,因為缺乏反射機制而導致靈活性差。
此外,該框架內部會對矩陣進行轉置,因此在傳遞矩陣給Effects時只需要傳遞默認的行主矩陣即可。
文件結構
首先是文件結構:
其中能夠暴露給程序使用的只有頭文件Effects.h
,里面可以存放多套不同的特效框架類的聲明,而關於每個框架類的實現部分都應當用一個獨立的源文件存放。而EffectHelper.h
則是用來幫助管理常量緩沖區的,服務於各種框架類的實現部分以及所屬的源文件,因此不應該直接使用。
理論上它也是可以做成靜態庫使用的,然后着色器代碼穩定后也不應當變動。在使用的時候只需要包含頭文件Effects.h
即可。
EffectHelper.h
該頭文件包含了一些有用的東西,但它需要在包含特效類實現的源文件中使用,且必須晚於Effects.h
和d3dUtil.h
包含。
在堆上進行類的內存對齊
有些類型需要在堆上按16字節對齊,比如XMVECTOR
和XMMATRIX
,雖然說拿這些對象作為類的成員不太合適,畢竟分配在堆上的話基本上無法保證內存按16字節對齊了,但還是希望能夠做到。在VS的corecrt_malloc.h
(只要有包含stdlib.h
, malloc.h
之一的頭文件都可以)中有這樣的一個函數:_aligned_malloc
,它可以指定需要分配的內存字節大小以及按多少字節對齊。其中對齊值必須為2的整數次冪的字節數。
void * _aligned_malloc(
size_t size, // [In]分配內存字節數
size_t alignment // [In]按多少字節內存來對齊
);
若一個類中包含有已經指定內存對齊的成員,則需要優先把這些成員放到最前。
然后與之對應的就是_aligned_free
函數了,它可以釋放之前由_aligned_malloc
分配得到的內存。
下面是類模板AlignedType
的實現,讓需要內存對齊的類去繼承該類即可。它重載了operator new
和operator delete
的實現:
// 若類需要內存對齊,從該類派生
template<class DerivedType>
struct AlignedType
{
static void* operator new(size_t size)
{
const size_t alignedSize = __alignof(DerivedType);
static_assert(alignedSize > 8, "AlignedNew is only useful for types with > 8 byte alignment! Did you forget a __declspec(align) on DerivedType?");
void* ptr = _aligned_malloc(size, alignedSize);
if (!ptr)
throw std::bad_alloc();
return ptr;
}
static void operator delete(void * ptr)
{
_aligned_free(ptr);
}
};
需要注意的是,繼承AlignedType
的類或者其成員必須本身有__declspec(align)
的標識。若是內部成員,在所有包含該標識的值中最大的align
值 必須是2的整數次冪且必須大於8。
下面演示了正確的和錯誤的行為:
// 錯誤!VertexPosColor按4字節對齊!
struct VertexPosColor : AlignedType<VertexPos>
{
XMFLOAT3 pos;
XMFLOAT4 color;
};
// 正確!Data按16字節對齊,因為pos本身是按16字節對齊的。
struct Data : AlignedType<VertexPos>
{
XMVECTOR pos;
int val;
};
// 正確!Vector類按16字節對齊
__declspec(align(16))
struct Vector : AlignedType<Vector>
{
float x;
float y;
float z;
float w;
};
這里AlignedType<T>
主要是用於BasicEffect::Impl
類,因為其內部包含了XMVECTOR
和XMMATRIX
類型的成員,且該類需要分配在堆上。
常量緩沖區管理
一個常量緩沖區可能會被創建、更新或者綁定到管線。若常量緩沖區的值沒有發生變化,我們不希望它進行無意義的更新。這里可以使用一個dirty
標記,確認它是否被修改過。在Effects
調用Apply
后,如果常量緩沖區的任一內部成員發生修改的話,我們就將數據更新到常量緩沖區並恢復該標記。
首先是抽象基類CBufferBase
:
struct CBufferBase
{
template<class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
CBufferBase() : isDirty() {}
~CBufferBase() = default;
BOOL isDirty;
ComPtr<ID3D11Buffer> cBuffer;
virtual HRESULT CreateBuffer(ID3D11Device * device) = 0;
virtual void UpdateBuffer(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindVS(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindHS(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindDS(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindGS(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindCS(ID3D11DeviceContext * deviceContext) = 0;
virtual void BindPS(ID3D11DeviceContext * deviceContext) = 0;
};
這么做是為了方便我們放入數組進行遍歷。
然后是派生類CBufferObject
,startSlot
指定了HLSL對應cbuffer的索引,T
則是C++對應的結構體,存儲臨時數據:
template<UINT startSlot, class T>
struct CBufferObject : CBufferBase
{
T data;
CBufferObject() : CBufferBase(), data() {}
HRESULT CreateBuffer(ID3D11Device * device) override
{
if (cBuffer != nullptr)
return S_OK;
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DYNAMIC;
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
cbd.ByteWidth = sizeof(T);
return device->CreateBuffer(&cbd, nullptr, cBuffer.GetAddressOf());
}
void UpdateBuffer(ID3D11DeviceContext * deviceContext) override
{
if (isDirty)
{
isDirty = false;
D3D11_MAPPED_SUBRESOURCE mappedData;
deviceContext->Map(cBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
memcpy_s(mappedData.pData, sizeof(T), &data, sizeof(T));
deviceContext->Unmap(cBuffer.Get(), 0);
}
}
void BindVS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->VSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
void BindHS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->HSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
void BindDS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->DSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
void BindGS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->GSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
void BindCS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->CSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
void BindPS(ID3D11DeviceContext * deviceContext) override
{
deviceContext->PSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
}
};
關於常量緩沖區臨時變量的修改則在后續的內容。
BasicEffect類--管理對象繪制的資源
首先是抽象基類IEffects
,它僅允許被移動,並且僅包含Apply
方法。
class IEffect
{
public:
// 使用模板別名(C++11)簡化類型名
template <class T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
IEffect() = default;
// 不支持復制構造
IEffect(const IEffect&) = delete;
IEffect& operator=(const IEffect&) = delete;
// 允許轉移
IEffect(IEffect&& moveFrom) = default;
IEffect& operator=(IEffect&& moveFrom) = default;
virtual ~IEffect() = default;
// 更新並綁定常量緩沖區
virtual void Apply(ID3D11DeviceContext * deviceContext) = 0;
};
原來的ID3DX11EffectPass
包含的方法Apply
用於在各個着色器階段綁定所需要的常量緩沖區、紋理等資源,並更新之前有所修改的常量緩沖區。現在我們實現Effects框架中的Apply
方法也是這么做的。
然后是派生類BasicEffect
,從它的方法來看,包含了單例獲取、渲染狀態的切換、修改常量緩沖區某一成員的值、應用變更四個大塊:
class BasicEffect : public IEffect
{
public:
BasicEffect();
virtual ~BasicEffect() override;
BasicEffect(BasicEffect&& moveFrom) noexcept;
BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;
// 獲取單例
static BasicEffect& Get();
// 初始化Basic.hlsli所需資源並初始化渲染狀態
bool InitAll(ID3D11Device * device);
//
// 渲染模式的變更
//
// 默認狀態來繪制
void SetRenderDefault(ID3D11DeviceContext * deviceContext);
// Alpha混合繪制
void SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
// 無二次混合
void SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 僅寫入模板值
void SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 對指定模板值的區域進行繪制,采用默認狀態
void SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 對指定模板值的區域進行繪制,采用Alpha混合
void SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
// 2D默認狀態繪制
void Set2DRenderDefault(ID3D11DeviceContext * deviceContext);
// 2D混合繪制
void Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
//
// 矩陣設置
//
void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);
void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);
void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);
void XM_CALLCONV SetReflectionMatrix(DirectX::FXMMATRIX R);
void XM_CALLCONV SetShadowMatrix(DirectX::FXMMATRIX S);
void XM_CALLCONV SetRefShadowMatrix(DirectX::FXMMATRIX RefS);
//
// 光照、材質和紋理相關設置
//
// 各種類型燈光允許的最大數目
static const int maxLights = 5;
void SetDirLight(size_t pos, const DirectionalLight& dirLight);
void SetPointLight(size_t pos, const PointLight& pointLight);
void SetSpotLight(size_t pos, const SpotLight& spotLight);
void SetMaterial(const Material& material);
void SetTexture(ID3D11ShaderResourceView * texture);
void SetEyePos(const DirectX::XMFLOAT3& eyePos);
//
// 狀態開關設置
//
void SetReflectionState(bool isOn);
void SetShadowState(bool isOn);
// 應用常量緩沖區和紋理資源的變更
void Apply(ID3D11DeviceContext * deviceContext);
private:
class Impl;
std::unique_ptr<Impl> pImpl;
};
XM_CALLCONV
即在第五章之前提到的__vectorcall
或__fastcall
約定。
BasicEffect::Impl類
之前在BasicEffect
中聲明了Impl
類,主要目的是為了將類的成員和方法定義都轉移到源文件中,並且還包含了HLSL五個cbuffer的C++結構體。不僅可以減少BasicEffect
類的壓力,還可以避免暴露上面的五個結構體:
class BasicEffect::Impl : public AlignedType<BasicEffect::Impl>
{
public:
//
// 這些結構體對應HLSL的結構體。需要按16字節對齊
//
struct CBChangesEveryDrawing
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX worldInvTranspose;
Material material;
};
struct CBDrawingStates
{
int isReflection;
int isShadow;
DirectX::XMINT2 pad;
};
struct CBChangesEveryFrame
{
DirectX::XMMATRIX view;
DirectX::XMFLOAT3 eyePos;
float pad;
};
struct CBChangesOnResize
{
DirectX::XMMATRIX proj;
};
struct CBChangesRarely
{
DirectX::XMMATRIX reflection;
DirectX::XMMATRIX shadow;
DirectX::XMMATRIX refShadow;
DirectionalLight dirLight[BasicEffect::maxLights];
PointLight pointLight[BasicEffect::maxLights];
SpotLight spotLight[BasicEffect::maxLights];
};
public:
// 必須顯式指定
Impl() : m_IsDirty() {}
~Impl() = default;
public:
// 需要16字節對齊的優先放在前面
CBufferObject<0, CBChangesEveryDrawing> m_CBDrawing; // 每次對象繪制的常量緩沖區
CBufferObject<1, CBDrawingStates> m_CBStates; // 每次繪制狀態變更的常量緩沖區
CBufferObject<2, CBChangesEveryFrame> m_CBFrame; // 每幀繪制的常量緩沖區
CBufferObject<3, CBChangesOnResize> m_CBOnResize; // 每次窗口大小變更的常量緩沖區
CBufferObject<4, CBChangesRarely> m_CBRarely; // 幾乎不會變更的常量緩沖區
BOOL m_IsDirty; // 是否有值變更
std::vector<CBufferBase*> m_pCBuffers; // 統一管理上面所有的常量緩沖區
ComPtr<ID3D11VertexShader> m_pVertexShader3D; // 用於3D的頂點着色器
ComPtr<ID3D11PixelShader> m_pPixelShader3D; // 用於3D的像素着色器
ComPtr<ID3D11VertexShader> m_pVertexShader2D; // 用於2D的頂點着色器
ComPtr<ID3D11PixelShader> m_pPixelShader2D; // 用於2D的像素着色器
ComPtr<ID3D11InputLayout> m_pVertexLayout2D; // 用於2D的頂點輸入布局
ComPtr<ID3D11InputLayout> m_pVertexLayout3D; // 用於3D的頂點輸入布局
ComPtr<ID3D11ShaderResourceView> m_pTexture; // 用於繪制的紋理
};
構造/析構/單例
這里用一個匿名空間保管單例對象的指針。當有一個實例被構造出來的時候就會給其賦值。后續就不允許再被實例化了,可以使用Get
方法獲取該單例。
namespace
{
// BasicEffect單例
static BasicEffect * g_pInstance = nullptr;
}
BasicEffect::BasicEffect()
{
if (g_pInstance)
throw std::exception("BasicEffect is a singleton!");
g_pInstance = this;
pImpl = std::make_unique<BasicEffect::Impl>();
}
BasicEffect::~BasicEffect()
{
}
BasicEffect::BasicEffect(BasicEffect && moveFrom) noexcept
{
pImpl.swap(moveFrom.pImpl);
}
BasicEffect & BasicEffect::operator=(BasicEffect && moveFrom) noexcept
{
pImpl.swap(moveFrom.pImpl);
return *this;
}
BasicEffect & BasicEffect::Get()
{
if (!g_pInstance)
throw std::exception("BasicEffect needs an instance!");
return *g_pInstance;
}
BasicEffect::InitAll方法
BasicEffect::InitAll
方法負責創建出所有的着色器和常量緩沖區,以及所有的渲染狀態:
bool BasicEffect::InitAll(ID3D11Device * device)
{
if (!device)
return false;
if (!pImpl->m_pCBuffers.empty())
return true;
if (!RenderStates::IsInit())
throw std::exception("RenderStates need to be initialized first!");
ComPtr<ID3DBlob> blob;
// 創建頂點着色器(2D)
HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.GetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader2D.GetAddressOf()));
// 創建頂點布局(2D)
HR(device->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout2D.GetAddressOf()));
// 創建像素着色器(2D)
HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader2D.GetAddressOf()));
// 創建頂點着色器(3D)
HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader3D.GetAddressOf()));
// 創建頂點布局(3D)
HR(device->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout3D.GetAddressOf()));
// 創建像素着色器(3D)
HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader3D.GetAddressOf()));
pImpl->m_pCBuffers.assign({
&pImpl->m_CBDrawing,
&pImpl->m_CBFrame,
&pImpl->m_CBStates,
&pImpl->m_CBOnResize,
&pImpl->m_CBRarely});
// 創建常量緩沖區
for (auto& pBuffer : pImpl->m_pCBuffers)
{
HR(pBuffer->CreateBuffer(device));
}
return true;
}
各種渲染狀態的切換
下面所有的渲染模式使用的是線性Wrap采樣器。
BasicEffect::SetRenderDefault方法--默認渲染
BasicEffect::SetRenderDefault
方法使用了默認的3D像素着色器和頂點着色器,並且其余各狀態都保留使用默認狀態:
void BasicEffect::SetRenderDefault(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}
BasicEffect::SetRenderAlphaBlend方法--Alpha透明混合渲染
該繪制模式關閉了光柵化裁剪,並采用透明混合方式。
void BasicEffect::SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetRenderNoDoubleBlend方法--無重復混合(單次混合)
該繪制模式用於繪制陰影,防止過度混合。需要指定繪制區域的模板值。
void BasicEffect::SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDoubleBlend.Get(), stencilRef);
deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetWriteStencilOnly方法--僅寫入模板值
該模式用於向模板緩沖區寫入用戶指定的模板值,並且不寫入到深度緩沖區和后備緩沖區。
void BasicEffect::SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), stencilRef);
deviceContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::SetRenderDefaultWithStencil方法--對指定模板值區域進行常規繪制
該模式下,僅對模板緩沖區的模板值和用戶指定的相等的區域進行常規繪制。
void BasicEffect::SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSCullClockWise.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}
BasicEffect::SetRenderAlphaBlendWithStencil方法--對指定模板值區域進行Alpha透明混合繪制
該模式下,僅對模板緩沖區的模板值和用戶指定的相等的區域進行Alpha透明混合繪制。
void BasicEffect::SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}
BasicEffect::Set2DRenderDefault方法--2D默認繪制
該模式使用的是2D頂點着色器和像素着色器,並修改為2D輸入布局。
void BasicEffect::Set2DRenderDefault(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
deviceContext->RSSetState(nullptr);
deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}
BasicEffect::Set2DRenderAlphaBlend方法--2D透明混合繪制
相比上面,多了透明混合狀態。
void BasicEffect::Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
deviceContext->RSSetState(RenderStates::RSNoCull.Get());
deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
deviceContext->OMSetDepthStencilState(nullptr, 0);
deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}
更新常量緩沖區
下面這些所有的方法會更新CBufferObject
中的臨時數據,數據臟標記被設為true
:
void XM_CALLCONV BasicEffect::SetWorldMatrix(DirectX::FXMMATRIX W)
{
auto& cBuffer = pImpl->m_CBDrawing;
cBuffer.data.world = XMMatrixTranspose(W);
cBuffer.data.worldInvTranspose = XMMatrixTranspose(InverseTranspose(W));
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void XM_CALLCONV BasicEffect::SetViewMatrix(FXMMATRIX V)
{
auto& cBuffer = pImpl->m_CBFrame;
cBuffer.data.view = XMMatrixTranspose(V);
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void XM_CALLCONV BasicEffect::SetProjMatrix(FXMMATRIX P)
{
auto& cBuffer = pImpl->m_CBOnResize;
cBuffer.data.proj = XMMatrixTranspose(P);
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void XM_CALLCONV BasicEffect::SetReflectionMatrix(FXMMATRIX R)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.reflection = XMMatrixTranspose(R);
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void XM_CALLCONV BasicEffect::SetShadowMatrix(FXMMATRIX S)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.shadow = XMMatrixTranspose(S);
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void XM_CALLCONV BasicEffect::SetRefShadowMatrix(DirectX::FXMMATRIX RefS)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.refShadow = XMMatrixTranspose(RefS);
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetDirLight(size_t pos, const DirectionalLight & dirLight)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.dirLight[pos] = dirLight;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetPointLight(size_t pos, const PointLight & pointLight)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.pointLight[pos] = pointLight;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetSpotLight(size_t pos, const SpotLight & spotLight)
{
auto& cBuffer = pImpl->m_CBRarely;
cBuffer.data.spotLight[pos] = spotLight;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetMaterial(const Material & material)
{
auto& cBuffer = pImpl->m_CBDrawing;
cBuffer.data.material = material;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetTexture(ID3D11ShaderResourceView * m_pTexture)
{
pImpl->m_pTexture = m_pTexture;
}
void BasicEffect::SetEyePos(const XMFLOAT3& eyePos)
{
auto& cBuffer = pImpl->m_CBFrame;
cBuffer.data.eyePos = eyePos;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetReflectionState(bool isOn)
{
auto& cBuffer = pImpl->m_CBStates;
cBuffer.data.isReflection = isOn;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
void BasicEffect::SetShadowState(bool isOn)
{
auto& cBuffer = pImpl->m_CBStates;
cBuffer.data.isShadow = isOn;
pImpl->m_IsDirty = cBuffer.isDirty = true;
}
BasicEffect::Apply方法--應用緩沖區、紋理資源並進行更新
BasicEffect::Apply
首先將所需要用到的緩沖區綁定到渲染管線上,並設置紋理,然后才是視情況更新常量緩沖區。
下面的緩沖區數組索引值同時也對應了之前編譯期指定的startSlot
值。
首先檢驗總的臟標記是否為true
,若有任意數據被修改,則檢驗每個常量緩沖區的臟標記,並根據該標記決定是否要更新常量緩沖區。
void BasicEffect::Apply(ID3D11DeviceContext * deviceContext)
{
auto& pCBuffers = pImpl->m_pCBuffers;
// 將緩沖區綁定到渲染管線上
pCBuffers[0]->BindVS(deviceContext);
pCBuffers[1]->BindVS(deviceContext);
pCBuffers[2]->BindVS(deviceContext);
pCBuffers[3]->BindVS(deviceContext);
pCBuffers[4]->BindVS(deviceContext);
pCBuffers[0]->BindPS(deviceContext);
pCBuffers[1]->BindPS(deviceContext);
pCBuffers[2]->BindPS(deviceContext);
pCBuffers[4]->BindPS(deviceContext);
// 設置紋理
deviceContext->PSSetShaderResources(0, 1, pImpl->m_pTexture.GetAddressOf());
if (pImpl->m_IsDirty)
{
pImpl->m_IsDirty = false;
for (auto& pCBuffer : pCBuffers)
{
pCBuffer->UpdateBuffer(deviceContext);
}
}
}
當然,目前BasicEffect
能做的事情還是比較有限的,並且還需要隨着HLSL代碼的變動而隨之調整。更多的功能會在后續教程中實現。
繪制平面陰影
使用XMMatrixShadow
可以生成陰影矩陣,根據光照類型和位置對幾何體投影到平面上的。
XMMATRIX XMMatrixShadow(
FXMVECTOR ShadowPlane, // 平面向量(nx, ny, nz, d)
FXMVECTOR LightPosition); // w = 0時表示平行光方向, w = 1時表示光源位置
通常指定的平面會稍微比實際平面高那么一點點,以避免深度緩沖區資源爭奪導致陰影顯示有問題。
使用模板緩沖區防止過度混合
一個物體投影到平面上時,投影區域的某些位置可能位於多個三角形之內,這會導致這些位置會有多個像素通過測試並進行混合操作,渲染的次數越多,顯示的顏色會越黑。
我們可以使用模板緩沖區來解決這個問題。
- 在之前的例子中,我們用模板值為0的區域表示非鏡面反射區,模板值為1的區域表示為鏡面反射區;
- 使用
RenderStates::DSSNoDoubleBlend
的深度模板狀態,當給定的模板值和深度/模板緩沖區的模板值一致時,通過模板測試並對模板值加1,繪制該像素的混合,然后下一次由於給定的模板值比深度/模板緩沖區的模板值小1,不會再通過模板測試,也就阻擋了后續像素的繪制; - 應當先繪制鏡面的陰影區域,再繪制正常的陰影區域。
着色器代碼的變化
Basic_PS_2D.hlsl文件變化如下:
#include "Basic.hlsli"
// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
float4 color = g_Tex.Sample(g_Sam, pIn.Tex);
clip(color.a - 0.1f);
return color;
}
Basic_PS_3D.hlsl文件變化如下:
#include "Basic.hlsli"
// 像素着色器(3D)
// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
// 提前進行裁剪,對不符合要求的像素可以避免后續運算
float4 texColor = g_Tex.Sample(g_Sam, pIn.Tex);
clip(texColor.a - 0.1f);
// 標准化法向量
pIn.NormalW = normalize(pIn.NormalW);
// 頂點指向眼睛的向量
float3 toEyeW = normalize(g_EyePosW - pIn.PosW);
// 初始化為0
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
int i;
[unroll]
for (i = 0; i < 5; ++i)
{
DirectionalLight dirLight = g_DirLight[i];
[flatten]
if (g_IsReflection)
{
dirLight.Direction = mul(dirLight.Direction, (float3x3) (g_Reflection));
}
ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
// 若當前在繪制反射物體,需要對光照進行反射矩陣變換
PointLight pointLight;
[unroll]
for (i = 0; i < 5; ++i)
{
pointLight = g_PointLight[i];
[flatten]
if (g_IsReflection)
{
pointLight.Position = (float3) mul(float4(pointLight.Position, 1.0f), g_Reflection);
}
ComputePointLight(g_Material, pointLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
SpotLight spotLight;
// 若當前在繪制反射物體,需要對光照進行反射矩陣變換
[unroll]
for (i = 0; i < 5; ++i)
{
spotLight = g_SpotLight[i];
[flatten]
if (g_IsReflection)
{
spotLight.Position = (float3) mul(float4(spotLight.Position, 1.0f), g_Reflection);
spotLight.Direction = mul(spotLight.Direction, (float3x3) g_Reflection);
}
ComputeSpotLight(g_Material, spotLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
ambient += A;
diffuse += D;
spec += S;
}
float4 litColor = texColor * (ambient + diffuse) + spec;
litColor.a = texColor.a * g_Material.Diffuse.a;
return litColor;
}
Basic_VS_2D.hlsl變化如下:
#include "Basic.hlsli"
// 頂點着色器(2D)
VertexPosHTex VS_2D(VertexPosTex vIn)
{
VertexPosHTex vOut;
vOut.PosH = float4(vIn.PosL, 1.0f);
vOut.Tex = vIn.Tex;
return vOut;
}
Basic_VS_3D.hlsl變化如下:
#include "Basic.hlsli"
// 頂點着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
VertexPosHWNormalTex vOut;
matrix viewProj = mul(g_View, g_Proj);
float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
float3 normalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
// 若當前在繪制反射物體,先進行反射操作
[flatten]
if (g_IsReflection)
{
posW = mul(posW, g_Reflection);
normalW = mul(normalW, (float3x3) g_Reflection);
}
// 若當前在繪制陰影,先進行投影操作
[flatten]
if (g_IsShadow)
{
posW = (g_IsReflection ? mul(posW, g_RefShadow) : mul(posW, g_Shadow));
}
vOut.PosH = mul(posW, viewProj);
vOut.PosW = posW.xyz;
vOut.NormalW = normalW;
vOut.Tex = vIn.Tex;
return vOut;
}
GameObject類與BasicEffect類的對接
由於GameObject
類也承擔了繪制方法,那么最后的Apply
也需要交給游戲對象來調用。因此GameObject::Draw
方法變更如下:
void GameObject::Draw(ID3D11DeviceContext * deviceContext, BasicEffect& effect)
{
// 設置頂點/索引緩沖區
UINT strides = m_VertexStride;
UINT offsets = 0;
deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &strides, &offsets);
deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R32_UINT, 0);
// 更新數據並應用
effect.SetWorldMatrix(m_Transform.GetLocalToWorldMatrixXM());
effect.SetTexture(m_pTexture.Get());
effect.SetMaterial(m_Material);
effect.Apply(deviceContext);
deviceContext->DrawIndexed(m_IndexCount, 0, 0);
}
場景繪制
現在場景只有牆體、地板、木箱和鏡面。
第1步: 鏡面區域寫入模板緩沖區
// *********************
// 1. 給鏡面反射區域寫入值1到模板緩沖區
//
m_BasicEffect.SetWriteStencilOnly(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第2步: 繪制不透明的反射物體
// ***********************
// 2. 繪制不透明的反射物體
//
// 開啟反射繪制
m_BasicEffect.SetReflectionState(true);
m_BasicEffect.SetRenderDefaultWithStencil(m_pd3dImmediateContext.Get(), 1);
m_Walls[2].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[3].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[4].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第3步: 繪制不透明反射物體的陰影
// ***********************
// 3. 繪制不透明反射物體的陰影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射開啟,陰影開啟
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 1);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
// 恢復到原來的狀態
m_BasicEffect.SetShadowState(false);
m_WoodCrate.SetMaterial(m_WoodCrateMat);
第4步: 繪制透明鏡面
// ***********************
// 4. 繪制透明鏡面
//
// 關閉反射繪制
m_BasicEffect.SetReflectionState(false);
m_BasicEffect.SetRenderAlphaBlendWithStencil(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第5步:繪制不透明的正常物體
// ************************
// 5. 繪制不透明的正常物體
//
m_BasicEffect.SetRenderDefault(m_pd3dImmediateContext.Get());
for (auto& wall : m_Walls)
wall.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
第6步:繪制不透明正常物體的陰影
// ************************
// 6. 繪制不透明正常物體的陰影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射關閉,陰影開啟
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 0);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_BasicEffect.SetShadowState(false); // 陰影關閉
m_WoodCrate.SetMaterial(m_WoodCrateMat);
最終繪制效果如下:
注意該樣例只生成點光燈到地板的陰影。你可以用各種攝像機模式來進行測試。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。