前言
在上一节中,展示了画多个点的一个例子,将数据存储在数组中,然后循环绘制每一个点。不过这种方式只能绘制点,由多个顶点组成的图形,比如三角形、长方形,需要一次把所有的顶点数据传递给顶点着色器才能绘制出来,用数组的方式一个一个点传进去就没办法实现了。不过,WebGL 提供了缓冲区对象 ,它可以一次性向顶点着色器传递大量的数据,这样就可以画出复杂的图形了。
Show me the code
我们先看下缓冲区对象是怎么使用的。
// 顶点着色器和片元着色器还是使用之前的,没有什么改变,这里就给省略了。
...
function main() {
// Retrieve <canvas> element
var canvas = document.getElementById('webgl');
// 获取 WebGL 上下文
var gl = getWebGLContext(canvas);
// 初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)
// 设置顶点位置
var n = initVertexBuffers(gl);
// 设置背景色
gl.clearColor(0, 0, 0, 1);
// 清空 <canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制
gl.drawArrays(gl.POINTS, 0, n);
}
function initVertexBuffers(gl) {
var vertices = new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
var n = 3; // 顶点个数
// 创建缓冲区对象
var vertexBuffer = gl.createBuffer();
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓冲区对象中写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
这次我们定义 initVertexBuffers 函数,它的作用是:创建缓冲区对象,并将多个顶点的数据保存在缓冲区中,然后将缓冲区传给顶点着色器。
函数的返回值是待绘制顶点的个数。WebGL 系统并不知道缓冲区中有多少个顶点的数据,即使知道了也不能确认是否都要画出来,所以我们要显示的告诉它要绘制多少个顶点。
缓冲区对象
缓冲区对象是 WebGL 系统中的一块存储区,可以在缓冲区对象中保存想要绘制的所有顶点数据。
使用缓冲区对象向顶点着色器传入多个顶点的数据,其步骤如下:
- 创建缓冲区对象(
gl.createBuffer()) - 绑定缓冲区对象(
gl.bindBuffer()) - 将数据写入缓冲区对象(
gl.bufferData()) - 将缓冲区对象分配给一个 attribute 变量(
gl.vertexAttribPointer()) - 开启 attribute 变量(
gl.enableVertexAttribArray())
创建缓冲区对象
var vertexBuffer = gl.createBuffer();
我们来看下创建缓冲区前后,WebGL 系统内容的状态:WebGL 系统中多出来一个新创建出来的缓冲区对象。
绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
创建缓冲区后,就是将缓冲区对象绑定到 WebGL 系统中已存在的 target(目标) 上,这个 “目标” 表示的是缓冲区对象的用途,只有指定了具体的用途,WebGL 系统才能够正确的处理其中的数据。
/**
* 允许使用 buffer 表示的缓冲区对象,并将其绑定到 target 表示的目标上。
* @param {*} target gl.ARRAY_BUFFER 表示缓冲区对象中包含的是 顶点的数据
* gl.ELEMENT_ARRAY_BUFFER 表示缓冲对象中包含的是 顶点的索引
* @param {*} buffer 由 gl.createBuffer() 返回的待绑定的缓冲区对象
*/
gl.bindBuffer(target, buffer);
绑定过缓冲区对象,WebGL 系统的内部状态变为:
向缓冲区对象中写入数据
这是第3步,开辟空间并向缓冲区中写入数据。
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
这个方法的效果是将 vertices 中的数据绑定到 gl.ARRAY_BUFFER 上的缓冲区对象。
我们是没办法向缓冲区对象中直接写入数据的,只能向“目标”写入数据,所以要向缓冲区写数据,必须先给目标绑定一个缓冲区对象。
/**
* 开辟存储空间,向绑定在target上的缓冲区对象写入数据data
* @param {*} target gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER
* @param {*} data 写入缓冲区对象的数据(类型化数组)
* @param {*} usage 表示程序将如何使用存储在缓冲区对象的数据。该参数将帮助 WebGL 优化操作,传错了也没什么问题。
* gl.STATIC_DRAW 只会向缓冲区对象中写入一次数据,但需要绘制很多次;
* gl.STREAM_DRAW 只会向缓冲区对象中写入一次数据,然后绘制若干次;
* gl.DYNAMIC_DRAW 会向缓冲区对象中多次写入数据,并绘制很多次;
*/
gl.bufferData(target, data, usage);
执行完绑定,WebGL 系统内部的状态为:
将缓冲区对象分配给 attribute 变量
在上一节中我们有学习到 gl.vertexAttrib[1234]f 系列函数为 attribute 变量分配值,但是这些方法一次只能向 attribute 变量传递一个值。而这一节用到的 gl.vertexAttribPointer() 方法可以将整个缓冲区对象(的指针)分配给 attribute 变量。
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
我们看下各个字段的含义:
/**
* 将绑定到 gl.ARRAY_BUFFER 的缓冲区对象分配给由 location 指定的 attribute 变量
* @param {*} location 指定待分配 attribute 变量的存储位置
* @param {*} size 指定缓冲区中每个顶点的分量个数(1到4)。若 size 比 attribute 变量需要的分量数小,则自动补全。
* @param {*} type 数据格式,表示缓冲区中数据格式
* gl.UNSIGNED_BYTE 无符号字节 Uint8Array
* gl.SHORT 短整形 Int16Array
* gl.UNSIGNED_SHORT 无符号短整形 Uint16Array
* gl.INT 整形 Int32Array
* gl.UNSIGNED_INT 无符号整形 Uint32Array
* gl.FLOAT 浮点型 Float32Array
* @param {*} normalized true/false,标明是否将非浮点型的数据归一化
* @param {*} stride 指定相邻两个顶点间的字节数,默认为0
* @param {*} offset 指定缓冲区对象中的偏移量,即 attribute 变量从缓冲区中的何处开始存储
*/
gl.vertexAttribPointer(location, size, type, normalized, stride, offset)
执行完之后,我们就将整个缓冲区对象分配给了 attribute 变量。现在距离完成 WebGL 绘制的准备工作就差一步了:进行最后的开启,使这次的分配真正生效。
开启 attribute 变量
为了使顶点着色器能够访问缓冲区对象中的数据,我们需要开启 attribute 变量。
gl.enableVertexAttribArray(a_Position);
只看函数名称,这个函数似乎是用来处理“顶点数组”的,但实际上它是处理缓冲区对象的。这个是历史原因,从 OpenGL 中继承过来的。
执行完成后,缓冲区对象和 attribute 变量就真正的连接起来了。
开启 attribute 变量后,就不能再用
gl.vertexAttrib[1234]f()向它传递数据了。除非使用gl.disableVertexAttribArray(location)显示的进行关闭。即这两种传递数据的方式不能同时使用。
画图的过程
我们来回顾下 gl.drawArray() 这个强大的方法,它能绘制各种图形。
/**
* 执行顶点着色器,按照mode参数指定的方式进行绘制
* @param {*} mode 绘制方式。gl.POINTS gl.LINES gl.LINE_STRIP gl.LINE_LOOP gl.TRIANGLES gl.TRIANGLE_STRIP gl.TRIANGLE_FAN
* @param {int} first 从哪个顶点开始绘制
* @param {int} count 绘制次数
*/
gl.drawArrays(mode, first, count);
现在我们画的还是一些点,所以 mode 用的是 gl.POINTS; first 用的是 0,表示从缓冲区的第一个坐标开始画;count 为 3,表示需要绘制 3 个点。
gl.drawArrays(gl.POINTS, 0, n); // n 为 3
所以顶点着色器一共执行了 3 次,缓存区中的顶点坐标被依次传递给 attribute 变量:
总结
现在我们知道了缓冲区对象,它可以一次传递给顶点着色器大量数据;我们也了解了缓冲区对象的使用方法和执行逻辑。但是这个一节我们画出来的依然是点,会在下一节介绍绘制其他图形的方法。