Draw Call 优化

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 方案几个方向做。

posted @ 2026-04-27 14:11  SimoonJia  阅读(7)  评论(0)    收藏  举报