之前的几篇文章我们一直在用 webgl 绘制一些 2 维的点线面,本篇文章开始介绍如何使用 webgl 绘制一个如下我使用 c4d 创建的正方体:
该正方体的 8 个顶点的坐标(右手坐标系)分别为:
v0: [-0.5, 0.5, 0.5]
v1: [0.5, 0.5, 0.5]
v2: [0.5, -0.5, 0.5]
v3: [-0.5, -0.5, 0.5]
v4: [-0.5, 0.5, -0.5]
v5: [0.5, 0.5, -0.5]
v6: [0.5, -0.5, -0.5]
v7: [-0.5, -0.5, -0.5]
顶点法
我们按照之前在《WebGL 绘制矩形的几种方法》中说过的顶点法,将各个面的顶点数据都传入 points
:
// 代码片段 1.1
const v0 = [-0.5, 0.5, 0.5]
const v1 = [0.5, 0.5, 0.5]
const v2 = [0.5, -0.5, 0.5]
const v3 = [-0.5, -0.5, 0.5]
const v4 = [-0.5, 0.5, -0.5]
const v5 = [0.5, 0.5, -0.5]
const v6 = [0.5, -0.5, -0.5]
const v7 = [-0.5, -0.5, -0.5]
const points = new Float32Array([
// 前面
...v4,
...v7,
...v5,
...v5,
...v7,
...v6,
// 后面
...v0,
...v1,
...v3,
...v1,
...v2,
...v3,
// 左面
...v4,
...v0,
...v7,
...v7,
...v0,
...v3,
// 右面
...v1,
...v5,
...v6,
...v6,
...v2,
...v1,
// 上面
...v4,
...v5,
...v1,
...v4,
...v1,
...v0,
// 下面
...v6,
...v7,
...v2,
...v2,
...v7,
...v3
])
坐标系
需要注意的是,这些顶点坐标到时候是传递给 gl_Position
的,而 gl_Position
接收的点的坐标,参照的应该是归一化设备坐标(NDC)系。NDC 坐标系上的点的坐标,又是由裁剪坐标系中的点,其 x、y、z 分量,各自除去最后一个分量 w 得到的。根据 MDN 上的说法,裁剪坐标系如下,z 轴应该是朝向屏幕内的(左手坐标系):
所以,在不进行任何其它转换的情况下,我们能看到的面向我们那个面(前面,为白色正方形),应该是由顶点 v4-v5-v6-v7 组成,它们的 z 轴坐标都为 -0.5(图示各个点只是表明大致位置,坐标并不准确)。
正反面
在 webgl 中的三角形有正反面的概念,正面三角形的顶点顺序是逆时针方向的,而反面三角形的顶点是顺时针方向,我们可以通过:
gl.enable(gl.CULL_FACE)
让 webgl 只绘制正面三角形。此时,对于前面(白色面),传入 points
的顶点顺序就应该是逆时针的。对于后面(黄色面),当立方体旋转到我们能看到的角度时,我们看到的其实是反面,所以传入 points
的顶点顺序应该是顺时针的。左面(蓝色面),因为 x 轴是向右的,所以能看到的应该是正面。右面(绿色面)能看到的应该是反面。
效果
下面是使用在《控制图形的形变》中介绍的旋转矩阵让立方体绕 X 轴和 Y 轴逆时针旋转 30° 后,绘制生成的立方体:
代码中添加了 gl.enable(gl.DEPTH_TEST)
,其目的在于让 webgl 不去绘制处于背面的像素。如果不添加,则在不旋转且没有添加 gl.enable(gl.CULL_FACE)
的情况下,我们看到的会是后绘制的后面,也就是一个黄色正方形。至于 6 个面的颜色是怎么添加的,后文在介绍索引法时会讲解,可以参看源码类比。
索引法
同《WebGL 绘制矩形的几种方法》中说过的索引法,无非是多了几个面而已。
基本绘制
顶点数据
像代码片段 1.1 中这样将每个面用到的点都存入 points
,各个点会被多次重复存入,有些繁琐。如果使用索引法绘制正方体则可以只将需要用到的点存入 points
一次即可:
// 代码片段 2
const points = new Float32Array([
...v0,
...v1,
...v2,
...v3,
...v4,
...v5,
...v6,
...v7
])
索引数据
然后再创建顶点的索引集合数据 indices
,按 webgl 规范要使用 new Uint8Array
创建,Uint8Array
数组类型表示一个 8 位无符号整型数组:
// 代码片段 2.1
const indices = new Uint8Array([
// 前面
4, 7, 6, 4, 6, 5,
// 后面
0, 1, 2, 0, 2, 3,
// 左面
0, 3, 7, 0, 7, 4,
// 右面
1, 5, 6, 1, 6, 2,
// 上面
4, 5, 1, 4, 1, 0,
// 下面
7, 3, 2, 7, 2, 6
])
indices
也要放进缓冲区对象。注意在 gl.bindBuffer()
和 gl.bufferData()
时,传入的第 1 个参数为 gl.ELEMENT_ARRAY_BUFFER
,表示存储的是顶点索引数据:
// 创建缓冲区对象
const indexBuffer = gl.createBuffer()
// 绑定缓冲区对象
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
// 将数据存入缓冲区对象
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)
最后在执行绘制时,使用的是 gl.drawElements()
方法,表示使用顶点索引的方式绘图:
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0)
它有 4 个参数:
- 第 1 个为
mode
,值同gl.drawArrays()
; - 第 2 个为
count
,传入要绘制的顶点的数量,本例中就是传入indices
数组中元素的个数; - 第 3 个为
type
,代表缓冲区数据的数据类型,gl.UNSIGNED_BYTE
意为类型为无符号 8 位整数; - 第 4 个为
offset
,代表索引数组开始绘制的位置。
这样也能绘制出正方体如下:
给 6 个面添加不同颜色
顶点数据
使用索引法绘制正方体时,如果想给正方体的 6 个面都添加上不同的颜色,就需要改变一下代码片段 2 里 points
的值,不再是仅列出需要用到的顶点坐标,而是按照各个面需要用到的顶点,通通排列出来(注意和代码片段 1.1 对比):
// 代码片段 2.2
const points = new Float32Array([
// 前面
...v4, // 0
...v7, // 1
...v6, // 2
...v5, // 3
// 后面
...v0, // 4
...v1, // 5
...v2, // 6
...v3, // 7
// 左面
...v0, // 8
...v3, // 9
...v7, // 10
...v4, // 11
// 右面
...v1, // 12
...v5, // 13
...v6, // 14
...v2, // 15
// 上面
...v4, // 16
...v5, // 17
...v1, // 18
...v0, // 19
// 下面
...v6, // 20
...v7, // 21
...v3, // 22
...v2 // 23
])
颜色数据
然后准备颜色数据:
const white = [1, 1, 1]
const red = [1, 0, 0]
const green = [0, 1, 0]
const blue = [0, 0, 1]
const yellow = [1, 1, 0]
const purple = [1, 0, 1]
const colors = new Float32Array([
// 前面
...white,
...white,
...white,
...white,
// 后面
...yellow,
...yellow,
...yellow,
...yellow,
// 左面
...blue,
...blue,
...blue,
...blue,
// 右面
...green,
...green,
...green,
...green,
// 上面
...red,
...red,
...red,
...red,
// 下面
...purple,
...purple,
...purple,
...purple
])
一个顶点就需要对应一个颜色,虽然会有重复数据,但好在目前顶点也不是很多。颜色数据同样使用缓冲区对象存储,然后让 aColor
从缓冲区对象中去获取:
const aColor = gl.getAttribLocation(program, 'aColor')
const colorBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)
gl.vertexAttribPointer(aColor, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(aColor)
aColor
是定义在顶点着色器源码里的 attribute
变量,然后通过将 aColor
赋值给 varying
变量 vColor
,最后在片元着色器源码中将 vColor
赋给 gl_FragColor
从而定义点的颜色。
索引数据
改变了顶点数据,那么对于代码片段 2.1 中的索引数据 indices
当然也得更改:
const indices = new Uint8Array([
// 前面
0, 1, 2, 0, 2, 3,
// 后面
4, 5, 6, 4, 6, 7,
// 左面
8, 9, 10, 8, 10, 11,
// 右面
12, 13, 14, 12, 14, 15,
// 上面
16, 17, 18, 16, 18, 19,
// 下面
20, 21, 22, 20, 22, 23
])
这些数字对应的就是代码片段 2.2 里的那些顶点数据后面的注释,比如 4
就是后面的 v0 点,而 8
就是左面的 v0 点。
如此,就可以得到一个 6 面不同颜色的正方体如下: