当前位置: 首页 > news >正文

利用 Direct3D 绘制几何体—7.编译着色器

在 Direct3D 中,着色器程序必须先被编译为一种可移植的字节码。接下来,图形驱动程序将获取这些字节码,并将其重新编译为针对当前系统 GPU 所优化的本地指令 [ATI1]。我们可以在运行期间用下列函数对着色器进行编译。

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

1. pFileName:我们希望编译的以 .hlsl 作为扩展名的 HLSL 源代码文件。

2. pDefines:在本书中,我们并不使用这个高级选项,因此总是将它指定为空指针。关于此参数的详细信息可参见 SDK 文档。

3. pInclude:在本书中,我们并不使用这个高级选项,因而总是将它指定为空指针。关于此参数的详细信息可详见 SDK 文档。

4. pEntrypoint:着色器的入口点函数名。一个 .hlsl 文件可能存有多个着色器程序(例如,一个顶点着色器和一个像素着色器),所以我们需要为待编译的着色器指定入口点。

5. pTarget:指定所用着色器类型和版本的字符串。在本书中,我们采用的着色器模型版本是 5.0 和 5.1。

        a) vs_5_0 与 vs_5_1:表示版本分别为 5.0 和 5.1 的顶点着色器(vertex shader)。

        b) hs_5_0 与 hs_5_1:表示版本分别为 5.0 和 5.1 的外壳着色器(hull shader)。

        c) ds_5_0 与 ds_5_1:表示版本分别为 5.0 和 5.1 的域着色器(domain shader)。

        d) gs_5_0 与 gs_5_1:表示版本分别为 5.0 和 5.1 的几何着色器(geometry shader)。

        e) ps_5_0 与 ps_5_1:表示版本分别为 5.0 和 5.1 的像素着色器(pixel shader)。

        f) cs_5_0 与 cs_5_1:表示版本分别为 5.0 和 5.1 的计算着色器(compute shader)。

6. Flags1:指示对着色器代码应当如何编译的标志。在 SDK 文档里,这些标志列出得不少,但是此书中我们仅用两种。

        a) D3DCOMPILE_DEBUG:用调试模式来编译着色器。

        b) D3DCOMPILE_SKIP_OPTIMIZATION:指示编译器跳过优化阶段(对调试很有用处)。

7. Flags2:我们不会用到处理效果文件的高级编译选项,关于它的信息请参见 SDK 文档。

8. ppCode:返回一个指向 ID3DBlob 数据结构的指针,它存储着编译好的着色器对象字节码。

9. ppErrorMsgs:返回一个指向 ID3DBlob 数据结构的指针。如果在编译过程中发生了错误,它便会储存报错的字符串。

ID3DBlob 类型描述的其实就是一段普通的内存块,这是该接口的两个方法:

        a) LPVOID GetBufferPointer:返回指向 ID3DBlob 对象中数据的 void* 类型的指针。由此可见,在使用此数据之前务必先要将它转换为适当的类型(参考下面的示例)。

        b) SIZE_T GetBufferSize:返回缓冲区的字节大小(即该对象中的数据大小)。

为了能够输出错误信息,我们在 d3dUtil.h/.cpp 文件中实现了下列辅助函数在运行时编译着色器:

// d3dUtil.cpp 第90行
ComPtr<ID3DBlob> d3dUtil::CompileShader(const std::wstring& filename,const D3D_SHADER_MACRO* defines,const std::string& entrypoint,const std::string& target)
{// 若处于调试模式,则使用调试标志UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)  compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endifHRESULT 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);// 将错误信息输出到调试窗口if(errors != nullptr)OutputDebugStringA((char*)errors->GetBufferPointer());ThrowIfFailed(hr);return byteCode;
}

以下是一个调用此函数的示例:

ComPtr<ID3DBlob> mvsByteCode = nullptr; // BoxApp.cpp 第65行
ComPtr<ID3DBlob> mpsByteCode = nullptr; // BoxApp.cpp 第66行// BoxApp.cpp 第354行
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 函数拼写错误,那么我们便会从调试窗口得到类似于下列的错误输出:

仅对着色器进行编译并不会使它与渲染流水线相绑定以供其使用。

1. 离线编译

我们不仅可以在运行期间编译着色器,还能够以单独的步骤(例如,将其作为构建整个工程过程中的一个独立环节,或是将其视为资源内容流水线(asset  content pipeline)流程的一部分)离线地(offline)编译着色器。这样做有原因若干:

1. 对于复杂的着色器来说,其编译过程可能耗时较长。因此,借助离线编译即可缩短应用程序的加载时间。

2. 以便在早于运行时的构建处理期间提前发现编译错误。

3. 对于 Windows 8 应用商店中的应用而言,必须采用离线编译这种方式。

我们通常用 .cso(即 compiled shader object,已编译的着色器对象)作为已编译着色器的扩展名。

为了以离线的方式编译着色器,我们将使用 DirectX 自带的 FXC 命令行编译工具。为了将 color.hlsl 文件中分别以 VS 和 PS 作为入口点的顶点着色器和像素着色器编译为调试版本的字节码,我们可以输入以下命令:

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 文件中分别以 VS 和 PS 作为入口点的顶点着色器和像素着色器编译为发行版本的字节码,则可以输入以下命令:

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"
参数描述
/Od禁用优化(对于调试十分有用)
/Zi开启调试信息
/T <string>着色器类型和着色器模型的版本
/E <string>着色器入口点
/Fo <string>经过编译的着色器对象字节码
/Fc <string>输出一个着色器的汇编文件清单(对于调试、检验指令数量、查阅生成的代码细节都是很有帮助的)

如果试图编译一个有语法错误的着色器,则 FXC 会将错误/警告消息输出到命令窗口。

既然已经按离线的方式把顶点着色器和像素着色器编译到 .cso 文件里,也就不需要在运行时对其进行编译(即,无须再调用 D3DCompileFromFile 方法)。但是,我们仍要将 .cso 文件中已编译好的着色器对象字节码加载到应用程序中,这可以由 C++ 的标准文件输入机制来加以实现,如:

// d3dUtil.cpp 第21行
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");

2. 生成着色器汇编代码

FXC 程序根据可选参数 /Fc 来生成可移植的着色器汇编代码。通过查阅着色器的汇编代码,既可核对着色器的指令数量,也能了解生成的代码细节——这是为了验证编译器所生成的代码与我们预想的是否一致。例如,如果我们在 HLSL 代码中写了一个条件语句,那么可能会认为汇编代码中将存在一条与之对应的分支指令。在可编程 GPU 发展的初期阶段中,在着色器里使用分支指令的代价是比较高昂的。因此,编译器时常会通过对两个分支展开求值,再对求值结果进行插值来整理条件语句,以避免采用分支指令并计算出正确的结果。例如,下列两组代码是等价的:

条件语句整理后

float x = 0;

// s == 1 (true) or s == 0 (false)

if(s)

        x = sqrt(y);

else

        x = 2*y;

float a = 2*y;

float b = sqrt(y);

float x = a + s*(b-a);

// s == 1: x = a + b - a = b = sqrt(y)

// s == 0: x = a + 0*(b - a) = a = 2*y

因此,若采用这种展开整理方法,我们将得到没有任何分支语句而效果却又与整理前相同的代码。但是,在不查阅着色器汇编代码的情况下,我们无法知道此展开过程是否发生,甚至不能验证生成的分支指令是否正确。有时,查看着色器汇编代码的目的是为了弄清它到底做了什么。下面就是一个由 color.hlsl 文件中顶点着色器生成的汇编代码示例:

//
// 生成自微软(R) HLSL着色器编译器 6.4.9844.0
//
//
// 缓冲区定义
//
// cbuffer cbPerObject
// {
//
//  float4x4 gWorldViewProj;      // 偏移量:  0 大小:  64
//
// }
//
//
// 资源绑定
//
// 名称          类型     格式    维度   槽  元素
// ------------ -------- ------ ----- --- ------- -----------  
// cbPerObject  cbuffer  NA     NA    0    1
//
//
//
// 输入签名
//
// 名称           索引        掩码   寄存器  系统值    格式      使用情况
// --------      ---------- ----- ------ -------- -------- --------- 
// POSITION      0            xyz   0      NONE     float    xyz
// COLOR         0            xyzw  1      NONE     float    xyzw
//
//
// 输出签名
//
// 名称          索引          掩码   寄存器  系统值    格式      使用情况
// --------     -----------  ----- ------ -------- -------- -------
// 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
//
// 初始化变量关系
//  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
//
#第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#第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 
// 大约使用了10个指令槽

3. 利用 Visual Studio 离线编译着色器

我们可以向工程内添加 .hlsl 文件,而 Visual Studio 会识别它们并提供编译的选项。这些在 UI 中配置的选项就是 FXC 程序的参数。在向 VS 工程中添加 HLSL 文件后,它将成为构建流程的一部分,而着色器也将会被 FXC 程序所编译。

但是,使用 VS 集成的 HLSL 工具却有一个缺点,即它只允许每个文件中仅有一个着色器程序。因此,这条限制将令顶点着色器和像素着色器不能共存于一个文件里。此外,我们有时希望以不同的预处理指令(preprocessor directives)编译同一个着色器程序,从而获取同一着色器的不同编译结果。同样地,如果使用集成的 VS 工具就不可能做到这一点,因为每输入一个 .hlsl 文件则只能输出一个 .cso 文件。


http://www.mrgr.cn/news/56597.html

相关文章:

  • 【前端】如何制作一个自己的网页(16)
  • 人工智能--数学基础
  • Vue使用OnlyOffice预览文档方案
  • Java | Leetcode Java题解之第491题非递减子序列
  • C++详解
  • Qt-界面优化控件样式设置(72)
  • OracleSQL语句 某字段重复数据只取一条
  • word中某些段落行间距无法更改
  • Java 之 Map遍历并删除的几种方法对比
  • 一种用于传感器网络的新型OPC UA PubSub协议绑定(MQTT-SN)
  • go 语言 Gin Web 框架的实现原理探究
  • Java | Leetcode Java题解之第501题二叉搜索树中的众数
  • 有什么好点子帮助更好的学习英语吗?
  • MySQL-事物隔离级别
  • C++ —— 实现一个日期类
  • 使用Mock库进行依赖注入的实用指南
  • TinyC编译器5—词法分析
  • git 下载慢
  • input标签v-model属性失效
  • 信发软件之展示excel文档——未来之窗行业应用跨平台架构
  • 图像处理学习笔记-20241021
  • Ubuntu配置FTP
  • eCAP超声波测距-ePWM电机调速
  • 影刀RPA实战:网页爬虫之我爱听评书
  • 数据结构 - 树,三探之代码实现
  • 如何看待AI技术的应用前景?