02.相机矩阵和透视矩阵的推导过程

430 阅读7分钟

往期

01.向量和矩阵

lookat相机矩阵

观察点移动

webgl中,xyz轴的可视范围均在[-1, 1],所以如果我们绘制一个(2, 2, 0)的点,没有进行任何矩阵变化的话,这个点是不会被绘制到canvas上的。但是如果我们能将视角中心移动到(2, 2, 0)那么我们就能看到这个点了。

02_zuobiao.png

可以理解为,相机矩阵是确定了一个新的原点,然后将坐标系移到这个原点的位置,去观察原来的点。

如果我们定义相机的位置在世界坐标系中的(x, y, z),那么我们实际就是将世界坐标系原点移动到(x,y,z)

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

上述矩阵为列优先。之后的矩阵都是列优先

但是我们不可能真的去移动世界坐标系,所以我们应该将所有的点反向运动,来达到相机坐标系和世界坐标系的重合

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

相机坐标系旋转

我们将相机当做一个物体,镜头对着原点,然后去绘制出相机的坐标系后,即使移动到原点后,相机的坐标系和世界坐标系还是不重合的。
因为我们没有考虑到坐标系轴的朝向,我们在重合世界坐标系和相机坐标系的时候,不止需要考虑原点平移重合,还需要考虑到旋转坐标系。

计算出相机的坐标系

要旋转坐标系,我们先需要计算出相机的坐标系。
使用过webgl三方库的应该都用过lookat函数,需要传入两个点,一个向量————目标点,观察点的位置,上方向。

02_camera.png

目标点和观察点的位置可以求出相机朝向的反方向,就是z轴

仅一个轴我们是得不到整个三维坐标系的,我们需要一个上方向(一般是世界坐标系中的y轴)。

使用上方向和z轴进行叉乘,我们可以得到相机坐标系的x轴(即垂直于z轴和上方向组成的平面的向量)

然后再通过z轴和x轴进行叉乘,得到相机坐标系的y轴

反向旋转矩阵

求出相机的xyz的轴以后,我们需要将相机的坐标系旋转后和世界坐标系重合。

02_rotate.png

我们先看一个简单的例子,我们假设虚线坐标是相机坐标,且求出x轴为(xx, xy),y轴为(yx, yy) 我们要讲相机坐标和世界坐标重合,那么就是顺时针旋转A,按照旋转矩阵公式,得出图片右侧的矩阵

[
  cos-A, -sin-A,
  sin-A, cos-A
]

如果我们假设两个轴的向量长度都为1 那么就可以直接将x轴和y轴的向量值带入到矩阵中得到

[
  xx, xy,
  yx, yy
]

在三维坐标中,两个坐标系的旋转重合矩阵其实也是如此,具体计算过程不多演示,直接给出公式

[
  xx, xy, xz, 0,
  yx, yy, yz, 0,
  zx, zy, zz, 0,
  0,  0,  0,  1,
]

旋转加平移结合

最终我们可以得到旋转和平移后的矩阵
我们把x,y,z轴所代表的归一化向量称为x, y, z。相机的位置点代表为eye,dot函数代表向量的点乘

[
  xx, xy, xz, 0,
  yx, yy, yz, 0,
  zx, zy, zz, 0,
  dot(x, -eye),  dot(y, -eye),  dot(z, -eye),  1,
]

物体的旋转和平移

想象一下,我们把webgl三维空间里的所有物体的顶点都和相机坐标轴相同位移和旋转,那么当相机坐标系和世界坐标系重合的时候,所有物体的位置,就是我们通过相机去观察的位置了。

我们只需要将上述得到的相机矩阵左乘所有的顶点就可以了。

代码实现

最后我们在基于上一章的类在实现一个相机的lookat方法

setLookat(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) {
  let zx = eyex - centerx
  let zy = eyey - centery
  let zz = eyez - centerz

  const zl = 1 / Math.sqrt(zx * zx + zy * zy + zz * zz)

  zx *= zl
  zy *= zl
  zz *= zl

  let xx = zz * upy - zy * upz
  let xy = zx * upz - zz * upx 
  let xz = zy * upx - zx * upy

  const xl = 1 / Math.sqrt(xx * xx + xy * xy + xz * xz)

  xx *= xl
  xy *= xl
  xz *= xl

  let yx = xz * zy - xy * zz 
  let yy = xx * zz - xz * zx
  let yz = xy * zx - xx * zy

  const yl = 1 / Math.sqrt(yx * yx + yy * yy + yz * yz)

  yx *= yl
  yy *= yl
  yz *= yl

  const e = this.elements;
  e[0] = xx
  e[1] = yx
  e[2] = zx
  e[3] = 0

  e[4] = xy
  e[5] = yy
  e[6] = zy
  e[7] = 0

  e[8] = xz
  e[9] = yz
  e[10] = zz
  e[11] = 0

  e[12] = -xx * eyex - xy * eyey - xz * eyez
  e[13] = -yx * eyex - yy * eyey - yz * eyez
  e[14] = -zx * eyex - zy * eyey - zz * eyez
  e[15] = 1
  
  return this
}

lookAt(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) {
  return this.multiply(new Matrix4().setLookat(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz));
}

前面通过叉乘和归一化求得相机的,xyz轴,然后带入公式,需要相机位置eye,目标点位置center(可以是原点也可以不是原点),上方向(一般和世界坐标的y轴一致)

透视矩阵

透视矩阵的意义是为了让人在webgl中看到的更符合现实生活中的效果——近大远小
将可视范围内的值映射到webgl的可视空间内

02_yingshe.png

(上图中的坐标系不太对,是左手坐标系。但是意思是一样的)

投影

我们将可视范围内的所有点都投影到最近的面上,如下图

02_per.png

根据相似三角形的比例关系,最终可以得到,P点在近面上的投影点S的坐标为(-n * Xp / Zp, -n * Yp / Zp, -n, 1)

投影面放大

然后我们将投影面放大和webgl可视空间内的大小保持一致

02_lachang.png

假设投影面上的Y轴坐标为Y1,放大后的Y轴坐标为Y2,投影面的高度为H,可视空间的高度为2则有
Y1/Y2 = H/2
那么
Y2 = Y1*2/H

结合上述投影的Y1结果,得到Y2 = -n * Yp * 2 / (Zp * H)
其中n * 2 / H 等于 cotA
所以最终得到
Y2 = -cotA * Yp / Zp

同理,X2也可以求出,但是需要需要除以宽高比

X2 = -cotA * Xp / (Zp * Aspect)

综合可以求出矩阵的3列,如下图

02_ttt.png

深度检测

上述结果以及求出了最终的x和y值,但是z值的结果都一致。如果就这样结束的话,所有的点都打平在一个面上,就不会有深度的情况,显示上就会存在问题。且z值和xy无关,所以根据上图中的内容,我们可以得到
Z2 = (K*Z + L)/-Z
Z2 = -K - L/Z
将Z=-n,Z2=-1和Z=-f,Z2=1代入

解得K=(f+n)/(n-f),L=2nf/(n-f)

至此得出透视矩阵

[
  cosA/Aspect, 0, 0, 0,
  0, cosA, 0, 0,
  0, 0, (near + far)/(near - far), 2 * near * far/(near - far),
  0, 0, -1, 0
]

代码实现

setPerspective(fov, aspect, near, far) {
  const r = 0.5 * Math.PI * fov / 180
  const ct = Math.cos(r) / Math.sin(r)
  const e = this.elements

  e[0] = ct / aspect
  e[1] = 0
  e[2] = 0
  e[3] = 0

  e[4] = 0
  e[5] = ct
  e[6] = 0
  e[7] = 0

  e[8] = 0
  e[9] = 0
  e[10] = (near + far) / (near - far)
  e[11] = -1

  e[12] = 0
  e[13] = 0
  e[14] = 2 * near * far / (near - far)
  e[15] = 0

  return this
}

最终效果

02_res.png

总结

本章两个重要的矩阵的推导过程,透视矩阵加相机矩阵结合可以使得webgl的渲染更加符合肉眼观察世界的效果。
至此我们已经将常用的基础矩阵实现了,接下来我们会开始一些实际的应用

相关代码gitee