自制游戏引擎 - Shader编译
bgfx跨平台shader方案
在学习example工程的时候,你会发现代码里导入的shader是编译过的二进制文件(*.bin)。再打开shader存放的源码目录,会发现它不是主流shader文件格式的后缀名,而是.sc(shader c file)文件。这是bgfx自制的GLSL like的shader语言,即以GLSL为语法基础,加上一些自定义的宏作为扩展和跨平台语法替换。下文简称这门bgfx shader language为BSL。
BSL和GLSL的语法区别:
- 每个shader文件开头必须用$input/$output写明输入/输出的数据类型,每一个数据类型需要写在varying.def.sc中来绑定语义
- varying.def.sc这个文件是唯一且必须提供的
- BSL的所有uniform变量均为float类型,不允许bool/int
- 单个参数的vec2/3/4构造函数被取消了,替代物是vec2/3/4_splat;2/3/4个参数的不受影响
- 构造矩阵需要使用mtxFromCols/mtxFromRows,指定行列
- mul(x, y)限定为x向量乘以y矩阵
- 更多的宏:https://github.com/bkaradzic/bgfx/blob/master/src/bgfx_shader.sh
这套方案位于tools/shaderc目录,从工程上可以分为这几块:
- 用来加自定义宏的预处理器
- fcpp : BSD开源: https://github.com/bagder/fcpp
- GLSL编译前端
- 对生成的GLSL进一步优化
- glsl-optimizer:https://github.com/aras-p/glsl-optimizer
- SPIRV是一门shader的中间语言,就像all to llvm,这是终极方案
- Vulkan使用的是SPIRV格式的字节码
- 可以感受下SPIRV之前shader跨平台是多么地低效,甚至需要HLSL转Cg转GLSL: https://zhuanlan.zhihu.com/p/25024372
- SPIRV转HLSL工具,SPRIV
- spirv-cross:https://github.com/KhronosGroup/SPIRV-Cross
- SPIRV的各种工具集
- shaderc
- bgfx shader编译工具的名字,跟 https://github.com/google/shaderc 没关系
Bgfx的Shader编译工具会在一开始从BSL编译出GLSL或者是HLSL作为初始的两种Shader语言,然后通过SPIRV转换
- OpenGL
- BSL -> GLSL
- OpenGL ES
- BSL -> GLSL ES
- Vulkan,WebGPU
- BSL -> GLSL -> SPIRIV
- DirectX
- BSL -> HLSL
- Metal
- BSL -> HLSL -> SPIRIV -> MSL
- 主机特定的渲染后端:AGC/GNM等
- BSL -> PSSL
- Nvindia的RHI
- bgfx暂未实现
Shaderc.exe是编译BSL的命令行工具,以example-01-cubes编译windows下dx11的shader为例:
1 | shaderc --type v --platform windows -p vs_4_0 -O 3 --varyingdef varying.def.sc -f vs_cubes.sc -o vs_cubes.bin |
Shaderc命令行参数的含义:
- type - 指定shader类型
- v - vertex
- f - fragment
- c - compute
- g - geometry
- d - domain
- h - hull
- 要注意vf以外的shader不是所有平台都支持的,具体需要查阅目标平台使用的图形API版本号
- platform - 指定转换将要使用编译后shader二进制的目标平台
- android
- 支持SPIRV,GLSL ES
- asm.js
- web平台,支持GLSL ES
- ios
- 支持MSL,GLSL ES
- linux
- 支持SPIRV,GLSL
- orbis
- PS平台
- osx
- 支持MSL,GLSL
- windows
- 支持HLSL,GLSL,GLSL ES,SPIRV
- android
- O - 0~3,指定优化等级
- varyingdef - 指定varying.def.sc这个绑定类型和语义的描述文件
- f - 指定输入shader文件
- o - 指定输出二进制文件
- 其他参数
- 单个参数查看信息
- v - 版本号
- h - 帮助文档
- 编译shader
- 不需要写其他信息,直接使用的参数
- verbose - 输出更多信息
- debug - 输出调试信息
- disasm - 生成反汇编文件
- Werror - 警告视为错误
- preprocess - 只做预处理,可以用来看BSL生成的GLSL长什么样
- raw - 看起来可以用来接原生的GLSL
- 先打开这个,然后在每个.sc文件头上加一行$raw就可以跳过预处理器,还有接下来的glsl优化
- profile - 指定shader model,默认是GLSL ES,控制逻辑比较多,需要再细看下
- TBD
- bin2c - 可以把bin文件嵌入到一个.h头文件里,省去磁盘IO
- i - 指定.sh(shader header file)的目录
- 不需要写其他信息,直接使用的参数
- 单个参数查看信息
目前bgfx这套方案遇到的一个问题是shader的开发效率,所有现成的HLSL或者GLSL源代码都无法直接应用,需要手动转译成BSL。比如之前提到的粒子系统需要对接别的中间件提供的shader,我们就需要把所有shader都写成是BSL,或者写一个HLSL/GLSL反向生成BSL的小工具。
我们希望实现BSL以外的其他方案能够直接使用常规的shader源代码,来减少工作量。
直接使用HLSL:
我们使用example-04-mesh这个例子来进行测试:
- 第一步是新建一个测试文件夹,把用到的.sc代码和shaderc.exe都放好位置,目录结构应该是这样子的:
- PlanATest
- common
- common.sh
- shaderlib.sh
- test
- shaderc.exe
- compile_hlsl.bat
- vs_mesh.sc
- fs_mesh.sc
- varying.def.sc
- bgfx_shader.sh
- common
- PlanATest
- 为了测试省力,先用–preprocess参数让编译器将BSL转译成HLSL,再用–raw参数让编译期跳过预处理器用HLSL代码直接生成二进制文件,我的编译脚本如下:
1
2
3
4
5
6
7set CURRENT_WORK_DIRECTORY=%~dp0
shaderc -i %CURRENT_WORK_DIRECTORY% --preprocess --type v --platform windows -p vs_4_0 -O 3 --varyingdef varying.def.sc -f vs_mesh.sc -o vs_mesh.hlsl
shaderc -i %CURRENT_WORK_DIRECTORY% --preprocess --type f --platform windows -p ps_4_0 -O 3 --varyingdef varying.def.sc -f fs_mesh.sc -o fs_mesh.hlsl
shaderc -i %CURRENT_WORK_DIRECTORY% --raw --type v --platform windows -p vs_4_0 -O 3 -f vs_mesh.hlsl -o vs_mesh_raw_hlsl.bin
shaderc -i %CURRENT_WORK_DIRECTORY% --raw --type f --platform windows -p ps_4_0 -O 3 -f fs_mesh.hlsl -o fs_mesh_raw_hlsl.bin
pause - 复制粘贴到..\bgfx\examples\runtime\shaders\dx11目录下测试,注意确认下你的渲染后端是选择了dx11,不是的话可以改一下bgfx::init的调用代码,把type强制设置为dx11,注意我这里生成的bin文件特意加了_raw_hlsl的后缀,顺带修改下加载shader名称的代码
1
2bgfx::Init init;
init.type = bgfx::RendererType::Direct3D11;1
m_program = loadProgram("vs_mesh_raw_hlsl", "fs_mesh_raw_hlsl");
- 编译执行,可以正常使用
- 第一步是新建一个测试文件夹,把用到的.sc代码和shaderc.exe都放好位置,目录结构应该是这样子的:
直接使用GLSL
- 从compile_hlsl.bat复制一份compile_glsl.bat,修改shade model参数和输入输出的文件名:
1
2
3
4
5
6
7set CURRENT_WORK_DIRECTORY=%~dp0
shaderc -i %CURRENT_WORK_DIRECTORY% --preprocess --type v --platform windows -p 150 -O 3 --varyingdef varying.def.sc -f vs_mesh.sc -o vs_mesh.glsl
shaderc -i %CURRENT_WORK_DIRECTORY% --preprocess --type f --platform windows -p 150 -O 3 --varyingdef varying.def.sc -f fs_mesh.sc -o fs_mesh.glsl
shaderc -i %CURRENT_WORK_DIRECTORY% --raw --type v --platform windows -p 150 -O 3 -f vs_mesh.glsl -o vs_mesh_raw_glsl.bin
shaderc -i %CURRENT_WORK_DIRECTORY% --raw --type f --platform windows -p 150 -O 3 -f fs_mesh.glsl -o fs_mesh_raw_glsl.bin
pause - 复制粘贴丢到..\bgfx\examples\runtime\shaders\glsl目录下测试,这里Windows系统必须主动去把bgfx::init调用强制改成OpenGL
1
2bgfx::Init init;
init.type = bgfx::RendererType::OpenGL;1
m_program = loadProgram("vs_mesh_raw_glsl", "fs_mesh_raw_glsl");
- 编译执行,可以正常使用
- 从compile_hlsl.bat复制一份compile_glsl.bat,修改shade model参数和输入输出的文件名:
更多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27static const Profile s_profiles[] =
{
{ ShadingLang::ESSL, 100, "100_es" },
{ ShadingLang::ESSL, 300, "300_es" },
{ ShadingLang::ESSL, 310, "310_es" },
{ ShadingLang::ESSL, 320, "320_es" },
{ ShadingLang::HLSL, 300, "s_3_0" },
{ ShadingLang::HLSL, 400, "s_4_0" },
{ ShadingLang::HLSL, 500, "s_5_0" },
{ ShadingLang::Metal, 1000, "metal" },
{ ShadingLang::PSSL, 1000, "pssl" },
{ ShadingLang::SpirV, 1311, "spirv13-11" },
{ ShadingLang::SpirV, 1411, "spirv14-11" },
{ ShadingLang::SpirV, 1512, "spirv15-12" },
{ ShadingLang::SpirV, 1010, "spirv10-10" },
{ ShadingLang::SpirV, 1010, "spirv" },
{ ShadingLang::GLSL, 120, "120" },
{ ShadingLang::GLSL, 130, "130" },
{ ShadingLang::GLSL, 140, "140" },
{ ShadingLang::GLSL, 150, "150" },
{ ShadingLang::GLSL, 330, "330" },
{ ShadingLang::GLSL, 400, "400" },
{ ShadingLang::GLSL, 410, "410" },
{ ShadingLang::GLSL, 420, "420" },
{ ShadingLang::GLSL, 430, "430" },
{ ShadingLang::GLSL, 440, "440" },
};
问题记录:
- 本来是打算用example-01-cubes作为例子,但是它用到了bgfx预定义的uniform变量里的u_modelViewProj这个mvp转换矩阵,这是我生成的hlsl里没有声明的,还需要额外把这些预定义uniforms的声明文件也找出来加进去,这是个很好解决的小问题。
- 上述试验使用的是没有经过编译的glsl或者hlsl直接传进去用,可以再试一下拿glslang或者dxc去编译完再丢进去
- 直接使用SPIRV字节码还需要看一下怎么传
- bgfx对于DirectX12使用的shader同DirectX11,没有走DXIL(DirectX Intermediate Language)
其他的shader跨平台方案
- Microsoft/ShaderConductor - https://github.com/Microsoft/ShaderConductor
- HLSL通过DirectXShaderCompiler(https://github.com/Microsoft/DirectXShaderCompiler)生成DXIL支持DirectX 12,生成SPIRV直接供Vulkan使用,其他后端需要传递给SPIRV-Cross,来生成GLSL支持OpenGL,生成GLSL ES支持OpenGL ES,生成MSL支持Metal,生成HLSL支持DirectX9/10/11。相比于bgfx的方案,DirectX12的支持更好了,另外就是DirectXShaderCompiler跟glslang + glsl-optimizer的优劣对比,这里先不继续展开。
参考资料
- bgfx doc - https://bkaradzic.github.io/bgfx/tools.html#shader-compiler-shaderc
- GLSL support in bgfx? - https://github.com/bkaradzic/bgfx/issues/213
- Shader交叉编译之梦 - https://zhuanlan.zhihu.com/p/49069689
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.