前言
之前我们已经在《WebGL 使用缓冲区绘制点线面》中通过缓冲区绘制了 webgl 中的基本图元 —— 点、线段和三角形,本文则主要介绍 webgl 中绘制矩形的几种方式。
一个矩形可以由 2 个三角形拼接而成,本文的目标是在一个宽为 400,高为 200 的 canvas 画布上,居中绘制一个长为 200,高为 100 的矩形:
具体的实现可以通过顶点法或是索引法,下面分别介绍。
顶点法
gl.TRIANGLES
在《WebGL 使用缓冲区绘制点线面》一文中我们绘制三角形使用的都是顶点法,绘制图 1 所示的矩形,只需要让写入缓冲区的数据 points 中传入的数据由原本的代表 3 个点坐标信息的 6 个元素,增加到代表 6 个点坐标信息的 12 个元素。注意,我们这只是绘制了 1 个二维的平面,webgl 主要是绘制三维图形的,在三维图形中,1 个面可能是面向我们的“正面”,也可能是“背面”。一般在输入正面的顶点信息时,都是按照逆时针的顺序输入。反之,背面则是按照顺时针的顺序:
const points = new Float32Array([
100, 50, 100, 150, 300, 50, 100, 150, 300, 150, 300, 50
])
然后在执行绘制时,让第 3 个参数,即绘制时需要用到的点的数量改为 6:
gl.drawArrays(gl.TRIANGLES, 0, 6)
即可绘制出矩形:
不难发现,v1、v2 点我们传入了 2 次。
坐标转换
与 《WebGL 使用缓冲区绘制点线面》中使用 webgl 坐标系不同,这次传入 points 的数据我采用的是 canvas 坐标系,这就需要对传入的数据进行坐标的转换,因为最终传给 gl_Position 的数据还得是参照 webgl 坐标系。转换的关键,就在于比例 —— 假设有 1 点,其 webgl 坐标系下的坐标为 (x, y),那么就 x 轴为例,这个点到 canvas 画布的最左边的距离,与 canvas 画布的宽度的比例,无论在哪个坐标系下,应该是一样的。
canvas 画布的宽高我通过 uniform 变量 uCanvasSize 传入:
const uCanvasSize = gl.getUniformLocation(program, 'uCanvasSize')
gl.uniform2f(uCanvasSize, canvas.width, canvas.height)
x 轴
我们现在传给 aPosition 的值都是 canvas 坐标系的值,所以在 canvas 坐标系中,点 (x, y) 到 canvas 画布的最左边的距离,为 aPosition.x。其与 canvas 画布的宽度的比例,为 aPosition.x / uCanvasSize.x;
在 webgl 坐标系中,点 (x, y) 到 canvas 画布的最左边的距离,为 1+x。其与 canvas 画布的宽度的比例,为 (1+x) / 2;
它们的比例应该是相同的,即 (1+x) / 2 = aPosition.x / uCanvasSize.x,则 x = aPosition.x * 2 / uCanvasSize.x - 1。
y 轴
同理可得,点 (x, y) 到 canvas 画布的最上边的距离,与 canvas 画布的高度的比例也是相同的。 即 aPosition.y / uCanvasSize.y = (1-y) / 2,可推导出 y = 1 - aPosition.y * 2 / uCanvasSize.y。
gl.TRIANGLE_STRIP
如果选择使用三角带绘制,则只需传入 4 个点的坐标信息,绘制时第 1 个三角形使用 v0、v1、v2;第 2 个三角形使用 v1、v2,v3 绘制:
const points = new Float32Array([100, 50, 100, 150, 300, 50, 300, 150])
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.TRIANGLE_FAN
如果图元使用三角扇,则需要新增 1 个点 v4,坐标为 (200, 100):
绘制时,v4、v0、v1 绘制一个三角形,v4、v1、v3 绘制一个三角形,v4、v3、v2 绘制一个三角形,v4、v2、v0 绘制一个三角形,所以 v0 需要传入 2 次:
const points = new Float32Array([
200, 100, // v4
100, 50, // v0
100, 150, // v1
300, 150, // v3
300, 50, // v2
100, 50 // v0
])
gl.drawArrays(gl.TRIANGLE_FAN, 0, 6)
索引法
通过上文可以看出,在使用顶点法时,向缓冲区存入的顶点数据,会有重复的情况。如果绘制的图形比较复杂,重复的数据就会占用更多的内存,我们可以通过索引法来解决这一问题。
索引法在绘制时使用的方法为gl.drawElements(mode, count, type, offset):
- mode 同
gl.drawArrays()中的第 1 个参数,用于指定图元类型,我就以传入gl.TRIANGLES为例,介绍索引法的用法; - count 用于指定绘制图形的顶点个数。我们要绘制矩形,图元又为
gl.TRIANGLES,所以需要 6 个点; - type 指定索引缓冲区中数据的格式,常用值为
gl.UNSIGNED_BYTE(数据范围从 0 到 255,即 8 位无符号整数) 和gl.UNSIGNED_SHORT(数据范围从 0 到 65535,即 16 位无符号整数); - offset 为索引数组中开始绘制的位置,以字节单位。
使用索引法时,points 中只需要 4 个点的坐标信息即可:
const points = new Float32Array([
100, 50, // v0
100, 150, // v1
300, 50, // v2
300, 150 // v3
])
另外我们需要创建顶点索引缓冲区对象,绑定时传入 gl.ELEMENT_ARRAY_BUFFER:
const elementBuffer = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementBuffer)
向缓冲区写入的顶点的索引数据,使用 new Uint8Array() 传入,让数据格式为 8 位无符号整型:
gl.bufferData(
gl.ELEMENT_ARRAY_BUFFER,
new Uint8Array([0, 1, 2, 1, 3, 2]),
gl.STATIC_DRAW
)
绑定了 gl.ELEMENT_ARRAY_BUFFER 的缓冲区是不需要进行分配或激活 attribute 变量的操作的,直接执行绘制即可:
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0)
