DirectX11--深入理解Effects11、使用著色器反射機制(Shader Reflection)實現一個複雜Effects框架
阿新 • • 發佈:2020-03-09
# 前言
如果之前你是跟隨本教程系列學習的話,應該能夠初步瞭解[Effects11(現FX11)](https://github.com/Microsoft/FX11)的實現機制,並且可以[編寫一個簡易的特效管理框架](https://www.cnblogs.com/X-Jun/p/9665452.html),但是隨著特效種類的增多,要管理的著色器、資源等也隨之變多。如果寫了一套由多個HLSL著色器組成特效,就仍需要在C++端編寫與HLSL相對應的特效框架,這樣寫起來依然是十分繁雜。以前學習龍書的DirectX11時,裡面使用的正是Effects11框架,不得不承認用它實現C++跟HLSL的互動的確方便了許多,但是時過境遷,微軟將會逐漸拋棄fx_5_0,且目前FX11也已經列為Archived,不再更新。都說如果要實現一個3D引擎的話,必須要有一個屬於自己的特效管理框架。
本文假定讀者已經讀過至少前13章的內容,或者有較為豐富的DirectX 11開發經歷。
學習目標:
1. **熟悉著色器反射機制**
2. **實現一個複雜Effects框架,瞭解該框架的使用**
**[DirectX11 With Windows SDK完整目錄](http://www.cnblogs.com/X-Jun/p/9028764.html)**
**[Github專案原始碼](https://github.com/MKXJun/DirectX11-With-Windows-SDK)**
**歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。**
# 先從Effects11(FX11)談起
DirectX的特效是包含管線狀態和著色器的集合,而Effects框架則正是用於管理這些特效的一套API。如果使用Effects11(FX11)框架的話,那麼在HLSL中除了本身的語法外,還支援Effects特有的語法,這些語法大部分經過解析後會轉化為在C++中使用Direct3D的API。
知己知彼,才能百戰不殆。要想寫好一個特效管理框架,首先要把Effects框架與C++的關係給分析透徹。下面的內容也會引用FX11的少量原始碼來佐證。
## Pass、Technique11、Group
**Pass**:一個Pass由一組需要用到的著色器和一些渲染狀態組成。通常情況下,我們至少需要一個頂點著色器和一個畫素著色器。如果是要進行流輸出,則至少需要一個頂點著色器和一個幾何著色器。而通用計算則需要的是計算著色器。除此之外,它在HLSL還支援一些額外的函式,用以改變一些渲染狀態。
**Technique11**:一個Technique由一個或多個Pass組成,用於建立一個渲染技術。有時候為了實現一種特效,需要歷經多個Pass的處理才能實現,我們稱之為**多通道渲染**。比如實現OIT(順序無關透明度),第一趟Pass需要完成透明畫素的收集,第二趟Pass則是將收集好的畫素按深度排序,並將透明混合的結果渲染到目標。
**Group**:一個Group由一個或多個Technique組成。
下面展示了一份比較隨性的fx5.0程式碼的部分**(注意:下面的程式碼不屬於HLSL的語法!)**:
```cpp
// 存在部分省略
GeometryShader pGSComp = CompileShader(gs_5_0, gsBase());
GeometryShader pGSwSO = ConstructGSWithSO(pGSComp, "0:Position.xy; 1:Position.zw; 2:Color.xy",
"3:Texcoord.xyzw; 3:$SKIP.x;", NULL, NULL, 1);
// 此處省略著色器函式...
technique11 T0
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(true, false, true)));
SetRasterizerState(g_NoCulling);
SetDepthStencilState(NULL, 0);
SetBlendState(EnableAlphaBlending, (float4)0, 0xFFFFFFFF);
}
Pass P1
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(pGSwSO);
SetPixelShader(NULL);
}
}
```
這裡面的函式呼叫大部分實際上都是在C++完成的,因此在Direct3D API中可以找到對應的原型:
```cpp
SetVertexShader() // 等價於ID3D11DeviceContext::VSSetShader
SetGeometryShader() // 等價於ID3D11DeviceContext::GSSetShader
SetPixelShader() // 等價於ID3D11DeviceContext::PSSetShader
SetRasterizerState() // 等價於ID3D11DeviceContext::RSSetState
SetDepthStencilState() // 等價於ID3D11DeviceContext::OMSetDepthStencilState
SetBlendState() // 等價於ID3D11DeviceContext::OMSetBlendState
ConstructGSWithSO() // 等價於ID3D11Device::CreateGeometryShaderWithStreamOutput
```
而像`VertexShader`、`PixelShader`這些僅存在於fx5.0的語法,在C++中對應的是`ID3D11VertexShader`、`ID3D11PixelShader`等等。
至於`CompileShader`,我們可以猜測內部使用的是類似`D3DCompile`這樣的函式,只不過這份原始碼肯定是需要經過特殊處理才能變成原生的HLSL程式碼。
在C++端,編譯fx5.0可以使用[**D3DCompile**](https://docs.microsoft.com/en-us/windows/win32/api/d3dcompiler/nf-d3dcompiler-d3dcompile)或[**D3DCompileFromFile**](https://docs.microsoft.com/zh-cn/windows/win32/api/d3dcompiler/nf-d3dcompiler-d3dcompilefromfile),然後再使用[**D3DX11CreateEffectFromMemory**](https://docs.microsoft.com/zh-cn/windows/win32/direct3d11/d3dx11createeffectfrommemory)創建出Effects。只不過會收到這樣的警告:
`X4717: Effects deprecated for D3DCompiler_47`
## 渲染狀態、取樣器狀態
在fx5.0中能夠創建出`SamplerState`、`RasterizerState`、`BlendState`和`DepthStencilState`,並且還能預先設定好內部的各項引數,就像下面這樣**(注意:下面的程式碼不屬於HLSL的語法!)**:
```cpp
SamplerState g_SamAnisotropic
{
Filter = ANISOTROPIC;
MaxAnisotropy = 4;
AddressU = WRAP;
AddressV = WRAP;
AddressW = WRAP;
};
RasterizerState g_NoCulling
{
FillMode = Solid;
CullMode = None;
FrontCounterClockwise = false;
}
```
實際上,取樣器的狀態和渲染狀態都是在C++中完成的,上面的程式碼翻譯成C++則變成類似這樣:
```cpp
// g_SamAnisotropic
CD3D11_SAMPLER_DESC sampDesc(CD3D11_DEFAULT());
sampDesc.Filter = D3D11_FILTER_ANISOTROPIC;
sampDesc.MaxAnisotropy = 4;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_WRAP;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_WRAP;
device->CreateSamplerState(&sampDesc, SSAnistropicWrap.GetAddressOf());
// g_NoCulling
CD3D11_RASTERIZER_DESC rasterizerDesc(CD3D11_DEFAULT());
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_NONE;
rasterizerDesc.FrontCounterClockwise = false;
device->CreateRasterizerState(&rasterizerDesc, RSNoCull.GetAddressOf()));
```
## 常量緩衝區
以前在用fx5.0寫常量緩衝區的時候是這樣的:
```cpp
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
float4x4 gTexTransform;
Material gMaterial;
};
```
在你聲明瞭cbuffer後,Effects11(FX11)會在C++端創建出對應的常量緩衝區:
```cpp
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DYNAMIC; // FX11內部使用的是D3D11_USAGE_DYNAMIC
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // FX11內部是0
cbd.ByteWidth = byteWidth;
return device->CreateBuffer(&cbd, nullptr, cBuffer.GetAddressOf());
```
### 隱藏指定暫存器槽的問題
已知常量緩衝區有16個暫存器槽,那麼,怎麼確定cbuffer當前使用的是哪個槽呢?
1. 有通過register(b#)指定暫存器槽位的`cbuffer`優先佔用
2. 除去那些顯式指定槽位的`cbuffer`,如果`cbuffer`裡面的成員有被當前著色器使用過,將會根據宣告順序按空餘槽位從小到大的順序佔用
根據上面的例子,cbPerFrame將使用slot(b0),而cbPerObject將使用slot(b1)。
現在讓我們省略所有的花括號,觀察下面的程式碼,根據下面兩種情況,問那三個未指定暫存器槽的cbuffer分別佔用了哪個slot?
1. 頂點著色器使用過第1、3、4、5個cbuffer裡面的變數
2. 畫素著色器使用過第2、3、4、6個cbuffer裡面的變數
```cpp
cbuffer CBChangesEveryInstanceDrawing : register(b0) { ... }
cbuffer CBChangesEveryObjectDrawing { ... }
cbuffer CBChangesEveryFrame { ... }
cbuffer CBDrawingStates { ... }
cbuffer CBChangesOnResize : register(b2) { ... }
cbuffer CBChangesRarely : register(b3) { ... }
```
答案如下:
1. CBChangesEveryFrame佔用了slot(b1),CBDrawingStates佔用了slot(b4)
2. CBChangesEveryObjectDrawing佔用了slot(b1),CBChangesEveryFrame佔用了slot(b4),CBDrawingStates佔用了slot(b5)
不僅是暫存器槽cb#,其餘的如t#、u#、s#等也是一樣的道理。
**只要當前資源沒有標定暫存器槽,並且沒有被著色器使用過,編譯後它們不會佔用暫存器槽。**
### 常量緩衝區的更新
在Effects11的C++端建立了常量緩衝區的同時,還會建立一份與cbuffer等大的記憶體副本,這麼做是為了減少常量緩衝區的更新次數(即CPU→GPU的寫入)。並且每個副本還要設定一個髒標記,即只有在資料發生變化的時候才會進行實際的提交。
在Effects11中,更新常量初值的方式如下:
```cpp
m_pFX->GetVariableByName("gWorld")->AsMatrix()->SetMatrix((float*)&M);
```
這裡實際上就是更新所屬常量緩衝區的記憶體副本中`gWorld`所屬的記憶體區域,然後將髒標記設定為`true`。
所有的更新結束後,通過呼叫`ID3DX11EffectPass::Apply`來執行實際的常量緩衝區更新:
```cpp
m_pTech->GetPassByIndex(p)->Apply(0, m_pd3dImmediateContext);
```
在完成更新後,Apply便會將常量緩衝區繫結到渲染管線上,例如執行下面的語句:
```cpp
m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, &pCB->pD3DObject);
```
不僅是常量緩衝區,Apply操作還會繫結著色器、著色器資源(SRV)、可讀寫資源(UAV)、取樣器、各種渲染狀態等。
翻看FX11的原始碼,我們可以找到更新常量緩衝區的地方。該函式會在Apply後呼叫:
```cpp
inline void CheckAndUpdateCB_FX(ID3D11DeviceContext *pContext, SConstantBuffer *pCB)
{
if (pCB->IsDirty && !pCB->IsNonUpdatable)
{
// CB out of date; rebuild it
pContext->UpdateSubresource(pCB->pD3DObject, 0, nullptr, pCB->pBackingStore, pCB->Size, pCB->Size);
pCB->IsDirty = false;
}
}
```
當然,如果cbuffer用的是DYNAMIC更新,則需要改為Map與UnMap的更新方式。
### 預設常量緩衝區(Default Constant Buffer)
如果一個變數沒有`static`或`const`修飾符,那麼編譯器將會認為它是屬於名為`$Globals`的預設常量緩衝區的一員。類似的,著色器入口點的`uniform`形參將會被認為是屬於另一個名為`$Params`的預設常量緩衝區。
考慮下面一段程式碼:
```cpp
uniform bool g_FogEnable; // 屬於$Gbloals
cbuffer CB0 : register(b0) { ... }
cbuffer CB1 : register(b1) { ... }
cbuffer CB2 { ... }
float4 PS(
PIN pin,
uniform int numLights /* 屬於$Params */
) : SV_Target
{
...
}
```
對於常量緩衝區槽位的安排,最終會按如下順序安排:
1. 有指定暫存器槽位的`cbuffer`優先佔用
2. 其次是`$Globals`佔用空餘槽位中值最小的那個
3. 緊接著`$Params`佔用空餘槽位中最小的那個
4. 剩餘有被該著色器使用的`cbuffer`按空餘槽位從小到大的順序佔用
因此,編譯器會這樣解釋:
```cpp
cbuffer CB0 : register(b0) { ... }
cbuffer CB1 : register(b1) { ... }
cbuffer $Globals : register(b2) { bool g_FogEnable; }
cbuffer $Params : register(b3) { int numLights; }
cbuffer CB2 : register(b4) { ... }
```
當然,直接宣告`$Globals`或`Globals`是不可能編譯通過的。
這就能解釋的通,為什麼我們在編譯HLSL程式碼時,b#的最大值只能到13(即我們只能指定14個自定義的常量緩衝區),但在標頭檔案`d3d11.h`卻又說有16個暫存器槽位了。因為剩餘的兩個槽位要讓位於`$Globals`和`$Params`這兩個預設常量緩衝區。
# 著色器反射
編譯好的著色器二進位制資料中蘊含著豐富的資訊,我們可以通過著色器反射機制來獲取自己所需要的東西,然後構建一個屬於自己的Effects類。
## D3DReflect函式--獲取著色器反射物件
在呼叫該函式之前需要使用`D3DCompile`或`D3DCompileFromFile`產生編譯好的著色器二進位制物件`ID3DBlob`:
```cpp
HRESULT D3DReflect(
LPCVOID pSrcData, // [In]編譯好的著色器二進位制資訊
SIZE_T SrcDataSize, // [In]編譯好的著色器二進位制資訊位元組數
REFIID pInterface, // [In]COM元件的GUID
void **ppReflector // [Out]輸出的著色器反射藉口
);
```
其中`pInterface`為`__uuidof(ID3D11ShaderReflection)`時,返回的是`ID3D11ShaderReflection`介面物件;而`pInterface`為`__uuidof(ID3D12ShaderReflection)`時,返回的是`ID3D12ShaderReflection`介面物件。
`ID3D11ShaderReflection`提供了大量的方法給我們獲取資訊,其中我們比較感興趣的主要資訊有:
1. 著色器本身的資訊
2. 常量緩衝區的資訊
3. 取樣器、資源的資訊
## D3D11_SHADER_DESC結構體--著色器本身的資訊
通過方法`ID3D11ShaderReflection::GetDesc`,我們可以獲取到`D3D11_SHADER_DESC`物件。這裡麵包含了大量的基礎資訊:
```cpp
typedef struct _D3D11_SHADER_DESC {
UINT Version; // 著色器版本、型別資訊
LPCSTR Creator; // 是誰建立的著色器
UINT Flags; // 著色器編譯/分析標籤
UINT ConstantBuffers; // 實際使用到常量緩衝區數目
UINT BoundResources; // 實際用到繫結的資源數目
UINT InputParameters; // 輸入引數數目(4x4矩陣為4個向量形參)
UINT OutputParameters; // 輸出引數數目
UINT InstructionCount; // 指令數
UINT TempRegisterCount; // 實際使用到的臨時暫存器數目
UINT TempArrayCount; // 實際用到的臨時陣列數目
UINT DefCount; // 常量定義數目
UINT DclCount; // 宣告數目(輸入+輸出)
UINT TextureNormalInstructions; // 未分類的紋理指令數目
UINT TextureLoadInstructions; // 紋理讀取指令數目
UINT TextureCompInstructions; // 紋理比較指令數目
UINT TextureBiasInstructions; // 紋理偏移指令數目
UINT TextureGradientInstructions; // 紋理梯度指令數目
UINT FloatInstructionCount; // 實際用到的浮點數指令數目
UINT IntInstructionCount; // 實際用到的有符號整數指令數目
UINT UintInstructionCount; // 實際用到的無符號整數指令數目
UINT StaticFlowControlCount; // 實際用到的靜態流控制指令數目
UINT DynamicFlowControlCount; // 實際用到的動態流控制指令數目
UINT MacroInstructionCount; // 實際用到的巨集指令數目
UINT ArrayInstructionCount; // 實際用到的陣列指令數目
UINT CutInstructionCount; // 實際用到的cut指令數目
UINT EmitInstructionCount; // 實際用到的emit指令數目
D3D_PRIMITIVE_TOPOLOGY GSOutputTopology; // 幾何著色器的輸出圖元
UINT GSMaxOutputVertexCount; // 幾何著色器的最大頂點輸出數目
D3D_PRIMITIVE InputPrimitive; // 輸入裝配階段的圖元
UINT PatchConstantParameters; // 待填坑...
UINT cGSInstanceCount; // 幾何著色器的例項數目
UINT cControlPoints; // 域著色器和外殼著色器的控制點數目
D3D_TESSELLATOR_OUTPUT_PRIMITIVE HSOutputPrimitive; // 鑲嵌器輸出的圖元型別
D3D_TESSELLATOR_PARTITIONING HSPartitioning; // 待填坑...
D3D_TESSELLATOR_DOMAIN TessellatorDomain; // 待填坑...
UINT cBarrierInstructions; // 計算著色器記憶體屏障指令數目
UINT cInterlockedInstructions; // 計算著色器原子操作指令數目
UINT cTextureStoreInstructions; // 計算著色器紋理寫入次數
} D3D11_SHADER_DESC;
```
其中,成員`Version`不僅包含了著色器版本,還包含著色器型別。下面的列舉值定義了著色器的型別,並通過巨集`D3D11_SHVER_GET_TYPE`來獲取:
```cpp
typedef enum D3D11_SHADER_VERSION_TYPE
{
D3D11_SHVER_PIXEL_SHADER = 0,
D3D11_SHVER_VERTEX_SHADER = 1,
D3D11_SHVER_GEOMETRY_SHADER = 2,
// D3D11 Shaders
D3D11_SHVER_HULL_SHADER = 3,
D3D11_SHVER_DOMAIN_SHADER = 4,
D3D11_SHVER_COMPUTE_SHADER = 5,
D3D11_SHVER_RESERVED0 = 0xFFF0,
} D3D11_SHADER_VERSION_TYPE;
#define D3D11_SHVER_GET_TYPE(_Version) \
(((_Version) >> 16) & 0xffff)
```
即:
```cpp
auto shaderType = static_cast(D3D11_SHVER_GET_TYPE(sd.Version));
```
## D3D11_SHADER_INPUT_BIND_DESC結構體--描述著色器資源如何繫結到著色器輸入
為了獲取著色器程式內宣告的一切給著色器使用的物件,從這個結構體入手是一種十分不錯的選擇。我們將使用`ID3D11ShaderReflection::GetResourceBindingDesc`方法,和列舉顯示介面卡那樣從索引0開始列舉一樣的做法,只要當前的索引值獲取失敗,說明已經獲取完所有的輸入物件:
```cpp
for (UINT i = 0;; ++i)
{
D3D11_SHADER_INPUT_BIND_DESC sibDesc;
hr = pShaderReflection->GetResourceBindingDesc(i, &sibDesc);
// 讀取完變數後會失敗,但這並不是失敗的呼叫
if (FAILED(hr))
break;
// 根據sibDesc繼續分析...
}
```
> 注意:那些在著色器程式碼中從未被當前著色器使用過的資源將不會被枚舉出來,並且在著色器除錯和著色器反射的時候看不到它們,而反彙編中也許能夠看到該變數被標記為unused。
現在先來看該結構體的成員:
```cpp
typedef struct _D3D11_SHADER_INPUT_BIND_DESC {
LPCSTR Name; // 著色器資源名
D3D_SHADER_INPUT_TYPE Type; // 資源型別
UINT BindPoint; // 指定的輸入槽起始位置
UINT BindCount; // 對於陣列而言,佔用了多少個槽
UINT uFlags; // D3D_SHADER_INPUT_FLAGS列舉複合
D3D_RESOURCE_RETURN_TYPE ReturnType; //
D3D_SRV_DIMENSION Dimension; // 著色器資源型別
UINT NumSamples; // 若為紋理,則為MSAA取樣數,否則為0xFFFFFFFF
} D3D11_SHADER_INPUT_BIND_DESC;
```
其中成員`Name`幫助我們使用著色器反射按名獲取資源,而成員`Type`幫助我們確定資源型別。這兩個成員一旦確定下來,對我們開展更詳細的著色器反射和實現自己的特效框架提供了巨大的幫助。具體列舉如下:
```cpp
typedef enum _D3D_SHADER_INPUT_TYPE {
D3D_SIT_CBUFFER,
D3D_SIT_TBUFFER,
D3D_SIT_TEXTURE,
D3D_SIT_SAMPLER,
D3D_SIT_UAV_RWTYPED,
D3D_SIT_STRUCTURED,
D3D_SIT_UAV_RWSTRUCTURED,
D3D_SIT_BYTEADDRESS,
D3D_SIT_UAV_RWBYTEADDRESS,
D3D_SIT_UAV_APPEND_STRUCTURED,
D3D_SIT_UAV_CONSUME_STRUCTURED,
D3D_SIT_UAV_RWSTRUCTURED_WITH_COUNTER,
// ...
} D3D_SHADER_INPUT_TYPE;
```
根據上述列舉可以分為常量緩衝區、取樣器、著色器資源、可讀寫資源四大類。對於取樣器、著色器資源和可讀寫資源我們只需要知道它設定在哪個slot即可,但對於常量緩衝區,我們還需要知道其內部的成員和位於哪一段記憶體區域。
## D3D11_SHADER_BUFFER_DESC結構體--描述一個著色器的常量緩衝區
在通過上面提到的列舉值判定出來是常量緩衝區後,我們就可以通過`ID3D11ShaderReflection::GetConstantBufferByName`迅速拿下常量緩衝區的反射,然後再獲取`D3D11_SHADER_BUFFER_DESC`的資訊:
```cpp
ID3D11ShaderReflectionConstantBuffer* pSRCBuffer = pShaderReflection->GetConstantBufferByName(sibDesc.Name);
// 獲取cbuffer內的變數資訊並建立對映
D3D11_SHADER_BUFFER_DESC cbDesc{};
hr = pSRCBuffer->GetDesc(&cbDesc);
if (FAILED(hr))
return hr;
```
> 注意:ID3D11ShaderReflectionConstantBuffer並不是COM元件,因此不能用ComPtr存放。
該結構體定義如下:
```cpp
typedef struct _D3D11_SHADER_BUFFER_DESC {
LPCSTR Name; // 常量緩衝區名稱
D3D_CBUFFER_TYPE Type; // D3D_CBUFFER_TYPE列舉值
UINT Variables; // 內部變數數目
UINT Size; // 緩衝區位元組數
UINT uFlags; // D3D_SHADER_CBUFFER_FLAGS列舉複合
} D3D11_SHADER_BUFFER_DESC;
```
根據成員`Variables`,我們就可以確定查詢變數的次數。
## D3D11_SHADER_VARIABLE_DESC結構體--描述一個著色器的變數
雖然有點想吐槽,常量緩衝區裡面存的是變數這個說法,但還是得這樣來看待:常量緩衝區內的資料是可以改變的,但是在著色器執行的時候,`cbuffer`內的任何變數就不可以被修改了。因此對C++來說,它是可變數,但對著色器來說,它是常量。
好了不扯那麼多,現在我們用這樣一個迴圈,通過`ID3D11ShaderReflectionVariable::GetVariableByIndex`來逐一列舉著色器變數的反射,然後獲取`D3D11_SHADER_VARIABLE_DESC`的資訊:
```cpp
// 記錄內部變數
for (UINT j = 0; j < cbDesc.Variables; ++j)
{
ID3D11ShaderReflectionVariable* pSRVar = pSRCBuffer->GetVariableByIndex(j);
D3D11_SHADER_VARIABLE_DESC svDesc;
hr = pSRVar->GetDesc(&svDesc);
if (FAILED(hr))
return hr;
// ...
}
```
`ID3D11ShaderReflectionVariable`不是COM元件,因此無需管釋放。
那麼`D3D11_SHADER_VARIABLE_DESC`的定義如下:
```cpp
typedef struct _D3D11_SHADER_VARIABLE_DESC {
LPCSTR Name; // 變數名
UINT StartOffset; // 起始偏移
UINT Size; // 大小
UINT uFlags; // D3D_SHADER_VARIABLE_FLAGS列舉複合
LPVOID DefaultValue; // 用於初始化變數的預設值
UINT StartTexture; // 從變數開始到紋理開始的偏移量[看不懂]
UINT TextureSize; // 紋理位元組大小
UINT StartSampler; // 從變數開始到取樣器開始的偏移量[看不懂]
UINT SamplerSize; // 取樣器位元組大小
} D3D11_SHADER_VARIABLE_DESC;
```
其中前三個引數是我們需要的,由此我們就可以構建出根據變數名來設定值和獲取值的一套方案。
講到這裡其實已經滿足了我們構建一個最小特效管理類的需求。但你如果想要獲得更詳細的變數資訊,則可以繼續往下讀,這裡只會粗略講述。
## D3D11_SHADER_TYPE_DESC結構體--描述著色器變數型別
現在我們已經獲得了一個著色器變數的反射,那麼可以通過`ID3D11ShaderReflectionVariable::GetType`獲取著色器變數型別的反射,然後獲取`D3D11_SHADER_TYPE_DESC`的資訊:
```cpp
ID3D11ShaderReflectionType* pSRType = pSRVar->GetType();
D3D11_SHADER_TYPE_DESC stDesc;
hr = pSRType->GetDesc(&stDesc);
if (FAILED(hr))
return hr;
```
`D3D11_SHADER_TYPE_DESC`的定義如下:
```cpp
typedef struct _D3D11_SHADER_TYPE_DESC {
D3D_SHADER_VARIABLE_CLASS Class; // 說明它是標量、向量、矩陣、物件,還是型別
D3D_SHADER_VARIABLE_TYPE Type; // 說明它是BOOL、INT、FLOAT,還是別的型別
UINT Rows; // 矩陣行數
UINT Columns; // 矩陣列數
UINT Elements; // 陣列元素數目
UINT Members; // 結構體成員數目
UINT Offset; // 在結構體中的偏移,如果不是結構體則為0
LPCSTR Name; // 著色器變數型別名,如果變數未被使用則為NULL
} D3D11_SHADER_TYPE_DESC;
```
如果它是個結構體,就還能通過`ID3D11ShaderReflectionType::GetMemberTypeByIndex`方法繼續獲取子類別。。。
# 實現一個複雜Effects框架需要考慮到的問題
在設計一個Effects框架時,你需要考慮這些問題:
1. 是使用常規HLSL程式碼,然後通過著色器反射來實現;還是像Effects11那樣,混雜著自定義語法,自己做程式碼分析
2. 如果是前者,那HLSL程式碼有什麼施加約束(如常量緩衝區、全域性變數的約束)
3. 你的Effects允許塞入一個著色器,還是六種著色器各一個,又還是任意數目的著色器
4. 你希望你的框架能提供多麼複雜的功能(取決於你想獲取多麼詳細的著色器反射資訊),以及 快取哪些資訊
5. 常量緩衝區使用DYNAMIC更新還是DEFAULT更新
6. 你如何定義一個Effect Pass(是否每個Effect Pass都需要提供獨立的形參儲存空間),它能夠管理哪些資源
因為不同的引擎對此需求可能有所不同,這取決於你怎麼去設計。
# EffectHelper類的使用
目前本人實現了一個功能儘可能簡化,但能夠滿足基本需求的`EffectHelper`類。它的功能和限制如下:
1. 支援原生HLSL程式碼
2. 允許塞入任意數目的著色器,但要求這些著色器在常量緩衝區和全域性變數的定義上沒有衝突。一種明智的做法是把所有用到的常量緩衝區、取樣器、著色器資源、可讀寫資源、全域性變數都放在同一個標頭檔案,然後每個著色器檔案都包含這個標頭檔案來使用;又或者是把所有著色器都寫到同一個檔案上
3. 該框架允許按名新增著色器,以及按名新增通道,在建立通道時按名指定使用哪些著色器
4. 和Effects11一樣,通過名稱來獲取HLSL常量緩衝區的變數,然後設定和獲取值
5. 每個通道需要單獨設定著色器形參(按名獲取),並且可以獨立設定光柵化狀態、混合狀態、深度/模板狀態,不設定則使用預設狀態。通過Apply應用當前通道。不支援Technique和Group這種形式
6. 類內部全域性設定和快取取樣器狀態、著色器資源、可讀寫資源
本文並不打算寫實現細節,整個框架原始碼在1500行以內,你可以觀察內部實現原理。現在主要介紹如何使用。
## EffectHelper::AddShader方法--新增著色器
在C++端,首先編譯著色器程式碼,得到編譯好的著色器二進位制資訊,然後通過`EffectHelper::AddShader`新增著色器:
```cpp
m_pEffectHelper = std::make_unique();
ComPtr blob;
// 建立頂點著色器(3D)
HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pEffectHelper->AddShader("Basic_VS_3D", m_pd3dDevice.Get(), blob.Get()));
// 建立頂點佈局(3D)
HR(m_pd3dDevice->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), 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(m_pEffectHelper->AddShader("Basic_PS_3D", m_pd3dDevice.Get(), blob.Get()));
```
## EffectHelper::AddEffectPass方法--新增渲染通道
在建立好著色器後,我們就可以新增渲染通道。首先要填充通道資訊,結構體`EffectPassDesc`定義如下:
```cpp
// 渲染通道描述
// 通過指定新增著色器時提供的名字來設定著色器
struct EffectPassDesc
{
LPCSTR nameVS = nullptr;
LPCSTR nameDS = nullptr;
LPCSTR nameHS = nullptr;
LPCSTR nameGS = nullptr;
LPCSTR namePS = nullptr;
LPCSTR nameCS = nullptr;
};
```
如果不需要使用某一著色器階段,則需指定為`nullptr`。通過設定`AddShader`使用的名稱來指定使用哪個著色器,然後就可以建立通道了:
```cpp
// 新增渲染通道
EffectPassDesc epDesc;
epDesc.nameVS = "Basic_VS_3D";
epDesc.namePS = "Basic_PS_3D";
HR(m_pEffectHelper->AddEffectPass("Basic_3D", m_pd3dDevice.Get(), &epDesc));
```
## 設定取樣器狀態、著色器資源、可讀寫資源
`EffectHelper`提供了按名設定和按槽設定兩種方式:
```cpp
class EffectHelper
{
public:
// ...
// 按槽設定取樣器狀態
void SetSamplerStateBySlot(UINT slot, ID3D11SamplerState* samplerState);
// 按名設定取樣器狀態(若存在同槽多名稱則只能使用按槽設定)
void SetSamplerStateByName(LPCSTR name, ID3D11SamplerState* samplerState);
// 按槽設定著色器資源
void SetShaderResourceBySlot(UINT slot, ID3D11ShaderResourceView* srv);
// 按名設定著色器資源(若存在同槽多名稱則只能使用按槽設定)
void SetShaderResourceByName(LPCSTR name, ID3D11ShaderResourceView* srv);
// 按槽設定可讀寫資源
void SetUnorderedAccessBySlot(UINT slot, ID3D11UnorderedAccessView* uav, UINT* pUAVInitialCount);
// 按名設定可讀寫資源(若存在同槽多名稱則只能使用按槽設定)
void SetUnorderedAccessByName(LPCSTR name, ID3D11UnorderedAccessView* uav, UINT* pUAVInitialCount);
// ...
};
```
## 通過IEffectConstantBufferVariable設定常量緩衝區變數
`EffectHelper`通過HLSL定義的常量緩衝區內變數的名稱來獲取可用於讀寫的介面:
```cpp
std::shared_ptr pWorld = m_pEffectHelper->GetConstantBufferVariable("g_World");
```
介面類`IEffectConstantBufferVariable`定義如下:
```cpp
// 常量緩衝區的變數
// 非COM元件
struct IEffectConstantBufferVariable
{
// 設定無符號整數,也可以為bool設定
virtual void SetUInt(UINT val) = 0;
// 設定有符號整數
virtual void SetSInt(INT val) = 0;
// 設定浮點數
virtual void SetFloat(FLOAT val) = 0;
// 設定無符號整數向量,允許設定1個到4個分量
// 著色器變數型別為bool也可以使用
// 根據要設定的分量數來讀取data的前幾個分量
virtual void SetUIntVector(UINT numComponents, const UINT data[4]) = 0;
// 設定有符號整數向量,允許設定1個到4個分量
// 根據要設定的分量數來讀取data的前幾個分量
virtual void SetSIntVector(UINT numComponents, const INT data[4]) = 0;
// 設定浮點數向量,允許設定1個到4個分量
// 根據要設定的分量數來讀取data的前幾個分量
virtual void SetFloatVector(UINT numComponents, const FLOAT data[4]) = 0;
// 設定無符號整數矩陣,允許行列數在1-4
// 要求傳入資料沒有填充,例如3x3矩陣可以直接傳入UINT[3][3]型別
virtual void SetUIntMatrix(UINT rows, UINT cols, const UINT* noPadData) = 0;
// 設定有符號整數矩陣,允許行列數在1-4
// 要求傳入資料沒有填充,例如3x3矩陣可以直接傳入INT[3][3]型別
virtual void SetSIntMatrix(UINT rows, UINT cols, const INT* noPadData) = 0;
// 設定浮點數矩陣,允許行列數在1-4
// 要求傳入資料沒有填充,例如3x3矩陣可以直接傳入FLOAT[3][3]型別
virtual void SetFloatMatrix(UINT rows, UINT cols, const FLOAT* noPadData) = 0;
// 設定其餘型別,允許指定設定範圍
virtual void SetRaw(const void* data, UINT byteOffset = 0, UINT byteCount = 0xFFFFFFFF) = 0;
// 獲取最近一次設定的值,允許指定讀取範圍
virtual HRESULT GetRaw(void* pOutput, UINT byteOffset = 0, UINT byteCount = 0xFFFFFFFF) = 0;
};
```
前面的矩陣可以這樣設定:
```cpp
XMMATRIX Eye = XMMatrixIdentity();
pWorld->SetFloatMatrix(4, 4, (const FLOAT*)&Eye);
```
要注意這樣的設定並不是立即生效到著色器內的。
## IEffectPass介面類
在完成各種資源繫結後,就可以來到渲染通道這邊了。`IEffectPass`定義如下:
```cpp
// 渲染通道
// 非COM元件
struct IEffectPass
{
// 設定光柵化狀態
virtual void SetRasterizerState(ID3D11RasterizerState* pRS) = 0;
// 設定混合狀態
virtual void SetBlendState(ID3D11BlendState* pBS, const FLOAT blendFactor[4], UINT sampleMask) = 0;
// 設定深度混合狀態
virtual void SetDepthStencilState(ID3D11DepthStencilState* pDSS, UINT stencilValue) = 0;
// 獲取頂點著色器的uniform形參用於設定值
virtual std::shared_ptr VSGetParamByName(LPCSTR paramName) = 0;
// 獲取域著色器的uniform形參用於設定值
virtual std::shared_ptr DSGetParamByName(LPCSTR paramName) = 0;
// 獲取外殼著色器的uniform形參用於設定值
virtual std::shared_ptr HSGetParamByName(LPCSTR paramName) = 0;
// 獲取幾何著色器的uniform形參用於設定值
virtual std::shared_ptr GSGetParamByName(LPCSTR paramName) = 0;
// 獲取畫素著色器的uniform形參用於設定值
virtual std::shared_ptr PSGetParamByName(LPCSTR paramName) = 0;
// 獲取計算著色器的uniform形參用於設定值
virtual std::shared_ptr CSGetParamByName(LPCSTR paramName) = 0;
// 應用著色器、常量緩衝區(包括函式形參)、取樣器、著色器資源和可讀寫資源到渲染管線
virtual void Apply(ID3D11DeviceContext* deviceContext) = 0;
};
```
可見每個渲染通道有自己獨立的三個渲染狀態,並存儲著著色器uniform形參的資訊允許使用者設定。
最後繪製前,我們要應用當前的渲染通道:
```cpp
m_pCurrEffectPass->Apply(m_pd3dImmediateContext.Get());
```
# 補充說明
該特效管理框架將會從第31章往後的專案開始使用。但這裡給出專案09用於新增和替換的一些原始碼以嚐鮮。目前並不會有較大的改動,如果使用過程中遇到什麼問題,可以在這裡評論反饋。
[EffectHelper_Project_09.zip](https://files.cnblogs.com/files/X-Jun/EffectHelper_Project_09.zip)
**[DirectX11 With Windows SDK完整目錄](http://www.cnblogs.com/X-Jun/p/9028764.html)**
**[Github專案原始碼](https://github.com/MKXJun/DirectX11-With-Windows-SDK)**
**歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡匯