【Unity】着色器变体笔记
【Unity】着色器变体笔记
https://docs.unity.cn/2022.3/Documentation/Manual/shader-variants-and-keywords.html
着色器变体可以看成是一种增强版的静态分支功能,同时具备了静态、动态两种分支的优点,是一种非常实用的实现着色器分支的方式。
具有大量着色器变体的着色器被称作“超级着色器”,Unity 的标准着色器就是其中之一。
优缺点
- 优点:允许和动态分支一样运行时切换分支,且和静态分支一样不会影响 GPU 性能。
- 缺点:大量变体可能导致生成时间、文件大小、内存使用和加载时间增加,预热着色器也更复杂。
总结而言即“空间换时间”。
工作原理
基本使用流程
- 首先利用变体的宏指令定义一些着色器关键字,可将其作为正常的静态分支条件使用。
- Unity 会识别这些关键字,并以此的各种组合情况编译成多个小型专用着色器程序。
- 在运行期间可以开关这些着色器关键词,Unity 将使用与其条件匹配的着色器程序。
变体编译流程
可以通过以下方式确定会产生的变体数量。
- 确定图形 API 数量:每一个图形 API 都需要单独编译所有的着色器。
- 确定着色器程序数量:每一个着色器阶段,如一个顶点阶段或片段阶段,都要单独编译变体。
- 影响着色器程序的关键字:当前使用的关键字组合,每一种组合都将产生一个变体,即使着色器代码中未使用。
- 重复数据删除:变体编译完成后,Unity 会识别相同的变体,并使它们共用字节码。
关于重复数据删除
如定义了一组关键字,虽然仅在顶点阶段使用,但默认情况下片段阶段依旧会因此编译出多个变体。但由于片段阶段实际没有使用这些变体,所以实际是相同的字节码,于是 Unity 就会对其合并。
重复数据删除可减少文件大小,但依旧没法避免编译时间和运行时内存使用、加载时间的浪费。所以有条件还是要尽可能去掉不必要的变体,比如通过显式声明相关关键字仅指定着色器阶段编译。
变体匹配流程
- 优先提供与关键字组合完全匹配的变体。
- 其次提供与关键字组合中交集关键字最多的变体。
- 若交集数量一样则提供关键字组合列表中最前面的变体。
- 如果完全不匹配则提供每个关键字声明中首个关键字的组合对应的变体。
变体关键字
声明关键字
https://docs.unity.cn/2022.3/Documentation/Manual/SL-MultipleProgramVariants.html
1 |
|
如:#pragma shader_feature_local_vertex A B C
(一个仅材质球范围且开发期间可确定的顶点阶段变体 A、B、C)
-
确定编译方式
multi_compile
:永远编译所有关键字的着色器变体。shader_feature
:仅编译正在使用的关键字(一起构建的材料中启用了该关键字)的变体。dynamic_branch
:使用动态分支而非变体,关键字将被替换为统一的布尔值。
如果启用的关键字在项目开发期间就可确定,且不会在运行时修改,则应使用
shader_feature
来减少变体数量。否则应使用multi_compile
来避免变体被错误剥离。 -
确定使用范围
_local
:表明相关关键字为本地关键字。
默认情况下,关键字是全局的。全局意味相关关键字的开关都统一受一个全局静态开关影响。如果希望能单独给每个材质设置不同的关键字开关,那这些关键字应声明为本地。
-
限制着色器阶段
_vertex
:顶点着色器阶段。_fragment
:片段着色器阶段。_hull
:壳着色器阶段。_domain
:域着色器阶段。_geometry
:几何着色器阶段。_raytracing
:光线追踪着色器阶段。
Unity 默认对所有阶段都进行相关关键字的变体编译,但如果某些阶段实际没有使用该关键字就会导致 Unity 编译多余的无效变体。而显式声明着色器阶段可以避免这一点,从而帮助 Unity 优化编译时间和剥离无效变体。
-
限制着色器模型和 GPU 功能
https://docs.unity.cn/2022.3/Documentation/Manual/SL-ShaderCompileTargets.html
除了直接使用
#pragma
,还可以使用#pragma require
和#pragma target
声明关键字,从而限制仅在部分着色器模型或 GPU 功能下编译相关关键字,从而起到类似限制着色器阶段一样的优化效果。 -
创建支持禁用关键字的变体
_
:下划线是一种特殊的变体名称,用于表示不启用关键字状态。
若相关关键字支持全部不启用的情况,那应当通过声明
_
关键字表明这一点,这样 Unity 才会编译不包含相关关键字情况的变体。如果是创建单个关键字时,Unity 还会隐含自动添加_
关键字。
其他一些关键字声明指令:
-
使用快捷方式创建关键字
Unity 内置一组宏参数可以方便快速创建 Unity 内置的关键字,具体见文档。
-
删除关键字声明。
利用
#pragma skip_variants <关键字> ...
宏指令可以移除指定的关键字声明。
声明关键字分支
将关键字用于实际的代码中构建分支有两种方式:
(实测无法使用)if(<关键字>)
:使用运行时 if 函数创建分支。#if <关键字>
:使用宏函数 if 创建分支。
启用或禁用关键字
通过脚本控制
- 全局关键字通过“静态函数”或"命令缓冲区对象函数"控制:
Shader.EnableKeyword
:启用关键字。Shader.DisableKeyword
:禁用关键字。
- 本地关键字通过“材质对象函数”或“计算着色器对象函数”控制:
Material.EnableKeyword
:启用关键字。Material.DisableKeyword
:禁用关键字。
从 Unity2021 开始,还新增了Shader.keywordSpace
等更高级的关键字 API,具体见文档。
https://docs.unity.cn/2021.3/Documentation/Manual/shader-keywords-scripts.html
通过检视面板控制
通过检视面板只能控制材质中的本地关键字,具体有两种方法:
- 通过 Debug 模式直接查看或编辑材质上的 Shader Keywords 属性。
- 通过自定义 GUI 或内置标签(如
[Toggle]
)从而自动根据属性值设置关键字。
关键字限制
所有着色器最多可以使用 4,294,967,294 个全局关键字。每个着色器最多可以使用 65,534 个本地关键字。
此外当着色器总共使用了 128 个以上的关键字时,会产生少量的运行时性能损失,所以要尽可能将关键字数量保持在低水平。
变体剥离与包含
减少变体数量
由于关键词采用组合的方式定义变体,所以变体数量会随着关键词数量急剧上升,导致大幅增加文件大小、构建时间、加载时间、内存占用等性能问题。因此减少变体,确保剥离不需要的变体是非常重要的。
在关键字声明阶段剥离变体
- 尽可能使用
shader_feature
而不是multi_compile
声明变体。 - 确保没有用
multi_compile
定义未使用的着色器关键字。 - 显式指定关键字适用的着色器阶段或着色器模型、功能。
使用预处理宏按平台剥离变体
不同的平台所需的着色器变体可能不同,可借此去除那些在部分平台用不到的变体。
https://docs.unity.cn/2022.3/Documentation/Manual/SL-BuiltinMacros.html
通过控制用户的质量设置剥离变体
不同平台的用户可调节的质量设置可能不同,可借此简化一些平台的关键字声明。
通过编辑器设置剥离变体
- 通过 Graphics 项目设置的 Shader Stripping 选项剥离不必要的内置变体。
- 确保没有将无需要的 Shader 放入 Always-included shaders 选项中(这里的 Shader 将永远编译所有变体)。
- 如果部分渲染管线功能并未使用,应将其关闭,以便 Unity 剥离相关变体。
通过编辑器脚本剥离变体
可用通过自定义着色器处理回调IPreprocessShaders.OnProcessShader
等,在程序中人为控制变体剥离。
避免变体剥离
虽然剥离变体有助于优化游戏,但若错误剥离了需要的变体就会导致显示异常,所以有时也需要想办法保护变体不被剥离。
- 使用
multi_compile
而不是shader_feature
,从而保证相关变体在运行时也始终存在。 - 使用
shader_feature
创建变体应确保 Shader 和“材质”或“变体集合”一起打包,从而使 Unity 能正确判断变体使用状况。 - 将 Shader 放入到“Always-included shaders”设置中,从而使 Unity 永远编译该 Shader 的所有变体。