主頁 >  其他 > DirectX11--深入理解Effects11、使用著色器反射機制(Shader Reflection)實作一個復雜Effects框架

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

2020-09-11 01:35:06 其他

前言

如果之前你是跟隨本教程系列學習的話,應該能夠初步了解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,以及有什么問題也可以在這里匯報,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/3891.html

標籤:其他

上一篇:Content Size Fitter 組件

下一篇:[2020.03]Unity ML-Agents v0.15.0(一)環境部署與試運行

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more