WebGL 使用缓冲区绘制点线面

367 阅读6分钟

之前我们学习了如何动态地绘制一个点,本文就以之为基础,继续介绍如何使用 webgl 绘制多个点,进而绘制最简单,同时也是最稳定的面 —— 三角面。其实 webgl 中的三维模型都是由一个个的小三角面组合而成的,要绘制面,首先需要绘制多个点,然后再连点成线,最后在线性图形内部逐个片元填上颜色就形成了面。

绘制 3 个点

准备缓冲区对象

要绘制多个点,就不太好像绘制 1 个点的时候那样使用 gl.vertexAttrib4f() 直接将顶点数据传递给着色器。而是需准备一个容器来存储各个顶点的数据,这个容器就是缓冲区对象,它是 webgl 中的一块内存区域,之后就可以通过缓冲区对象一次性向着色器传入多个顶点数据:

const buffer = gl.createBuffer()

webgl 可以通过绑定点 —— gl.ARRAY_BUFFERgl.ELEMENT_ARRAY_BUFFER —— 操控全局范围内的许多数据,可以把绑定点想象成一个 webgl 内部的全局变量。通过 bindBuffer() 方法将缓冲区对象绑定到绑定点:

gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
  • 第 1 个参数 target 传递 gl.ARRAY_BUFFER 意为缓冲区存储的是顶点的数据(顶点坐标等)。如果传递 gl.ELEMENT_ARRAY_BUFFER 表明存储的数据是顶点的索引值;
  • 第 2 个参数就是创建好的缓冲区对象。

01.png

将数据存入缓冲区对象

准备好了缓冲区,就可以使用 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 之间的连接,可以想象成还有个开关没有闭合:

02.png

需要激活 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)

效果如下:

1.png

如果传 gl.LINE_STRIP 则绘制的是一系列连接的线段,但是最后 1 个点到第 1 个点之间不会连接:

2.png

如果传 gl.LINES 则是每 2 个点绘制 1 个线段。我们提供了 3 个点,所以只会画 1 条线段:

2024-10-23_155901.png

三角面

绘制三角面就是将 mode 改为 gl.TRIANGLES 即可:

gl.drawArrays(gl.TRIANGLES, 0, 3)

3.png

三角带

另外,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

2024-10-24_112113.png

三角扇

如果传入的 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

5.png

绘制不同大小的点

如果要绘制多个大小不同的点,该怎么办呢?此时可以设置个 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 个参数用于以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度):

6.png

pointsnew Float32Array 得到的强类型数组,其有个属性 BYTES_PER_ELEMENT 可以获取数组中每个元素所占用的字节数,因为是 32 位浮点数,所以也就是 32 / 8 = 4 字节。代码片段一对应的 points 里只有顶点位置属性,是紧密打包的,没有交错属性,所以传 0(或者也可以传 BYTES * 2);代码片段二对应的 points 则是每 3 个元素描述一个顶点,所以传 BYTES * 3

  • 第 6 个参数指定数据的偏移量。数组中一行的开头就是位置属性,所以涉及 aPosition 时,代码片段一和代码片段二都传了 0;而顶点大小属性,则是数组中一行的第 3 个元素,前面还有 2 个元素, 所以在代码片段二中,设置 aPointSize 时传入的是 BYTES * 2

感谢.gif 点赞.png