WebGL 正交 3D
本文是关于 WebGL 的系列文章的后续内容。 第一个视频从基础知识开始,上一个视频介绍了 2D 矩阵。如果您尚未阅读这些文章,请先阅读。 在上一篇文章中,我们介绍了二维矩阵的运作方式。我们已经讨论过,平移、旋转、缩放,甚至从像素投影到剪辑空间,都可以通过 1 个矩阵和一些神奇的矩阵数学运算来完成。从那里再迈出一步,就可以制作 3D 内容了。 在之前的二维示例中,我们将二维点 (x, y) 与 3x3 矩阵相乘。如需进行 3D 处理,我们需要 3D 点 (x, y, z) 和 4x4 矩阵。我们将上一个示例改为 3D 示例。我们将再次使用 F,但这次是 3D“F”。 我们首先需要做的是更改顶点着色器以处理 3D 内容。下面是旧着色器。
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
uniform mat3 u_matrix;
void main() {
// Multiply the position by the matrix.
gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>
这是新版
<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
uniform mat4 u_matrix;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
}
</script>
操作变得更加简单! 然后,我们需要提供 3D 数据。
...
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
...
// Fill the buffer with the values that define a letter 'F'.
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
// left column
0, 0, 0,
30, 0, 0,
0, 150, 0,
0, 150, 0,
30, 0, 0,
30, 150, 0,
// top rung
30, 0, 0,
100, 0, 0,
30, 30, 0,
30, 30, 0,
100, 0, 0,
100, 30, 0,
// middle rung
30, 60, 0,
67, 60, 0,
30, 90, 0,
30, 90, 0,
67, 60, 0,
67, 90, 0]),
gl.STATIC_DRAW);
}
接下来,我们需要将所有矩阵函数从 2D 更改为 3D。下面是 makeTranslation、makeRotation 和 makeScale 的 2D(之前)版本
function makeTranslation(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}
function makeRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c,-s, 0,
s, c, 0,
0, 0, 1
];
}
function makeScale(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}
下面是更新后的 3D 版本。
function makeTranslation(tx, ty, tz) {
return [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
tx, ty, tz, 1
];
}
function makeXRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
1, 0, 0, 0,
0, c, s, 0,
0, -s, c, 0,
0, 0, 0, 1
];
};
function makeYRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
];
};
function makeZRotation(angleInRadians) {
var c = Math.cos(angleInRadians);
var s = Math.sin(angleInRadians);
return [
c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
];
}
function makeScale(sx, sy, sz) {
return [
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1,
];
}
请注意,我们现在有 3 个旋转函数。在 2D 中,我们只需要一个,因为我们实际上只围绕 Z 轴旋转。不过,为了实现 3D 效果,我们还希望能够围绕 x 轴和 y 轴旋转。从这些图表中可以看出,它们都非常相似。如果我们计算这些值,您会发现它们会像之前一样简化
Z 旋转
newX = x * c + y * s;
newY = x * -s + y * c;
Y 旋转
newX = x * c + z * s;
newZ = x * -s + z * c;
X 旋转
newY = y * c + z * s;
newZ = y * -s + z * c;
我们还需要更新投影函数。这是旧版
function make2DProjection(width, height) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}
从像素转换为剪辑空间。在首次尝试将其扩展到 3D 时,我们可以尝试
function make2DProjection(width, height, depth) {
// Note: This matrix flips the Y axis so 0 is at the top.
return [
2 / width, 0, 0, 0,
0, -2 / height, 0, 0,
0, 0, 2 / depth, 0,
-1, 1, 0, 1,
];
}
就像我们需要将 x 和 y 从像素转换为剪辑空间一样,我们也需要对 z 执行相同的操作。在本例中,我还会创建 Z 空间像素单位。我会为深度传入类似于 width
的值,因此我们的空间将从 0 到 width 像素宽,从 0 到 height 像素高,但深度将从 -depth / 2 到 +depth / 2。最后,我们需要更新用于计算矩阵的代码。
// Compute the matrices
var projectionMatrix =
make2DProjection(canvas.width, canvas.height, canvas.width);
var translationMatrix =
makeTranslation(translation[0], translation[1], translation[2]);
var rotationXMatrix = makeXRotation(rotation[0]);
var rotationYMatrix = makeYRotation(rotation[1]);
var rotationZMatrix = makeZRotation(rotation[2]);
var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);
// Multiply the matrices.
var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, projectionMatrix);
// Set the matrix.
gl.uniformMatrix4fv(matrixLocation, false, matrix);
我们遇到的第一个问题是,我们的几何图形是扁平的 F,因此很难看到任何 3D 图形。为了解决这个问题,我们将几何图形扩展为 3D 图形。我们当前的 F 由 3 个矩形组成,每个矩形包含 2 个三角形。如需将其制作成 3D 图形,总共需要 16 个矩形。这里列出的只是其中一部分。16 个矩形 x 每个矩形 2 个三角形 x 每个三角形 3 个顶点 = 96 个顶点。如果您想查看所有这些信息,请查看示例的源代码。我们必须绘制更多顶点,以便
// Draw the geometry.
gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);
移动滑块时,很难看出它是 3D 图形。我们来尝试为每个矩形着色不同的颜色。为此,我们将向顶点着色器添加另一个属性,并添加一个可变项,以便将其从顶点着色器传递到片段着色器。以下是新的顶点着色器
<script id="3d-vertex-shader" type="x-shader/x-vertex">
attribute vec4 a_position;
attribute vec4 a_color;
uniform mat4 u_matrix;
varying vec4 v_color;
void main() {
// Multiply the position by the matrix.
gl_Position = u_matrix * a_position;
// Pass the color to the fragment shader.
v_color = a_color;
}
</script>
我们需要在片段着色器中使用该颜色
<script id="3d-vertex-shader" type="x-shader/x-fragment">
precision mediump float;
// Passed in from the vertex shader.
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
</script>
我们需要查找位置以提供颜色,然后设置另一个缓冲区和属性以向其提供颜色。
...
var colorLocation = gl.getAttribLocation(program, "a_color");
...
// Create a buffer for colors.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(colorLocation);
// We'll supply RGB as bytes.
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
// Set Colors.
setColors(gl);
...
// Fill the buffer with colors for the 'F'.
function setColors(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array([
// left column front
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
200, 70, 120,
// top rung front
200, 70, 120,
200, 70, 120,
...
...
gl.STATIC_DRAW);
}
糟糕,这是什么情况?事实证明,3D“F”的所有不同部分(正面、背面、侧面等)都会按照它们在几何图形中显示的顺序进行绘制。但这并不能给出我们想要的结果,因为后面的元素有时会在前面的元素之后绘制。WebGL 中的三角形具有正面和背面的概念。朝向前方的三角形的顶点顺时针排列。背面朝上的三角形的顶点是逆时针方向。
WebGL 只能绘制朝向前方或朝向后方的三角形。我们可以通过以下方式开启该功能
gl.enable(gl.CULL_FACE);
我们只需在程序开始时执行一次此操作。启用该功能后,WebGL 会默认“剔除”朝向后面的三角形。在本例中,“Culling”是“不绘制”的别称。请注意,就 WebGL 而言,三角形是顺时针还是逆时针取决于该三角形在剪裁空间中的顶点。换句话说,在您对顶点着色器中的顶点应用数学运算后,WebGL 会确定三角形是朝前还是朝后。这意味着,例如,在 X 轴上按 -1 缩放的顺时针三角形会变成逆时针三角形,或者围绕 X 轴或 Y 轴旋转 180 度的顺时针三角形会变成逆时针三角形。由于我们停用了 CULL_FACE,因此可以看到顺时针(正面)和逆时针(背面)三角形。现在,我们已启用该功能,因此每当正面朝向的三角形因缩放、旋转或任何其他原因而翻转时,WebGL 都不会绘制它。这很好,因为当您在 3D 中旋转某个对象时,通常希望朝向您的三角形被视为朝向前方。
您好!所有三角形都去哪儿了?事实证明,其中许多设备的方向都错了。旋转手机,当您查看另一面时,就会看到这些字符。幸运的是,这个问题很容易解决。我们只需查看哪些顶点是反向的,然后交换其中 2 个顶点即可。例如,如果一个向后三角形是
1, 2, 3,
40, 50, 60,
700, 800, 900,
我们只需翻转最后 2 个顶点,即可使其向前。
1, 2, 3,
700, 800, 900,
40, 50, 60,
这样就更接近了,但还有一个问题。即使所有三角形都朝向正确的方向,并且系统会剔除朝向后面的三角形,但仍有部分三角形应该位于后面,但却被绘制在应该位于前面的三角形上。输入深度缓冲区。
深度缓冲区(有时称为 Z 缓冲区)是一个由 depth
个像素组成的矩形,其中每个深度像素对应于用于创建图像的每个颜色像素。在 WebGL 绘制每个颜色像素时,它还可以绘制深度像素。它会根据我们从顶点着色器返回的 Z 值执行此操作。就像我们必须将 X 和 Y 转换为剪辑空间一样,Z 也位于剪辑空间或(-1 到 +1)中。然后,该值会转换为深度空间值(0 到 +1)。在 WebGL 绘制颜色像素之前,它会检查相应的深度像素。如果其要绘制的像素的深度值大于相应深度像素的值,则 WebGL 不会绘制新的颜色像素。否则,它会使用来自片段着色器的颜色绘制新的颜色像素,并使用新的深度值绘制深度像素。这意味着,位于其他像素后面的像素不会绘制。开启此功能的操作与使用
gl.enable(gl.DEPTH_TEST);
我们还需要先将深度缓冲区清除为 1.0,然后才能开始绘制。
// Draw the scene.
function drawScene() {
// Clear the canvas AND the depth buffer.
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...
在下一篇文章中,我将介绍如何使其具有透视效果。
为什么属性是 vec4,但 gl.vertexAttribPointer 大小为 3
注重细节的读者可能已经注意到,我们将这两个属性定义为
attribute vec4 a_position;
attribute vec4 a_color;
这两个参数都是“vec4”,但当我们告知 WebGL 如何从缓冲区中提取数据时,我们使用了
gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribPointer(colorLocation, 3, gl.UNSIGNED_BYTE, true, 0, 0);
其中每个值中的“3”表示只提取每个属性的 3 个值。之所以能这样,是因为在顶点着色器中,WebGL 会为您未提供的值提供默认值。默认值为 0, 0, 0, 1,其中 x = 0、y = 0、z = 0 和 w = 1。这就是为什么在旧的 2D 顶点着色器中,我们必须显式提供 1。我们传入了 x 和 y,并且需要为 z 提供 1,但由于 z 的默认值为 0,因此我们必须显式提供 1。不过,对于 3D 图形,即使我们未提供“w”,它也会默认设为 1,这是矩阵运算正常运行所需的值。