之前我们学习了如何动态地绘制一个点,本文就以之为基础,继续介绍如何使用 webgl 绘制多个点,进而绘制最简单,同时也是最稳定的面 —— 三角面。其实 webgl 中的三维模型都是由一个个的小三角面组合而成的,要绘制面,首先需要绘制多个点,然后再连点成线,最后在线性图形内部逐个片元填上颜色就形成了面。
绘制 3 个点
准备缓冲区对象
要绘制多个点,就不太好像绘制 1 个点的时候那样使用 gl.vertexAttrib4f()
直接将顶点数据传递给着色器。而是需准备一个容器来存储各个顶点的数据,这个容器就是缓冲区对象,它是 webgl 中的一块内存区域,之后就可以通过缓冲区对象一次性向着色器传入多个顶点数据:
const buffer = gl.createBuffer()
webgl 可以通过绑定点 —— gl.ARRAY_BUFFER
或 gl.ELEMENT_ARRAY_BUFFER
—— 操控全局范围内的许多数据,可以把绑定点想象成一个 webgl 内部的全局变量。通过 bindBuffer()
方法将缓冲区对象绑定到绑定点:
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
- 第 1 个参数 target 传递
gl.ARRAY_BUFFER
意为缓冲区存储的是顶点的数据(顶点坐标等)。如果传递gl.ELEMENT_ARRAY_BUFFER
表明存储的数据是顶点的索引值; - 第 2 个参数就是创建好的缓冲区对象。
将数据存入缓冲区对象
准备好了缓冲区,就可以使用 gl.bufferData()
往里面放顶点数据了:
const points = new Float32Array([-0.5, -0.5, 0, 0.5, 0.5, -0.5])
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
- 第 1 个参数 target 同
bindBuffer()
的第 1 个参数保持一致; - 第 2 个参数就是要保存的数据。在 js 中是可以往普通数组里放各种类型的数据的,如果直接使用普通数组存放顶点数据,webgl 处理起来肯定就比较麻烦,所以 webgl 中缓冲区对象接收的是类型化数组,诸如使用
new Float32Array()
来创建,其元素都是单精度 32 bit 的浮点数,如果你不懂这是啥意思,可以简单理解为这个数组里的元素在计算机中存储时,都是用 32 位来表示的小数; - 第 3 个参数表示如何使用缓冲区对象中的数据,
gl.STATIC_DRAW
表示写入一次,多次绘制,这种模式下,数据在缓冲区中保持不变,适合那些不经常更改的静态数据,如渲染的是建筑物、地形等。该参数用于帮助 webgl 优化操作,还可以传gl.STREAM_DRAW
(数据被写入一次,但仅用于几次绘制操作,之后可能不再使用或会被新的数据覆盖,如渲染水流、烟雾时) 或gl.DYNAMIC_DRAW
(数据会被频繁写入和绘制使用,数据是一次性的或短暂使用的,如爆炸效果)。即使传入错误的值,也仅仅是会降低程序运行的效率。
至此,我们已经把 js 中的顶点数据传给了 webgl 中的缓冲区对象。
设置 attribute 变量
接着就是使用 vertexAttribPointer()
,让显卡从当前绑定到 gl.ARRAY_BUFFER
的缓冲区对象中去读取顶点数据,分配给 attribute 变量 aPosition
:
// 代码片段一
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)
vertexAttribPointer()
方法有好几个参数:
- 第 1 个是 attribute 的存储位置;
- 第 2 个参数为每个顶点的分量个数,从 1 到 4,缺失的分量会自动补全。比如此处为每 2 个元素代表一个点,则第 3 个分量值自动设为 0,第 4 个分量值为 1;
- 第 3 个用于指定数据的格式,我们在
buffer
里存的数据都是单精度 32 位浮点数,所以这里传的枚举值为gl.FLOAT
(浮点型); - 第 4 个表示是否将值归一化,当第 3 个参数传了
gl.FLOAT
时该参数无效,直接传false
即可; - 对于第 5、6 个参数,此处先均为
0
,在文章的最后介绍绘制不同大小的点时再对它们进行说明。
激活 attribute 变量与绘制
至此,缓冲区对象和 attribute 变量 aPosition
之间的连接,可以想象成还有个开关没有闭合:
需要激活 attribute 变量:
gl.enableVertexAttribArray(aPosition)
最后执行绘制,传入的 3
表示要绘制 3 个点:
gl.drawArrays(gl.POINTS, 0, 3)
绘制三角形与三角面
绘制好了 3 个点,再绘制三角形或三角面就很简单了。首先可以将控制点大小的 gl_PointSize = 24.0;
删除,它已经无效了。
三角形
在绘制三角形时只需将传给 gl.drawArrays()
的第 1 个参数,绘制方式 mode 改为 gl.LINE_LOOP
,表示绘制的是闭合线条即可:
gl.drawArrays(gl.LINE_LOOP, 0, 3)
效果如下:
如果传 gl.LINE_STRIP
则绘制的是一系列连接的线段,但是最后 1 个点到第 1 个点之间不会连接:
如果传 gl.LINES
则是每 2 个点绘制 1 个线段。我们提供了 3 个点,所以只会画 1 条线段:
三角面
绘制三角面就是将 mode 改为 gl.TRIANGLES
即可:
gl.drawArrays(gl.TRIANGLES, 0, 3)
三角带
另外,mode 还可以传递 gl.TRIANGLE_STRIP
表明绘制的是一系列条带状的三角面,比如 points
添加一个顶点的数据,并且在 gl.drawArrays()
绘制时,第 3 个参数指定需要用到 4 个点:
const points = new Float32Array([0,0.5,-0.5,-0.5,1,0.5,0.5,-0.5])
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
则效果如下,其绘制的三角面数量为顶点数 - 2
:
三角扇
如果传入的 mode 为 gl.TRIANGLE_FAN
,然后将顶点数据做些修改:
const points = new Float32Array([0, -0.5, -0.5, 0.3, 0, 0.5, 0.5, 0.3])
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
则绘制的会是三角扇,其绘制的三角面数量也为顶点数 - 2
:
绘制不同大小的点
如果要绘制多个大小不同的点,该怎么办呢?此时可以设置个 attribute 变量 aPointSize
赋给 gl_PointSize
:
<script type="notjs" id="vsSource">
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
// 点的坐标
gl_Position = aPosition;
// 点的大小
gl_PointSize = aPointSize;
}
</script>
更改 points
的值,使其不仅包含顶点位置属性(下面数组中每一行的前两个元素),也包含顶点大小属性(下面数组中每一行的第 3 个元素):
const points = new Float32Array([
-0.5, -0.5, 10,
0, 0.5, 20,
0.5, -0.5, 30
])
然后同样需要获取 aPointSize
的内存地址,并通过 vertexAttribPointer()
使其赋值:
// 代码片段二
const aPointSize = gl.getAttribLocation(program, 'aPointSize')
// 获取 points 中每个元素所占用的字节数
const BYTES = points.BYTES_PER_ELEMENT // 4
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, BYTES * 2)
gl.enableVertexAttribArray(aPointSize)
数据偏移
现在来介绍下之前没有说的 vertexAttribPointer()
的第 5、6 个参数:
- 第 5 个参数用于以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度):
points
是 new Float32Array
得到的强类型数组,其有个属性 BYTES_PER_ELEMENT
可以获取数组中每个元素所占用的字节数,因为是 32 位浮点数,所以也就是 32 / 8 = 4 字节。代码片段一对应的 points
里只有顶点位置属性,是紧密打包的,没有交错属性,所以传 0
(或者也可以传 BYTES * 2
);代码片段二对应的 points
则是每 3 个元素描述一个顶点,所以传 BYTES * 3
。
- 第 6 个参数指定数据的偏移量。数组中一行的开头就是位置属性,所以涉及
aPosition
时,代码片段一和代码片段二都传了0
;而顶点大小属性,则是数组中一行的第 3 个元素,前面还有 2 个元素, 所以在代码片段二中,设置aPointSize
时传入的是BYTES * 2
。