乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

Drawcall和SetPassCall和Batching

DrawCall 是 CPU 向 GPU 发送的一次渲染指令,包含了 GPU 绘制所需的全部信息:几何数据、纹理、Shader、Buffer 等。

1
2
3
4
5
6
7
CPU 准备渲染数据

设置渲染状态(材质、Shader、纹理绑定)← SetPass Call

提交 DrawCall(告诉 GPU 画什么、怎么画)

GPU 执行光栅化、着色、输出像素

关键点:DrawCall 本身消耗资源,但准备阶段(SetPass Call)通常比 DrawCall 本身更耗性能。
开启批处理后,Unity 可以用一次 SetPass Call 复用同一材质状态,然后提交多个 DrawCall。所以降低 SetPass Call 往往比降低 DrawCall 更重要。

DrawCall(绘制调用)

每次调用 DrawMesh、DrawRenderer 等,就是一个 Draw Call。GPU 收到命令后开始渲染对应的几何体。
GPU 渲染时,同一批次的对象必须使用相同的材质和纹理。一旦纹理不同,就必须切换渲染状态,触发新的 DrawCall。

1
2
3
Image A(纹理1)→ DrawCall 1
Image B(纹理2)→ DrawCall 2(纹理不同,无法合批)
Image C(纹理1)→ 可以与 A 合批,不增加 DrawCall

SetPass Call

在 Draw Call 之前,如果需要切换渲染状态(shader、材质属性、纹理等),CPU 就要先做一次 SetPass Call 来通知 GPU 切换 Pass。
SetPass Call 才是真正的性能瓶颈,因为它涉及 shader 编译、状态切换等开销。

例如场景里有 5 个物体:

1
2
3
4
5
物体A - 材质1 (Shader: Standard, 纹理: tex_a)
物体B - 材质1 (同上)
物体C - 材质1 (同上)
物体D - 材质2 (Shader: Standard, 纹理: tex_b)
物体E - 材质3 (Shader: Unlit, 纹理: tex_c)

渲染顺序:

1
2
3
4
5
6
7
8
SetPass Call → 切换到材质1
Draw Call → 绘制物体A
Draw Call → 绘制物体B
Draw Call → 绘制物体C
SetPass Call → 切换到材质2
Draw Call → 绘制物体D
SetPass Call → 切换到材质3
Draw Call → 绘制物体E

结果:5 个 Draw Call,3 个 SetPass Call。

Pass 是什么

一个 Pass 就是 shader 里的一次完整渲染流程,包含一套完整的顶点着色器 + 片元着色器,以及对应的渲染状态(混合模式、深度测试、剔除等)。
一个材质可以有多个 Pass,每个 Pass 对同一个物体渲染一遍。

  • 具体例子:描边效果
    描边通常需要两个 Pass:第一个 Pass 画描边(把模型沿法线方向放大,只渲染背面),第二个 Pass 画正常颜色。
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
Shader "Custom/Outline"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_OutlineColor ("Outline Color", Color) = (0,0,0,1)
_OutlineWidth ("Outline Width", Float) = 0.02
}

SubShader
{
// ---- Pass 0:描边 ----
Pass
{
Name "OUTLINE"
Cull Front // 只渲染背面,正面被裁掉

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

float _OutlineWidth;
float4 _OutlineColor;

struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; };
struct v2f { float4 pos : SV_POSITION; };

v2f vert(appdata v)
{
v2f o;
// 沿法线方向把顶点往外推
float3 expanded = v.vertex.xyz + v.normal * _OutlineWidth;
o.pos = UnityObjectToClipPos(float4(expanded, 1));
return o;
}

fixed4 frag(v2f i) : SV_Target
{
return _OutlineColor; // 纯色描边
}
ENDCG
}

// ---- Pass 1:正常渲染 ----
Pass
{
Name "BASE"
Cull Back // 只渲染正面

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

float4 _Color;

struct appdata { float4 vertex : POSITION; };
struct v2f { float4 pos : SV_POSITION; };

v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
return o;
}

fixed4 frag(v2f i) : SV_Target
{
return _Color;
}
ENDCG
}
}
}

渲染这一个物体时,GPU 会走两遍:

1
2
3
4
5
SetPass Call → 激活 Pass 0 (OUTLINE),设置 Cull Front 状态
Draw Call → 用 Pass 0 的 shader 绘制物体(画出描边)

SetPass Call → 激活 Pass 1 (BASE),设置 Cull Back 状态
Draw Call → 用 Pass 1 的 shader 绘制物体(画出本体)

所以 1 个物体、1 个材质,产生了 2 个 SetPass Call + 2 个 Draw Call。

Batching

Static Batching / GPU Instancing 能合并 Draw Call,但不能合并 SetPass Call 。

1
2
3
// GPU Instancing:同一材质的100个物体(DrawCall x 100)
SetPass Call × 1 ← 只切换一次状态
Draw Call × 1 ← 合并成一次绘制(instanced)

而 Dynamic Batching 同时减少两者,因为它把多个 mesh 合并成一个,用同一个材质一次画完。

总结

  • Draw Call 多 → GPU 压力大
  • SetPass Call 多 → CPU 压力大(状态切换开销)
  • 先降 SetPass Call(合并材质、用图集),再降 Draw Call(Batching、Instancing)

Unity Stats 窗口里显示的 “SetPass Calls” 和 “Batches” 分别对应这两个指标,看这两个数比只看 Draw Call 更有参考价值。