案例 3.6兼容 SRP Batcher入门
目标:把材质属性放进 UnityPerMaterial 常量缓冲区,让这个 Shader 能被 SRP Batcher 加速;并写一个小工具批量检查兼容性。
先简单回顾 SRP Batcher 做了什么(原理见第 2 章)。传统流程里,每画一个物体,CPU 都要把这个物体的变换矩阵和材质参数重新打包、上传给 GPU,Draw Call 一多,CPU 就耗在这些“设置状态”的工作上。SRP Batcher 的思路是:每个材质的参数常驻在 GPU 显存里的一块固定区域,只有材质被修改时才更新;每个物体的矩阵等数据也放在一块大缓冲区里。画连续多个使用同一个 Shader 变体的物体时,CPU 只需要告诉 GPU“这次用第几块材质数据、第几块物体数据”,即使它们的材质各不相同。
要做到“每个材质一块固定布局的数据”,Shader 必须满足两个条件:
- 所有引擎内置的逐物体变量(
unity_ObjectToWorld等)在名为UnityPerDraw的常量缓冲区里。这一条 URP 的Core.hlsl(经由UnityInput.hlsl)已经替你做好了。 - 所有材质属性放在名为
UnityPerMaterial的常量缓冲区里,而且同一个 SubShader 的所有 Pass、所有变体里,这个缓冲区的内容和顺序必须完全一致。这一条要你自己保证。
// HLSLINCLUDE 里的代码会被自动拼到本 SubShader 每个 Pass 的 HLSLPROGRAM 前面。 // 把 CBUFFER 写在这里,所有 Pass 的 UnityPerMaterial 布局天然一致。 HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Cutoff; CBUFFER_END // 纹理和采样器不是“数值”,不能也不需要放进 CBUFFER TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); ENDHLSL
// GLSLINCLUDE(对应 HLSLINCLUDE)里的代码会被自动拼到本 SubShader 每个 Pass 的 GLSLPROGRAM 前面。 // 把 CBUFFER 写在这里,所有 Pass 的 UnityPerMaterial 布局天然一致。 GLSLINCLUDE // CBUFFER_START(UnityPerMaterial) uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform float _Cutoff; // CBUFFER_END // 纹理和采样器不是“数值”,不能也不需要放进 CBUFFER uniform sampler2D _BaseMap; ENDGLSL
这里用了一个 ShaderLab 特性:写在 SubShader 里、Pass 外面的 HLSLINCLUDE … ENDHLSL,会被自动拼接到这个 SubShader 每一个 Pass 的 HLSLPROGRAM 之前。把 #include、CBUFFER 和贴图声明放在这里,后面加 ShadowCaster、DepthOnly Pass 时,“所有 Pass 布局一致”就自动成立了。这也是 URP 自带 Shader 把属性声明集中写在 LitInput.hlsl、UnlitInput.hlsl 里,然后每个 Pass 都 include 同一个文件的原因。
几个细节:
- 贴图与采样器不进 CBUFFER。它们不是数值,绑定方式不同。但贴图的
_ST、_TexelSize是float4数值,要放进去。 - CBUFFER 里不要写
#if。哪怕某个属性只在某个关键字开启时才用到,也要无条件声明,否则不同变体的缓冲区大小不同,SRP Batcher 就失效了。 - 全局变量不进 CBUFFER。用
Shader.SetGlobalXXX设置的变量(如某个全局风场强度)应该声明在 CBUFFER 外面,并且不出现在 Properties 里。
批量检查兼容性的小工具
选中 Shader 资源时,Inspector 里会显示一行 “SRP Batcher: compatible / not compatible” 及原因。项目里 Shader 多了,一个个点很累,我们写一个菜单命令批量检查。Unity 没有公开这个查询接口,但 Shader Inspector 内部调用的两个函数可以通过反射拿到:
// Unity 没有公开这两个函数,Shader Inspector 内部就是调用它们显示 “SRP Batcher: compatible / not compatible” static readonly MethodInfo s_GetCode = typeof(ShaderUtil).GetMethod("GetSRPBatcherCompatibilityCode", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); static readonly MethodInfo s_GetReason = typeof(ShaderUtil).GetMethod("GetSRPBatcherCompatibilityIssueReason", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); /// 返回 null 表示兼容,否则返回不兼容的原因 public static string Check(Shader shader, int subShader = 0) { if (s_GetCode == null || s_GetReason == null) return "(当前 Unity 版本找不到内部 API)"; int code = (int)s_GetCode.Invoke(null, new object[] { shader, subShader }); if (code == 0) return null; return (string)s_GetReason.Invoke(null, new object[] { shader, subShader, code }); }
在 Project 窗口选中 Ch03 文件夹,执行 ,Console 里就会逐个打印结果。下表是在 Unity 2022.3.62f3 上对本章 Shader 实际运行的输出:
| Shader | 结果 | 原因(工具原样输出) |
|---|---|---|
| SolidColor(案例 3.1) | 不兼容 | Material property is found in another cbuffer than "UnityPerMaterial" (_BaseColor) |
| Textured / VertexColor / AlphaClip / Fog | 不兼容 | 同上,括号里分别报出第一个“漏网”的属性 |
| SRPBatcher(案例 3.6)及之后所有 | 兼容 | — |
为了让你认识另外两种常见的不兼容原因,我们还临时写过两个反面例子(不在配套工程里):一个在 CBUFFER 里用 #ifdef _EXTRA_ON 包了一个属性,另一个让 ForwardPass 和 DepthOnly Pass 的 CBUFFER 少一个成员。工具给出的原因分别是:
UnityPerMaterial CBuffer inconsistent size inside a SubShader (<Unnamed Pass 0>) UnityPerMaterial CBuffer inconsistent size inside a SubShader (<Unnamed Pass 1>)
看到 “inconsistent size” 就去对比各 Pass 和各 #if 分支里的 CBUFFER。
