WebGL Tutorial
and more

复合VBO

撰写时间:2023-07-27

修订时间:2023-07-28

复合VBO是指一个同时绑定多个顶点属性的VBO

核心原始代码

我们先来看不借助于辅助类时的代码:

function initVBOs() { let verticesColors = [ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 ]; vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticesColors), gl.STATIC_DRAW); } function render() { const FB_SIZE = Float32Array.BYTES_PER_ELEMENT; gl.clear(gl.COLOR_BUFFER_BIT); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.vertexAttribPointer(program['aPosition'], 3, gl.FLOAT, false, FB_SIZE * 7, FB_SIZE * 0); gl.enableVertexAttribArray(program['aPosition']); gl.vertexAttribPointer(program['aColor'], 4, gl.FLOAT, false, FB_SIZE * 7, FB_SIZE * 3); gl.enableVertexAttribArray(program['aColor']); gl.drawArrays(gl.TRIANGLES, 0, 3); }

不同于之前的例子,变量verticesColors同时存储了顶点位置与顶点颜色的数据,因此我们将此类的VBO称为复合VBO。复合VBO的创建方法与之前均一样。

重点在于对于复合VBO,如何正确地调用vertexAttribPointer方法。

变量vbo的数据源中,共有3个顶点。

let verticesColors = [ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, // V0: position (3), color (4) -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, // V1: position (3), color (4) 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 // V2: position (3), color (4) ];

构成每个顶点的数据中,均由3个元素来表示顶点位置,4个元素来表示顶点颜色。由于复合VBO由一个VBO同时绑定到多个顶点属性,因此,需两次调用vertexAttribPointer来进行绑定。并且,由于需要从一个数组中分多次分别提取顶点位置与顶点颜色的数据,每次调用该方法都有细微区别。

下面的代码先绑定至aPosition顶点属性:

const FB_SIZE = Float32Array.BYTES_PER_ELEMENT; gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.vertexAttribPointer(program['aPosition'], 3, gl.FLOAT, false, FB_SIZE * 7, FB_SIZE * 0);

关键在于最后两个参数,用形参stride来指定每两个不同顶点之间相距多少个元素,以及用形参offset表示第一个指向绑定到相应顶点属性的元素,其偏移值为多少。这两个参数,都是以字节为单位来计算的。类型化数组都有一个属性BYTES_PER_ELEMENT,返回该种类型的数组中每个元素占多少字节。由于我们是使用Float32Array的类型来创建的

首先,我们看到,每个顶点中,有3个成分是顶点位置,有4个成分是顶点颜色,因此,每个顶点的数据横跨7个元素的距离。由于需要以字节为单位计算,因此,需将形参stride的值指定为FB_SIZE * 7

对于要绑定到aPosition顶点属性的首个数组元素,其在数组中的偏移值为0,因此需将形参offset的值指定为FB_SIZE * 0

下面代码再绑定至aColor顶点属性:

gl.vertexAttribPointer(program['aColor'], 4, gl.FLOAT, false, FB_SIZE * 7, FB_SIZE * 3);

对于要绑定到aColor顶点属性的首个数组元素,其在数组中的偏移值为3,因此需将形参offset的值指定为FB_SIZE * 3

运行应用

注意,简单VBO与复杂VBOvertexAttribPointer方法中,形参stride的意义是不一样的。简单VBOstride始终为0,表示没有其他因素参杂,但实际上,第一个顶点到第二个顶点的间距并不为0。而复杂VBOstride,为多种元素所构成的成分的总和。

let vertices = [ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ]; // stride seems should be 3, yet actually, stride = 0 gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); let verticesColors = [ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, // V0: position (3), color (4) -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, // V1: position (3), color (4) 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 // V2: position (3), color (4) ]; // stride = 7 gl.vertexAttribPointer(program['aPosition'], 3, gl.FLOAT, false, FB_SIZE * 7, FB_SIZE * 0);

VBO的职责与结构

在创建VBO时,应向vertexAttribPointerdrawArrays提供相应的信息。

应包括这些参数:arrData(关于顶点位置及颜色的数组数据)、 compsSizes(成分大小)、attribsNames(顶点属性名称),以及verticesNum(顶点数量)。而顶点数量可以计算出来。

根据简单VBO及复合VBO的特点,attrsNamescompsSizes应采取数组的形式。

重构WebGLUtils

WebGLUtils-v1.js复制为WebGLUtils-v2.js

在文件的开始部分添加下面代码:

const FB_SIZE = Float32Array.BYTES_PER_ELEMENT;

先修改createVBO方法。重构该方法的意图是,让其对简单VBO及复合VBO都能适用。例如,我们希望这样调用该方法:

vbo1 = glu.createVBO([ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ], 3, 'aPosition'); vbo2 = glu.createVBO([ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 ], [3, 4], ['aPosition', 'aColor']);

实现的途径是让该方法的形参compsSizesattribsNames均能接收单一的数值或数组。

createVBO(arrData, compsSizes, attribsNames) { let gl = this.gl; let vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arrData), gl.STATIC_DRAW); vbo.strideSize = 0; if (Array.isArray(compsSizes)) { compsSizes.forEach(value => vbo.strideSize += value); vbo.verticesNum = arrData.length / vbo.strideSize; vbo.compsSizes = compsSizes; } else { vbo.verticesNum = arrData.length / compsSizes; vbo.compsSizes = Array.of(compsSizes); } if (Array.isArray(attribsNames)) { vbo.attribsNames = attribsNames; } else { vbo.attribsNames = Array.of(attribsNames); } return vbo; }

每个所创建的VBO对象,都将包装以下属性名称:

  • strideSize
  • compsSizes
  • attribsNames
  • verticesNum

前3个是vertexAttribPointer方法所需要,最后一个是drawArrays方法所需要。

代码:

vbo.strideSize = 0; if (Array.isArray(compsSizes)) { compsSizes.forEach(value => vbo.strideSize += value); vbo.verticesNum = arrData.length / vbo.strideSize; vbo.compsSizes = compsSizes; } else { vbo.verticesNum = arrData.length / compsSizes; vbo.compsSizes = Array.of(compsSizes); }

根据实参compsSizes是否数组来设置相应的属性值。

如果实参compsSizes是数组,则strideSize的值为各个顶点各成分之和;verticesNum表示顶点的数量,其值等于数组的长度除以每个顶点各成分之和;直接将参数compsSizes的值赋值于属性compsSizes

如果实参compsSizes为单值,则strideSize的值0verticesNum的值等于数组的长度除以这个单值;通过Array.of(compsSizes)将其转换为数组后赋值于属性compsSizes

同样,代码:

if (Array.isArray(attribsNames)) { vbo.attribsNames = attribsNames; } else { vbo.attribsNames = Array.of(attribsNames); }

根据实参attribsNames是否数组来设置相应的属性值。如果为数组,则直接赋值于vboattribsNames属性;如果不是数组,则转化为数组后再赋值。

经这样转化后,vbocompsSizes属性及attribsNames属性均统一为数组的形式。

删除原来的bindVboToAttrib方法,添加bindVboToAttribs方法,代码如下:

bindVboToAttribs(vbo) { let gl = this.gl; let program = this.program; gl.bindBuffer(gl.ARRAY_BUFFER, vbo); let index = 0; for (let attribName of vbo.attribsNames) { gl.vertexAttribPointer(program[attribName], vbo.compsSizes[index], gl.FLOAT, false, FB_SIZE * vbo.strideSize, FB_SIZE * getOffsetFromCompsSizes(vbo.compsSizes, index)); gl.enableVertexAttribArray(program[attribName]); index++; } function getOffsetFromCompsSizes(compsSizes, index) { let sum = 0; let currIndex = 0; while (currIndex < index) { sum += compsSizes[currIndex]; currIndex++; } return sum; } }

内嵌函数getOffsetFromCompsSizes

function getOffsetFromCompsSizes(compsSizes, index) { let sum = 0; let currIndex = 0; while (currIndex < index) { sum += compsSizes[currIndex]; currIndex++; } return sum; }

根据成分数组及所绑定的顶点属性名称的数组的索引值来返回相应的偏移值。例如,对于

vbo2 = glu.createVBO([ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 ], [3, 4], ['aPosition', 'aColor']);

这样的vbo,该函数将能计算出绑定至第一个顶点属性aPosition的元素偏移值为0,而绑定至第二个顶点属性aColor的元素偏移值为3。如果有更多绑定的顶点属性,该函数均能自动计算出其偏移值。

有此作为支撑,在bindVboToAttribs方法中就能以统一的方式来调用vertexAttribPointer方法。

compound-vbo.html复制为compound-vbo-refactored.html。核心代码如下:

function initVBOs() { let verticesColors = [ 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 1.0, -0.5, -0.5, 0.0, 0.0, 1.0, 0.0, 1.0, 0.5, -0.5, 0.0, 0.0, 0.0, 1.0, 1.0 ]; vbo = glu.createVBO(verticesColors, [3, 4], ['aPosition', 'aColor']); } function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.bindVboToAttribs(vbo); gl.drawArrays(gl.TRIANGLES, 0, vbo.verticesNum); }

我们注意到,经重构后,连drawArrays方法也直接以vbo作为参数传入,这为以后更高阶的数据封装打下了基础。

运行应用

重构后的简单VBO

为测试重构后的代码也能适用于简单VBO,我们新建一个名为simple-vbo-refactored.html的文件,主要代码如下:

function initVBOs() { verticesVBO = glu.createVBO([ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ], 3, 'aPosition'); colorsVBO = glu.createVBO([ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ], 4, ['aColor']); } function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.bindVboToAttribs(verticesVBO); glu.bindVboToAttribs(colorsVBO); gl.drawArrays(gl.TRIANGLES, 0, verticesVBO.verticesNum); }

可以看到,verticesVBO的最后一个参数是一个字符串,而colorsVBO的最后一个参数是仅带有1个元素的字符串数组,这些方式在createVBO方法中均得到了支持。

运行应用

Playground

在下面的WebGLEditor中,顶点着色器、片断着色器以及主程序均与上节中一致,您可以试着改变它们的一些变量,然后按Run按钮,观察修改后的效果。

attribute vec4 aPosition; attribute vec4 aColor; varying vec4 vColor; void main() { gl_Position = aPosition; vColor = aColor; }
precision mediump float; varying vec4 vColor; void main() { gl_FragColor = vColor; }
import WebGLUtils from '/tutorials/webgl/fundamentals/examples/js/esm/WebGLUtils-v2.js'; let glu, gl; let verticesVBO, colorsVBO; init(); function init() { initContext(); initVBOs(); render(); } function initContext() { glu = new WebGLUtils({ vertexAttribs: ['aPosition', 'aColor'], renderFunc: render }); gl = glu.gl; gl.clearColor(0, 0, 0, 1); } function initVBOs() { verticesVBO = glu.createVBO([ 0.0, 0.5, 0.0, -0.5, -0.5, 0.0, 0.5, -0.5, 0.0 ], 3, 'aPosition'); colorsVBO = glu.createVBO([ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ], 4, ['aColor']); } function render() { gl.clear(gl.COLOR_BUFFER_BIT); glu.bindVboToAttribs(verticesVBO); glu.bindVboToAttribs(colorsVBO); gl.drawArrays(gl.TRIANGLES, 0, verticesVBO.verticesNum); }

参考资源

  1. WebGL 1.0 Specification