DirectX11–深入理解Effects11、使用著色器反射機制(Shader Reflection)實現一個複雜Effects框架
- 2020 年 3 月 9 日
- 筆記
前言
如果之前你是跟隨本教程系列學習的話,應該能夠初步了解Effects11(現FX11)的實現機制,並且可以編寫一個簡易的特效管理框架,但是隨著特效種類的增多,要管理的著色器、資源等也隨之變多。如果寫了一套由多個HLSL著色器組成特效,就仍需要在C++端編寫與HLSL相對應的特效框架,這樣寫起來依然是十分繁雜。以前學習龍書的DirectX11時,裡面使用的正是Effects11框架,不得不承認用它實現C++跟HLSL的交互的確方便了許多,但是時過境遷,微軟將會逐漸拋棄fx_5_0,且目前FX11也已經列為Archived,不再更新。都說如果要實現一個3D引擎的話,必須要有一個屬於自己的特效管理框架。
本文假定讀者已經讀過至少前13章的內容,或者有較為豐富的DirectX 11開發經歷。
學習目標:
- 熟悉著色器反射機制
- 實現一個複雜Effects框架,了解該框架的使用
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的語法!):
// 存在部分省略 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中可以找到對應的原型:
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或D3DCompileFromFile,然後再使用D3DX11CreateEffectFromMemory創建出Effects。只不過會收到這樣的警告:
X4717: Effects deprecated for D3DCompiler_47
渲染狀態、取樣器狀態
在fx5.0中能夠創建出SamplerState
、RasterizerState
、BlendState
和DepthStencilState
,並且還能預先設置好內部的各項參數,就像下面這樣(注意:下面的程式碼不屬於HLSL的語法!):
SamplerState g_SamAnisotropic { Filter = ANISOTROPIC; MaxAnisotropy = 4; AddressU = WRAP; AddressV = WRAP; AddressW = WRAP; }; RasterizerState g_NoCulling { FillMode = Solid; CullMode = None; FrontCounterClockwise = false; }
實際上,取樣器的狀態和渲染狀態都是在C++中完成的,上面的程式碼翻譯成C++則變成類似這樣:
// 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寫常量緩衝區的時候是這樣的:
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++端創建出對應的常量緩衝區:
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當前使用的是哪個槽呢?
- 有通過register(b#)指定暫存器槽位的
cbuffer
優先佔用 - 除去那些顯式指定槽位的
cbuffer
,如果cbuffer
裡面的成員有被當前著色器使用過,將會根據聲明順序按空餘槽位從小到大的順序佔用
根據上面的例子,cbPerFrame將使用slot(b0),而cbPerObject將使用slot(b1)。
現在讓我們省略所有的花括弧,觀察下面的程式碼,根據下面兩種情況,問那三個未指定暫存器槽的cbuffer分別佔用了哪個slot?
- 頂點著色器使用過第1、3、4、5個cbuffer裡面的變數
- 像素著色器使用過第2、3、4、6個cbuffer裡面的變數
cbuffer CBChangesEveryInstanceDrawing : register(b0) { ... } cbuffer CBChangesEveryObjectDrawing { ... } cbuffer CBChangesEveryFrame { ... } cbuffer CBDrawingStates { ... } cbuffer CBChangesOnResize : register(b2) { ... } cbuffer CBChangesRarely : register(b3) { ... }
答案如下:
- CBChangesEveryFrame佔用了slot(b1),CBDrawingStates佔用了slot(b4)
- CBChangesEveryObjectDrawing佔用了slot(b1),CBChangesEveryFrame佔用了slot(b4),CBDrawingStates佔用了slot(b5)
不僅是暫存器槽cb#,其餘的如t#、u#、s#等也是一樣的道理。
只要當前資源沒有標定暫存器槽,並且沒有被著色器使用過,編譯後它們不會佔用暫存器槽。
常量緩衝區的更新
在Effects11的C++端創建了常量緩衝區的同時,還會創建一份與cbuffer等大的記憶體副本,這麼做是為了減少常量緩衝區的更新次數(即CPU→GPU的寫入)。並且每個副本還要設置一個臟標記,即只有在數據發生變化的時候才會進行實際的提交。
在Effects11中,更新常量初值的方式如下:
m_pFX->GetVariableByName("gWorld")->AsMatrix()->SetMatrix((float*)&M);
這裡實際上就是更新所屬常量緩衝區的記憶體副本中gWorld
所屬的記憶體區域,然後將臟標記設置為true
。
所有的更新結束後,通過調用ID3DX11EffectPass::Apply
來執行實際的常量緩衝區更新:
m_pTech->GetPassByIndex(p)->Apply(0, m_pd3dImmediateContext);
在完成更新後,Apply便會將常量緩衝區綁定到渲染管線上,例如執行下面的語句:
m_pd3dImmediateContext->VSSetConstantBuffers(0, 1, &pCB->pD3DObject);
不僅是常量緩衝區,Apply操作還會綁定著色器、著色器資源(SRV)、可讀寫資源(UAV)、取樣器、各種渲染狀態等。
翻看FX11的源碼,我們可以找到更新常量緩衝區的地方。該函數會在Apply後調用:
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
的默認常量緩衝區。
考慮下面一段程式碼:
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 { ... }
對於常量緩衝區槽位的安排,最終會按如下順序安排:
- 有指定暫存器槽位的
cbuffer
優先佔用 - 其次是
$Globals
佔用空餘槽位中值最小的那個 - 緊接著
$Params
佔用空餘槽位中最小的那個 - 剩餘有被該著色器使用的
cbuffer
按空餘槽位從小到大的順序佔用
因此,編譯器會這樣解釋:
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
和Globals
這兩個默認常量緩衝區。
著色器反射
編譯好的著色器二進位數據中蘊含著豐富的資訊,我們可以通過著色器反射機制來獲取自己所需要的東西,然後構建一個屬於自己的Effects類。
D3DReflect函數–獲取著色器反射對象
在調用該函數之前需要使用D3DCompile
或D3DCompileFromFile
產生編譯好的著色器二進位對象ID3DBlob
:
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
提供了大量的方法給我們獲取資訊,其中我們比較感興趣的主要資訊有:
- 著色器本身的資訊
- 常量緩衝區的資訊
- 取樣器、資源的資訊
D3D11_SHADER_DESC結構體–著色器本身的資訊
通過方法ID3D11ShaderReflection::GetDesc
,我們可以獲取到D3D11_SHADER_DESC
對象。這裡面包含了大量的基礎資訊:
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
來獲取:
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)
即:
auto shaderType = static_cast<D3D11_SHADER_VERSION_TYPE>(D3D11_SHVER_GET_TYPE(sd.Version));
D3D11_SHADER_INPUT_BIND_DESC結構體–描述著色器資源如何綁定到著色器輸入
為了獲取著色器程式內聲明的一切給著色器使用的對象,從這個結構體入手是一種十分不錯的選擇。我們將使用ID3D11ShaderReflection::GetResourceBindingDesc
方法,和枚舉顯示適配器那樣從索引0開始枚舉一樣的做法,只要當前的索引值獲取失敗,說明已經獲取完所有的輸入對象:
for (UINT i = 0;; ++i) { D3D11_SHADER_INPUT_BIND_DESC sibDesc; hr = pShaderReflection->GetResourceBindingDesc(i, &sibDesc); // 讀取完變數後會失敗,但這並不是失敗的調用 if (FAILED(hr)) break; // 根據sibDesc繼續分析... }
注意:那些在著色器程式碼中從未被當前著色器使用過的資源將不會被枚舉出來,並且在著色器調試和著色器反射的時候看不到它們,而反彙編中也許能夠看到該變數被標記為unused。
現在先來看該結構體的成員:
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
幫助我們確定資源類型。這兩個成員一旦確定下來,對我們開展更詳細的著色器反射和實現自己的特效框架提供了巨大的幫助。具體枚舉如下:
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
的資訊:
ID3D11ShaderReflectionConstantBuffer* pSRCBuffer = pShaderReflection->GetConstantBufferByName(sibDesc.Name); // 獲取cbuffer內的變數資訊並建立映射 D3D11_SHADER_BUFFER_DESC cbDesc{}; hr = pSRCBuffer->GetDesc(&cbDesc); if (FAILED(hr)) return hr;
注意:ID3D11ShaderReflectionConstantBuffer並不是COM組件,因此不能用ComPtr存放。
該結構體定義如下:
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
的資訊:
// 記錄內部變數 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
的定義如下:
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
的資訊:
ID3D11ShaderReflectionType* pSRType = pSRVar->GetType(); D3D11_SHADER_TYPE_DESC stDesc; hr = pSRType->GetDesc(&stDesc); if (FAILED(hr)) return hr;
D3D11_SHADER_TYPE_DESC
的定義如下:
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框架時,你需要考慮這些問題:
- 是使用常規HLSL程式碼,然後通過著色器反射來實現;還是像Effects11那樣,混雜著自定義語法,自己做程式碼分析
- 如果是前者,那HLSL程式碼有什麼施加約束(如常量緩衝區、全局變數的約束)
- 你的Effects允許塞入一個著色器,還是六種著色器各一個,又還是任意數目的著色器
- 你希望你的框架能提供多麼複雜的功能(取決於你想獲取多麼詳細的著色器反射資訊),以及 快取哪些資訊
- 常量緩衝區使用DYNAMIC更新還是DEFAULT更新
- 你如何定義一個Effect Pass(是否每個Effect Pass都需要提供獨立的形參存儲空間),它能夠管理哪些資源
因為不同的引擎對此需求可能有所不同,這取決於你怎麼去設計。
EffectHelper類的使用
目前本人實現了一個功能儘可能簡化,但能夠滿足基本需求的EffectHelper
類。它的功能和限制如下:
- 支援原生HLSL程式碼
- 允許塞入任意數目的著色器,但要求這些著色器在常量緩衝區和全局變數的定義上沒有衝突。一種明智的做法是把所有用到的常量緩衝區、取樣器、著色器資源、可讀寫資源、全局變數都放在同一個頭文件,然後每個著色器文件都包含這個頭文件來使用;又或者是把所有著色器都寫到同一個文件上
- 該框架允許按名添加著色器,以及按名添加通道,在創建通道時按名指定使用哪些著色器
- 和Effects11一樣,通過名稱來獲取HLSL常量緩衝區的變數,然後設置和獲取值
- 每個通道需要單獨設置著色器形參(按名獲取),並且可以獨立設置光柵化狀態、混合狀態、深度/模板狀態,不設置則使用默認狀態。通過Apply應用當前通道。不支援Technique和Group這種形式
- 類內部全局設置和快取取樣器狀態、著色器資源、可讀寫資源
本文並不打算寫實現細節,整個框架源碼在1500行以內,你可以觀察內部實現原理。現在主要介紹如何使用。
EffectHelper::AddShader方法–添加著色器
在C++端,首先編譯著色器程式碼,得到編譯好的著色器二進位資訊,然後通過EffectHelper::AddShader
添加著色器:
m_pEffectHelper = std::make_unique<EffectHelper>(); ComPtr<ID3DBlob> 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
定義如下:
// 渲染通道描述 // 通過指定添加著色器時提供的名字來設置著色器 struct EffectPassDesc { LPCSTR nameVS = nullptr; LPCSTR nameDS = nullptr; LPCSTR nameHS = nullptr; LPCSTR nameGS = nullptr; LPCSTR namePS = nullptr; LPCSTR nameCS = nullptr; };
如果不需要使用某一著色器階段,則需指定為nullptr
。通過設置AddShader
使用的名稱來指定使用哪個著色器,然後就可以創建通道了:
// 添加渲染通道 EffectPassDesc epDesc; epDesc.nameVS = "Basic_VS_3D"; epDesc.namePS = "Basic_PS_3D"; HR(m_pEffectHelper->AddEffectPass("Basic_3D", m_pd3dDevice.Get(), &epDesc));
設置取樣器狀態、著色器資源、可讀寫資源
EffectHelper
提供了按名設置和按槽設置兩種方式:
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定義的常量緩衝區內變數的名稱來獲取可用於讀寫的介面:
std::shared_ptr<IEffectConstantBufferVariable> pWorld = m_pEffectHelper->GetConstantBufferVariable("g_World");
介面類IEffectConstantBufferVariable
定義如下:
// 常量緩衝區的變數 // 非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; };
前面的矩陣可以這樣設置:
XMMATRIX Eye = XMMatrixIdentity(); pWorld->SetFloatMatrix(4, 4, (const FLOAT*)&Eye);
要注意這樣的設置並不是立即生效到著色器內的。
IEffectPass介面類
在完成各種資源綁定後,就可以來到渲染通道這邊了。IEffectPass
定義如下:
// 渲染通道 // 非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<IEffectConstantBufferVariable> VSGetParamByName(LPCSTR paramName) = 0; // 獲取域著色器的uniform形參用於設置值 virtual std::shared_ptr<IEffectConstantBufferVariable> DSGetParamByName(LPCSTR paramName) = 0; // 獲取外殼著色器的uniform形參用於設置值 virtual std::shared_ptr<IEffectConstantBufferVariable> HSGetParamByName(LPCSTR paramName) = 0; // 獲取幾何著色器的uniform形參用於設置值 virtual std::shared_ptr<IEffectConstantBufferVariable> GSGetParamByName(LPCSTR paramName) = 0; // 獲取像素著色器的uniform形參用於設置值 virtual std::shared_ptr<IEffectConstantBufferVariable> PSGetParamByName(LPCSTR paramName) = 0; // 獲取計算著色器的uniform形參用於設置值 virtual std::shared_ptr<IEffectConstantBufferVariable> CSGetParamByName(LPCSTR paramName) = 0; // 應用著色器、常量緩衝區(包括函數形參)、取樣器、著色器資源和可讀寫資源到渲染管線 virtual void Apply(ID3D11DeviceContext* deviceContext) = 0; };
可見每個渲染通道有自己獨立的三個渲染狀態,並存儲著著色器uniform形參的資訊允許用戶設置。
最後繪製前,我們要應用當前的渲染通道:
m_pCurrEffectPass->Apply(m_pd3dImmediateContext.Get());
補充說明
該特效管理框架將會從第31章往後的項目開始使用。但這裡給出項目09用於添加和替換的一些源程式碼以嘗鮮。目前並不會有較大的改動,如果使用過程中遇到什麼問題,可以在這裡評論回饋。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。