WebGL 正交 3D

Gregg Tavares
Gregg Tavares

WebGL 正交 3D

这篇博文是一系列关于 WebGL 的博文的延续。第一篇从基础知识开始,上一篇介绍关于 2D 矩阵的 2D 矩阵。如果您未阅读这些报告,请先查看。 在上一篇博文中,我们介绍了二维矩阵的工作原理。我们讨论了平移、旋转、缩放,甚至从像素投影到剪辑空间,所有这些操作都可以通过 1 个矩阵和一些魔法矩阵数学完成。要打造 3D 模型,还有一小步就要完成了。 在之前的 2D 示例中,我们获得了 2D 点 (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 轴上旋转 1 个点,因为我们实际上只是绕 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 像素,高度为 0 像素,高度为 0 像素,但深度为 -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 个顶点 x 每个三角形有 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>

我们需要在 fragment 着色器中使用该颜色

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

已经很接近了,但还有一个问题。即使所有三角形都朝向正确的方向,并且朝后的三角形已被剔除,我们仍然有一些位置应在应该位于前面的三角形之上绘制应该在后面的三角形。 输入“DEPTH BUFFER”。 深度缓冲区(有时称为 Z-Buffer)是一个包含 depth 像素的矩形,用于制作图片的每个颜色像素对应一个深度像素。在绘制每个颜色像素时,WebGL 也可以绘制深度像素。它基于我们从 Z 的顶点着色器返回的值执行此操作。就像我们必须针对 X 和 Y 转换为裁剪空间一样,因此 Z 位于裁剪空间内或(-1 到 +1)。然后将该值转换为深度空间值(0 到 +1)。在绘制颜色像素之前,WebGL 会检查相应的深度像素。如果它要绘制的像素的深度值大于相应深度像素的值,则 WebGL 不会绘制新的颜色像素。否则,它会使用 fragment 着色器中的颜色绘制新的颜色像素,并使用新的深度值绘制深度像素。这意味着,系统不会绘制位于其他像素后的像素。启用这一功能几乎就像使用

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

如果您注重细节,您可能已经注意到,我们将 2 个属性定义为

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,这是矩阵数学计算所需的值。