乌啦呀哈呀哈乌啦!

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

0%

Draw Call和SetPass Call

Draw Call (绘制调用): CPU 告诉 GPU “请按照这些状态,把这个网格画出来” 的一次命令。这个命令本身有开销。Draw Call 太多,CPU 就会因为一直忙于下达指令而成为瓶颈。目标:减少 Draw Call 的总次数。
SetPass Call (状态切换调用): 在 Draw Call 之前,CPU 需要告诉 GPU 使用哪个 Shader、材质属性(颜色、贴图等)是什么。切换这些状态(特别是材质)是一个非常耗时的操作。目标:减少渲染状态的切换次数。

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(绘制调用)

DrawCall 是 CPU 向 GPU 发送的一次渲染指令,包含了 GPU 绘制所需的全部信息:几何数据、纹理、Shader、Buffer 等。
CPU 先准备好数据,并通过调用图形API接口命令GPU对指定物体进行渲染一次的操作,称为一次DrawCall。

每次调用 DrawMesh、DrawRenderer 等,就是一个 Draw Call。GPU 收到命令后开始渲染对应的几何体。

当收到一个DrawCall时,GPU会按照指令,根据渲染状态和输入的顶点信息,指定一个网格(Mesh)进行渲染,绘制材质。
GPU 渲染时,同一批次的对象必须使用相同的材质和纹理。一旦纹理不同,就必须切换渲染状态,触发新的 DrawCall。

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

为了CPU和GPU可以进行并行工作,需要一个命令缓冲区,由CPU向其中添加命令,然后由GPU从中读取命令,这样就实现了通过CPU准备数据,通知GPU进行渲染。

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。