首页/ 学科题库/ webgl/ WebGL 交错顶点布局与索引缓冲复用
webgl 实战教程

WebGL 交错顶点布局与索引缓冲复用

2026-09-13 约 9 分钟 juanwangdev

WebGL 交错顶点布局与索引缓冲复用

一句话结论:交错布局让同一顶点的属性连续存放以提高缓存命中,索引缓冲让顶点被多个三角形共用,两者结合是顶点数据优化的第一步。

交错与分离顶点布局

是什么

顶点布局描述顶点属性在内存中的排列方式。交错布局把同一顶点的位置、法线、UV 等属性连续存放,多个顶点依次排开;分离布局把同一种属性全部放在一段缓冲里,不同属性各占一段。

解决什么问题

它解决的是顶点读取效率问题。GPU 读取顶点属性时按缓存行加载数据,如果同一顶点的属性分散在不同缓冲中,一次读取就要跳转多个内存区域,缓存命中率下降;交错存放则让一次加载就把整个顶点读进缓存。

为什么这样做

顶点着色器对每个顶点的处理是独立的,但执行单元同时处理相邻顶点。把相邻顶点的数据放在连续地址上,硬件预取就能一次命中多个顶点,因此交错布局更契合 GPU 的访问模式。分离布局的优势在于更新频率差异大的属性可以单独更新,不必重写整段顶点数据。

怎么用

JavaScript
// 交错布局:位置 3 个 float + UV 2 个 float,共 20 字节
const STRIDE = 5 * 4;
const interleaved = new Float32Array([
  // x, y, z,  u, v
  -0.5, -0.5, 0, 0, 0,
   0.5, -0.5, 0, 1, 0,
   0.0,  0.5, 0, 0.5, 1,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, interleaved, gl.STATIC_DRAW);

gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, STRIDE, 0);
gl.vertexAttribPointer(aTexCoord, 2, gl.FLOAT, false, STRIDE, 3 * 4);
对比项交错布局分离布局
内存局部性好,一个顶点连续差,属性间跨度大
缓存命中率高较低
局部更新需重写相邻顶点区间可单独更新某类属性
缓冲数量1 个每个属性 1 个

常见坑

  • 交错布局却按单属性设置步长 → 原因:漏改 stride → 处理:步长统一为单顶点字节数
  • 全部用分离布局导致属性绑定过多 → 原因:忽略顶点属性数量上限 → 处理:优先合并为交错布局
  • 局部更新交错数据时整段重传 → 原因:未按顶点区间计算偏移 → 处理:用 bufferSubData 精确写入目标区间

索引缓冲与顶点复用

是什么

索引缓冲(IBO)是存放顶点序号的一段缓冲,配合 drawElements 使用。绘制时 GPU 按索引顺序取顶点,同一个顶点可以被多个三角形重复引用,从而避免重复存储。

解决什么问题

它解决顶点数据冗余问题。用 drawArrays 绘制三角形列表时,每个三角形都要独占三个顶点,共享顶点被反复存储;一个规则网格的顶点会被六个三角形共用,冗余率非常高,索引缓冲把顶点数量降到接近网格点数。

为什么这样做

顶点数据的主要成本在传输与显存占用,而不在索引本身。索引使用 16 位无符号整数时,每个三角形只需 6 字节索引,远小于一个顶点的存储开销。因此设计上把“顶点数据”和“组织方式”分离,让同一份顶点被多次引用。

怎么用

JavaScript
// 4 个顶点组成一个矩形,用 6 个索引画出两个三角形
const positions = new Float32Array([
  -0.5, -0.5, 0,
   0.5, -0.5, 0,
   0.5,  0.5, 0,
  -0.5,  0.5, 0,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);

// 逆时针缠绕,保证面朝向正确
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);

常见坑

  • 索引值超出顶点数量 → 原因:索引从 0 开始编号,写成了 1 起 → 处理:核对最大索引等于顶点数减一
  • 顶点数超过 65535 仍用 UNSIGNED_SHORT → 原因:16 位溢出 → 处理:改用 UNSIGNED_INT(需 WebGL 2.0 或对应扩展)
  • 面显示不全 → 原因:缠绕顺序与剔除设置冲突 → 处理:统一逆时针并检查 cullFace 与 frontFace
  • 索引缓冲绑定被后续操作覆盖 → 原因:ELEMENT_ARRAY_BUFFER 绑定属于顶点数组状态 → 处理:绘制前重新绑定确保状态正确

要点总结

  • 交错布局把同一顶点的属性连续存放,缓存命中率更高
  • 分离布局适合属性更新频率差异大的场景,但缓冲数量更多
  • 索引缓冲用序号复用顶点,网格类数据可显著降低顶点存储量
  • 索引从 0 开始,最大索引等于顶点数减一
  • 缠绕顺序决定面朝向,与面剔除设置必须一致

常见问题

交错布局和分离布局哪个更快?

多数情况下交错布局更快,因为同一个顶点的位置、法线、UV 连续存放,顶点着色器读取时能命中同一段缓存,分离布局则需要同时绑定多段缓冲,缓存局部性更差。

索引缓冲能节省多少顶点数据?

节省比例取决于网格共享度。以规则网格为例,三角形数量约为顶点数量的两倍,用索引后顶点数可从每三角形三点降到接近网格点数,数据量通常能减少一半以上。

为什么用索引绘制后部分面看不见?

多半是顶点缠绕顺序反了。默认正面是逆时针,顺序错误的面会被面剔除掉,可调整索引顺序或修改 frontFace 设置。

drawArrays 和 drawElements 怎么选?

顶点几乎不共享的简单图元用 drawArrays 更直接;网格、模型这类顶点被多个三角形共用的场景用 drawElements 配合索引缓冲。

想在手机上练这篇的配套题目?
使用微信 卷王开发者小程序/App,首页顶部扫一扫识别下方二维码,即可解锁本讲对应真题与错题本。
了解产品 App
← 上一篇 WebGL 顶点缓冲对象创建与 attribute 布局 下一篇 → WebGL 顶点数据动态更新与状态机缓存