DirectX11--深入理解Effects11、使用着色器反射機制(Shader Reflection)實現一個復雜Effects框架


前言

如果之前你是跟隨本教程系列學習的話,應該能夠初步了解Effects11(現FX11)的實現機制,並且可以編寫一個簡易的特效管理框架,但是隨着特效種類的增多,要管理的着色器、資源等也隨之變多。如果寫了一套由多個HLSL着色器組成特效,就仍需要在C++端編寫與HLSL相對應的特效框架,這樣寫起來依然是十分繁雜。以前學習龍書的DirectX11時,里面使用的正是Effects11框架,不得不承認用它實現C++跟HLSL的交互的確方便了許多,但是時過境遷,微軟將會逐漸拋棄fx_5_0,且目前FX11也已經列為Archived,不再更新。都說如果要實現一個3D引擎的話,必須要有一個屬於自己的特效管理框架。

本文假定讀者已經讀過至少前13章的內容,或者有較為豐富的DirectX 11開發經歷。

學習目標:

  1. 熟悉着色器反射機制
  2. 實現一個復雜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

而像VertexShaderPixelShader這些僅存在於fx5.0的語法,在C++中對應的是ID3D11VertexShaderID3D11PixelShader等等。

至於CompileShader,我們可以猜測內部使用的是類似D3DCompile這樣的函數,只不過這份源碼肯定是需要經過特殊處理才能變成原生的HLSL代碼。

在C++端,編譯fx5.0可以使用D3DCompileD3DCompileFromFile,然后再使用D3DX11CreateEffectFromMemory創建出Effects。只不過會收到這樣的警告:

X4717: Effects deprecated for D3DCompiler_47

渲染狀態、采樣器狀態

在fx5.0中能夠創建出SamplerStateRasterizerStateBlendStateDepthStencilState,並且還能預先設置好內部的各項參數,就像下面這樣(注意:下面的代碼不屬於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當前使用的是哪個槽呢?

  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里面的變量
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中,更新常量初值的方式如下:

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)

如果一個變量沒有staticconst修飾符,那么編譯器將會認為它是屬於名為$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
{
    ...
}

對於常量緩沖區槽位的安排,最終會按如下順序安排:

  1. 有指定寄存器槽位的cbuffer優先占用
  2. 其次是$Globals占用空余槽位中值最小的那個
  3. 緊接着$Params占用空余槽位中最小的那個
  4. 剩余有被該着色器使用的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) { ... }

當然,直接聲明$GlobalsGlobals是不可能編譯通過的。

這就能解釋的通,為什么我們在編譯HLSL代碼時,b#的最大值只能到13(即我們只能指定14個自定義的常量緩沖區),但在頭文件d3d11.h卻又說有16個寄存器槽位了。因為剩余的兩個槽位要讓位於$Globals$Params這兩個默認常量緩沖區。

着色器反射

編譯好的着色器二進制數據中蘊含着豐富的信息,我們可以通過着色器反射機制來獲取自己所需要的東西,然后構建一個屬於自己的Effects類。

D3DReflect函數--獲取着色器反射對象

在調用該函數之前需要使用D3DCompileD3DCompileFromFile產生編譯好的着色器二進制對象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提供了大量的方法給我們獲取信息,其中我們比較感興趣的主要信息有:

  1. 着色器本身的信息
  2. 常量緩沖區的信息
  3. 采樣器、資源的信息

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框架時,你需要考慮這些問題:

  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添加着色器:

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,以及有什么問題也可以在這里匯報。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM