返回首页 WebGL 中文版

WebGL 基础

图像处理

2D 转换、旋转、伸缩、矩阵

3D

结构与组织

文本

WebGL 3D 摄像机

在过去的章节里我们将 F 移动到截锥的前面,因为 makePerspective 函数从原点(0,0,0)度量它,并且截锥的对象从 -zNear 到 -zFar 都在它前面。

视点前面移动的物体似乎没有正确的方式去做吗?在现实世界中,你通常会移动你的相机来给建筑物拍照。

将摄像机移动到对象前

你通常不会将建筑移动到摄像机前。

将对象移动到摄像机前

但在我们最后一篇文章中,我们提出了一个投影,这就需要物体在 Z 轴的原点前面。为了实现它,我们想做的是把摄像机移动到原点,然后把所有的其它物体都移动恰当的距离,所以它相对于摄像机仍然是在同一个地方。

将对象移动到视图

我们需要有效地将现实中的物体移动到摄像机的前面。能达到这个目的的最简单的方法是使用“逆”矩阵。一般情况下的逆矩阵的计算是复杂的,但从概念上讲,它是容易的。逆是你用来作为其他数值的对立的值。例如,123 的是相反数是 -123。缩放比例为5的规模矩阵的逆是 1/5 或 0.2。在 X 域旋转 30° 的矩阵的逆是一个在 X 域旋转 -30° 的矩阵。

直到现在我们已经使用了平移,旋转和缩放来影响我们的 'F' 的位置和方向。把所有的矩阵相乘后,我们有一个单一的矩阵,表示如何将 “F” 以我们希望的大小和方向从原点移动到相应位置。使用摄像机我们可以做相同的事情。一旦我们的矩阵告诉我们如何从原点到我们想要的位置移动和旋转摄像机,我们就可以计算它的逆,它将给我们一个矩阵来告诉我们如何移动和旋转其它一切物体的相对数量,这将有效地使摄像机在点(0,0,0),并且我们已经将一切物体移动到它的前面。

让我们做一个有一圈 'F' 的三维场景,就像上面的图表那样。

下面是实现代码。

  var numFs = 5;
  var radius = 200;

  // Compute the projection matrix
  var aspect = canvas.clientWidth / canvas.clientHeight;
  var projectionMatrix =
      makePerspective(fieldOfViewRadians, aspect, 1, 2000);

  // Draw 'F's in a circle
  for (var ii = 0; ii < numFs; ++ii) {
    var angle = ii * Math.PI * 2 / numFs;

    var x = Math.cos(angle) * radius;
    var z = Math.sin(angle) * radius;
    var translationMatrix = makeTranslation(x, 0, z);

    // Multiply the matrices.
    var matrix = translationMatrix;
    matrix = matrixMultiply(matrix, projectionMatrix);

    // Set the matrix.
    gl.uniformMatrix4fv(matrixLocation, false, matrix);

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

就在我们计算出我们的投影矩阵之后,我们就可以计算出一个就像上面的图表中显示的那样围绕 ‘F’ 旋转的摄像机。

  // Compute the camera's matrix
  var cameraMatrix = makeTranslation(0, 0, radius * 1.5);
  cameraMatrix = matrixMultiply(
      cameraMatrix, makeYRotation(cameraAngleRadians));

然后,我们根据相机矩阵计算“视图矩阵”。“视图矩阵”是将一切物体移动到摄像机相反的位置,这有效地使摄像机相对于一切物体就像在原点(0,0,0)。

  // Make a view matrix from the camera matrix.
  var viewMatrix = makeInverse(cameraMatrix);

最后我们需要应用视图矩阵来计算每个 ‘F’ 的矩阵

    // Multiply the matrices.
    var matrix = translationMatrix;
    matrix = matrixMultiply(matrix, viewMatrix);  // <=-- added
    matrix = matrixMultiply(matrix, projectionMatrix);

一个摄像机可以绕着一圈 “F”。拖动 cameraAngle 滑块来移动摄像机。

这一切都很好,但使用旋转和平移来移动一个摄像头到你想要的地方,并且指向你想看到的地方并不总是很容易。例如如果我们想要摄像机总是指向特定的 ‘F’ 就要进行一些非常复杂的数学计算来决定当摄像机绕 ‘F’ 圈旋转的时候如何旋转摄像机来指向那个 ‘F’。

幸运的是,有一个更容易的方式。我们可以决定摄像机在我们想要的地方并且可以决定它指向什么,然后计算矩阵,这个矩阵可以将把摄像机放到那里。基于矩阵的工作原理这非常容易实现。

首先,我们需要知道我们想要摄像机在什么位置。我们将称之为 CameraPosition。然后我们需要了解我们看过去或瞄准的物体的位置。我们将把它称为 target。如果我们将 CameraPosition 减去 target 我们将得到一个向量,它指向从摄像头获取目标的方向。让我们称它为 zAxis。因为我们知道摄像机指向 -Z 方向,我们可以从另一方向做减法 cameraPosition - target。我们将结果规范化,并直接复制到 z 区域矩阵。

+----+----+----+----+
|    |    |    |    |
+----+----+----+----+
|    |    |    |    |
+----+----+----+----+
| Zx | Zy | Zz |    |
+----+----+----+----+
|    |    |    |    |
+----+----+----+----+

这部分矩阵表示的是 Z 轴。在这种情况下,是摄像机的 Z 轴。一个向量的标准化意味着它代表了 1.0。如果你回到二维旋转的文章,在哪里我们谈到了如何与单位圆以及二维旋转,在三维中我们需要单位球面和一个归一化的向量来代表在单位球面上一点。

虽然没有足够的信息。只是一个单一的向量给我们一个点的单位范围内,但从这一点到东方的东西?我们需要把矩阵的其他部分填好。特别的 X 轴和 Y 轴类零件。我们知道这 3 个部分是相互垂直的。我们也知道,“一般”我们不把相机指向。因为,如果我们知道哪个方向是向上的,在这种情况下(0,1,0),我们可以使用一种叫做“跨产品和“计算 X 轴和 Y 轴的矩阵。

我不知道一个跨产品意味着在数学方面。我所知道的是,如果你有 2 个单位向量和你计算的交叉产品,你会得到一个向量,是垂直于这 2 个向量。换句话说,如果你有一个向量指向东南方,和一个向量指向上,和你计算交叉产品,你会得到一个向量指向北西或北东自这2个向量,purpendicular 到东南亚和。根据你计算交叉产品的顺序,你会得到相反的答案。

现在,我们有 xAxis,我们可以通过 zAxisxAxis 得到摄像机的 yAxis

现在我们所要做的就是将 3 个轴插入一个矩阵。这使得矩阵可以指向物体,从 cameraPosition 指向 target。我们只需要添加 position

+----+----+----+----+
| Xx | Xy | Xz |  0 |  <- x axis
+----+----+----+----+
| Yx | Yy | Yz |  0 |  <- y axis
+----+----+----+----+
| Zx | Zy | Zz |  0 |  <- z axis
+----+----+----+----+
| Tx | Ty | Tz |  1 |  <- camera position
+----+----+----+----+

下面是用来计算 2 个向量的交叉乘积的代码。

function cross(a, b) {
  return [a[1] * b[2] - a[2] * b[1],
          a[2] * b[0] - a[0] * b[2],
          a[0] * b[1] - a[1] * b[0]];
}

这是减去两个向量的代码。

function subtractVectors(a, b) {
  return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
}

这里是规范化一个向量(使其成为一个单位向量)的代码。

function normalize(v) {
  var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
  // make sure we don't divide by 0.
  if (length > 0.00001) {
    return [v[0] / length, v[1] / length, v[2] / length];
  } else {
    return [0, 0, 0];
  }
}

下面是计算一个 "lookAt" 矩阵的代码。

function makeLookAt(cameraPosition, target, up) {
  var zAxis = normalize(
      subtractVectors(cameraPosition, target));
  var xAxis = cross(up, zAxis);
  var yAxis = cross(zAxis, xAxis);

  return [
     xAxis[0], xAxis[1], xAxis[2], 0,
     yAxis[0], yAxis[1], yAxis[2], 0,
     zAxis[0], zAxis[1], zAxis[2], 0,
     cameraPosition[0],
     cameraPosition[1],
     cameraPosition[2],
     1];
}

这是我们如何使用它来使相机随着我们移动它指向在一个特定的 ‘F’ 的。

  ...

  // Compute the position of the first F
  var fPosition = [radius, 0, 0];

  // Use matrix math to compute a position on the circle.
  var cameraMatrix = makeTranslation(0, 50, radius * 1.5);
  cameraMatrix = matrixMultiply(
      cameraMatrix, makeYRotation(cameraAngleRadians));

  // Get the camera's postion from the matrix we computed
  cameraPosition = [
      cameraMatrix[12],
      cameraMatrix[13],
      cameraMatrix[14]];

  var up = [0, 1, 0];

  // Compute the camera's matrix using look at.
  var cameraMatrix = makeLookAt(cameraPosition, fPosition, up);

  // Make a view matrix from the camera matrix.
  var viewMatrix = makeInverse(cameraMatrix);

  ...

下面是结果。

拖动滑块,注意到相机追踪一个 ‘F’。

请注意,您可以不只对摄像机使用 “lookAt” 函数。共同的用途是使一个人物的头跟着某人。使小塔瞄准一个目标。使对象遵循一个路径。你计算目标的路径。然后你计算出目标在未来几分钟在路径的什么地方。把这两个值放进你的 lookAt 函数,你会得到一个矩阵,使你的对象跟着路径并且朝向路径。