WebGL Orthographic 3D

发布于 2022-03-24 12:49:53 字数 9876 浏览 1166 评论 0

这篇文章是一系列关于 WebGL 的文章的延续。
第一个 从基础开始, 前一个是关于 2d 矩阵的 关于 2D 矩阵 的。
如果您还没有阅读这些内容,请先查看它们。

在上一篇文章中,我们讨论了二维矩阵的工作原理。 我们谈到了平移、旋转、缩放,甚至从像素投影到剪辑空间都可以通过 1 个矩阵和一些魔法矩阵数学来完成。 做 3D 只是从那里迈出的一小步。

在我们之前的 2D 示例中,我们将 2D 点 (x, y) 乘以 3×3 矩阵。 要进行 3D,我们需要 3D 点 (x, y, z) 和 4×4 矩阵。

让我们以最后一个示例为例,将其更改为 3D。 我们将再次使用 F,但这次是 3D 'F'。

我们需要做的第一件事是更改顶点着色器以处理 3D。 这是旧的着色器。

<script 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 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 个旋转函数。 我们只需要一个二维的,因为我们实际上只围绕 Z 轴旋转。 现在虽然要做 3D,但我们也希望能够绕 x 轴和 y 轴旋转。 你可以看到他们都非常相似。 如果我们要解决它们,您会看到它们像以前一样简化

Z rotation

newX = x * c + y * s;
newY = x * -s + y * c;

Y rotation

newX = x * c + z * s;
newZ = x * -s + z * c;

X rotation

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 空间像素单元。 我将为深度传递一些类似于 宽度 ,因此我们的空间将为 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 个矩形 * 每个矩形 2 个三角形 * 每个三角形 3 个顶点是 96 个顶点。 如果您想查看所有这些示例,请查看示例中的源代码。

我们必须绘制更多的顶点,所以

    // Draw the geometry.
    gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

这是那个版本

移动滑块很难判断它是 3D。 让我们尝试为每个矩形涂上不同的颜色。 为此,我们将向顶点着色器添加另一个属性和一个变量,以将其从顶点着色器传递到片段着色器。

这是新的顶点着色器

<script 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 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 默认会“剔除”背面三角形。
在这种情况下,“剔除”是“不绘图”的一个花哨的词。

请注意,就 WebGL 而言,三角形是顺时针还是逆时针取决于该三角形在剪辑空间中的顶点。 换句话说,WebGL 会在您对顶点着色器中的顶点应用数学运算之后确定一个三角形是前还是后。 这意味着例如,在 X 中按 -1 缩放的顺时针三角形变为逆时针三角形,或者围绕 X 或 Y 轴旋转 180 度的顺时针三角形变为逆时针三角形。
因为我们禁用了 CULL_FACE,我们可以看到顺时针(前)和逆时针(后)三角形。 现在我们已经打开了它,任何时候正面三角形由于缩放或旋转或任何原因而翻转,WebGL 都不会绘制它。
这是一件好事,因为当您在 3D 中转身时,您通常希望将面向您的三角形视为正面。

嘿! 所有的三角形都去哪儿了? 事实证明,他们中的许多人都面临着错误的方向。 旋转它,当你看另一边时,你会看到它们出现。 幸运的是,它很容易修复。 我们只看哪些是后向的并交换它们的 2 个顶点。 例如,如果一个后向三角形是

           1,   2,   3,
          40,  50,  60,
         700, 800, 900,

we just flip the last 2 vertices to make it forward.

           1,   2,   3,
         700, 800, 900,
          40,  50,  60,

这更接近了,但还有一个问题。 即使所有的三角形都朝向正确的方向并且背面的三角形被剔除,我们仍然有一些地方应该在后面的三角形被绘制在应该在前面的三角形上。

输入深度缓冲区。

深度缓冲区,有时称为 Z 缓冲区,是一个由 深度 像素组成的矩形,用于制作图像的每个颜色像素对应一个深度像素。 当 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

对于那些注重细节的人,您可能已经注意到我们将 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 并且我们需要一个 1 来代表 z,但是因为 z 的默认值是 0,所以我们必须显式地提供一个 1。尽管对于 3D,即使我们不提供一个 'w' 它默认为 1,这正是我们所需要的矩阵数学工作。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

执手闯天涯

暂无简介

文章
评论
485 人气
更多

推荐作者

微信用户

文章 0 评论 0

小情绪

文章 0 评论 0

ゞ记忆︶ㄣ

文章 0 评论 0

笨死的猪

文章 0 评论 0

彭明超

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文