前言
如果之前你是跟隨本教程系列學習的話,應該能夠初步了解Effects11(現FX11)的實作機制,并且可以撰寫一個簡易的特效管理框架,但是隨著特效種類的增多,要管理的著色器、資源等也隨之變多,如果寫了一套由多個HLSL著色器組成特效,就仍需要在C++端撰寫與HLSL相對應的特效框架,這樣寫起來依然是十分繁雜,以前學習龍書的DirectX11時,里面使用的正是Effects11框架,不得不承認用它實作C++跟HLSL的互動的確方便了許多,但是時過境遷,微軟將會逐漸拋棄fx_5_0,且目前FX11也已經列為Archived,不再更新,都說如果要實作一個3D引擎的話,必須要有一個屬于自己的特效管理框架,
本文假定讀者已經讀過至少前13章的內容,或者有較為豐富的DirectX 11開發經歷,
學習目標:
- 熟悉著色器反射機制
- 實作一個復雜Effects框架,了解該框架的使用
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入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_DEFAULT
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和$Params這兩個默認常量緩沖區,
著色器反射
編譯好的著色器二進制資料中蘊含著豐富的資訊,我們可以通過著色器反射機制來獲取自己所需要的東西,然后構建一個屬于自己的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 initialCount);
// 按名設定可讀寫資源(若存在同槽多名稱則只能使用按槽設定)
void SetUnorderedAccessByName(LPCSTR name, ID3D11UnorderedAccessView* uav, UINT initialCount);
// ...
};
通過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用于添加和替換的一些源代碼以嘗鮮,目前并不會有較大的改動,如果使用程序中遇到什么問題,可以在這里評論反饋,
EffectHelper_Project_09.zip
DirectX11 With Windows SDK完整目錄
Github專案原始碼
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/3891.html
標籤:其他
