🎮 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 "Custom/Example" { SubShader { Pass { } 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 Renderer.material.color = Color.red; 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 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 () { propBlock.SetColor(colorID, color); renderer.SetPropertyBlock(propBlock); } }
3.4 Texture2DArray
特性
说明
用途
相同 size/format/flags 的纹理集合
场景
大量同 PBR 材质对象
优势
降低 DrawCall
灵活性
网格可不同,通过参数区分
四、填充率 (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 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 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 优化
10.2 OverDraw 优化
10.3 OverShading 优化
10.4 带宽优化
十一、参考资料
转载请注明来源 ,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com