缓冲顶点与VAO
WebGL2 的顶点状态核心是 VAO,不是每帧把 bindBuffer + vertexAttribPointer 散装一遍。VAO 记下:各 attribute 的 buffer、步长、偏移、是否启用,以及 ELEMENT_ARRAY_BUFFER。热路径只 bindVertexArray 再 draw*。
1. 为什么不能散装 attribute
ARRAY_BUFFER 绑定本身 不会 被 VAO 记住;VAO 记住的是调用 vertexAttribPointer 当时 绑在 ARRAY_BUFFER 上的那个 buffer。每帧重复 pointer 设置等于:
- 把 CPU 同步点塞进每一趟 draw
- 上一网格的 attrib 漏关,下一网格读到脏指针
- instance / 静态网格共用一套全局 attrib,divisor 串台
export type Attrib = {
location: number;
size: number;
type: number;
normalized: boolean;
stride: number;
offset: number;
divisor?: number;
};
export function createVao(
gl: WebGL2RenderingContext,
vertex: WebGLBuffer,
attribs: readonly Attrib[],
index?: WebGLBuffer,
): WebGLVertexArrayObject {
const vao = gl.createVertexArray();
if (!vao) throw new Error("createVertexArray failed");
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, vertex);
for (const a of attribs) {
if (a.location < 0) continue;
gl.enableVertexAttribArray(a.location);
gl.vertexAttribPointer(
a.location,
a.size,
a.type,
a.normalized,
a.stride,
a.offset,
);
if (a.divisor !== undefined) {
gl.vertexAttribDivisor(a.location, a.divisor);
}
}
if (index) gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, index);
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
return vao;
}
export function drawVao(
gl: WebGL2RenderingContext,
vao: WebGLVertexArrayObject,
mode: number,
count: number,
indexType?: number,
): void {
gl.bindVertexArray(vao);
if (indexType === undefined) {
gl.drawArrays(mode, 0, count);
} else {
gl.drawElements(mode, count, indexType, 0);
}
gl.bindVertexArray(null);
}
stride / offset 以 字节 计。交错 float x,y,u,v 的 stride 是 16,不是 4。
2. 更新数据,不要重建 VAO
几何没变、只改顶点值:对已有 buffer bufferSubData(或 orphan:bufferData 同尺寸再写)。VAO 仍指向同一对象。
export function writeBuffer(
gl: WebGL2RenderingContext,
buffer: WebGLBuffer,
data: ArrayBufferView,
usage: number,
): void {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
void usage;
}
容量不够再 bufferData 扩容,然后 不必 重绑 pointer——除非你换了另一个 buffer 对象。换 buffer 才需要重新 vertexAttribPointer 一次。
适用:静态网格一次上传;CPU 动画顶点低频 bufferSubData。
不适用:每顶点一次 drawArrays;WebGL1 生产路径(那里 VAO 是扩展,本体系不走 experimental-webgl)。
3. WebGPU 对照
没有 VAO 对象。GPUVertexBufferLayout 写进 pipeline,绘制时 pass.setVertexBuffer(slot, buffer)。布局编译期定死,热路径同样不要每 draw 重建 pipeline。
4. 丢失
webglcontextlost 之后 VAO / buffer 句柄全部作废。资源表按「几何源数据还在 CPU」重建,不要缓存旧 WebGLBuffer。见 上下文生命周期。
5. 失败形态
| 症状 | 原因 |
|---|---|
| 第一网格对、第二网格花 | 没 VAO,attrib 从上一趟漏下来 |
| 全黑 | location === -1,或没 enableVertexAttribArray |
| 顶点拧成条带 | stride/offset 按 float 个数而不是字节 |
| 实例全叠在一起 | vertexAttribDivisor 仍是 0 |
| 恢复后立刻 INVALID_OPERATION | 丢失后还在 bindVertexArray(旧句柄) |
权威资料
- Khronos — WebGL 2.0 Vertex Array Objects
- OpenGL ES 3.0 — Vertex Arrays
- MDN — WebGLVertexArrayObject
- W3C — GPUVertexBufferLayout
核对日期:2026-08-26