实例化与间接绘制
万级散点 / 标记 / 粒子,瓶颈几乎总是 draw call 次数,不是 VS 里多几个 MAD。一条几何 + 一条 instance buffer(位移、颜色、ID),drawArraysInstanced 一次提交。不要每点 drawArrays。
1. Instance buffer
网格 VAO 里:顶点 attrib divisor = 0,实例 attrib divisor = 1。每实例步过一行 instance buffer。
export type MarkerInstance = {
x: number;
y: number;
sx: number;
sy: number;
r: number;
g: number;
b: number;
a: number;
};
export function packMarkers(list: readonly MarkerInstance[]): Float32Array {
const out = new Float32Array(list.length * 8);
list.forEach((m, i) => {
const o = i * 8;
out[o] = m.x;
out[o + 1] = m.y;
out[o + 2] = m.sx;
out[o + 3] = m.sy;
out[o + 4] = m.r;
out[o + 5] = m.g;
out[o + 6] = m.b;
out[o + 7] = m.a;
});
return out;
}
export function drawMarkers(
gl: WebGL2RenderingContext,
vao: WebGLVertexArrayObject,
instanceCount: number,
vertsPerInstance: number,
): void {
gl.bindVertexArray(vao);
gl.drawArraysInstanced(gl.TRIANGLES, 0, vertsPerInstance, instanceCount);
gl.bindVertexArray(null);
}
CPU 场景图仍是 source of truth。可见集变了:重打 instance 数组,bufferSubData(或容量翻倍后 bufferData)。几何四边形只上传一次。合批规则见 06;这里只要求 同一 shader + 同一纹理图集 才能进同一 VAO。
适用:监控散点、地图 POI、粒子、草。量级从几千到几十万,先实例化,再谈计算剔除。
不适用:每实例不同 mesh / 不同 program(那是多条 VAO,不是 divisor);对象 < 几百且要逐个复杂裁剪——CPU 直接画更简单。
2. 间接绘制:WebGL2 弱、WebGPU 强
WebGL2 没有 核心 draw*Indirect。能做的是:CPU 算好 instanceCount 再 drawArraysInstanced;或探测 WEBGL_multi_draw 把多个同状态合批收成一次 API。GPU 自己写 draw 参数、compute 剔除后再画,这是 WebGPU 的活:
export function encodeIndirect(
pass: GPURenderPassEncoder,
pipeline: GPURenderPipeline,
vertex: GPUBuffer,
indirect: GPUBuffer,
): void {
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertex);
pass.drawIndirect(indirect, 0);
}
indirect buffer 由 compute pass 写入 {vertexCount, instanceCount, firstVertex, firstInstance}。生产默认仍是 WebGL2 实例化;只有已经探测到 adapter、且确实需要 GPU-driven 剔除时,才为间接绘制切 WebGPU。
3. 更新策略
| 变化 | 动作 |
|---|---|
| 相机动、标记不动 | 只改 viewProj uniform / UBO |
| 标记增删、视口裁剪 | 重打 instance 数组 |
| 单点拖拽 | bufferSubData 那一行,或把拖中点从 instance 拿出来单独画 |
不要为了「少拷一次」每帧 bufferData 全量 orphan 十万行——移动端比 bufferSubData 脏区更疼。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 一万个点 10fps | 每点一次 drawArrays,不是 VS 太重 |
| 实例全重合 | attrib divisor 仍是 0 |
| 拖一个点全体跳 | 把 world 矩阵当 uniform 而不是 instance |
| 增点后花屏 | instanceCount 超过 buffer 实际行数 |
| 间接绘制桌面黑 | 当核心 API 用了,没走 WebGPU / 扩展探测 |
权威资料
- Khronos — drawArraysInstanced
- Khronos — WEBGL_multi_draw
- MDN — drawArraysInstanced
- W3C — drawIndirect
核对日期:2026-08-26