🎨 GPU Instancing 渲染优化:15,000 个单位 1 个 DrawCall
文档目标:详解如何在 Unity 中实现海量同屏单位的高效渲染,从MaterialPropertyBlock 到 DrawMeshInstancedIndirect 的完整技术路线。
1. 渲染瓶颈在哪里?
当屏幕上有 1000 个相同的史莱姆时,传统的渲染流程是:- CPU 告诉 GPU:“准备好,我要画史莱姆 A 了,它在 (1,0,1)。”
- GPU 绘制 A。
- CPU 告诉 GPU:“准备好,我要画史莱姆 B 了,它在 (2,0,1)。”
- GPU 绘制 B。
2. Level 1: 基础 Instancing (MaterialPropertyBlock)
最简单的优化,不需要改 Shader(如果是 Standard Shader)或仅需少量改动。✅ 适用场景
- 单位数量 < 1023 (受限于 Constant Buffer 大小)。
- 每个单位颜色、透明度不同,但网格相同。
💻 代码实现
Enable GPU Instancing。
3. Level 2: 进阶 Instancing (DrawMeshInstanced)
当不需要 GameObject,只需要纯粹的渲染时(例如弹幕、掉落物)。✅ 适用场景
- 单位数量 > 1000。
- 不需要物理碰撞,或者物理逻辑在 JobSystem 中处理。
💻 代码实现
Matrix4x4 数组并上传,带宽压力依然存在。
4. Level 3: 终极优化 (DrawMeshInstancedIndirect)
Indirect 的意思是:CPU 甚至不知道要画多少个,数量由 GPU (Compute Shader) 决定,或者参数直接存在 GPU 显存里(ComputeBuffer)。✅ 适用场景
- Vampire Survivors 规模:10,000+ 单位。
- 逻辑完全在 Compute Shader 或 Job System 中运行。
- 剔除 (Culling):在 GPU 中做视锥体剔除,看不见的不画。
💻 核心架构
- ComputeBuffer: 存储所有怪物数据的结构体(位置、旋转、缩放、颜色)。
- ArgsBuffer: 存储绘制参数(Mesh 顶点数、实例数量、起始索引等)。
- Shader: 修改顶点着色器,直接从 Buffer 读取数据。
💻 Shader 示例 (HLSL)
💻 C# 驱动脚本
5. 性能对比 (同屏 10,000 个方块)
| 方法 | Draw Calls | FPS (PC) | CPU 开销 |
|---|---|---|---|
| GameObjects | ~10,000 | 4 fps | 100% (Main Thread) |
| Static Batching | ~50 | 15 fps | High (Memory Overhead) |
| GPU Instancing | ~10 | 45 fps | Medium (Matrix Upload) |
| Indirect | 1 | 120+ fps | Zero |
6. 常见坑点 (Troubleshooting)
-
阴影 (Shadows):自定义 Shader 需要手动添加
SHADOW_CASTERpass 并支持 Instancing 宏,否则阴影会消失或不跟随。 - LOD:Indirect 模式下做 LOD 比较麻烦,需要把不同 LOD 的单位分到不同的 Buffer 绘制,或者在 Shader 中通过距离丢弃顶点(退化为点)。
- 设备兼容性:Compute Shader 在古老的手机(OpenGLES 3.0 以下)上不支持。需要回退方案。
7. 结论
对于 Vampirefall:- 精英怪/Boss:使用 Level 1 (MaterialPropertyBlock)。
- 普通怪群 (500+):使用 Level 2 (DrawMeshInstanced) 配合 Job System。
- 经验宝石/弹幕 (5000+):必须使用 Level 3 (Indirect)。