此文上接 WebGL 系列文章,从基础概念开始,
上一篇是三维透视投影,如果没读过请从那里开始。
在上篇文章中我们将 F 移动到了视锥中,原因是 m4.perspective
默认将相机放在了原点(0, 0, 0)并且视锥的范围是 -zNear
到 -zFar
。
将物体移动到视场中可能并不是正确的方法,在实际生活中通常是移动相机去拍摄建筑物。
将物体移动到相机前面并不是常见做法。
但在上节中由于投影的原因物体需要在 -Z 轴上,我们通过将相机移动到原点,
物体移动到相机前来保持原始的相对位置。
高效的将物体移动到相机前是非常重要的。最简单的方式是使用一个“逆向”矩阵,
计算逆矩阵的数学原理比较复杂但概念很简单,逆就是你想通过一个值去抵消一个值。
例如,123 的逆就是 -123 ,一个缩放为 5 的缩放矩阵的逆是缩放为 1/5 或 0.2
的缩放矩阵,一个绕 X 轴旋转 30° 的旋转矩阵的逆是绕 X 旋转 -30°。
目前为止我们使用过平移,旋转和缩放去控制 ‘F’ 的位置和姿态,
将这些矩阵相乘后得到一个矩阵,可以将物体从原始位置移动到期望的位置,大小和姿态。
我们可以对相机进行同样的操作,一旦有了相机从原点移动旋转到目标位置的矩阵后,
就可以计算出它的逆矩阵,利用这个逆矩阵可以不动相机,将物体从相反的方向移动到相机前。
让我们来做一个三维场景,像上图一样有一圈 ‘F’ 。
这是代码。
- function drawScene() {
- var numFs = 5;
- var radius = 200;
-
- ...
-
- // 计算矩阵
- var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
- var zNear = 1;
- var zFar = 2000;
- var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
-
- var cameraMatrix = m4.yRotation(cameraAngleRadians);
- cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
-
- // 通过相机矩阵计算视图矩阵
- var viewMatrix = m4.inverse(cameraMatrix);
-
- // 将投影空间移动到视图空间(摄像机前方的空间)
- var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
-
- // 绘制一圈‘F’
- 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;
- // 从视图投影矩阵开始
- // 计算 F 的矩阵
- var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
-
- // 设置矩阵
- gl.uniformMatrix4fv(matrixLocation, false, matrix);
-
- // 绘制几何体
- var primitiveType = gl.TRIANGLES;
- var offset = 0;
- var count = 16 * 6;
- gl.drawArrays(primitiveType, offset, count);
- }
- }
就在我们计算出投影矩阵之后,您可以看到我们计算了一个像上图中那样围绕 ‘F’ 旋转的相机。
- // 计算相机矩阵
- var cameraMatrix = m4.yRotation(cameraAngleRadians);
- cameraMatrix = m4.translate(cameraMatrix, 0, 0, radius * 1.5);
然后通过相机矩阵计算“视图矩阵”,视图矩阵是将所有物体以相反于相机的方向运动, 就好像相机位于原点(0,0,0)
- // 通过相机矩阵计算“视图矩阵”
- var viewMatrix = m4.inverse(cameraMatrix);
然后我们将它们组合(相乘)形成一个组合矩阵。
- // 创建一个组合投影矩阵. 这将应用于透视和以相机为原点中心进行运动
- var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
最后,我们使用该空间作为放置每个‘F’的起始空间
- var x = Math.cos(angle) * radius;
- var z = Math.sin(angle) * radius;
- var matrix = m4.translate(viewProjectionMatrix, x, 0, z);
换句话说,每个F
的视图投影都是相同的。 同样的视角,同样的相机。
看!一个绕 F 旋转的相机。拖动 cameraAngle
滑块移动相机。
这样做没什么问题,但是有时利用旋转和平移去移动相机,
让它到达期望的位置并看向期望的方向并不容易。
例如你想让它总是看向一个特定的 F , 而相机又在绕一圈 F 旋转,
这时计算会变的相当复杂。
幸好这有一个简单的方法,我们可以同时定义相机位置和朝向,然后矩阵就可以将相机放在那,
基于矩阵这个工作就会变得非常简单。
首先我们需要知道相机的期望位置,将它叫做 cameraPosition
,
然后需要知道看向或对准的目标位置,将它叫做 target
。
如果将 target
减去 cameraPosition
就会得到相机的朝向,
将它叫做 zAxis
。由于我们知道相机看向的是 -Z 方向,
所以可以用另一种方式相减 cameraPosition - target
,
将结果单位化后直接赋给矩阵的 z
区域。
+----+----+----+----+
| | | | |
+----+----+----+----+
| | | | |
+----+----+----+----+
| Zx | Zy | Zz | |
+----+----+----+----+
| | | | |
+----+----+----+----+
矩阵的这个区域代表的是 Z 轴。在这个例子中相机的 Z-axis 进行了单位化,
单位化也就是一个做一个类似 1.0 的矢量,如果你回到二维旋转的文章,
那里讲到的单位圆在二维旋转中用法,在三维中需要一个单位球,单位向量表示单位球上的点。
这些信息还不够,只给了一个单位圆上点,如何来确定物体的姿态呢?
这就需要填充矩阵的其他区域,尤其是 X 轴和 Y 轴。通常情况下我们知道它们互相垂直,
如果再知道哪里是上方,在该例中是(0,1,0),就可以使用“叉乘”去计算矩阵的 X 轴和 Y 轴。
我不知道叉乘的数学意义是什么,但我知道将两个单位向量叉乘后可以得到一个和它们都垂直的向量。
换句话说,如果你有一个向量指向东南方,一个向量指向上方,
叉乘后会得到一个指向西南方或东北方的矢量,因为这两个矢量都和东南方和上方垂直。
相乘的顺序不同的到结果相反。
在任何情况下我们可以通过叉乘zAxis
和
up
得到相机的 xAxis
现在我们有了 xAxis
可以叉乘 zAxis
和 xAxis
的到相机的 yAxis
现在将三个轴插入矩阵中,会给我们提供一个从 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 | <- 相机位置
+----+----+----+----+
这是计算叉乘的代码
- 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]);
- // 确定不会除以 0
- if (length > 0.00001) {
- return [v[0] / length, v[1] / length, v[2] / length];
- } else {
- return [0, 0, 0];
- }
- }
这是计算“朝向”矩阵的代码
- var m4 = {
- lookAt: function(cameraPosition, target, up) {
- var zAxis = normalize(
- subtractVectors(cameraPosition, target));
- var xAxis = normalize(cross(up, zAxis));
- var yAxis = normalize(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’ 的用法。
- ...
-
- // 计算第一个 F 的位置
- var fPosition = [radius, 0, 0];
-
- // 计算相机在圆上的位置矩阵
- var cameraMatrix = m4.yRotation(cameraAngleRadians);
- cameraMatrix = m4.translate(cameraMatrix, 0, 50, radius * 1.5);
-
- // 获得矩阵中相机的位置
- var cameraPosition = [
- cameraMatrix[12],
- cameraMatrix[13],
- cameraMatrix[14],
- ];
-
- var up = [0, 1, 0];
-
- // 计算相机的朝向矩阵
- var cameraMatrix = m4.lookAt(cameraPosition, fPosition, up);
-
- // 通过相机矩阵获得视图矩阵
- var viewMatrix = m4.inverse(cameraMatrix);
-
- ...
这是结果。
拖动滑块观察相机是如何追踪单个 ‘F’ 的。
你也可以对其他东西使用“lookAt”方法而不只是相机。通常是让角色视线跟着某人,
将炮塔指向目标,让物体沿着路径移动。你可以算出物体当前在路径上的位置和不久后的位置,
然后将这两个值放入 lookAt
方法,可以让物体沿着路径移动并且朝着路径的方向。
在继续之前,您可能需要查看关于矩阵命名的简短说明.
接下来让我们学习动画。
lookAt 标准
大多数三维数学库都有lookAt
方法,通常它是用于计算“视图矩阵”
而不是“相机矩阵”。换句话说这个矩阵将所有物体移动到相机前而不是将相机移动到物体前。
我发现它并不好用,前面指出,一个 lookAt 方法有很多用处,当你需要视图矩阵的时候只需要调用inverse
方法, 但是如果你使用 lookAt
让角色跟随另一个角色或者炮台跟随目标的时候, lookAt
方法返回世界坐标中的朝向和位置转换会好一些。