游戏性能优化:Draw Call、LOD 与遮挡剔除
游戏性能优化核心:Draw Call、LOD 与遮挡剔除
游戏流畅度由帧率决定,而每一帧的渲染工作需要在 16 毫秒(60 FPS)或 33 毫秒(30 FPS)内完成。当场景变得复杂,渲染压力会迅速上升。理解并运用 Draw Call 优化、LOD(细节层次) 和 遮挡剔除,是每位开发者必备的性能调控技能。本教程将用通俗语言剖析三者的原理与实践,帮助你构建高效的游戏场景。
1. 理解 Draw Call:渲染的"指令次数"
1.1 什么是 Draw Call?
Draw Call 是 CPU 向 GPU 发出的一次“绘制命令”。它告诉 GPU:“用这个着色器、这个网格、这些纹理和参数,渲染一次物体。” 每次调用都伴随着状态设置、数据上传等开销。Draw Call 越多,CPU 负担越重,可能成为性能瓶颈。
关键认知:Draw Call 瓶颈通常在 CPU 端,而非 GPU。即使 GPU 渲染很快,CPU 发不出足够多的指令,帧率也无法提升。
1.2 为何 Draw Call 会影响性能?
- 状态切换开销:切换材质、Shader、纹理等状态需要消耗 CPU 周期。每切换一次,驱动层都需要重新验证和设置硬件状态。
- 批次提交开销:即使状态不变,每次 Draw Call 仍需经历 API 调用、命令缓冲写入等固定成本。
- CPU 与 GPU 的通信延迟:当 Draw Call 数量达到数千或数万时,CPU 可能花大量时间在图形指令生成上,使游戏逻辑、物理等其他系统得不到资源。
1.3 降低 Draw Call 的核心策略
1.3.1 静态批处理(Static Batching)
将场景中标记为 Static 的不动物体预先合并成一个或几个复合网格,运行时只需少量 Draw Call。适用于场景建筑、石头等永不变化的物体。
Unity 示例:勾选物体的 Static 标记,引擎会自动在 Build 阶段合并网格。
注意:合并后的网格会占用更多内存,且合并物体不能单独移动或隐藏。
1.3.2 动态批处理(Dynamic Batching)
对场景中小于一定顶点数(如 < 900 顶点)的动态物体,运行时自动合并它们为一组 Draw Call。适用于大量小物件(如金币、子弹壳)。
限制:物体需使用相同材质,且着色器属性不能包含多 Pass 等不兼容特性。
1.3.3 GPU Instancing(GPU 实例化)
将使用相同网格和材质的大量物体,通过一次 Draw Call 用 GPU 实例化渲染。每个实例可通过材质属性块(MaterialPropertyBlock)传递位置、颜色等差异数据。
适用场景:树木、草丛、批量敌人等。比动态批处理更灵活,因为不对网格做合并,内存友好。
// Shader 中需启用实例化支持
#pragma multi_compile_instancing
struct appdata { ... UNITY_VERTEX_INPUT_INSTANCE_ID ... };
1.3.4 合图(Texture Atlas)与材质合并
- 合图:将多个小纹理合并为一张大图,使不同物体共用同一材质,从而合并 Draw Call。
- 材质合并:在设计阶段就将所有修饰性平面(如墙上的海报)使用统一纹理和参数,减少材质种类。
1.3.5 利用 SRP Batcher(Unity)
在 Unity 可编程渲染管线中,使用 SRP Batcher 可大幅降低常量和材质属性的设置开销,让同一种类的多个材质也能高效渲染。只需让 Shader 兼容 SRP Batcher 即可,无需额外操作。
1.4 分析工具
- Unity:使用 Frame Debugger 逐帧查看 Draw Call 来源,定位不合理的合批。
- Unreal:使用
stat scenerendering命令查看 Mesh Draw Calls 数量,并借助 RenderDoc 深入分析。
2. LOD(细节层次):距离决定精度
2.1 LOD 原理
LOD 根据物体与摄像机的距离,动态切换不同细节级别的网格模型。近距离用高精度模型(LOD0),中距离用简化模型(LOD1),远距离用更简化的版本或甚至不渲染。
核心价值:在不被察觉的质量损失下,大幅减少渲染三角面数和顶点处理量,同时还能使用更简单的着色器或材质。
2.2 LOD 切换参数
- 切换距离:手动设置或通过屏幕相对高度百分比(如 Unity 的 Screen Relative Transition Height)控制。
- 过渡方式:硬性切换(瞬间替换)或 Dithered Crossfade(使用抖动过渡避免突变感)。
- LOD 偏差:允许在全局设置中调整 LOD 切换阈值,适应不同画质选项。
2.3 实现 LOD 的常见方式
2.3.1 手动制作 LOD 资产
在美术环节,为角色/道具创建多个细节级别的网格(如 LOD0 10,000 面,LOD1 2,000 面,LOD2 500 面)。可使用自动减面工具(如 Simplygon、Blender 的 Decimate 修改器)生成,但需手动调整保持轮廓和大形。
2.3.2 引擎内配置 LOD Group
- Unity:在预制件上添加
LOD Group组件,将不同精度网格拖入对应级别,设置切换阈值。勾选Animate Cross-fading可启用抖动淡出。 - Unreal:为骨架网格体导入多个 LOD,在编辑器 LOD 设置中调整每级的 Screen Size 百分比。支持自动生成 LOD 工具。
2.3.3 材质 LOD
除了几何体,也可以为远距离物体切换更廉价的着色器。例如远处角色移除法线贴图或改用 Unlit 着色器,在 LOD 组件中绑定不同材质即可。
2.4 高级技巧:Imposter 与 Billboard
在极远距离,甚至可以使用一个朝摄像机方向旋转的纹理面片(Billboard)或预先渲染好的 Imposter 去替代复杂模型。常用于树木、远景建筑群。
2.5 注意事项
- 切换突兀:确保各级 LOD 之间轮廓接近,过渡距离保守,避免玩家注意到模型突然变化。
- 蒙皮网格开销:动画模型的 LOD 若仍保持骨骼蒙皮,则顶点减少不一定大幅降低 CPU 蒙皮开销。必要时可用顶点动画等替代远处的蒙皮。
- 内存占用:LOD 会增加资产内存,需平衡复杂度和数量。
3. 遮挡剔除:不画看不见的东西
3.1 什么是遮挡剔除?
渲染管线在投影之前,剔除那些完全被其他不透明物体挡住的物体。如果一件物品不在视野内或被墙/建筑完全遮住,GPU 就不需要处理它,从而节省大量 Shading 和像素填充成本。
与视锥体剔除的区别:视锥体剔除只排除摄像机视野外的物体,而遮挡剔除解决的是视野内但被遮挡的物体。
3.2 遮挡剔除的实现方法
3.2.1 预计算遮挡剔除(Baked Occlusion Culling)
最常见的方法,将关卡空间划分成若干单元,离线计算每个单元中可见的物体集合。运行时根据摄像机所在单元快速查询可见集。
- Unity:使用
Occlusion Culling窗口烘焙,需要标记遮挡物和被遮挡物为Static。调整Smallest Occluder和Smallest Hole参数控制粒度。 - Unreal:内置预计算可见性系统,可在世界设置中启用,并调增 Cell Size 精度。
优点:运行时开销极低,兼容性好。
缺点:仅适用于静态场景;大场景烘焙时间长;存储数据量大。
3.2.2 硬件遮挡查询(Hardware Occlusion Queries)
使用 GPU 的遮挡查询功能,先渲染一个物体包围盒到深度缓冲,然后查询该包围盒是否产生了可见像素,以此决定是否渲染完整模型。
- 适用:动态物体或移动平台场景。
- 挑战:查询结果需延迟一帧获取,可能导致“突然出现”。通常结合上一帧可见性预测或分组查询,使延迟感知不大。
3.2.3 软件光栅化遮挡剔除(Software Occlusion Culling)
在 CPU 端用简化的光栅化推算出物体的可见性,再提交 Draw Call。例如 Umbra(常用于 3A 游戏)或 Unity HDRP 的 Shader Graph 中基于 Compute Shader 的自定义方案。这类方法比 GPU 查询延迟更低,且不打断 GPU 流水线。
3.2.4 层次化 Z 缓冲剔除(Hierarchical Z-Buffer Occlusion)
现代 GPU 硬件普遍支持 Hi-Z 剔除,在早期深度测试阶段自动剔除大部分被遮挡的三角形。开发者无需显式编码,只需确保深度缓冲提前渲染(通过 Z-Prepass)即可充分利用。
实践建议:为不透明物体统一执行一个 Z-Prepass(仅写深度),可让 Hi-Z 剔除覆盖全场景,大幅减少像素着色开销。
3.3 场景设计中的遮挡技巧
- 利用大遮挡物:场景中适当布置墙壁、山体、大型建筑,可自然形成遮挡区域。
- Portal/Cells 系统:室内场景可划分为多个区域,通过门窗 Portal 实现手动的可见性裁剪(类似 Portal 剔除)。
- 闭塞物体积:在 Unreal 或 Unity 中使用 Occlusion Portal 或 Occlusion Area 显式定义视野分割线,帮助遮挡剔除系统更好地工作。
3.4 优化遮挡剔除效果
- 调整遮挡物阈值:Unity 中
Smallest Occluder设置过大,会导致小物品不能遮挡环境;过小则烘焙数据爆炸。 - 避免过度透明:半透明物体并不写入深度缓冲,无法成为遮挡体,且通常总被渲染。尽量控制半透明物体数量,或使用抖动透明(Dither Transparency)来保留深度写入能力。
- 调试可视化:Unity Scene 视图的遮挡剔除可视模式(Occlusion Culling 窗口→Visualization)帮助检查哪些物体没被剔除,以及遮挡体的有效范围。
4. 三者的协同优化
- Draw Call 优化是前提:减少 CPU 负担后,才有更多资源运行 LOD 和遮挡剔除逻辑。
- LOD 减少几何负载:远距离物体三角面数减少,不仅降低顶点处理量,也减少遮挡剔除系统需要考虑的物体数量。
- 遮挡剔除卸载像素负载:即使物体有很多 Draw Call 和细密的 LOD0,若被墙挡住,像素着色器不会执行,大幅节省 GPU。
一个典型的优化工作流:
- 使用 Frame Debugger 找出 Draw Call 过高的源头,做合批或实例化。
- 为场景中所有显眼物体配置 LOD Group,确保远距离使用低面模型。
- 烘焙 静态遮挡剔除,再结合动态物体的软件/硬件遮挡查询。
- 添加 Z-Prepass 让硬件 Hi-Z 剔除自动生效。
- 通过性能面板监控三角面数、Draw Call、可视多边形数,迭代调整。
结语
Draw Call、LOD 和遮挡剔除,构成了图形性能优化的黄金三角。掌握它们,不是记住某几个参数,而是建立一种“权衡思维”:用更少的指令绘制更少的三角,且只绘制真正会显示的部分。从项目一开始就将这些机制融入资产生产和关卡设计中,远比后期“打补丁”式优化高效十倍。