一、传统引擎层面的合批(Batching)与实例化

传统优化的核心是减少渲染状态的切换(State Changes),因为每次切换材质、Shader 或绑定新纹理,都会在 CPU 端产生高昂的驱动层验证开销。

1. GPU Instancing(GPU 实例化)

  • 原理:使用单次 Draw Call 绘制大量具有相同网格(Mesh)和相同材质(Material)的对象。位置、旋转、颜色等差异化数据通过 Compute Buffer 或 Instance Buffer 传递给 Shader 动态计算。
  • 适用场景:大规模植被(草、树)、粒子群、飞行的箭矢、建筑群。
  • 引擎实践:在 Unity 中勾选材质的 Enable GPU Instancing,或使用 Graphics.DrawMeshInstancedIndirect;在 UE 中广泛使用 HISM(Hierarchical Instanced Static Mesh)。

2. SRP Batcher(Unity 可渲染管线专有)

  • 原理:不同于传统的动态或静态合批,SRP Batcher 不要求网格相同。只要使用同一个 Shader 变体,它就能通过在 GPU 显存中预先缓存材质属性(CBUFFER),将原本分离的 Draw Call 放入一个大的 Batch 中提交,极大降低 CPU 开销。
  • 适用场景:大量使用同一种底层 Shader(如 URP/HDRP 的 Lit Shader)但参数不同的场景物体。

3. 静态合批(Static Batching)与动态合批(Dynamic Batching)

  • 静态合批:在构建时(或场景加载时)将所有标记为 Static 且共享相同材质的 Mesh 合并为一个巨大的 Mesh。代价是内存占用显著增加。
  • 动态合批:CPU 在每帧动态组合共享材质的小顶点网格。由于 CPU 需要进行额外的顶点变换计算,如果网格顶点数过多,开销反而会高于原本的 Draw Call,在现代多核 CPU 和 GPU 环境下已逐渐被边缘化。

二、材质与状态切换优化

Draw Call 的昂贵很大程度上来自于其附带的状态切换(比如 SetTexture, SetShader)。

  • 纹理图集(Texture Atlasing):将多个小道具的纹理合并到一张大图中。这样即使网格不同,也可以共享同一个材质,从而创造合批条件。
  • Texture Arrays(纹理数组) / Bindless Textures(无绑定纹理):现代图形 API 允许在 Shader 中一次性绑定一个纹理数组或使用无绑定技术。通过网格的自定义数据传递 Index,Shader 根据 Index 采样不同的纹理,彻底消除因切换纹理带来的 Draw Call 打断。
  • Shader 变体控制(Variant Stripping):减少不必要的宏定义分支。如果两个材质仅因为一个不起眼的宏不同而被编译为两个变体,它们就无法被引擎(如 SRP Batcher)合批。

三、现代范式:GPU-Driven Rendering Pipeline

对于追求极致性能和高密度的场景,优化的尽头是把 Culling(剔除)和 Draw Call 提交的权力从 CPU 交给 GPU

1. GPU Culling(计算着色器剔除)

将所有的场景包围盒数据传递给 Compute Shader。利用 GPU 的海量并行计算能力执行:

  • Frustum Culling(视锥体剔除)
  • Occlusion Culling(遮挡剔除):通常配合上一帧的深度缓冲(Hi-Z Culling)进行计算。剔除完成后,由 Compute Shader 直接生成并填充绘制参数(Draw Arguments)。

2. Indirect Drawing(间接绘制)

传统的 Draw Call 由 CPU 明确指明绘制多少个顶点。而在 GPU-Driven 管线中,使用如 DrawInstancedIndirect 或现代 API 的 ExecuteIndirect。CPU 只需要发出一个命令,GPU 会读取由 Compute Shader 生成的 Buffer 中的数据(即剔除后存活下来的实例数量和参数)来执行渲染。这意味着场景中有 10 万棵树还是 10 棵树,对 CPU 的开销几乎是一样的。

3. Mesh Shading / Nanite 架构

这是当前引擎(如 Unreal Engine 5 的 Nanite)最前沿的渲染策略。

  • 传统的顶点流水线(Vertex -> Rasterizer)对极小三角形的效率极低。
  • Mesh Shader 替代了传统的 Vertex/Geometry/Tessellation 阶段,允许在 GPU 端以类似于 Compute Shader 的方式,按簇(Meshlets)级别进行细粒度的 Culling、LOD 切换和顶点生成。这不仅彻底解决了 Draw Call 瓶颈,还顺带解决了多边形渲染瓶颈。

性能排查建议:在进行优化前,永远先看 Profiler(比如 RenderDoc、PIX 或引擎自带的 Frame Debugger)。如果是 CPU 端的 RenderThreadRenderLoop 被压垮,那确实是 Draw Call / 状态切换问题;如果是 GPU 耗时过长,盲目减少 Draw Call 有时会导致合批后的 Mesh 过大,反而加剧了 GPU 端的 Overdraw 或显存带宽压力。