Unity-Shader变体与剔除
Shader 变体把某些功能选择提前到编译期,使运行时执行更专门的程序。它把部分运行时成本换成构建时间、包体和管理成本,并不是开关越多越好。
旧笔记中的剔除回调仍有参考价值,但“shader_feature 是材质级、multi_compile 是全局级”混淆了两组概念。下面以 Unity 2022.3 的模型为基础,区分变体生成、关键字作用域和构建剔除。
1. 组合如何增长
假设一个 Pass 有三个独立的二选一关键字组,候选组合是 2*2*2=8。再加一个三选一组,就变成 24。多个 Pass、图形 API 和着色阶段还会扩大编译任务。
1 | |
第一行两种状态,第二行三种状态,理论上组合为六种;是否保留到最终包内,还会受平台与剔除规则影响。局部关键字减少作用域冲突,并不会自动减少这六种组合。
2. 两个需要分开的维度
shader_feature 与 multi_compile 的重要区别在于构建时如何保留候选变体。前者允许依据使用情况剔除,后者适合需要预留运行时组合的情形,但仍可能受到项目和管线的其他剔除规则影响。
_local 决定关键字作用域与全局覆盖关系。是否由材质设置、是否允许全局关键字覆盖,不能只靠 shader_feature 这个单词判断。
旧版全局 keyword 数量上限也不能当作永久常数。文档和实现都有版本边界,升级时应重新核对;更稳定的目标是保持名字明确、组合有边界。
3. 静态与动态分支怎样取舍
光照模型、资源布局等差异很大的功能,可能适合静态变体。短小且整个 Draw 保持一致的判断,动态 Uniform 分支可能更合适。
动态分支不增加同样形式的编译组合,但也不保证免费:编译器可能展开两条路径,寄存器占用也可能受到影响。应结合生成代码和目标设备测量。
4. 先从配置减少候选
先核对构建中会用到的管线资源、画质级别和平台功能。对于 URP,不能只在当前选中的 Asset 里关闭功能,而忽略包内其他画质资源仍然需要该变体。
其次审查材质功能是否真的需要相互独立。若两个模式互斥,用同一个关键字组表达,比两个独立布尔开关更接近有效状态空间。
5. 自定义剔除应写成可审计规则
IPreprocessShaders.OnProcessShader 允许检查编译数据并删除确认不需要的组合。规则应说明 Shader、Pass、平台和不成立的功能组合,避免按模糊名字批量删掉。
1 | |
遍历列表删除时可以从后往前。实际 API 的 Keyword 构造和 ShaderCompilerData 字段需按项目版本实现;不要把旧脚本中的一批业务 Shader 名称直接迁移到新项目。
6. 收集、保留与预热不是一回事
运行时收集得到的是“测试过程中走到的组合”,不等于完整需求。脚本切换关键字、运行时创建材质和可下载内容,都可能使用场景静态分析没有见到的路径。
Shader Variant Collection 可以参与保留与预热流程,但不能代替覆盖计划。代码/字节码进入包内,与驱动完成某个图形状态下的程序准备也不是完全相同的时间点。
验收至少包含编辑器与 Player、冷启动与热运行、主要画质和图形 API。出现缺变体时,应检查剔除日志和实际关键字组合,不依赖“编辑器正常”判断发布包正确。
总结
先控制功能状态空间,再选择静态或动态表达,最后用配置和明确规则剔除。变体管理的目标是让所有需要的组合可靠存在,让不可能发生的组合不进入构建。