draw call 多会卡,本质是 CPU 向 GPU 提交命令的开销太高,不只是“画得多”,而是“调度成本高”。
Draw Call 优化核心目标:减少提交次数、减少状态切换、避免无效提交。
1. 什么是 Draw Call
一次 draw call,通常就是一次:
gl.drawElements(...)
或在 Three.js 里对应一次 mesh 的提交。
每一次提交,浏览器/驱动/GPU 都要走一整套流程:
- 设置 shader(program 切换)
- 绑定材质状态(blend、depthTest 等)
- 绑定纹理
- 绑定顶点 buffer
- 上传 uniform
- 驱动校验状态
- CPU 发命令给 GPU
这些都不是“免费”的。
2. 为什么多了会卡(核心原因)
(1)CPU 成为瓶颈
很多人以为渲染卡是 GPU 不够,其实常见是 CPU 卡。
如果有:
- 10000 个 mesh
- 每个 mesh 一个 drawcall
那每帧 CPU 要发 10000 次命令。
60 FPS:
10000 × 60 = 60万次提交/秒
CPU 光“派活”就忙不过来。
(2)Driver Call 很贵
真正贵的是驱动层(Driver Overhead)。
一次 drawcall 不是简单函数调用:
JS → WebGL API → 浏览器图形层 → GPU Driver → GPU Command Buffer
中间很多验证与状态同步。
所以:
小物体很多 = 往往比一个大物体更慢
哪怕总三角面一样。
3. 状态切换也贵
如果每个 drawcall 还切:
- 材质
- shader
- 纹理
- blend mode
会更慢:
drawcall 多 + state change 多 = 双重灾难
这也是为什么材质合批很重要。
4. GPU 也会被“喂不饱”
CPU 提交太慢:
GPU 等 CPU 发命令
GPU 空闲但帧率低。
这叫:
CPU-bound rendering
性能分析经常看到:
- GPU 占用不高
- 帧率却低
就是这个。
5. 举例
情况 A
10000 个 cube
10000 drawcalls
每个12个三角形
很可能卡。
情况 B
合成一个 geometry:
1 drawcall
12万个三角形
反而更快。
因为:
提交成本 << 顶点计算成本
6. Three.js 为什么常强调减少 Draw Call
因为 WebGL 尤其敏感。
经验:
| DrawCalls | 情况 |
|---|---|
| <100 | 很轻松 |
| 100~500 | 常见 |
| 1000+ | 开始危险 |
| 3000+ | 容易卡 |
| 10000+ | 通常有问题 |
(场景复杂度不同会变化)
一、最有效的优化手段
1)Instancing(批量实例化,收益最大)
适合大量重复模型:
- 树、路灯、楼
- 点位 marker
- 粒子/传感器
- 重复设备
在 Three.js 中:
const mesh = new THREE.InstancedMesh(geometry,material,10000
);
原来:
10000 objects
10000 draw calls
可能变成:
1 draw call
原理
同一个:
- Geometry
- Material
- Shader
只提交一次,GPU画很多实例。
2)Merge Geometry(静态合批)
静态对象合并:
BufferGeometryUtils.mergeGeometries()
例如:
1000墙体 -> 1个Mesh
1000 drawcalls -> 1
适合:
- 建筑
- 地块
- 静态装饰
3)共享材质(减少状态切换)
很多时候卡的不只是 drawcall,而是:
- Program 切换
- Material 切换
- Texture 切换
避免:
new MeshStandardMaterial() // 一万个
改成复用同一个 material。
4)纹理图集(Texture Atlas)
原来:
100个模型
100张贴图
100次切换
改:
1张图集
1套材质
明显减少状态变化。
二、减少“无效 Draw Call”
5)Frustum Culling(视锥裁剪)
看不到别提交。
屏幕只看到300个
没必要提交5000个
Three.js 默认有基础裁剪。
还能做:
- Chunk culling
- Portal culling
- Occlusion culling
6)LOD
远处降模:
近:
10000 triangles
远:
500 triangles
甚至 billboard。
7)隐藏对象别只设 visible,还要避免提交
很多人只:
mesh.visible = false
但大型系统可以进一步:
- chunk卸载
- 实例池回收
- 动态加载
三、减少状态切换
有时 drawcall 不高也卡,是 state change。
避免频繁切:
- shader
- blend
- depth
- shadow
按材质排序渲染:
同材质一起画
比乱序快。
四、透明对象特别注意
透明经常破坏批处理:
透明对象可能需要单独排序
会导致:
- drawcall上涨
- overdraw上涨
尽量少透明。
五、高级方案(面试加分)
GPU Driven
更高级:
- Multi Draw
- Indirect Draw
- GPU Culling
- Clustered Rendering
- WebGPU GPU-driven
这是现代渲染路线。
六、Three.js 实战排查
看 drawcall:
console.log(renderer.info.render.calls)
比如:
calls: 4200
说明该优化了。
经验值(大概):
| DrawCalls | 状态 |
|---|---|
| <100 | 很轻 |
| 100-500 | 常见 |
| 1000+ | 危险 |
| 3000+ | 容易卡 |
七、优化优先级(实战)
优先做:
1 InstancedMesh
2 Geometry Merge
3 共享材质 + 图集
4 Frustum Culling
5 LOD
通常解决 80% 问题。
一句话总结
能批处理就批处理
能合并就合并
看不见别画
远处少画
少切状态
Draw Call 优化主要从批处理(Instancing、Merge)、减少状态切换(共享材质、图集)、减少无效提交(Culling、LOD)以及更高级的 GPU Driven 方案几个方向做。
