ShaderLab[1] 是 Unity 的声明式着色器语言,用于实现“数据驱动”渲染。这里的数据驱动,意思是将 multi-pass,pass 内的 tags,render state setup 等信息写在 ShaderLab 中,渲染管线读取这些信息动态地执行渲染逻辑。
我自己也造了一门类 ShaderLab 语言,包含 ShaderLab 的核心功能。
这里贴一个语法展示代码,后文详述。
0. ShaderLab 作用
渲染管线(如 URP,HDRP)决定了一个渲染能力,而 ShaderLab 则能以数据驱动的方式让我们能在渲染管线内自由发挥,而不再是将 shader 的配置硬写在引擎代码中,没有灵活性可言。
ShaderLab 本质就是在底层着色语言(主要是 HLSL)的基础上,提供各种的声明信息,这些信息用于驱动渲染管线。
1. 如何给引擎加上这个功能?
- 在引擎中编写一个 Shader 类,用于记录声明信息
- 渲染管线根据声明信息进行渲染
- 写一个 ShaderLab 的编译器,用于读取文件并生成对应的 Shader 对象
2. 语言设计
首先,语言设计大致参考 ShaderLab,但有许多不同的地方。语言的设计上以当前引擎提供的功能而定,未来随时根据需求而修改。
- HLSL:unity 将 hlsl 的代码分布在各个 pass 内,并支持
#include
,因此整个 shader 的功能是由多文件决定了。我这里直接将 HLSL 用一个外部文件来实现,并在后边的Pass
中指定VS
和PS
的 entry point(函数名)。两者是基本等价的。我在示例中使用了<meta>
,了解 Unity 的 asset 机制的朋友应该不陌生,就是用于指定文件的。我引擎内部也包含类似AssetDatabase
的功能。用 meta 而不直接用相对路径的好处是与路径解耦(当然,想通过 meta 直接找到对应代码就比较麻烦了,不过简单提供一下查询工具即可)。 - RootSignature:DX12 需要明确 Root Signature,我这里将其也实现在 shader 中(日后可能会有改动,目前暂时先实现成这样),多个pass 是共用一个 Root Signature 的。
- Properties:基本同于 Unity,不过纹理的默认值可以通过
meta
指定 - Pass
- entry point:将 entry point 直接放在了 Pass 的后边(函数对应的代码都在开头的 HLSL 里)
- queue:将 Queue 作为 pass 内的一个语句而不是作为 tag
- blend:对 Blend 的语法进行了优化(
[]
是用于多 RT 的,如不写则默认 RT 0)
3. 引擎中的 Shader 类
大致如下
4. 渲染管线设计
在渲染管线的一个 pass 内,对所有物体(进行一定的裁剪过滤)进行渲染。根据 queue 分成两类(opaque 和 transparent),前者优先根据同 shader 同材质排序,后者根据视距排序。
渲染过程中,尽量减少 RootSignature 的切换(opaque 的排序方式就是为了这点)。利用 DX12 提供的反射功能自动更新 SRV,CBV 等。对一个子网格渲染时,会使用 shader 的所有 pass(会根据 pass 的 tags 进行一定的过滤,如 LightMode
),每个 pass 对应一个 PSO。
5. 编译器实现
用 ANTLR4 编写着色器语法,详情请查阅:Shader.g4
接着利用 ANTLR4 C++ 运行时和生成的 lexer,parser,visitor,实现 compiler
6. 实例
7. 总结
从全部硬编写的管线,到架构出 FrameGraph,再搞定根据 shader 类声明信息实现数据驱动渲染,最后再为 shader 类专门实现一个声明式语言。终于,渲染的架构设计上不再让我感觉 low 了。这是我第一次自己设计语言,第一次实现灵活的渲染架构。真的不容易,我每天都得为了框架设计不断思考,生怕写出一个毫无灵活性的差劲设计。
最后,有关注我的朋友应该知道,我最近一直都在写着一个新引擎,再进行一定的完善后,近期就会写一篇文章宣传一下,这里预先贴一下引擎链接。
Utopia Game Enginegithub.com参考
- ^ShaderLab https://docs.unity3d.com/Manual/SL-Shader.html