WebGL 缓冲区概述
缓冲区(Buffer)是 GPU 内存中的一块区域,用于存储顶点数据、索引数据等图形渲染所需的信息。使用缓冲区可以高效地将数据从 CPU 传输到 GPU。
缓冲区类型
1. 顶点缓冲区对象(VBO - Vertex Buffer Object)
存储顶点属性数据,如位置、颜色、法线、纹理坐标等。
2. 索引缓冲区对象(IBO/EBO - Index/Element Buffer Object)
存储顶点索引,用于定义图元的连接方式,减少重复顶点数据。
3. 顶点数组对象(VAO - Vertex Array Object)
存储顶点属性配置的状态,简化绘制调用前的设置。
VBO(顶点缓冲区对象)详解
创建和使用 VBO
javascript// 1. 创建缓冲区 const vbo = gl.createBuffer(); // 2. 绑定缓冲区(指定当前操作的缓冲区类型) gl.bindBuffer(gl.ARRAY_BUFFER, vbo); // 3. 上传数据到 GPU const vertices = new Float32Array([ // 位置(x, y, z) 颜色(r, g, b) -0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // 顶点1 0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // 顶点2 0.0, 0.5, 0.0, 0.0, 0.0, 1.0 // 顶点3 ]); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 4. 配置顶点属性指针 gl.vertexAttribPointer( 0, // 属性位置(对应着色器中的 layout(location=0)) 3, // 每个属性的分量数(这里是3:x, y, z) gl.FLOAT, // 数据类型 false, // 是否归一化 6 * 4, // 步长(每个顶点的字节数:6个float * 4字节) 0 // 偏移量 ); gl.enableVertexAttribArray(0); // 启用位置属性 // 配置颜色属性 gl.vertexAttribPointer( 1, // 属性位置 3, // 分量数(r, g, b) gl.FLOAT, false, 6 * 4, // 步长 3 * 4 // 偏移量(跳过位置数据) ); gl.enableVertexAttribArray(1); // 启用颜色属性
缓冲区使用模式
javascript// gl.STATIC_DRAW:数据不经常改变,多次绘制 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); // gl.DYNAMIC_DRAW:数据经常改变,多次绘制 gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW); // gl.STREAM_DRAW:数据每次绘制都改变 gl.bufferData(gl.ARRAY_BUFFER, data, gl.STREAM_DRAW);
IBO/EBO(索引缓冲区对象)详解
创建和使用 IBO
javascript// 顶点数据(4个顶点定义一个四边形) const vertices = new Float32Array([ -0.5, 0.5, 0.0, // 左上 (0) -0.5, -0.5, 0.0, // 左下 (1) 0.5, -0.5, 0.0, // 右下 (2) 0.5, 0.5, 0.0 // 右上 (3) ]); // 索引数据(2个三角形 = 6个索引) const indices = new Uint16Array([ 0, 1, 2, // 第一个三角形 0, 2, 3 // 第二个三角形 ]); // 创建并绑定 VBO gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 创建并绑定 IBO const ibo = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); // 绘制(使用索引绘制) gl.drawElements( gl.TRIANGLES, // 绘制模式 6, // 索引数量 gl.UNSIGNED_SHORT, // 索引数据类型 0 // 偏移量 );
VAO(顶点数组对象)详解
VAO 存储了所有顶点属性的配置状态,包括:
- 启用的顶点属性
- 每个属性的配置(大小、类型、步长、偏移)
- 绑定的 VBO
WebGL 2.0 / WebGL 1.0 + OES_vertex_array_object 扩展
javascript// 创建 VAO const vao = gl.createVertexArray(); // WebGL 2.0 // const vao = ext.createVertexArrayOES(); // 使用扩展 // 绑定 VAO(后续配置将存储在 VAO 中) gl.bindVertexArray(vao); // 配置顶点属性(这些配置会被 VAO 记录) gl.bindBuffer(gl.ARRAY_BUFFER, positionVBO); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); gl.bindBuffer(gl.ARRAY_BUFFER, colorVBO); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(1); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo); // 解绑 VAO gl.bindVertexArray(null); // 绘制时只需绑定 VAO gl.bindVertexArray(vao); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
完整示例代码
javascriptclass Mesh { constructor(gl) { this.gl = gl; this.vertexCount = 0; this.indexCount = 0; // WebGL 2.0 创建 VAO this.vao = gl.createVertexArray(); this.vbo = gl.createBuffer(); this.ibo = gl.createBuffer(); } setVertices(vertices, attributes) { const gl = this.gl; gl.bindVertexArray(this.vao); // 上传顶点数据 gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo); gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 配置属性 let offset = 0; const stride = attributes.reduce((sum, attr) => sum + attr.size, 0) * 4; attributes.forEach((attr, index) => { gl.vertexAttribPointer( index, attr.size, gl.FLOAT, false, stride, offset ); gl.enableVertexAttribArray(index); offset += attr.size * 4; }); this.vertexCount = vertices.length / (stride / 4); } setIndices(indices) { const gl = this.gl; gl.bindVertexArray(this.vao); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); this.indexCount = indices.length; } draw() { const gl = this.gl; gl.bindVertexArray(this.vao); if (this.indexCount > 0) { gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount); } } } // 使用示例 const mesh = new Mesh(gl); mesh.setVertices( new Float32Array([/* 顶点数据 */]), [ { size: 3 }, // 位置 { size: 3 }, // 颜色 { size: 2 } // 纹理坐标 ] ); mesh.setIndices(new Uint16Array([/* 索引数据 */])); mesh.draw();
性能优化建议
- 减少状态切换:使用 VAO 减少绘制前的配置开销
- 合并缓冲区:将多个小网格合并到一个大缓冲区
- 使用索引绘制:减少顶点数据重复
- 选择合适的绘制模式:
STATIC_DRAW:静态几何体DYNAMIC_DRAW:频繁更新的数据STREAM_DRAW:每帧都更新的数据
- 批量绘制:使用实例化渲染(Instanced Rendering)绘制多个相同对象