第21章 SRP Batcher深入

第21章 SRP Batcher深入

理论讲解

21.1 SRP Batcher工作原理

SRP Batcher(Scriptable Render Pipeline Batcher)是Unity为SRP(包括URP和HDRP)设计的渲染批处理优化系统。它通过减少CPU开销来提高渲染性能,特别是在渲染大量相似对象时效果显著。

SRP Batcher的核心机制:

  1. 材质属性批处理:将具有相同材质但不同属性的对象分组
  2. 常量缓冲区优化:减少常量缓冲区的更新频率
  3. 绘制调用合并:将多个绘制调用合并为一个

工作流程:

  1. 收集阶段:收集所有需要渲染的对象
  2. 分组阶段:根据材质和属性相似性分组
  3. 批处理阶段:将相似对象合并为单个绘制调用
  4. 渲染阶段:执行批处理后的绘制调用

性能优势:

  • 减少CPU到GPU的通信开销
  • 降低API调用次数
  • 提高GPU利用率
  • 特别适用于大量相似对象的场景

21.2 材质兼容性要求

为了使材质能够被SRP Batcher优化,必须满足特定的兼容性要求。

主要兼容性要求:

  1. CBUFFER布局一致性:所有材质必须使用相同的CBUFFER布局
  2. 纹理采样限制:纹理必须在特定位置进行采样
  3. 材质属性访问:避免在片元着色器中直接访问材质属性
  4. 着色器变体兼容性:确保着色器变体兼容

CBUFFER使用规范:

  • 使用UNITY_INSTANCING_BUFFER_STARTUNITY_INSTANCING_BUFFER_END
  • 使用UNITY_DEFINE_INSTANCED_PROP定义实例化属性
  • 避免在片元着色器中使用材质属性

不兼容的着色器特征:

  • 在片元着色器中访问材质属性
  • 使用不兼容的纹理采样模式
  • 复杂的条件分支逻辑
  • 动态分支

21.3 CBUFFER宏使用

CBUFFER(Constant Buffer)是GPU上存储常量数据的缓冲区,正确使用CBUFFER宏对于SRP Batcher兼容性至关重要。

主要CBUFFER宏:

  1. UNITY_INSTANCING_BUFFER_START(name):开始实例化缓冲区
  2. UNITY_INSTANCING_BUFFER_END:结束实例化缓冲区
  3. UNITY_DEFINE_INSTANCED_PROP(type, name):定义实例化属性
  4. UNITY_ACCESS_INSTANCED_PROP(buffer, name):访问实例化属性

CBUFFER布局要求:

  • 所有材质必须使用相同的CBUFFER结构
  • 属性顺序必须一致
  • 数据类型必须匹配
  • 对齐要求必须满足

示例CBUFFER结构:

1
2
3
4
5
UNITY_INSTANCING_BUFFER_START(PerInstance)
UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
UNITY_DEFINE_INSTANCED_PROP(float4, _UVScaleOffset)
UNITY_DEFINE_INSTANCED_PROP(float4, _EmissionColor)
UNITY_INSTANCING_BUFFER_END(PerInstance)

21.4 性能提升原理

SRP Batcher通过多种机制实现性能提升:

CPU开销减少:

  1. 减少API调用:将多个绘制调用合并
  2. 减少状态切换:减少渲染状态的切换
  3. 优化内存访问:提高缓存命中率

GPU利用率提升:

  1. 减少批处理开销:降低GPU前端处理开销
  2. 提高并行性:更好地利用GPU并行处理能力
  3. 优化内存带宽:减少不必要的数据传输

适用场景:

  • 大量相似几何体
  • 相似材质但不同属性
  • 静态和动态对象混合

21.5 调试与分析工具

Unity提供了多种工具来调试和分析SRP Batcher的性能。

Frame Debugger:

  • 检查批处理效果
  • 分析绘制调用
  • 识别批处理失败原因

Profiler工具:

  • CPU性能分析
  • 绘制调用统计
  • 内存使用情况

SRP Batcher统计信息:

  • 批处理数量
  • 绘制调用减少量
  • 性能提升百分比

21.6 常见兼容性问题

在实现SRP Batcher兼容的着色器时,常遇到以下问题:

纹理采样问题:

  • 在错误位置进行纹理采样
  • 使用不兼容的纹理采样模式
  • 动态纹理索引

材质属性访问:

  • 在片元着色器中直接访问材质属性
  • 使用不兼容的属性类型
  • 复杂的属性计算

着色器变体问题:

  • 过多的着色器变体
  • 不兼容的变体组合
  • 关键字冲突

代码示例

21.7 SRP Batcher兼容的材质系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class SRPBatcherMaterialSystem : MonoBehaviour
{
[Header("SRP Batcher Settings")]
public bool enableSRPBatcherOptimization = true;
public bool logBatcherData = true;
public bool visualizeBatching = false;

[Header("Material Configuration")]
public Material baseMaterial;
public List<Material> compatibleMaterials = new List<Material>();
public string[] shaderKeywords = new string[0];

[Header("Performance Settings")]
public float batchUpdateInterval = 0.1f;
public int maxBatchSize = 1023; // Unity限制
public bool enableDynamicBatching = true;
public bool enableInstancing = true;

private List<Renderer> m_CompatibleRenderers = new List<Renderer>();
private Dictionary<Material, List<Renderer>> m_MaterialBatches = new Dictionary<Material, List<Renderer>>();
private float m_LastBatchUpdate = 0f;

[System.Serializable]
public class BatcherAnalysisData
{
public int totalRenderers;
public int batchableRenderers;
public int nonBatchableRenderers;
public int materialBatches;
public int totalBatches;
public int potentialDrawCallsSaved;
public float batchEfficiency;
public bool isSRPBatcherCompatible;
public System.DateTime lastUpdate;
}

[System.Serializable]
public class BatcherPerformanceData
{
public int drawCallsBefore;
public int drawCallsAfter;
public int drawCallsSaved;
public float drawCallReductionPercentage;
public int batchedObjects;
public float batchingUpdateTimeMS;
public bool isOptimal;
}

public BatcherAnalysisData analysisData = new BatcherAnalysisData();
public BatcherPerformanceData performanceData = new BatcherPerformanceData();

void Start()
{
if (enableSRPBatcherOptimization)
{
InitializeBatcherSystem();
}
}

void Update()
{
if (enableSRPBatcherOptimization && Time.time - m_LastBatchUpdate >= batchUpdateInterval)
{
UpdateBatcherSystem();
m_LastBatchUpdate = Time.time;
}
}

private void InitializeBatcherSystem()
{
FindCompatibleRenderers();
AnalyzeMaterialCompatibility();

if (logBatcherData)
{
Debug.Log("[SRPBatcherMaterialSystem] Initialized batcher optimization system");
}
}

private void FindCompatibleRenderers()
{
m_CompatibleRenderers.Clear();
m_MaterialBatches.Clear();

var allRenderers = FindObjectsOfType<Renderer>();
foreach (var renderer in allRenderers)
{
if (renderer != null && renderer.enabled && IsMaterialSRPBatcherCompatible(renderer.sharedMaterials))
{
m_CompatibleRenderers.Add(renderer);

// 按材质分组
foreach (var material in renderer.sharedMaterials)
{
if (material != null)
{
if (!m_MaterialBatches.ContainsKey(material))
{
m_MaterialBatches[material] = new List<Renderer>();
}
if (!m_MaterialBatches[material].Contains(renderer))
{
m_MaterialBatches[material].Add(renderer);
}
}
}
}
}
}

private bool IsMaterialSRPBatcherCompatible(Material[] materials)
{
foreach (var material in materials)
{
if (material != null && !IsSingleMaterialSRPBatcherCompatible(material))
{
return false;
}
}
return true;
}

private bool IsSingleMaterialSRPBatcherCompatible(Material material)
{
if (material == null) return false;

// 检查Shader是否支持SRP Batcher
var shader = material.shader;
if (shader == null) return false;

// 检查是否使用了不兼容的功能
// 这里可以添加更详细的兼容性检查

// 检查材质属性数量和类型
var propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
var propertyType = ShaderUtil.GetPropertyType(shader, i);
if (propertyType == ShaderUtil.ShaderPropertyType.TexEnv)
{
// 检查纹理属性是否可能影响批处理
var propertyName = ShaderUtil.GetPropertyName(shader, i);
// 这里可以添加纹理兼容性检查
}
}

return true;
}

private void AnalyzeMaterialCompatibility()
{
analysisData.totalRenderers = FindObjectsOfType<Renderer>().Length;
analysisData.batchableRenderers = m_CompatibleRenderers.Count;
analysisData.nonBatchableRenderers = analysisData.totalRenderers - analysisData.batchableRenderers;
analysisData.materialBatches = m_MaterialBatches.Count;
analysisData.totalBatches = CalculateTotalBatches();
analysisData.potentialDrawCallsSaved = analysisData.totalRenderers - analysisData.totalBatches;
analysisData.batchEfficiency = analysisData.totalRenderers > 0 ?
(float)analysisData.potentialDrawCallsSaved / analysisData.totalRenderers : 0f;
analysisData.isSRPBatcherCompatible = analysisData.batchableRenderers > 0;
analysisData.lastUpdate = System.DateTime.Now;
}

private int CalculateTotalBatches()
{
int totalBatches = 0;
foreach (var kvp in m_MaterialBatches)
{
// 计算每个材质批次的批处理数量(考虑maxBatchSize限制)
int renderersInBatch = kvp.Value.Count;
totalBatches += Mathf.CeilToInt((float)renderersInBatch / maxBatchSize);
}
return totalBatches;
}

private void UpdateBatcherSystem()
{
FindCompatibleRenderers();
AnalyzeMaterialCompatibility();

if (logBatcherData)
{
LogBatcherPerformance();
}
}

private void LogBatcherPerformance()
{
var log = new System.Text.StringBuilder();
log.AppendLine("=== SRP Batcher Performance ===");
log.AppendLine($"Total Renderers: {analysisData.totalRenderers}");
log.AppendLine($"Batchable Renderers: {analysisData.batchableRenderers}");
log.AppendLine($"Non-Batchable Renderers: {analysisData.nonBatchableRenderers}");
log.AppendLine($"Material Batches: {analysisData.materialBatches}");
log.AppendLine($"Total Batches: {analysisData.totalBatches}");
log.AppendLine($"Potential Draw Calls Saved: {analysisData.potentialDrawCallsSaved}");
log.AppendLine($"Batch Efficiency: {analysisData.batchEfficiency:F2}");
log.AppendLine($"Is Compatible: {analysisData.isSRPBatcherCompatible}");

Debug.Log(log.ToString());
}

// 获取兼容的渲染器列表
public List<Renderer> GetCompatibleRenderers()
{
return new List<Renderer>(m_CompatibleRenderers);
}

// 获取材质批次信息
public Dictionary<Material, List<Renderer>> GetMaterialBatches()
{
return new Dictionary<Material, List<Renderer>>(m_MaterialBatches);
}

// 获取批处理分析数据
public BatcherAnalysisData GetAnalysisData()
{
return analysisData;
}

// 获取批处理性能数据
public BatcherPerformanceData GetPerformanceData()
{
return performanceData;
}

// 强制重新分析材质兼容性
public void ForceReanalyzeMaterials()
{
FindCompatibleRenderers();
AnalyzeMaterialCompatibility();
}

// 添加兼容的材质
public void AddCompatibleMaterial(Material material)
{
if (material != null && !compatibleMaterials.Contains(material))
{
compatibleMaterials.Add(material);
ForceReanalyzeMaterials();
}
}

// 移除兼容的材质
public void RemoveCompatibleMaterial(Material material)
{
if (material != null)
{
compatibleMaterials.Remove(material);
ForceReanalyzeMaterials();
}
}

// 检查材质是否兼容SRP Batcher
public bool IsMaterialCompatible(Material material)
{
return IsSingleMaterialSRPBatcherCompatible(material);
}

// 获取批处理统计信息
public string GetBatcherStats()
{
var stats = new System.Text.StringBuilder();
stats.AppendLine("=== SRP Batcher Statistics ===");
stats.AppendLine($"Total Renderers: {analysisData.totalRenderers}");
stats.AppendLine($"Batchable: {analysisData.batchableRenderers}");
stats.AppendLine($"Non-Batchable: {analysisData.nonBatchableRenderers}");
stats.AppendLine($"Material Types: {analysisData.materialBatches}");
stats.AppendLine($"Efficiency: {analysisData.batchEfficiency:F2}");
stats.AppendLine($"Potential Savings: {analysisData.potentialDrawCallsSaved} draw calls");
stats.AppendLine($"Last Update: {analysisData.lastUpdate}");

return stats.ToString();
}

// 优化材质以提高SRP Batcher兼容性
public void OptimizeMaterialForSRPBatcher(Material material)
{
if (material == null) return;

// 确保启用了GPU实例化(如果适用)
if (enableInstancing)
{
material.enableInstancing = true;
}

// 移除可能影响批处理的关键字
var keywordsToRemove = new List<string>();
foreach (var keyword in material.shaderKeywords)
{
// 这里可以根据需要移除特定的关键字
// 以提高批处理兼容性
}

foreach (var keyword in keywordsToRemove)
{
material.DisableKeyword(keyword);
}
}

// 批量优化多个材质
public void OptimizeMaterialsForSRPBatcher(List<Material> materials)
{
foreach (var material in materials)
{
OptimizeMaterialForSRPBatcher(material);
}
}

void OnDestroy()
{
m_CompatibleRenderers.Clear();
m_MaterialBatches.Clear();
}
}

21.8 SRP Batcher兼容的着色器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
// SRP Batcher兼容的Lit Shader示例
Shader "Custom/SRPBatcherCompatibleLit"
{
Properties
{
[MainTexture] _BaseMap("Base Map", 2D) = "white" {}
[MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)

_BumpMap("Normal Map", 2D) = "bump" {}
_BumpScale("Normal Scale", Range(0, 2)) = 1.0

_Metallic("Metallic", Range(0, 1)) = 0.0
_Smoothness("Smoothness", Range(0, 1)) = 0.5

// Instance-specific properties (必须使用UNITY_DEFINE_INSTANCED_PROP)
_InstanceColor("Instance Color", Color) = (1, 1, 1, 1)
_InstanceScale("Instance Scale", Vector) = (1, 1, 1, 1)
}

SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "Lit"
"IgnoreProjector" = "True"
"ShaderModel" = "4.5"
}

Pass
{
Name "ForwardLit"
Tags { "LightMode" = "UniversalForward" }

HLSLPROGRAM
#pragma target 4.5

// Material Keywords
#pragma shader_feature_local _NORMALMAP
#pragma shader_feature_local _SPECULARHIGHLIGHTS_OFF
#pragma shader_feature_local _ENVIRONMENTREFLECTIONS_OFF
#pragma shader_feature_local _SPECULAR_SETUP
#pragma shader_feature_local_fragment _RECEIVE_SHADOWS_OFF

// Universal Render Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT

// Unity defined keywords
#pragma multi_compile_fog
#pragma multi_compile_instancing // 必须启用实例化
#pragma instancing_options renderinglayer // 实例化选项

#pragma vertex LitPassVertex
#pragma fragment LitPassFragment

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

// Instance-specific CBUFFER (SRP Batcher兼容的关键)
// 所有材质实例属性必须在这里定义
UNITY_INSTANCING_BUFFER_START(PerInstance)
UNITY_DEFINE_INSTANCED_PROP(float4, _InstanceColor)
UNITY_DEFINE_INSTANCED_PROP(float3, _InstanceScale)
UNITY_INSTANCING_BUFFER_END(PerInstance)

// Material CBUFFER (保持一致的布局)
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _BumpScale;
half _Metallic;
half _Smoothness;
CBUFFER_END

// Textures (必须在全局作用域定义)
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_BumpMap); SAMPLER(sampler_BumpMap);

// Input structure
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID // 必须包含实例ID
};

// Varyings structure
struct Varyings
{
float4 positionCS : SV_POSITION;
float3 positionWS : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float4 tangentWS : TEXCOORD2;
float2 uv : TEXCOORD3;
float4 shadowCoord : TEXCOORD4;

UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};

// Vertex function
Varyings LitPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input); // 设置实例ID
UNITY_TRANSFER_INSTANCE_ID(input, output); // 传递实例ID
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

// 获取实例属性
float3 instanceScale = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _InstanceScale);

// 应用实例缩放
float3 scaledPosition = input.positionOS.xyz * instanceScale;

VertexPositionInputs vertexInput = GetVertexPositionInputs(scaledPosition);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);

output.positionCS = vertexInput.positionCS;
output.positionWS = vertexInput.positionWS;
output.normalWS = normalInput.normalWS;
output.tangentWS = half4(normalInput.tangentWS, normalInput.sign);
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);

#if defined(_MAIN_LIGHT_SHADOWS)
output.shadowCoord = TransformWorldToShadowCoord(output.positionWS);
#endif

return output;
}

// Fragment function
half4 LitPassFragment(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input); // 设置实例ID
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

// Sample base map
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);

// 获取实例颜色
half4 instanceColor = UNITY_ACCESS_INSTANCED_PROP(PerInstance, _InstanceColor);

// Combine base color with instance color and material color
half3 color = baseMap.rgb * _BaseColor.rgb * instanceColor.rgb;

// Sample and apply normal map if enabled
#ifdef _NORMALMAP
half4 normalMap = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, input.uv);
half3 normalTS = UnpackNormalScale(normalMap, _BumpScale);

// Transform normal from tangent space to world space
half3 normalWS = TransformTangentToWorld(normalTS,
half3x3(input.tangentWS.xyz,
cross(input.normalWS, input.tangentWS.xyz) * input.tangentWS.w,
input.normalWS));
normalWS = NormalizeNormalPerPixel(normalWS);
#else
half3 normalWS = input.normalWS;
#endif

// Get main light
Light mainLight = GetMainLight();

// Simple lighting calculation
half NdotL = saturate(dot(normalWS, mainLight.direction));
half3 lighting = mainLight.color * NdotL;

// Apply lighting
color *= (lighting + 0.2); // Add ambient

// Apply shadows if enabled
#if defined(_MAIN_LIGHT_SHADOWS) && !defined(_RECEIVE_SHADOWS_OFF)
half shadowAtten = MainLightShadow(input.shadowCoord);
color *= shadowAtten;
#endif

// Apply alpha
half alpha = baseMap.a * _BaseColor.a * instanceColor.a;

return half4(color, alpha);
}
ENDHLSL
}

// Shadow Caster Pass (也需要SRP Batcher兼容)
Pass
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }

ZWrite On
ZTest LEqual
ColorMask 0

HLSLPROGRAM
#pragma target 4.5

#pragma multi_compile_instancing
#pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW

#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/ShadowCaster.hlsl"

float3 _LightDirection;

// Shadow pass也需要实例化CBUFFER
UNITY_INSTANCING_BUFFER_START(PerInstanceShadow)
UNITY_DEFINE_INSTANCED_PROP(float3, _InstanceScale)
UNITY_INSTANCING_BUFFER_END(PerInstanceShadow)

struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};

Varyings ShadowPassVertex(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

// Apply instance scale in shadow pass too
float3 instanceScale = UNITY_ACCESS_INSTANCED_PROP(PerInstanceShadow, _InstanceScale);
float3 scaledPosition = input.positionOS.xyz * instanceScale;

float3 positionWS = TransformObjectToWorld(scaledPosition);
float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

output.positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection));

#if UNITY_REVERSED_Z
output.positionCS.z = min(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
output.positionCS.z = max(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif

return output;
}

half4 ShadowPassFragment(Varyings input) : SV_Target
{
return 0;
}
ENDHLSL
}
}

Fallback "Universal Render Pipeline/Lit"
}

21.9 SRP Batcher性能分析工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class SRPBatcherAnalyzer : MonoBehaviour
{
[Header("Analysis Settings")]
public bool enableAnalysis = true;
public bool logAnalysisData = true;
public float analysisUpdateInterval = 0.5f;

[Header("Performance Thresholds")]
public float minBatchEfficiency = 0.1f;
public int maxDrawCallsBeforeWarning = 1000;
public float maxAnalysisTimeMS = 16.0f; // 60 FPS budget

private float m_LastAnalysisTime = 0f;
private int m_PreviousDrawCalls = 0;
private int m_CurrentDrawCalls = 0;

[System.Serializable]
public class SRPBatcherMetrics
{
public int totalObjects;
public int batchableObjects;
public int nonBatchableObjects;
public int totalMaterials;
public int uniqueMaterials;
public int potentialBatches;
public int actualBatches;
public int drawCallsSaved;
public float batchEfficiency;
public float estimatedFPSImprovement;
public bool meetsPerformanceThresholds;
public System.DateTime lastUpdate;
}

[System.Serializable]
public class SRPBatcherRecommendations
{
public List<string> optimizationSuggestions;
public List<string> materialIssues;
public List<string> shaderIssues;
public int priorityIssuesCount;
public System.DateTime lastUpdate;
}

public SRPBatcherMetrics metrics = new SRPBatcherMetrics();
public SRPBatcherRecommendations recommendations = new SRPBatcherRecommendations();

void Start()
{
if (enableAnalysis)
{
InitializeAnalysis();
}
}

void Update()
{
if (enableAnalysis && Time.time - m_LastAnalysisTime >= analysisUpdateInterval)
{
AnalyzeSRPBatcherPerformance();
m_LastAnalysisTime = Time.time;
}
}

private void InitializeAnalysis()
{
// 初始化分析系统
AnalyzeSRPBatcherPerformance();

if (logAnalysisData)
{
Debug.Log("[SRPBatcherAnalyzer] Initialized SRP Batcher analysis system");
}
}

private void AnalyzeSRPBatcherPerformance()
{
var startTime = Time.realtimeSinceStartup;

// 收集场景数据
var allRenderers = FindObjectsOfType<Renderer>();
var allMaterials = new HashSet<Material>();
var batchableRenderers = new List<Renderer>();

foreach (var renderer in allRenderers)
{
if (renderer != null && renderer.enabled)
{
// 检查每个材质的SRP Batcher兼容性
bool isBatchable = true;
foreach (var material in renderer.sharedMaterials)
{
if (material != null)
{
allMaterials.Add(material);
if (!IsMaterialSRPBatcherCompatible(material))
{
isBatchable = false;
}
}
}

if (isBatchable)
{
batchableRenderers.Add(renderer);
}
}
}

// 计算指标
metrics.totalObjects = allRenderers.Length;
metrics.batchableObjects = batchableRenderers.Count;
metrics.nonBatchableObjects = metrics.totalObjects - metrics.batchableObjects;
metrics.totalMaterials = 0; // 总材质实例数
foreach (var renderer in allRenderers)
{
if (renderer != null && renderer.enabled)
{
metrics.totalMaterials += renderer.sharedMaterials.Length;
}
}
metrics.uniqueMaterials = allMaterials.Count;
metrics.potentialBatches = CalculatePotentialBatches(batchableRenderers);
metrics.actualBatches = CalculateActualBatches(batchableRenderers);
metrics.drawCallsSaved = metrics.batchableObjects - metrics.actualBatches;
metrics.batchEfficiency = metrics.batchableObjects > 0 ?
(float)metrics.drawCallsSaved / metrics.batchableObjects : 0f;
metrics.estimatedFPSImprovement = EstimateFPSImprovement(metrics.drawCallsSaved);
metrics.meetsPerformanceThresholds =
metrics.batchEfficiency >= minBatchEfficiency &&
metrics.actualBatches < maxDrawCallsBeforeWarning;
metrics.lastUpdate = System.DateTime.Now;

// 生成建议
GenerateRecommendations(allRenderers, allMaterials, batchableRenderers);

var analysisTime = (Time.realtimeSinceStartup - startTime) * 1000f;
if (analysisTime > maxAnalysisTimeMS && logAnalysisData)
{
Debug.LogWarning($"[SRPBatcherAnalyzer] Analysis took {analysisTime:F2}ms, exceeding threshold of {maxAnalysisTimeMS}ms");
}
}

private bool IsMaterialSRPBatcherCompatible(Material material)
{
if (material == null) return false;

// 检查Shader是否为URP兼容
var shader = material.shader;
if (shader == null) return false;

// 检查是否使用了不兼容的特性
// 这里可以添加更详细的兼容性检查逻辑

// 检查材质属性
var propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
var propertyType = ShaderUtil.GetPropertyType(shader, i);
if (propertyType == ShaderUtil.ShaderPropertyType.Float ||
propertyType == ShaderUtil.ShaderPropertyType.Vector ||
propertyType == ShaderUtil.ShaderPropertyType.Color)
{
// 检查标量、向量、颜色属性
// 这些通常与SRP Batcher兼容
}
else if (propertyType == ShaderUtil.ShaderPropertyType.TexEnv)
{
// 纹理属性需要特殊处理
// 大多数情况下是兼容的
}
}

// 检查是否启用了GPU实例化
if (!material.enableInstancing)
{
// 如果可能,建议启用实例化
}

return true;
}

private int CalculatePotentialBatches(List<Renderer> batchableRenderers)
{
// 简化的批次计算 - 实际Unity使用更复杂的算法
// 考虑maxBatchSize限制(Unity为1023)
const int maxBatchSize = 1023;
return Mathf.CeilToInt((float)batchableRenderers.Count / maxBatchSize);
}

private int CalculateActualBatches(List<Renderer> batchableRenderers)
{
// 这里简化处理,实际Unity的批处理算法更复杂
// 考虑材质、渲染状态、几何体等因素
return CalculatePotentialBatches(batchableRenderers);
}

private float EstimateFPSImprovement(int drawCallsSaved)
{
// 简化的FPS提升估算
// 实际提升取决于多种因素
if (drawCallsSaved <= 0) return 0f;

// 假设每个draw call节省0.1ms,这只是一个估算
float timeSavedMS = drawCallsSaved * 0.1f;
float currentFrameTime = 16.67f; // 假设当前60FPS
float newFrameTime = Mathf.Max(1f, currentFrameTime - timeSavedMS);
float newFPS = 1000f / newFrameTime;
float currentFPS = 1000f / currentFrameTime;

return newFPS - currentFPS;
}

private void GenerateRecommendations(
Renderer[] allRenderers,
HashSet<Material> allMaterials,
List<Renderer> batchableRenderers)
{
recommendations.optimizationSuggestions = new List<string>();
recommendations.materialIssues = new List<string>();
recommendations.shaderIssues = new List<string>();

// 检查非批处理对象
int nonBatchableCount = allRenderers.Length - batchableRenderers.Count;
if (nonBatchableCount > 0)
{
recommendations.optimizationSuggestions.Add(
$"有 {nonBatchableCount} 个对象无法批处理,考虑优化这些对象的材质或Shader");
}

// 检查材质数量
if (allMaterials.Count > 50)
{
recommendations.optimizationSuggestions.Add(
"材质数量较多,考虑合并相似材质以提高批处理效率");
}

// 检查批处理效率
if (metrics.batchEfficiency < 0.5f)
{
recommendations.optimizationSuggestions.Add(
"批处理效率较低,建议检查材质兼容性");
}

recommendations.priorityIssuesCount = recommendations.materialIssues.Count +
recommendations.shaderIssues.Count;
recommendations.lastUpdate = System.DateTime.Now;
}

// 获取SRP Batcher指标
public SRPBatcherMetrics GetMetrics()
{
return metrics;
}

// 获取优化建议
public SRPBatcherRecommendations GetRecommendations()
{
return recommendations;
}

// 检查特定材质的兼容性
public bool CheckMaterialCompatibility(Material material)
{
return IsMaterialSRPBatcherCompatible(material);
}

// 获取批处理分析报告
public string GetAnalysisReport()
{
var report = new System.Text.StringBuilder();
report.AppendLine("=== SRP Batcher Analysis Report ===");
report.AppendLine($"Total Objects: {metrics.totalObjects}");
report.AppendLine($"Batchable Objects: {metrics.batchableObjects}");
report.AppendLine($"Non-Batchable Objects: {metrics.nonBatchableObjects}");
report.AppendLine($"Total Materials: {metrics.totalMaterials}");
report.AppendLine($"Unique Materials: {metrics.uniqueMaterials}");
report.AppendLine($"Potential Batches: {metrics.potentialBatches}");
report.AppendLine($"Actual Batches: {metrics.actualBatches}");
report.AppendLine($"Draw Calls Saved: {metrics.drawCallsSaved}");
report.AppendLine($"Batch Efficiency: {metrics.batchEfficiency:F2}");
report.AppendLine($"Estimated FPS Improvement: {metrics.estimatedFPSImprovement:F1}");
report.AppendLine($"Meets Thresholds: {metrics.meetsPerformanceThresholds}");
report.AppendLine($"Last Update: {metrics.lastUpdate}");

report.AppendLine("\n=== Recommendations ===");
foreach (var suggestion in recommendations.optimizationSuggestions)
{
report.AppendLine($"• {suggestion}");
}

return report.ToString();
}

// 获取性能摘要
public string GetPerformanceSummary()
{
return $"Efficiency: {metrics.batchEfficiency:F2}, " +
$"Saved: {metrics.drawCallsSaved} DC, " +
$"FPS+: {metrics.estimatedFPSImprovement:F1}";
}

// 强制重新分析
public void ForceReanalyze()
{
AnalyzeSRPBatcherPerformance();
}

// 导出分析数据
public string ExportAnalysisData()
{
var data = new System.Text.StringBuilder();
data.AppendLine("SRP_Batcher_Analysis_Data");
data.AppendLine($"Timestamp: {System.DateTime.Now}");
data.AppendLine($"TotalObjects: {metrics.totalObjects}");
data.AppendLine($"BatchableObjects: {metrics.batchableObjects}");
data.AppendLine($"NonBatchableObjects: {metrics.nonBatchableObjects}");
data.AppendLine($"BatchEfficiency: {metrics.batchEfficiency}");
data.AppendLine($"DrawCallsSaved: {metrics.drawCallsSaved}");
data.AppendLine($"EstimatedFPSImprovement: {metrics.estimatedFPSImprovement}");

return data.ToString();
}
}

实践练习

21.10 练习1:SRP Batcher兼容性检查

目标:创建一个系统来检查材质的SRP Batcher兼容性

步骤

  1. 创建兼容性检查工具
  2. 实现材质分析功能
  3. 检测不兼容的Shader特征
  4. 生成兼容性报告
  5. 验证检查结果

检查要点

  • CBUFFER布局一致性
  • 材质属性访问方式
  • 纹理采样位置

21.11 练习2:SRP Batcher优化着色器

目标:修改现有Shader以支持SRP Batcher

步骤

  1. 分析现有Shader的兼容性
  2. 修改CBUFFER结构
  3. 更新材质属性访问
  4. 测试批处理效果
  5. 验证性能提升

优化方面

  • 统一CBUFFER布局
  • 实例化属性定义
  • 纹理采样优化

21.12 练习3:批处理性能分析

目标:创建性能分析工具

步骤

  1. 实现批处理统计功能
  2. 计算性能提升指标
  3. 生成分析报告
  4. 提供优化建议
  5. 验证分析准确性

分析维度

  • 绘制调用减少
  • CPU开销降低
  • 内存使用优化

21.13 练习4:材质批处理系统

目标:创建自动材质批处理系统

步骤

  1. 实现材质分组算法
  2. 优化材质属性
  3. 自动检测兼容性
  4. 测试批处理效果
  5. 验证性能表现

系统特性

  • 自动分组
  • 兼容性检测
  • 性能监控

21.14 练习5:综合优化方案

目标:创建完整的SRP Batcher优化方案

步骤

  1. 集成所有优化功能
  2. 实现自动化优化
  3. 提供可视化界面
  4. 测试各种场景
  5. 验证整体效果

方案特性

  • 模块化设计
  • 自动化处理
  • 性能监控

总结

第21章深入探讨了SRP Batcher的工作原理和实现方法。SRP Batcher是Unity为SRP设计的重要性能优化技术,通过减少CPU开销来提高渲染性能。

关键要点总结:

  1. 工作原理:通过批处理相似对象减少绘制调用
  2. 兼容性要求:需要统一的CBUFFER布局和正确的属性访问
  3. CBUFFER宏使用:正确使用UNITY_INSTANCING_BUFFER相关宏
  4. 性能提升:显著减少CPU开销,提高渲染效率
  5. 调试分析:使用工具监控和优化批处理效果

SRP Batcher的正确实现可以显著提升渲染性能,特别是在渲染大量相似对象的场景中。理解其工作原理和兼容性要求对于优化URP项目性能至关重要。