D3D12渲染技术之编译Shader

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxw167/article/details/82687376

很多人只知道写Shader,但是并不了解DX或者OpenGL是如何编译Shader的,我们写的Shader是一种文本文件,它可以被DX或者OpenGL读取,说明它们提供了接口编译Shader。3D引擎都与DX或者OpenGL相关的,本篇博客就给读者介绍如何编译Shader的。
在Direct3D中,必须首先将着色器程序编译为可移植字节码, 然后,图形驱动程序将采用此字节码并将其再次编译为系统GPU 的最佳本机指令。 在运行时,我们可以使用以下函数编译着色器:

HRESULT D3DCompileFromFile(
  LPCWSTR pFileName,
  const D3D_SHADER_MACRO *pDefines,
  ID3DInclude *pInclude,
  LPCSTR pEntrypoint,
  LPCSTR pTarget,
  UINT Flags1,
  UINT Flags2,
  ID3DBlob **ppCode,
  ID3DBlob **ppErrorMsgs);

介绍一下,上面接口的参数含义:
pFileName:包含我们要编译的HLSL源代码的.hlsl文件的名称。
pDefines:我们不使用的高级选项; 请参阅SDK文档。 我们总是在案例中指定null;
pInclude:我们不使用的高级选项; 请参阅SDK文档。 我们总是在案例中指定null。
pEntrypoint:着色器入口点的函数名称, .hlsl可以包含多个着色器程序(例如,一个顶点着色器和一个像素着色器),因此我们需要指定我们想要编译的特定着色器的入口点。
pTarget:一个字符串,指定我们正在使用的着色器程序类型和版本。 在本博客中,我们以5.0和5.1为目标。
a)vs_5_0和vs_5_1:分别为顶点着色器5.0和5.1。
b)hs_5_0和hs_5_1:分别为Hull着色器5.0和5.1。
c)ds_5_0和ds_5_1:域着色器5.0和5.1。
d)gs_5_0和gs_5_1:分别为几何着色器5.0和5.1。
e)ps_5_0和ps_5_1:分别为像素着色器5.0和5.1。
f)cs_5_0和cs_5_1:分别计算着色器5.0和5.1。
Flags1:标志,用于指定着色器代码的编译方式, SDK文档中列出了相当多的这些标志,但我们在本博客中使用的只有两个:
a)D3DCOMPILE_DEBUG:在调试模式下编译着色器。
b)D3DCOMPILE_SKIP_OPTIMIZATION:指示编译器跳过优化(对调试很有用)。
Flags2:我们不使用的高级效果编译选项; 请参阅SDK文档。
ppCode:返回指向存储已编译着色器对象字节码的ID3DBlob数据结构的指针。
ppErrorMsgs:返回指向ID3DBlob数据结构的指针,该数据结构存储包含编译错误的字符串(如果有)。
类型ID3DBlob只是一个通用的内存块,有两种方法:
LPVOID GetBufferPointer:返回数据的void *,因此在使用之前必须将其转换为适当的类型(请参阅下面的示例)。
SIZE_T GetBufferSize:返回缓冲区的字节大小。
为了支持错误输出,我们实现了以下帮助函数,以便在运行时在d3dUtil.h / .cpp中编译着色器:

ComPtr<ID3DBlob> d3dUtil::CompileShader(
   const std::wstring& filename,
   const D3D_SHADER_MACRO* defines,
   const std::string& entrypoint,
   const std::string& target)
{
 // Use debug flags in debug mode.
  UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG) 
  compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif

  HRESULT hr = S_OK;

  ComPtr<ID3DBlob> byteCode = nullptr;
  ComPtr<ID3DBlob> errors;
  hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE,
    entrypoint.c_str(), target.c_str(), compileFlags, 0, &byteCode, &errors);

  // Output errors to debug window.
  if(errors != nullptr)
    OutputDebugStringA((char*)errors->GetBufferPointer());

  ThrowIfFailed(hr);

  return byteCode;
  }
Here is an example of calling this function:
ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;
mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl",
  nullptr, "VS", "vs_5_0");
mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", 
  nullptr, "PS", "ps_5_0");

HLSL错误和警告将通过ppErrorMsgs参数返回, 例如,如果我们错误拼写了mul函数,那么我们将以下错误输出到调试窗口:
Shaders\color.hlsl(29,14-55): error X3004: undeclared identifier ‘mu’
编译着色器不会将其绑定到渲染管道以供使用,我们将在后面博客中看到如何做到这一点。
我们可以在单独的步骤(例如,构建步骤或作为资产内容管道流程的一部分)离线编译它们,而不是在运行时编译着色器。 有几个原因可以做到这一点:

对于复杂的着色器,编译可能需要很长时间, 因此,离线编译将使加载时间更快。

在构建过程的早期而不是在运行时,很容易看到着色器编译错误。
对编译的着色器使用.cso(已编译的着色器对象)扩展名是常见做法。

要离线编译着色器,我们使用DirectX附带的FXC工具。 这是一个命令行工具。 要分别使用入口点VS和PS编译存储在color.hlsl中的顶点和像素着色器,我们将编写:

fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /Od /Zi /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

要分别使用存储在color.hlsl中的顶点和像素着色器,我们将编写:

fxc "color.hlsl" /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"

这里写图片描述
如果尝试使用语法错误编译着色器,FXC会将错误/警告输出到命令窗口。 例如,如果我们错误地命名color.hlsl效果文件中的变量:

// Should be gWorldViewProj, not worldViewProj!
vout.PosH = mul(float4(vin.Pos, 1.0f), worldViewProj);

然后我们从首次亮相输出窗口中列出的这一个错误(顶部错误是要修复的关键错误)中得到了很多错误:

color.hlsl(29,42-54): error X3004: undeclared identifier ‘worldViewProj’
color.hlsl(29,14-55): error X3013: ‘mul’: no matching 2 parameter intrinsic function
color.hlsl(29,14-55): error X3013: Possible intrinsic functions are:
color.hlsl(29,14-55): error X3013:   mul(float|half…

在编译时获取错误消息比运行时更方便。
我们已经展示了如何将顶点和像素着色器脱机编译为.cso文件。 因此,我们不再需要在运行时执行它(即,我们不需要调用D3DCompileFromFile)。 但是,我们仍然需要将.cso文件中已编译的着色器对象字节码加载到我们的应用程序中。 这可以使用标准C ++文件输入机制完成,如下所示:

ComPtr<ID3DBlob> d3dUtil::LoadBinary(const std::wstring& filename)
{
  std::ifstream fin(filename, std::ios::binary);

  fin.seekg(0, std::ios_base::end);
  std::ifstream::pos_type size = (int)fin.tellg();
  fin.seekg(0, std::ios_base::beg);

  ComPtr<ID3DBlob> blob;
  ThrowIfFailed(D3DCreateBlob(size, blob.GetAddressOf()));

  fin.read((char*)blob->GetBufferPointer(), size);
  fin.close();

  return blob;
}
…
ComPtr<ID3DBlob> mvsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_vs.cso");
ComPtr<ID3DBlob> mpsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_ps.cso");

FXC的/ Fc可选参数生成汇编代码, 不时地查看着色器的程序集对于检查着色器指令计数以及查看正在生成的代码类型非常有用 - 有时它可能与期望的不同。 例如,如果HLSL代码中有条件语句,那么我们可能希望汇编代码中存在分支指令。 在可编程GPU的早期阶段,着色器中的分支过去很昂贵,因此有时编译器会通过评估两个分支来扁平化条件语句,然后在两者之间进行插值以选择正确的答案。 也就是说,以下代码将给出相同的答案:
这里写图片描述
因此,扁平化方法在没有任何分支的情况下给出了相同的结果,但是如果不查看汇编代码,我们就不知道是否发生了扁平化,或者是否生成了真正的分支指令。 关键是有时候你想要查看程序集以查看实际情况,以下是color.hlsl中为顶点着色器生成的装配示例:

// Generated by Microsoft (R) HLSL Shader Compiler 6.4.9844.0
//
//
// Buffer Definitions: 
//
// cbuffer cbPerObject
// {
//
//  float4x4 gWorldViewProj;      // Offset:  0 Size:  64
//
// }
//
//
// Resource Bindings:
//
// Name                 Type Format     Dim Slot Elements
// ------------------------------ ---------- ------- ----------- ---- ---------
// cbPerObject            cbuffer   NA     NA  0     1
//
//
//
// Input signature:
//
// Name         Index  Mask Register SysValue Format  Used
// -------------------- ----- ------ -------- -------- ------- ------
// POSITION         0  xyz     0   NONE  float  xyz 
// COLOR          0  xyzw    1   NONE  float  xyzw
//
//
// Output signature:
//
// Name         Index  Mask Register SysValue Format  Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_POSITION       0  xyzw    0   POS  float  xyzw
// COLOR          0  xyzw    1   NONE  float  xyzw
//
vs_5_0
dcl_globalFlags refactoringAllowed | skipOptimization
dcl_constantbuffer cb0[4], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xyzw
dcl_output_siv o0.xyzw, position
dcl_output o1.xyzw
dcl_temps 2
//
// Initial variable locations:
//  v0.x <- vin.PosL.x; v0.y <- vin.PosL.y; v0.z <- vin.PosL.z; 
//  v1.x <- vin.Color.x; v1.y <- vin.Color.y; v1.z <- vin.Color.z; v1.w <- vin.Color.w; 
//  o1.x <- <VS return value>.Color.x; 
//  o1.y <- <VS return value>.Color.y; 
//  o1.z <- <VS return value>.Color.z; 
//  o1.w <- <VS return value>.Color.w; 
//  o0.x <- <VS return value>.PosH.x; 
//  o0.y <- <VS return value>.PosH.y; 
//  o0.z <- <VS return value>.PosH.z; 
//  o0.w <- <VS return value>.PosH.w
//
#line 29 "color.hlsl"
mov r0.xyz, v0.xyzx
mov r0.w, l(1.000000)
dp4 r1.x, r0.xyzw, cb0[0].xyzw // r1.x <- vout.PosH.x
dp4 r1.y, r0.xyzw, cb0[1].xyzw // r1.y <- vout.PosH.y
dp4 r1.z, r0.xyzw, cb0[2].xyzw // r1.z <- vout.PosH.z
dp4 r1.w, r0.xyzw, cb0[3].xyzw // r1.w <- vout.PosH.w

#line 32
mov r0.xyzw, v1.xyzw // r0.x <- vout.Color.x; r0.y <- vout.Color.y;
           // r0.z <- vout.Color.z; r0.w <- vout.Color.w
mov o0.xyzw, r1.xyzw
mov o1.xyzw, r0.xyzw
ret 
// Approximately 10 instruction slots used

Visual Studio 2013具有一些用于编译着色器程序的集成支持, 可以将.hlsl文件添加到项目中,Visual Studio(VS)将识别它们并提供编译选项。 这些选项为FXC参数提供UI, 将HLSL文件添加到VS项目时,它将成为构建过程的一部分,并且着色器将使用FXC进行编译。
这里写图片描述
使用VS集成HLSL支持的一个缺点是它每个文件只支持一个着色器程序,因此,我们不能将顶点和像素着色器都存储在一个文件中。 此外,有时我们希望使用不同的预处理器指令编译相同的着色器程序,以获得着色器的不同变体。 同样,使用集成的VS支持是不可能的,因为它是每个.hlsl输入的一个.cso输出。

猜你喜欢

转载自blog.csdn.net/jxw167/article/details/82687376
今日推荐