尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Shader变体优化指南:避免爆炸性增长

Shader变体优化指南:避免爆炸性增长 一、先理解什么是 Shader 变体一个 Shader 文件 ≠ 一个 Shader 一个 Shader 文件可能编译出【几十~上万个】不同版本 为什么因为 Shader 里有开关(关键字 Keyword) #pragma multi_compile _ FOG_ON ← 开关1雾效 #pragma multi_compile _ SHADOW_ON ← 开关2阴影 每个开关有开/关两种状态 ↓ 不同开关组合 不同的实际Shader 变体变体 关键字组合2个开关(各开/关) → 2×2 4个变体 ┌──────┬──────┬─────────────────┐ │ FOG │SHADOW│ 变体 │ ├──────┼──────┼─────────────────┤ │ 关 │ 关 │ 变体1(都不要) │ │ 开 │ 关 │ 变体2(只雾) │ │ 关 │ 开 │ 变体3(只阴影) │ │ 开 │ 开 │ 变体4(都要) │ └──────┴──────┴─────────────────┘本质Shader 用关键字应对不同渲染需求有没有雾、有没有阴影、有几个光源…编译器为每种组合生成一个独立的编译版本这就是变体。二、为什么变体会爆炸组合是指数级增长变体数量 每个关键字状态数 相乘(笛卡尔积) 2个开关2×2 4 3个开关2×2×2 8 5个开关2^5 32 10个开关2^10 1024 20个开关2^20 1048576 (百万级!) ⚠️ 每加一个关键字变体数【翻倍】 → 指数爆炸现实中一个Standard Shader ├── 光照模式 ├── 阴影 ├── 雾效(多种) ├── Lightmap(多种) ├── 法线贴图开关 ├── 高光开关 ├── 自发光开关 └── ... 十几种关键字 ↓ 轻松几千~上万变体变体爆炸的两大危害危害1编译时间爆炸 每个变体都要单独编译 → 打包时Shader编译几十分钟~几小时 → 上万变体 构建噩梦 危害2内存/包体爆炸 编译后的变体都要存储和加载 → 包体变大 → 运行时Shader占内存 → 加载慢(甚至卡顿)三、核心武器三种关键字指令理解multi_compile/shader_feature/dynamic_branch的区别是关键。1. multi_compile —— 全部保留#pragma multi_compile _ FOG_ON 特点 ├── 编译出【所有】变体组合全部打进包 ├── 无论项目用不用都保留 └── 运行时可自由切换关键字 用途 └── 全局的、运行时会动态切换的功能 (如雾效开关、画质等级切换) 代价 └── 变体全保留 → 数量多 → 包大编译慢2. shader_feature —— 按需保留关键优化#pragma shader_feature _ FOG_ON 特点★核心区别★ ├── 只保留【项目材质实际用到】的变体 ├── 打包时扫描所有材质没被引用的变体【剔除】 └── 未使用的组合不会进包 用途 └── 由材质配置决定的功能 (如这个材质用不用法线贴图做好就固定了) 优势 └── 自动剔除无用变体 → 大幅减少数量 ✅对比图解假设Shader有关键字 NORMALMAP2个变体 变体A(无法线贴图) / 变体B(有法线贴图) 项目里所有材质都开了法线贴图 multi_compile ┌──────┬──────┐ │变体A │变体B │ ← 都打包(即使A没材质用) └──────┴──────┘ 浪费 shader_feature ┌──────┐ │变体B │ ← 只打包被用到的B剔除A └──────┘ 省 ✅核心原则能用 shader_feature 就别用 multi_compile。前者按需保留后者全量保留。3. dynamic_branch —— 不产生变体现代方案#pragma multi_compile _ FOG_ON → 编译期分支(生成变体) vs 运行时 if 分支(uniform变量) → 不生成变体 现代GPU支持动态分支 用一个 uniform bool 在shader里 if 判断 → 不需要编译多个变体 → 只有1份Shader运行时判断 代价 运行时每像素判断有轻微性能开销 (现代GPU代价小) 适合 开关频繁切换、变体本会爆炸的情况四、什么时候用哪个决策树 这个功能运行时需要动态切换吗 │ ├─ 否材质做好就固定 │ → shader_feature ✅ (自动剔除无用变体) │ └─ 是运行时要切换 │ ├─ 切换不频繁 变体不多 │ → multi_compile │ └─ 切换频繁 / 变体会爆炸 / 分支简单 → dynamic_branch (运行时if无变体)举例┌────────────────────┬─────────────────┐ │ 功能 │ 推荐 │ ├────────────────────┼─────────────────┤ │ 材质用不用法线贴图 │ shader_feature │ │ 材质开不开自发光 │ shader_feature │ │ 全局雾效开关 │ multi_compile │ │ 画质等级(运行时切) │ multi_compile │ │ 简单的效果开关 │ dynamic_branch │ └────────────────────┴─────────────────┘五、剔除无用变体的方法即使用了这些指令还有额外手段进一步剔除。1. Shader Variant Collection变体收集思路只打包实际会用到的变体 流程 1. 运行游戏Unity记录实际用到的变体 (Graphics Settings → 记录) 2. 保存成 Shader Variant Collection 资产 3. 打包时只包含收集到的变体 4. 预热(Warmup)这些变体避免运行时卡顿// 预热变体集合避免首次使用时编译卡顿publicShaderVariantCollectionsvc;voidStart(){svc.WarmUp();// 提前编译防卡顿}2. Strip 剔除构建时过滤Graphics Settings 里的剥离选项 ├── 剔除不用的雾效模式 ├── 剔除不用的Lightmap模式 ├── 剔除不用的实例化变体 └── ... 或用代码接口在构建时剔除 IPreprocessShaders 接口// 构建时自定义剔除变体classShaderStripper:IPreprocessShaders{publicvoidOnProcessShader(Shadershader,ShaderSnippetDatasnippet,IListShaderCompilerDatadata){// 遍历data移除不需要的变体for(intidata.Count-1;i0;i--){if(不需要这个变体){data.RemoveAt(i);// 剔除}}}}3. 减少关键字本身├── 合并功能相近的关键字 ├── 删除废弃/不用的关键字 ├── 拆分Shader(不同用途分开各自变体少) └── 用 local_keyword(局部关键字Unity 2019) → 关键字作用域限定减少全局组合六、local vs global 关键字Unity 2019 支持局部关键字 #pragma multi_compile ... → 全局关键字(占全局槽位) #pragma multi_compile_local ... → 局部关键字(只属于此Shader) 全局关键字有数量上限(如256/512个) ↓ 局部关键字不占全局槽 → 避免全局关键字耗尽 ↓ 推荐Shader私有的关键字用 _local 版本七、如何查看/诊断变体1. Shader Inspector 选中Shader → Inspector → Compile and show code → 查看变体数量 2. 编译统计 Inspector 显示 X variants → 数字过大就要警惕 3. 构建日志 打包时Console输出编译的变体数和耗时 4. Frame Debugger 看运行时实际用了哪些变体警惕信号⚠️ 单个Shader变体数 几千 ⚠️ 打包Shader编译时间 几分钟 ⚠️ Shader包体异常大 ⚠️ 首次显示某效果时卡顿(变体运行时编译)八、运行时编译卡顿问题现象某个特效/材质首次出现时 → 卡一下 原因 该变体没预编译 → 运行时现编译 → 卡顿(hitch) 解决 ├── ShaderVariantCollection.WarmUp() 预热 ├── 在加载界面提前预热常用变体 └── 确保用到的变体都被打包九、常见误区误区真相“变体越多功能越强”变体多编译慢包大内存高是负担“multi_compile更保险”全量保留浪费能用shader_feature就用“变体只影响包体”也影响编译时间、内存、加载卡顿“关键字随便加”每个关键字让变体翻倍指数爆炸“dynamic_branch有性能问题不能用”现代GPU代价小很多场景合适“打包全部变体最安全”上万变体构建噩梦要剔除十、核心要点总结Shader变体控制 1. 什么是变体 Shader关键字(开关)的不同组合 每个组合编译成独立版本 变体 2. 为什么爆炸 变体 关键字状态相乘(指数) 每加1个关键字 → 变体翻倍 → 编译慢 包大 内存高 加载卡 3. 三种关键字指令 ├── multi_compile全量保留(运行时动态切换用) ├── shader_feature按需保留★(材质固定的功能) │ → 自动剔除没被材质引用的变体 └── dynamic_branch运行时if不产生变体 4. 核心原则 能用shader_feature就别用multi_compile ★ 5. 进一步剔除 ├── ShaderVariantCollection(只打用到的预热) ├── IPreprocessShaders构建时剔除 ├── Graphics Settings剥离选项 ├── 减少/合并关键字 └── local关键字(不占全局槽) 6. 诊断 Inspector看变体数、构建日志、Frame Debugger 警惕单Shader几千变体、编译几分钟 7. 卡顿 变体运行时现编译会卡 → WarmUp预热一句话Shader 用关键字应对不同渲染需求每种关键字组合编译成一个变体变体数随关键字指数级爆炸导致编译慢、包体大、内存高。控制核心是能固定的功能用shader_feature按需保留、自动剔除无用变体运行时切换的才用multi_compile简单开关考虑dynamic_branch不产生变体再配合变体收集与构建时剔除把变体数量压到最小。
返回列表