🎮 Unity 渲染优化完全手册:从 DrawCall 到高级渲染技术

💡 渲染优化的价值

  • 渲染管线太复杂,性能瓶颈在哪里?
  • DrawCall、OverDraw、OverShading 都是啥?
  • LOD、Shader 变体、后期处理,怎么优化?
  • 如何系统性地优化整个渲染流程?

这篇文章! 将深入解析 Unity 渲染管线的每个性能瓶颈,从 DrawCall 到后期处理,提供完整的渲染优化策略!


一、渲染优化概览

1.1 性能瓶颈图谱

1
2
3
4
5
6
7
8
9
10
11
12
渲染性能瓶颈:
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ CPU 端 │ │ GPU 端 │ │ 带宽 │ │ 显存 │ │
│ │ │ │ │ │ │ │ │ │
│ │ DrawCall│ │ OverDraw│ │ 纹理采样│ │ 资源大小 │ │
│ │ 合批 │ │ OverSha │ │ VBO上传 │ │ 缓存策略 │ │
│ │ RenderState│ ding │ │ Blit │ │ 压缩格式 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────────┘

1.2 优化重点

优化项 影响 难度
DrawCall CPU → GPU 通信开销 ⭐⭐⭐
OverDraw GPU 填充率压力 ⭐⭐⭐⭐
OverShading GPU 着色浪费 ⭐⭐⭐⭐⭐
带宽 纹理/VBO 传输 ⭐⭐⭐⭐

二、DrawCall 详解

2.1 核心概念对比

指标 定义 关系 目标
setPass Calls Shader Pass 切换次数 ≤ DrawCall 越少越好
Batches 实际绘制的批次总和 = 各项之和 越少越好
Draw Calls OpenGL glDrawElements 调用次数 ≈ Batches 越少越好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DrawCall 层级关系:
┌─────────────────────────────────────────────────────────┐
│ │
│ Batches (总绘制批次) │
│ ├── 动态合批 │
│ ├── 静态合批 │
│ ├── 字体 Mesh │
│ ├── 图片 Mesh │
│ └── 粒子系统等 │
│ │
│ DrawCalls (OpenGL 调用) ≈ Batches │
│ │
│ setPass Calls (材质切换) ≤ DrawCalls │
│ │
└─────────────────────────────────────────────────────────┘

2.2 setPass Calls

说明 细节
Pass Shader 中的 Pass 代码块
setPass 材质切换操作
目标 setPass Calls << DrawCalls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Shader 中的 Pass
Shader "Custom/Example"
{
SubShader
{
// Pass 1
Pass
{
// 渲染代码
}

// Pass 2
Pass
{
// 渲染代码
}
}
}

2.3 DrawCall 分析

1
2
3
4
5
6
7
8
9
10
11
Frame Debugger 分析:
┌─────────────────────────────────────────────────────────┐
│ │
│ 理想情况: │
│ 一个 Batches → 调用栈里只有一次 glDrawElements │
│ │
│ 问题排查: │
│ 粒子停止播放但未 Deactive → setPass 增加, DrawCall = 0 │
│ → 检查粒子系统生命周期管理 │
│ │
└─────────────────────────────────────────────────────────┘

2.4 合批问题诊断

工具 用途
Frame Debugger 查看当前帧绘制详情
BatchBreakingCause GitHub 开源项目,诊断合批失败原因
1
2
3
4
5
6
7
8
9
10
合批失败常见原因:
┌─────────────────────────────────────────────────────────┐
│ ❌ 不同材质 (Material) │
│ ❌ 不同 Shader │
│ ❌ 不同 Lightmap │
│ ❌ 不同材质属性参数 │
│ ❌ 动态光照打断合批 │
│ ❌ 距离太远超出动态合批范围 │
│ │
└─────────────────────────────────────────────────────────┘

三、RenderState 优化

3.1 RenderState 切换

版本 方法 说明
5.5 前 Material.SetPassFast 渲染状态切换
5.5+ 内置优化 自动优化
1
2
3
4
5
6
7
8
9
10
RenderState 包含:
┌─────────────────────────────────────────────────────────┐
│ │
│ • Blend (混合模式) │
│ • Depth Test (深度测试) │
│ • Depth Write (深度写入) │
│ • Culling (裁剪模式) │
│ • Stencil (模板测试) │
│ │
└─────────────────────────────────────────────────────────┘

3.2 Material Instance

问题 影响
材质参数被修改 生成 Material Instance
Instance 材质 打断合批
数量激增 setPass Calls 增加
1
2
3
4
5
6
7
// ❌ 生成 Material Instance
Renderer.material.color = Color.red; // 创建 Instance

// ✅ 使用 MaterialPropertyBlock
MaterialPropertyBlock propBlock = new MaterialPropertyBlock();
propBlock.SetColor("_Color", Color.red);
Renderer.SetPropertyBlock(propBlock);

3.3 MaterialPropertyBlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ✅ MaterialPropertyBlock 示例
public class ColoredCube : MonoBehaviour
{
public Color color = Color.white;
private MaterialPropertyBlock propBlock;
private Renderer renderer;
private int colorID;

void Start()
{
renderer = GetComponent<Renderer>();
propBlock = new MaterialPropertyBlock();
colorID = Shader.PropertyToID("_Color");
}

void Update()
{
// 不创建 Material Instance
propBlock.SetColor(colorID, color);
renderer.SetPropertyBlock(propBlock);
}
}

3.4 Texture2DArray

特性 说明
用途 相同 size/format/flags 的纹理集合
场景 大量同 PBR 材质对象
优势 降低 DrawCall
灵活性 网格可不同,通过参数区分
1
2
3
4
5
6
7
// Texture2DArray 使用示例
// 适合: 大量同种材质的怪物、树木等
// 潜力: UI、大规模场景渲染

// Shader 中采样 Texture2DArray
// uniform sampler2DArray _TextureArray;
// float4 color = tex2DARRAY(_TextureArray, float3(uv, arrayIndex));

四、填充率 (FillRate) 与 OverDraw

4.1 OverDraw 概念

1
2
3
4
5
6
7
8
9
10
11
12
OverDraw 示意:
┌─────────────────────────────────────────────────────────┐
│ │
│ 单像素被渲染多次 → 填充率浪费 │
│ │
│ 前景 UI: ████████████████████████████████████████████ │
│ 中景: ████████████████████████████ │
│ 背景: ██████████████████ │
│ │
│ 某像素可能被绘制 3 次 → OverDraw = 3x │
│ │
└─────────────────────────────────────────────────────────┘

4.2 OverDraw 检测标准

指标 定义 建议值
总填充数 单帧总填充像素 越低越好
平均填充倍数 总填充 / 分辨率 <2x
单像素最大填充数 最热点的填充次数 尽可能低

4.3 OverDraw 优化策略

场景 问题 解决方案
全屏 UI 背景场景被遮挡 Deactive 主相机
半屏 UI 部分遮挡 主场景渲染为背景 RT
Mask 组件 产生额外 OverDraw 使用 RectMask2D
空响应区 空纹理仍渲染 使用 Empty Click UI
常驻粒子 烟雾等特效 OffScreen Particle Rendering

4.4 Empty Click UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ✅ 空响应区 Graphic - 无渲染
public class EmptyGraphic : Graphic
{
protected override void OnPopulateMesh(VertexHelper vh)
{
// 不生成任何顶点 - 只响应点击,不渲染
}

public override bool Raycast(Vector2 sp, Camera eventCamera)
{
// 保留点击检测
return base.Raycast(sp, eventCamera);
}
}

4.5 OverDraw 检测

方法 说明
Scene View Overdraw 模式可视化
Shader Replacement 技术实现方式
透明层级 超过 4 层明显影响帧率
1
2
3
4
5
6
7
8
9
10
11
12
13
OverDraw 可视化:
┌─────────────────────────────────────────────────────────┐
│ │
│ Scene View → Overdraw 模式 │
│ │
│ 颜色含义: │
│ 🟦 蓝色 = 1x 绘制 (理想) │
│ 🟩 绿色 = 2x 绘制 │
│ 🟨 黄色 = 3x 绘制 │
│ 🟧 橙色 = 4x 绘制 (警告) │
│ 🟥 红色 = 5x+ 绘制 (严重) │
│ │
└─────────────────────────────────────────────────────────┘

五、OverShading (过着色)

5.1 概念解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
OverShading 原理:
┌─────────────────────────────────────────────────────────┐
│ │
│ GPU 不是单像素采样 │
│ → 使用 2x2 像素块 │
│ │
│ 狭长 Triangle 问题: │
│ ┌───┐ │
│ │ / │ ← 三角形只占 4 个像素 │
│ │/ │ │
│ └───┘ │
│ │
│ 但 GPU 仍然处理 2x2 块 = 4+ 像素 │
│ → 其他像素的着色计算被浪费 │
│ │
└─────────────────────────────────────────────────────────┘

5.2 OverShading 优化

策略 说明
找出低利用率 Mesh 线下简化
使用 LOD 实时简化网格
优化顶点密度 避免过度细分

六、LOD (Level Of Detail)

6.1 LOD 系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// LOD Group 使用示例
public class SetupLOD : MonoBehaviour
{
public GameObject[] lodLevels;

void Start()
{
LODGroup lodGroup = gameObject.AddComponent<LODGroup>();

LOD[] lods = new LOD[lodLevels.Length];
float[] screenSizes = { 0.6f, 0.3f, 0.1f }; // 屏幕占比阈值

for (int i = 0; i < lodLevels.Length; i++)
{
Renderer[] renderers = lodLevels[i].GetComponentsInChildren<Renderer>();
lods[i] = new LOD(screenSizes[i], renderers);
}

lodGroup.SetLODs(lods);
}
}

6.2 渲染密度分析

指标 定义 目标
顶点渲染密度 每单位像素的顶点数 越低越好
平均渲染像素值 模型每帧渲染的像素数 根据距离优化

七、带宽 (Bandwidth) 优化

7.1 带宽瓶颈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
带宽消耗:
┌─────────────────────────────────────────────────────────┐
│ │
│ 主要来源: │
│ • 纹理采样 │
│ • VBO 上传 │
│ • RenderTexture Blit │
│ │
│ 优化方向: │
│ • 减少纹理数量和大小 │
│ • 使用纹理压缩 │
│ • 优化 Blit 操作 │
│ │
└─────────────────────────────────────────────────────────┘

7.2 Shader 复杂度 (ALU)

优化项 说明
减少计算 简化数学运算
分支优化 避免动态分支
精度选择 使用 half/fix 精度

八、后期处理优化

8.1 后效优化策略

策略 说明
屏幕区域差异化 不同区域使用不同强度
Distort 兼容 需要 Gradient 信息
对象去分化 同一对象不同部位区分
Blit 优化 注意带宽瓶颈

8.2 Stencil Buffer

1
2
3
4
5
6
7
8
9
10
11
12
Stencil Buffer 时序:
┌─────────────────────────────────────────────────────────┐
│ │
│ OnRenderImage 执行前: │
│ → Stencil buffer 被 Clear │
│ → 依赖 Depth Buffer │
│ │
│ 使用注意: │
│ • 后处理前确保 Depth 正确 │
│ • 谨慎使用 Stencil 操作 │
│ │
└─────────────────────────────────────────────────────────┘

九、性能瓶颈定位

9.1 不透明物体渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
不透明渲染调用栈:
┌─────────────────────────────────────────────────────────┐
│ │
│ MeshRenderer.Render (非蒙皮网格) │
│ ↓ │
│ Mesh.DrawVBO (Draw Call) │
│ ↓ │
│ Material.SetPassFast (设置渲染状态) │
│ │
│ 调用次数: │
│ • 与材质数目成正比 │
│ • 与批次成正比 │
│ • 在每次 DrawCall 前执行 │
│ │
└─────────────────────────────────────────────────────────┘

9.2 不透明优化方向

因素 影响 优化重点
物体个数 正比 ⭐⭐⭐⭐⭐
材质数目 正比 ⭐⭐⭐⭐
单物体面片数 不敏感 ⭐⭐

优化公式

1
不透明渲染耗时 ≈ f(物体个数) > f(材质数目) > f(单物体面片数)

9.3 半透明物体渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
半透明渲染调用栈:
┌─────────────────────────────────────────────────────────┐
│ │
│ MeshRenderer.Render (场景半透明) │
│ ↓ │
│ Mesh.DrawVBO (Draw Call) │
│ ↓ │
│ Mesh.CreateVBO (NGUI 消耗通常在此) │
│ ↓ │
│ ParticleSystem.ScheduleGeometryJobs │
│ ↓ │
│ ParticleSystem.GeometryJobs (子线程) │
│ ↓ │
│ ParticleSystem.SubmitVBO (粒子渲染) │
│ ↓ │
│ BatchRenderer.Add (UGUI 消耗通常在此) │
│ │
└─────────────────────────────────────────────────────────┘

9.4 粒子系统优化

优化方向 目标
减少粒子系统个数 降低 ScheduleGeometryJobs 开销
减少粒子个数 降低 SubmitVBO 开销

十、优化检查清单

10.1 DrawCall 优化

  • DrawCall < 250
  • setPass Calls << DrawCalls
  • 使用 MaterialPropertyBlock
  • 避免 Material Instance
  • 合理使用动态/静态合批
  • 考虑 Texture2DArray

10.2 OverDraw 优化

  • 平均填充倍数 <2x
  • 全屏 UI 遮挡时禁用主相机
  • 使用 RectMask2D 替代 Mask
  • 使用 Empty Click UI
  • 常驻粒子使用离屏渲染

10.3 OverShading 优化

  • 简化低使用率 Mesh
  • 实现 LOD 系统
  • 优化顶点密度

10.4 带宽优化

  • 减少纹理大小
  • 使用压缩格式
  • 优化 Blit 操作
  • 降低 Shader 复杂度

十一、参考资料


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com