若是以前你是跟随本教程系列学习的话,应该可以初步了解Effects11(现FX11)的实现机制,而且能够编写一个简易的特效管理框架,可是随着特效种类的增多,要管理的着色器、资源等也随之变多。若是写了一套由多个HLSL着色器组成特效,就仍须要在C++端编写与HLSL相对应的特效框架,这样写起来依然是十分繁杂。之前学习龙书的DirectX11时,里面使用的正是Effects11框架,不得不认可用它实现C++跟HLSL的交互的确方便了许多,可是时过境迁,微软将会逐渐抛弃fx_5_0,且目前FX11也已经列为Archived,再也不更新。都说若是要实现一个3D引擎的话,必需要有一个属于本身的特效管理框架。html
本文假定读者已经读过至少前13章的内容,或者有较为丰富的DirectX 11开发经历。git
学习目标:github
DirectX11 With Windows SDK完整目录windows
Github项目源码api
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。数组
DirectX的特效是包含管线状态和着色器的集合,而Effects框架则正是用于管理这些特效的一套API。若是使用Effects11(FX11)框架的话,那么在HLSL中除了自己的语法外,还支持Effects特有的语法,这些语法大部分通过解析后会转化为在C++中使用Direct3D的API。缓存
知己知彼,才能百战不殆。要想写好一个特效管理框架,首先要把Effects框架与C++的关系给分析透彻。下面的内容也会引用FX11的少许源码来佐证。框架
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当前使用的是哪一个槽呢?
cbuffer
优先占用cbuffer
,若是cbuffer
里面的成员有被当前着色器使用过,将会根据声明顺序按空余槽位从小到大的顺序占用根据上面的例子,cbPerFrame将使用slot(b0),而cbPerObject将使用slot(b1)。
如今让咱们省略全部的花括号,观察下面的代码,根据下面两种状况,问那三个未指定寄存器槽的cbuffer分别占用了哪一个slot?
cbuffer CBChangesEveryInstanceDrawing : register(b0) { ... } cbuffer CBChangesEveryObjectDrawing { ... } cbuffer CBChangesEveryFrame { ... } cbuffer CBDrawingStates { ... } cbuffer CBChangesOnResize : register(b2) { ... } cbuffer CBChangesRarely : register(b3) { ... }
答案以下:
不只是寄存器槽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的更新方式。
若是一个变量没有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类。
在调用该函数以前须要使用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
提供了大量的方法给咱们获取信息,其中咱们比较感兴趣的主要信息有:
经过方法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));
为了获取着色器程序内声明的一切给着色器使用的对象,从这个结构体入手是一种十分不错的选择。咱们将使用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便可,但对于常量缓冲区,咱们还须要知道其内部的成员和位于哪一段内存区域。
在经过上面提到的枚举值断定出来是常量缓冲区后,咱们就能够经过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
,咱们就能够肯定查询变量的次数。
虽然有点想吐槽,常量缓冲区里面存的是变量这个说法,但仍是得这样来看待:常量缓冲区内的数据是能够改变的,可是在着色器运行的时候,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;
其中前三个参数是咱们须要的,由此咱们就能够构建出根据变量名来设置值和获取值的一套方案。
讲到这里其实已经知足了咱们构建一个最小特效管理类的需求。但你若是想要得到更详细的变量信息,则能够继续往下读,这里只会粗略讲述。
如今咱们已经得到了一个着色器变量的反射,那么能够经过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框架时,你须要考虑这些问题:
由于不一样的引擎对此需求可能有所不一样,这取决于你怎么去设计。
目前本人实现了一个功能尽量简化,但可以知足基本需求的EffectHelper
类。它的功能和限制以下:
本文并不打算写实现细节,整个框架源码在1500行之内,你能够观察内部实现原理。如今主要介绍如何使用。
在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()));
在建立好着色器后,咱们就能够添加渲染通道。首先要填充通道信息,结构体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); // ... };
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
定义以下:
// 渲染通道 // 非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,以及有什么问题也能够在这里汇报。