who:变体是什么
why:为什么会产生变体,为什么要注意变体
Shader 写逻辑时复杂的逻辑不能用 if 来处理:
GPU 按"像素块"并行执行同一指令,如 2x2 或 4x4 像素一组。组内任一像素走 true 分支,GPU 就须把 true 和 false 两条路径全跑一遍,再用掩码扔掉不需要的结果,即"分支发散"或"双轨执行"。
那就得用 #pragma multi_compile 或 #pragma shader_feature 声明 Keyword
Unity 在编译期把 Shader 拆成多份独立 GPU 程序,每份对应一组 Keyword 组合,代码里用 #ifdef 或 #if defined() 区分逻辑。
运行时通过Material.EnableKeyword切换变体,GPU拿到的是已裁剪好的、无分支判断的专用版本
how:怎么去控制变体,收集变体,剔除变体
shader关键字过多 导致的变体过多导致的AB过大
为什么需要收集shader变体:
1.收集后预加载为了优化运行卡顿问题
2.需要对shader进行无用变体剔除,为了优化内存
预热:
运行时首次使用特定变体时,Unity 需在目标平台(尤其是移动端)实时编译,导致帧率下降
预编译指定变体并缓存结果
怎么去收集变体:
方法1:
这个是相对自动的方法,使用方法是在ProjectSetting>Graphics的最下面,先Clear掉当前的记录,然后进行游戏,尽量覆盖大多数游戏内容,之后点击Save to asset保存。
也是总有跑不到的逻辑,容易漏掉某些变体
方法2:定制化变体收集工具
1.新增一个场景把所有需要打包的预制体都加到该场景中
2.收集代码里面所有运行时设置EnableKeyword的逻辑
3.在当场景运行收集变体时,依次运行所有EnableKeyword的逻辑
完整方案:
更完整的推荐方案(混合)
按投入产出,建议做成 3 层,而不是二选一:
A. 静态层(最稳、最该先做)
扫描打包范围内所有 Material / 预制体引用材质:
记录 shader + shaderKeywords + 常用 Pass
直接写入 ShaderVariantCollection
这能吃掉「资源上已经定好」的绝大部分 shader_feature
B. 模拟运行时开关层(方法2的正确形态)
收集项目里所有:
- Material Inspector + Toggle(_XXX) / KeywordEnum
Shader 里把属性绑到 keyword,Unity 保存材质时自动 开关 keyword,运行时不必写代码:
Toggle(_DISSOLVE_ON) _Dissolve("溶解开关", Float) = 0
#pragma shader_feature_local _DISSOLVE_ON
你们 QMUIEffectUberEX.shader 里大量是这样写的。在 Inspector 勾选/改枚举,底层就是改 material.shaderKeywords,等价于 Enable/Disable。
KeywordEnum(R, G, B, A) _DistortChannel 会生成 _DISTORTCHANNEL_R 等,改下拉框即可切换。 - 自定义 ShaderGUI(Editor 里间接改 keyword)
你们 QMUberEffectGUI.cs 在 Draw 面板时调用 material.EnableKeyword / DisableKeyword,本质仍是改材质 keyword,只是不用业务运行时脚本,而是 Editor 调材质时 同步。 - 直接改 material.shaderKeywords
material.shaderKeywords = new\[\] { "_DISSOLVE_ON", "_DISTORT_ON" };
和逐个 EnableKeyword/DisableKeyword 效果一样,适合一次性设组合。 - URP / Unity 管线自动设置(multi_compile 常见)
很多 非 local 的 multi_compile 不需要你手动 EnableKeyword,例如:
_MAIN_LIGHT_SHADOWS、_SHADOWS_SOFT → URP 按 URP Asset / 光源 / 阴影设置 自动设全局 keyword
INSTANCING_ON → 材质勾选 Enable GPU Instancing 且 Renderer 走 instancing 时自动生效
LIGHTMAP_ON、FOG 等 → 场景有 Lightmap、开启 Fog 时引擎设置
这类是 管线/引擎改全局 keyword,不是你在业务里 material.EnableKeyword。 - Unity 2021.2+:LocalKeyword / GlobalKeyword API
比字符串 EnableKeyword 更安全(编译期校验、少 GC):
LocalKeyword kw = new LocalKeyword(material.shader, "_DISSOLVE_ON");
material.SetKeyword(kw, true);
或全局:
Shader.EnableKeyword("_MAIN_LIGHT_SHADOWS");
仍是"设 keyword",只是 API 换了。
C. 游玩追踪层(方法1保留)
分场景/分模块多次 Clear → Play → Save
再 Merge 多个 SVC
CI 或打包前自动跑一遍「主流程冒烟」(登录、主城、战斗、探索...)
额外强烈建议:
Development Player 真机再收一轮(补 Editor 收不到的变体)
构建时用 IPreprocessShaders:只保留「合并 SVC 并集」里的变体(或反过来:白名单外剥离),避免包体失控
加载时对关键 SVC 做 WarmUp / WarmUpProgressively(防卡顿另说,和「收全」是两件事)
最终完整的变体收集方案:
1.点击工具后依次打开所有场景
2.最后进入一个空场景实例化所有会打包的预制体,并且记录预制用到的材质或者shader第三步需要用到
3.填入几个会动态变化的Keyword,需要对第二步记录的所有材质判断存不存在该Keyword,
如果存在则需要在代码里对该材质开关一遍对应Keyword,并且确保至少渲染一帧(unity才能收集到)
4.手动跑游戏收集变体
Shader变体 预热:
AssetBundle.LoadAsset("shaderVariants").WarmUp();
需要注意:
Unity 官方对 Legacy AssetBundle 的描述也是:编译某个 Bundle 的 Shader 时,使用的是"该 Bundle 内"的 Material、SVC 等信息。
意思就是shader和SVC是需要一起打包的,不然某些变体可能就会被剔除