WebGL 绘制彩色正方体

207 阅读6分钟

之前的几篇文章我们一直在用 webgl 绘制一些 2 维的点线面,本篇文章开始介绍如何使用 webgl 绘制一个如下我使用 c4d 创建的正方体:

2024-04-16_102452.png

该正方体的 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 轴应该是朝向屏幕内的(左手坐标系):

1.png

所以,在不进行任何其它转换的情况下,我们能看到的面向我们那个面(前面,为白色正方形),应该是由顶点 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 面不同颜色的正方体如下:

感谢.gif 点赞.png