3470 字
17 分钟

Unity XR性能优化核心知识完全指南

从CPU到GPU,从渲染到内存,深度解析Unity XR性能优化的核心知识点,涵盖Draw Call、批次处理、Overdraw、内存带宽、UI优化等关键领域。

前言#

XR(VR/AR/MR)应用对性能要求极为严苛——需要同时保证高帧率渲染、物理计算、交互系统稳定运行。稍有卡顿就会严重影响用户体验。


渲染批次基础#

Draw Call 与 SetPass#

理解渲染批次,先要搞清楚 Draw CallSetPass 的区别。

概念说明
Draw CallCPU 向 GPU 发送的一条命令,告诉 GPU “画这个网格(Mesh)“
SetPass Call统计 GPU 准备渲染状态(切换 Shader、贴图、Blend 模式等)的次数

简单比喻:

  • SetPass = 工厂里工人为下一个生产任务更换工具、调整机器设置、准备好原材料
  • Draw Call = 工人开始用这套设置实际组装产品

一个 SetPass Call 可以为一次或多次 Draw Call 服务,只要这些 Draw Call 使用完全相同的渲染状态。这就是合批(Batching)的基础。


四大批次技术#

动态批次(Dynamic Batching)#

Unity 在运行时由 CPU 自动合并满足限制条件的多个动态物体网格,合成一个大网格一次性提交给 GPU。

// 动态批次自动生效,无需手动开启
// 适用场景:大量小型相同材质物体(如场景草地、碎石)

重要:动态批次是 内置渲染管线(Build-in) 的重要优化手段。在 SRP(URP/HDRP)管线中主要采用 SRP Batcher,URP 渲染管线可以使用动态批次。

静态批次(Static Batching)#

对不移动的物体标记 Batching Static,Unity 在构建时合并相同材质的物体。

// 在 Inspector 中勾选 Batching Static
// 或使用 API 强制合并
StaticBatchingUtility.Combine(gameObject);

静态批次规则:

  • 必须勾选 Batching Static
  • 必须相同材质、相同渲染状态(Blend/ZTest/Pass)
  • 每批次最多 64,000 个顶点
  • 运行时不能改变 Transform(位置、旋转、缩放)
  • 物体不能改变网格数据

内存警告:静态批次会占用额外内存,因为合并后的网格是唯一存在的。合并物体越多,顶点越多,内存越大。

释放静态批次内存:

// 对象不再使用时,手动释放
Resources.UnloadAsset(meshData);
AssetBundle.Unload(true);

GPU Instancing#

GPU Instancing 允许 CPU 一次 Draw Call 通知 GPU 渲染 N 个相同网格但不同变换/颜色的物体。

原理对比:

技术CPU 端GPU 端
动态/静态批次合并网格,有计算和内存开销收到一个大网格
GPU Instancing只传递数据,开销小承担更多矩阵运算
// Shader 中添加支持
#pragma multi_compile_instancing
// 材质球上勾选 Enable GPU Instancing

适用场景:

  • 大量相同物体(树木、森林、岩石、子弹等)
  • 实例之间只有 Transform、颜色、UV 偏移等微小变化
  • 动态物体,但网格和材质固定,性能瓶颈在 CPU 时

不适用场景:

  • 每个物体材质/贴图不同
  • 实例数量很少(几十个以内,开销可能反而更大)
  • 大量独立网格和材质
  • SkinnedMeshRenderer(不支持)

SRP Batcher#

SRP Batcher 是 SRP(URP/HDRP) 下的 CPU 优化技术,通过将物体和材质数据缓存到 GPU,减少计算和上传开销。

核心特点:

  • 不直接减少 Draw Call 数量,但减少 CPU 到 GPU 的数据传输
  • 让同一 Shader 的不同材质对象共用已缓存的 GPU 常量缓冲
  • Draw Call 数量不变,但 CPU 渲染线程时间显著下降

注意:Frame Debugger 中显示的是 SRP Batch,而不是每个物体的单独 Draw Call。实际上 GPU 仍在分别绘制,CPU 端提交被优化了。

SRP Batcher 使用要求:

  1. Shader 必须声明 UnityPerDraw 和 UnityPerMaterial 的 Constant Buffer
  2. 对象不能使用 MaterialPropertyBlocks
  3. 对象需要是 MeshRenderer 或 SkinnedMeshRenderer

四种技术优先级#

渲染管线优先级顺序
内置渲染管线静态批次 > GPU Instancing > 动态批次 > 单独绘制
SRP 管线静态批次 > SRP Batcher > GPU Instancing > 单独绘制

注意:静态批次执行后就不会再执行 GPU Instancing,但会执行 SRP Batcher。


GPU 性能优化#

Overdraw(过度绘制)#

Overdraw 指同一像素在一帧中被重复绘制多次,上一次计算结果会被覆盖或叠加。

优化思路一:针对不透明物体#

1. 从前向后渲染

  • Unity 内部处理不透明物体已采用这种方式
  • 使用 Early-Z(提前深度测试)在片元着色器运行之前丢弃被遮挡的片元

2. 遮挡剔除(Occlusion Culling)

// Window > Rendering > Occlusion Culling
// 避免完全不可见的物体进入渲染
优化思路二:针对半透明物体#

1. 减少透明层数

  • UI 背景、HUD 尽量合并图层,少用大面积透明混合

2. Alpha Test 替代 Alpha Blend

  • 树叶、栅栏等使用 Alpha Test(Clip)替代 Alpha Blend,让深度测试能提前丢弃片元

3. 排序优化

  • 透明物体按从远到近绘制,减少不必要的重复混合

4. 特殊替代方案

  • 粒子系统远处改用广告牌效果,降低粒子数量和透明叠加
优化思路三:针对 UI 系统#

1. 图集 + 批次 — 减少 Draw Call,但避免大图集带来过多 Overdraw

2. 减少叠层 — UI 设计时避免多层全屏半透明面板

3. RectMask2D 替代 Mask

  • 普通 Mask 相当于多了一层全屏绘制,浪费填充率
  • RectMask2D 是更轻量级的矩形裁剪,不会产生额外的全屏 OverDraw

4. Stencil 遮罩

  • 复杂形状裁剪使用 Stencil(模板缓冲),需要在自定义 Shader 中实现

5. UI Renderer 独立渲染

  • 避免与 3D 场景互相叠加 OverDraw

内存带宽优化#

内存带宽 = 单位时间内,显存(VRAM)和 GPU 内部之间能传输的数据量。

GPU 架构比喻:

  • ALU = 只做加减乘除、逻辑运算的工人
  • Shader Core = 负责跑一条着色器指令的一组工人
  • SM(流式多处理器)= 一个小工厂,内有成百上千个工人
  • 显存 = 存储原料的大仓库
  • 内存带宽 = 从大仓库运原料到小工厂的输送带

优化思路:减少显存读写量,让 GPU 少搬运,多干活

1. 减少分辨率和像素相关开销#
  • 降低渲染分辨率(动态分辨率、注视点渲染)
  • 降低抗锯齿采样
  • 减少 OverDraw
2. 优化纹理相关开销#
  • 使用 Mipmap:远处物体采样低分辨率贴图,减少带宽浪费
  • 压缩纹理格式:ASTC/ETC2 等压缩格式显著减少带宽占用
  • 贴图合并:把粗糙度、金属度纹理合并,减少采样次数
  • 合理分辨率:角色贴图、UI 纹理尽量使用满足需求的最小分辨率
3. 提高缓存利用率#
  • 避免 Shader 中随机纹理采样,尽量连续访问
  • 减少冗余读取,能一次采样合并的就合并
  • 前向渲染排序让 Early-Z 提前丢掉被挡片元
4. 减少不必要的数据存储#
  • 避免冗余拷贝(同一贴图拷贝到多个 RT)
  • 利用 GPU 共享内存缓存重复计算的数据
  • 压缩 G-Buffer(延迟渲染阶段存储几何信息的缓冲区)
5. 优化 RenderTarget#
  • 使用低精度格式
  • 屏幕后处理效果用 1/2 或 1/4 分辨率处理后放大
  • 多个后处理效果合并到一个 Pass,避免重复拷贝

网格优化#

顶点与片元基础#

  • 顶点 = 3D 模型的”钢筋骨架”,决定模型形状
  • 片元 = 光栅化阶段产生的候选像素点,决定屏幕上每个像素的最终颜色

GPU 渲染流程:先有骨架(顶点),再填满砖块(片元),最后变成完整画面。

顶点优化思路#

从本质上减少顶点以及顶点相关的计算

  1. 降低顶点数量

    • LOD(Level of Detail)
    • 网格简化
    • 使用 Impostor(公告牌替代品)优化远距离物体
  2. Billboard(广告牌)

    • 多角度烘焙到球面或半球面纹理图集
    • Octahedral Impostor 优化
  3. 蒙皮优化

    • 减少骨骼数量、减少顶点权重、合并骨骼等
  4. 顶点动画优化

    • 把复杂的程序形变改为 VAT(顶点动画纹理)
  5. 顶点着色器优化

    • 减少 v2f 里插值数量(TEXCOORD 通道)
    • 移动端着色器中数值类型尽量用 half
    • 避免大量 sin/cos/pow,用 x*x 替代 pow(x,2.0)
    • 预计算复杂函数结果到纹理
  6. 合批处理

  7. 剔除先于变化 — 使用遮挡剔除、视锥剔除

片元优化思路#

从本质上减少片元以及片元相关的计算

  1. 降低分辨率(动态分辨率)
  2. 减少全屏 Pass
  3. 避免 Early-Z 失效(透明物体会导致失效)
  4. Shader 中避免 if/else 分支,用 step/lerp 替代

内存堆栈优化#

常见内存分配问题与解决方案#

// ❌ 错误:每帧分配字符串
void Update() {
string name = gameObject.name; // 每次都分配新字符串
}
// ✅ 正确:使用 String.Empty 或缓存
private string tempName = String.Empty;

优化建议清单:

序号优化项说明
1定期清理资源Resources.UnloadUnusedAssets()
2字符串拼接使用 StringBuilder
3配置数据使用 Scriptable Object
4标签比较使用 Gameobject.CompareTag
5协程缓存避免 new WaitForSeconds
6LINQ避免使用 LINQ 和正则表达式
7分帧执行Update 逻辑改为每 N 帧执行一次
8禁用调试日志正式发布时关闭 Debug.Log
9删除空函数删除空的 Update/Start 等
10缓存组件不要在 Update 里 GetComponent
11字符串使用 String.Empty 替代 ""
12数据结构单纯传递数据用 struct 而非 class
13对象池频繁创建对象时用缓存池

分帧执行示例#

private int interval = 3;
void Update()
{
if (Time.frameCount % interval == 0)
{
ExampleExpensiveFunction();
}
}

禁用调试日志#

public class GameLog : MonoBehaviour
{
[SerializeField] private bool logEnable = true;
private void Awake()
{
Debug.unityLogger.logEnabled = logEnable;
}
}

Shader 参数优化#

// ❌ 一般做法
material.SetFloat("_DissolveAmount", 1);
// ✅ 优化做法:使用 PropertyToID
private int dissolveAmountProperty = Shader.PropertyToID("_DissolveAmount");
private MaterialPropertyBlock propertyBlock;
void Update()
{
targetRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetFloat(dissolveAmountProperty, 1);
targetRenderer.SetPropertyBlock(propertyBlock);
}

CPU-Slicing#

将一些脚本的 Update 逻辑放在一个统一的 Update 管理器,分帧执行:

// 第一帧执行 A 脚本 Update
// 第二帧执行 B 脚本 Update
// ...

贴图与物理优化#

贴图优化#

XR 一体机建议贴图 Max Size 设为 1024

Mipmap 设置#
参数推荐值说明
MipMapON远处使用低分辨率
Filter ModeTrilinear在 mip 级别间平滑过渡
MipMap FilterKaiser过滤更清晰
Aniso Level4~8各项异性,角度越大越清晰
Wrap ModeRepeat重复模式

VR 头显推荐配置

  • MipMap: ON
  • Filter Mode: Trilinear
  • MipMap Filter: Kaiser
  • Aniso Level: 4~8
  • Wrap Mode: Repeat

UI 贴图设置:Clamp + 不需要 MipMap

注意:开启 Mipmap 的贴图会多占用 33% 显存。

物理优化#

优化项说明
减少 MeshCollider少用复杂网格碰撞体
删除无用碰撞体删除没用的碰撞器和刚体
Prebake Collision MeshesPlayer > Optimation 勾选
Reuse Collision CallbacksProject Settings > Physics 勾选
关闭可读写模型导入设置中关闭 Read/Write

UI 优化深度解析#

Canvas 重建机制#

Canvas 的本质作用:

  1. 收集子节点的 UI 元素(Graphic)信息
  2. 处理渲染顺序(Sorting、Camera、RenderMode)
  3. 管理重建,监听子物体的变动
  4. 批量生成 UI 顶点网格、更新材质信息
  5. 最终在渲染阶段合批并提交给 GPU

核心问题:一个 Canvas 统一管理它下面所有子 UI 元素,若更新其中一个子元素会让整个 Canvas 重建。

导致 Canvas 重建的操作:

  • 修改 RectTransform(位置、尺寸、角度)
  • 动态布局变化(LayoutGroup、ContentSizeFitter)
  • 频繁改变父子关系
  • 透明度变化
  • 修改材质
  • 脚本修改 Shader

动静分离策略#

方案效果
大 Canvas500 个 UI + 血条 + 按钮… → 血条变化 → 重建 500 个 UI
分离 Canvas450 个静态 UI + 小 Canvas(50 个动态血条) → 血条变化只重建 50 个

注意:Canvas 不是越大越好,合并后可能产生更多 DrawCall。

UI 优化清单#

  1. 不需要交互的取消勾选 Raycast target
  2. 移除默认的 Graphic caster
  3. 手动指定 Canvas 的 event camera
  4. 少用自适应布局组件
  5. 使用 SpriteAtlas 图集
  6. canvas.enabled = false 替代 SetActive(避免 UI 重建)
  7. 避免用 Animator 控制 UI,优先使用:
    • DoTween 插件做缓动效果
    • 协程或 Lerp 实现插值动画
    • Shader 实现动态效果(进度条流光、动态血条等)

音频优化#

音频加载类型选择#

Load Type说明适用场景
Decompress On Load加载即解压,占用内存多短音效
Compressed In Memory内存中压缩,占 CPU中等长度音效
Streaming边读边播,占用内存最少BGM 等长音频

音频优化建议#

  • Preload Audio Data:短音效勾选,长音效关闭
  • 使用 WAV 格式(未压缩,解码快)
  • 推荐压缩格式:OGG/MP3
  • 音频压缩可节省存储空间

总结:性能优化检查清单#

优化领域核心要点
渲染批次减少 Draw Call,根据管线选择合适的批次技术
GPU 优化减少 Overdraw,使用遮挡剔除,优化 Shader
内存带宽使用 Mipmap,压缩纹理格式,减少不必要的数据传输
网格优化LOD,减少三角面,使用合适的公告牌技术
内存堆栈减少堆分配,缓存组件,使用对象池,分帧执行
UI 优化动静分离,避免 Canvas 重建,使用图集
音频优化选择合适的 Load Type,使用压缩格式
贴图优化合理分辨率,启用 Mipmap,选择压缩格式
物理优化简化碰撞体,启用回调复用

工具建议:使用 Unity Profiler 和 Frame Debugger 定期检查性能瓶颈,针对性优化。


参考资源#


本文整合自 Unity XR 训练营学习资料,结合 PDF 与 Word 两份文档编写。如有疑问欢迎交流!

Unity XR性能优化核心知识完全指南
https://gzhblog.cn/posts/2026-02/unity-xr性能优化核心知识完全指南/
作者
384400
发布于
2026-02-15
许可协议
CC BY-NC-SA 4.0

这篇文章是否对你有帮助?