GameTech/URP 渲染实战

案例 3.6兼容 SRP Batcher入门

目标:把材质属性放进 UnityPerMaterial 常量缓冲区,让这个 Shader 能被 SRP Batcher 加速;并写一个小工具批量检查兼容性。

先简单回顾 SRP Batcher 做了什么(原理见第 2 章)。传统流程里,每画一个物体,CPU 都要把这个物体的变换矩阵和材质参数重新打包、上传给 GPU,Draw Call 一多,CPU 就耗在这些“设置状态”的工作上。SRP Batcher 的思路是:每个材质的参数常驻在 GPU 显存里的一块固定区域,只有材质被修改时才更新;每个物体的矩阵等数据也放在一块大缓冲区里。画连续多个使用同一个 Shader 变体的物体时,CPU 只需要告诉 GPU“这次用第几块材质数据、第几块物体数据”,即使它们的材质各不相同。

GPU 显存(常驻) UnityPerMaterial(每个材质一块) 材质 A_BaseMap_ST_BaseColor… 材质 B同样的布局 材质 C… UnityPerDraw(每个物体一块) 物体 1矩阵/SH… 物体 2 物体 3 一个 SRP Batch(同一 Shader 变体) Draw 物体1 → 绑定 材质A 块 + 物体1 块Draw 物体2 → 绑定 材质B 块 + 物体2 块 Draw 物体3 → 绑定 材质A 块 + 物体3 块只切换偏移,不重新上传数据
图 3-10SRP Batcher 的数据组织:材质数据和物体数据都常驻显存,绘制时只切换“用哪一块”

要做到“每个材质一块固定布局的数据”,Shader 必须满足两个条件:

  1. 所有引擎内置的逐物体变量(unity_ObjectToWorld 等)在名为 UnityPerDraw 的常量缓冲区里。这一条 URP 的 Core.hlsl(经由 UnityInput.hlsl)已经替你做好了。
  2. 所有材质属性放在名为 UnityPerMaterial 的常量缓冲区里,而且同一个 SubShader 的所有 Pass、所有变体里,这个缓冲区的内容和顺序必须完全一致。这一条要你自己保证。
Assets/Cookbook/Ch03/C03_06_SRPBatcher.shader(节选:hlslinclude)
// 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 同一个文件的原因。

几个细节:

批量检查兼容性的小工具

选中 Shader 资源时,Inspector 里会显示一行 “SRP Batcher: compatible / not compatible” 及原因。项目里 Shader 多了,一个个点很累,我们写一个菜单命令批量检查。Unity 没有公开这个查询接口,但 Shader Inspector 内部调用的两个函数可以通过反射拿到:

Assets/Cookbook/Ch03/Editor/SRPBatcherCheck.cs(节选:check)
// 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 文件夹,执行 Cookbook › Ch03 › Check SRP Batcher Compatibility,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。

图 3-1160 个立方体,每个都有自己独立的材质(颜色不同),全部使用案例 3.6 的 Shader。在 Frame Debugger 里它们会被合进少数几个 SRP Batch,而不是 60 次完整的状态设置
怎么确认真的合批了打开 Window › Analysis › Frame Debugger,展开 DrawOpaqueObjects,能看到 “SRP Batch” 条目以及每个批次包含的绘制次数;没能合批时,Frame Debugger 会写出原因(比如“与上一个绘制使用了不同的 Shader 变体”)。第 29 章会系统对比 SRP Batcher、GPU Instancing 与静态/动态合批。