WebGL 之视图矩阵

554 阅读7分钟

亚里士多德说,事物如果要显得美,一方面要靠它本身的特质,一方面也要靠观众的认识能力。之前我们学习了 webgl 里的 MVP 矩阵中的 M,模型矩阵,它解决的就是物体本身的旋转缩放或平移;本文继续学习 V,视图矩阵,它解决的则是观察者的平移旋转问题。 视图矩阵有 3 大要素:

3 大要素

  • 视点(或观察点):可以理解为人的眼睛或下图中的摄像机;
  • 目标点:就是我们要观察的物体;
  • 上方向(或正方向):如果你倒立着看,那么世界也随之颠倒,所以确定上方向是必需的。

1.png

推导

当我们改变摄像机的角度和位置时,在屏幕上看到的物体呈现的画面,也会随之改变。那么我们要如何呈现正确的画面呢?由于物体之间的运动是相对的,我们可以想办法改为让处于某个角度和位置的摄像机所对应的坐标系 uvw,进行一定的平移或旋转,转换到默认状态的 xyz 坐标系 —— 让零点重合,让视线方向为 z 轴的负方向,上方向为 y 轴的正方向。这个转换矩阵,即为视图矩阵(即 MVP 矩阵中的 V,View 矩阵)。

图例 1:
2.png

下面我们定义一个函数 getViewMatrix 来获取视图矩阵,它的参数就是视图矩阵的 3 大要素:

function getViewMatrix(
  // 视点
  eyex,
  eyey,
  eyez,
  // 目标点
  lookAtx,
  lookAty,
  lookAtz,
  // 上方向
  upx,
  upy,
  upz
) {
  // 视点
  const eye = new Float32Array([eyex, eyey, eyez])
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz])
  // 上方向
  const up = new Float32Array([upx, upy, upz])
  // ...剩余代码
}

首先我们来确定图例 1 中的坐标系的 ew 轴,也就是视线的方向,而视线是由视点向目标点的射线,所以可通过向量差求得。向量差的几何意义如下图所示,OA - OB 获取到的就是向量 BA:

3.png

假设点 O 坐标为 (0, 0),A 为(x1, y1) ,B 为(x2, y2),则 OA - OB = (x1 - x2, y1 - y2)。由是可得,向量差函数 minus 如下:

function minus(a, b) {
  return new Float32Array([a[0] - b[0], a[1] - b[1], a[2] - b[2]])
}

ew 轴即为目标点 lookAt 减去视点 eye,用变量 w 接收:

const w = minus(lookAt, eye)

现在,我们有了 w 和上方向(默认为原点到点 (0, 1, 0) 的方向),此时上方向不一定会垂直于 ew 轴。根据数学知识,2 个向量的叉乘结果,也是一个向量,并且为这 2 个向量所在平面的法线向量,所以可以让经过归一化函数 normalized 处理的w(包括之后的得到的 uv 都会被归一化处理)和上方向 up 叉乘,得到垂直于 ew 轴和上方向的轴,用变量 u 接收:

// 向量叉乘函数
function cross(a, b) {
  return new Float32Array([
    a[1] * b[2] - a[2] * b[1],
    a[2] * b[0] - a[0] * b[2],
    a[0] * b[1] - a[1] * b[0]
  ])
}

// 垂直于视线和上方向的轴
const u = cross(w, up)

有了 uw,就可以再通过向量的叉乘获取垂直于视线和 u 的轴,用变量 v 接收:

const v = cross(u, w)

至此,我们已经获取了 uvw 所代表的轴,它们构成了互相垂直的坐标系,也可以说是摄像机坐标系。接着要做的事无非就是:

  1. 将摄像机坐标系的原点移动到世界坐标系的原点;
  2. 进行旋转,让视线(w 所代表的轴)与世界坐标系的 z 轴的负方向重合。

平移原点

关于平移矩阵,在《控制图形的形变》中也推导过了,结果如下,xyz 为平移的距离:

[
    1, 0, 0, x,
    0, 1, 0, y,
    0, 0, 1, z,
    0, 0, 0, 1
]

让图例 1 中的 uvw 坐标系平移到 xyz 坐标系,需要移动的距离,可以通过向量的点积获取。我们定义点积函数 dot 如下:

function dot(a, b) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}

a · b 的几何意义是去获取向量 b 在向量 a 方向上的投影,那么 dot([1,0,0], eye)dot([0,1,0], eye)dot([0,0,1], eye) 获取的就是视点在世界坐标系 x,y,z 轴方向上的长度(其实分别就是 eyexeyeyeyez),所以实现让摄像机位于世界坐标系的零点平移矩阵如下:

[
   1, 0, 0, -eyex,
   0, 1, 0, -eyey,
   0, 0, 1, -eyez,
   0, 0, 0,  1
]

旋转坐标系

因为涉及到了坐标系的旋转,那就先来看一下如何将一个 3 维直角坐标系 xyz 绕 z 轴逆时针旋转某个角度 α 获取新矩阵 x'y'z:

4.png

之前在《控制图形的形变》我们已经推导过获取绕 z 轴旋转某个弧度的矩阵的函数 getRotateZMatrix,知道了绕 z 轴旋转矩阵,它正好可以用来实现坐标系的旋转(这里改为直接使用角度):

// 矩阵一
[
    cosα, -sinα, 0, 0,
    sinα, cosα,  0, 0,
    0,    0,     1, 0,
    0,    0,     0, 1
]

坐标系 xyz 的基向量为:

x(1, 0, 0)
y(0, 1, 0)
z(0, 0, 1)

旋转后的坐标系 x'y'z 的基向量为:

x'(cosα, sinα, 0)
y'(-sinα, cosα, 0)
z(0, 0, 1)

结合观察旋转矩阵,不难发现,将 xyz 旋转对齐到 xyz 的矩阵与 xyz 的基向量有如下图所示的对应关系:

1.png

可以发现旋转矩阵的第 1、2、3 列分别为 x' 、 y' 、z' 的 3 个分量。其实世界坐标系旋转对齐到摄像机坐标系的旋转矩阵也是如此,第 1、2、3 列分别为 uvw 的 3 个分量。注意其中 w 的各个分量都应该添加上负号,因为摄像机坐标系的 w 轴方向与世界坐标系的 z 轴方向相反:

// 矩阵二
[
    u[0], v[0], -w[0], 0,
    u[1], v[1], -w[1], 0,
    u[2], v[2], -w[2], 0,
    0,    0,    0,     1
]

我们需要推导的应该是从摄像机坐标系旋转对齐到世界坐标系的旋转矩阵。对于矩阵一的情况而言,就是让 α 变为 -α,由于 cos-α = cosα,sin-α = -sinα,代入计算后会发现新矩阵为旧矩阵的转置矩阵,对于矩阵二其实也适用,所以我们需要的旋转矩阵应为矩阵二的转置:

// 矩阵三
[
    u[0],  u[1],  u[2],  0,
    v[0],  v[1],  v[2],  0,
   -w[0], -w[1], -w[2],  0,
    0,     0,     0,     1
]

最终的视图矩阵,就是平移原点矩阵乘上旋转坐标系的矩阵:

[
    u[0],  u[1],  u[2],  u[0] * -eyex - u[1] * eyey - u[2] * eyez,
    v[0],  v[1],  v[2],  v[0] * -eyex - v[1] * eyey - v[2] * eyez,
   -w[0], -w[1], -w[2],  w[0] * eyex + w[1] * eyey + w[2] * eyez,
    0,     0,     0,     1
]

在写代码时,往 gl.uniformMatrix4fv() 传矩阵时记得要使用列主序。

使用

现在,我们就可以使用视图矩阵来灵活地控制摄像机的视角和位置,在屏幕上得到不同角度的正方体了,比如视点坐标 (0.5, 0, 1),目标点坐标为 (0, 0, 0),就应该能看到立方体的前面(白色)和右面(绿色):

One More Thing

在实际项目中,如果需要使用到视图矩阵,我们可以直接使用之前介绍过的 gl-matrix 库提供的方法 lookAt(),传入的参数中 eye 为视点坐标数组,center 为目标点坐标数组,up 为上方向坐标数组:

const { mat4 } = glMatrix
const viewMatrix = mat4.create()
const eye = [0.5, 0.5, 1]
const center = [0, 0, 0]
const up = [0, 1, 0]
mat4.lookAt(viewMatrix, eye, center, up)
gl.uniformMatrix4fv(uMatrix, false, viewMatrix)

感谢.gif 点赞.png