GAMES101 系列总结(一):线性代数与模型变换

最近开始重新系统学习计算机图形学的知识,于是目光放到了GAMES101,一边看,一边做,一边总结吧。这次总结下线性代数的基础知识,以及游戏中是如何做模型变换的。最后完成一下作业1.

线性代数基础知识

向量

  1. 向量就是一个有方向,有长度的线段,用数学符号写作:a\vec{a}
  2. 或者表示为一个从起点指向终点的的线段,比如有两个点A,B,那么从A指向B的向量可以表示为AB=BA\vec{AB}=B - A
  3. 向量没有绝对的起点,也就是说,如果在空间中移动一个向量,向量本身是没变的
  4. 向量的长度表示为:a||\vec{a}||,向量除以自身长度就是改方向的单位向量:aa\frac{\vec{a}}{||\vec{a}||}
  5. 向量相加符合平行四边形法则或者叫三角形法则

可以用向量表示坐标

如果有两个单位向量相互垂直,我们选择其中一个是X\vec{X},一个是Y\vec{Y},我们可以表示一个向量A=(xy)\vec{A}=\binom{x}{y},或者AT=(x,y)\vec{A}^T=(x,y)

我们假设这个向量起点是原点,那么终点就是我们平时在几何中说的(x,y)点

向量的点乘

向量之间可以点乘,假设有两个方向不同的向量a,b\vec{a}, \vec{b},假设将他们的起点放到一起,他们之间会存在一个夹角,假设为θ\theta

那么ab=abcosθ\vec{a} \cdot \vec{b} = ||\vec{a}||||\vec{b}||cos\theta

向量的点乘有交换律和结合律。

ab=baa(b+c)=ab+ac(ka)b=a(kb)=k(ab)\vec{a} \cdot \vec{b} = \vec{b} \cdot \vec{a} \\ \vec{a} \cdot (\vec{b} + \vec{c}) = \vec{a} \cdot \vec{b} + \vec{a} \cdot \vec{c} \\ (k\vec{a}) \cdot \vec{b} = \vec{a} \cdot (k\vec{b}) = k(\vec{a} \cdot \vec{b})

用坐标方式表示向量点乘为:

ab=(xaya)(xbyb)=xaxb+yaybab=[xayaza][xbybzb]=xaya+xbyb+zazb\vec{a} \cdot \vec{b} = \binom{x_a}{y_a} \cdot \binom{x_b}{y_b} = x_ax_b + y_ay_b \\ \vec{a} \cdot \vec{b} = \begin{bmatrix} x_a \\ y_a \\ z_a \end{bmatrix} \cdot \begin{bmatrix} x_b \\ y_b \\ z_b \end{bmatrix} = x_ay_a + x_by_b + z_az_b

向量点乘的应用有:

  • 我们可以用两个向量之间点乘的结果和0比较来判断二者之间是锐角还是钝角。
  • 求两个向量之间的夹角
  • 求一个向量在另一个向量上的投影

向量的叉乘

向量还有一种乘法是叉乘

向量叉乘的方向遵循右手定则,假设a×b\vec{a} \times \vec{b},那么结果的方向就是右手四指从向量a的方向转向b的方向握紧,然后竖起大拇指是,拇指的方向。也就是说叉乘的结果垂直于a和b所在的平面

那么叉乘的长度其实是a×b=absinθ||\vec{a} \times \vec{b}||=||\vec{a}||||\vec{b}||sin\theta

向量的叉乘并不支持结合律,准确的说,是交换叉乘顺序的结果是相反的,即方向是反的:a×b=b×a\vec{a} \times \vec{b} = -\vec{b} \times \vec{a}

用矩阵的方式表示向量叉乘为:

a×b=[yazbybzaxazbxbzaxaybxbya]a×b=Ab=[0zayaza0xayaxa0][xbybzb]\vec{a} \times \vec{b} = \begin{bmatrix} y_az_b - y_bz_a \\ x_az_b - x_bz_a \\ x_ay_b - x_by_a \end{bmatrix} \\ \vec{a} \times \vec{b} = A * \vec{b} = \begin{bmatrix} 0 & -z_a & y_a \\ z_a & 0 & -x_a \\ -y_a & x_a & 0 \\ \end{bmatrix} \begin{bmatrix} x_b \\ y_b \\ z_b \end{bmatrix}

叉乘可以判断一个向量在另一个向量的左边还是右边,这个比较好理解,右手定则旋转的时候,是顺时针还是逆时针转,结果是相反的。

还有一个作用是判断一个点是否在一个三角形内部

上图中,如果向量BC叉乘向量BP,向量CA叉乘向量CP,向量AB叉乘向量AP的符号都是相同的,那么说明P点在内部

矩阵

矩阵就是一个m行,n列的二维数组。

两个矩阵可以做乘法的前提是,第一个矩阵的列数和第二个矩阵的行数是相同的。

也就是说一个M行N列的矩阵可以和一个N行P列的矩阵相乘,结果是一个M行P列的矩阵。

假设A,B两个矩阵相乘的到C矩阵,其中A的每一项为aija_{ij}, B的每一项为BijB_{ij}, C的每一项为cijc_{ij} ,那么cij=k=0k=Naikbkjc_{ij} = \sum_{k=0}^{k=N}a_{ik}b_{kj}

这里比较重要的一点是,如何讲坐标转换的方程组写成矩阵的形式

比如,如何讲二维坐标系上的点根据y轴做对称

用方程组来写就是

{x=x;y=y\begin{cases} x' = -x; \\ y' = y \end{cases}

用矩阵的方式来写就是

[1001][xy]=[xy]\begin{bmatrix} -1 & 0 \\ 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} -x \\ y \end{bmatrix}

每个矩阵都有自己的转置矩阵和逆矩阵

A的转置矩阵写作ATA_T,且(AB)T=BTAT(AB)^T=B^TA^T

A的逆矩阵写作A1A^{-1},且AA1=IAA^{-1} = I,其中I是单位矩阵,任何矩阵乘上单位矩阵等于什么都没做,也就是说任何矩阵乘上A矩阵所产生的变化都可以通过再乘上A的逆矩阵还原

如何用矩阵做变换(Transform)

2D变换

  1. 缩小

当图片缩放s倍的时候,用方程式来表示就是

{x=sxy=sy\begin{cases} x' = sx \\ y' = sy \\ \end{cases}

对应的缩放矩阵就是:

[s00s]\begin{bmatrix} s & 0 \\ 0 & s \end{bmatrix}

  1. 反转

[1001]\begin{bmatrix} -1 & 0 \\ 0 & 1 \\ \end{bmatrix}

  1. 切变

[1a01]\begin{bmatrix} 1 & a \\ 0 & 1 \end{bmatrix}

  1. 旋转

[cosθsinθsinθcosθ]\begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \end{bmatrix}

到目前为止,我们所有的变换都可以通过矩阵的方式来表达,因为我们之前的这些变换都可以用以下方程式来表达:

{x=ax+byy=cx+dy\begin{cases} x' = ax + by \\ y' = cx + dy \end{cases}

表示为矩阵就是

[xy]=[abcd][xy]\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} a & b \\ c & d \\ \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix}

但是有个问题是,这种方式无法表现平移,因为平移无法写成这种形式

  1. 平移

平移的方程组是这样的:

{x=x+tx;y=y+ty;\begin{cases} x' = x + t_x;\\ y' = y + t_y; \end{cases}

如果讲缩放,旋转,平移都用矩阵来表示,应该如下:

[xy]=[abcd][xy]+[txty]\begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} a & b \\ c & d \end{bmatrix} \begin{bmatrix} x \\ y \end{bmatrix} + \begin{bmatrix} t_x \\ t_y \end{bmatrix}

于是这个时候我们就要引入齐次坐标,即加入w,这个时候2D的点坐标表示为(x, y, 1),2D的向量表示为(x,y,0)

当表示点的时候,w为1,当表示向量的时候,w为0,而且这样有一个很神奇的地方,就是两个点相减,w会变成0,恰好正是向量,点和向量相加,w为1,也是个点

用了齐次坐标之后,我们就可以统一旋转,缩放,平移到一个矩阵中了

平移用齐次坐标表示为:

[xyz]=[10tx01ty001][xy1]=[x+txy+ty1]\begin{bmatrix} x' \\ y' \\ z' \end{bmatrix} = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \\ \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y \\ 1 \end{bmatrix}

  1. 混合

我们现在可以用齐次坐标来分别表示旋转,平移,缩放

S(sx,sy)=[sx000sy0001]R(θ)=[cosθsinθ0sinθcosθ0001]T(tx,ty)=[10tx01ty001]S(s_x, s_y) = \begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ R(\theta) = \begin{bmatrix} cos\theta & -sin\theta & 0 \\ sin\theta & cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix} \\ T(t_x, t_y) = \begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}

那么我们如何将这些操作混合呢?

可以看到上图,我们现平移后旋转,与先旋转后,平移的结果是不同的,因为我们的旋转矩阵是绕着原点旋转的

所以我们一般会规定顺序来进行混合操作,先操作的先和当前点的坐标左乘,得到新的点之后,再和接下来的操作矩阵左乘就好。

然后虽然我们的顺序不能变,因为矩阵相乘没有交换律,但是矩阵相乘是有结合律的,也就是:

所以我们可以实现的到旋转,缩放,平移矩阵相乘的结果作为变换矩阵,然后再和每一个点去左乘

这里其实还有一个问题,就是,我们如果就想让一个点绕自己的左下角旋转怎么办?

很简单,将左下角平移到原点,然后旋转,最后再将左下角平移回去:

3D变换

3d的变换其实和2D没什么区别,只是齐次坐标有四个维度而已

变换矩阵如下:

[xyz1]=[abctxdeftyghotz0001][xyz1]\begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & c & t_x \\ d & e & f & t_y \\ g & h & o & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix}

其中缩放矩阵可以写作;

S(sx,sy,sz)=[sx0000xy0000sz00001]S(s_x, s_y, s_z) = \begin{bmatrix} s_x & 0 & 0 & 0\\ 0 & x_y & 0 & 0\\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

平移矩阵可以表示为:

T(tx,ty,tz)=[100tx010ty001tz0001]T(t_x, t_y, t_z) = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}

而旋转就比较复杂了,因为它可以分为绕不同轴的旋转

Rx(θ)=[10000cosθsinθ00sinθcosθ00001]Ry(θ)=[cosθ0sinθ00100sinθ0cosθ00001]Rz(θ)=[cosθsinθ00sinθcosθ0000100001]R_x(\theta) = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & cos\theta & -sin\theta & 0 \\ 0 & sin\theta & cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \\ R_y(\theta) = \begin{bmatrix} cos\theta & 0 & sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ sin\theta & 0 & cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \\ R_z(\theta) = \begin{bmatrix} cos\theta & -sin\theta & 0 & 0 \\ sin\theta & cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}

这里还有一个公式,可以将任意一个旋转矩阵分解为三个轴方向的旋转矩阵相乘。下面这种方式表示绕n\vec{n}轴旋转θ\theta角度。

R(n,α)=cosαI+(1cosα)nnT+sinα[0nznynz0nxnynx0]R(\vec{n}, \alpha) = cos\alpha\vec{I} + (1 - cos\alpha)\vec{n}\vec{n}^T + sin\alpha\begin{bmatrix} 0 & -n_z & n_y \\ n_z & 0 & -n_x \\ -n_y & n_x & 0 \\ \end{bmatrix}

图形学中的观测变换

我们在做游戏开发的时候,写Shader的时候,经常会用到一个叫做MVP矩阵的东西,将模型上的点坐标变为屏幕上的坐标。

这里的MVP指的分别是Model,View,Projection,即模型变换,视图变换和投影变换

模型变换是将坐标从模型自身的坐标系变为游戏世界坐标系上的坐标

视图变换则是将世界坐标系上的坐标变为观察空间的坐标

投影变换则是将观察空间的坐标变为裁剪空间,这一步其实并没有做投影到二维平面的操作,具体的投影操作是渲染管线中写在GPU中的,一般不在Shader处理。

我们这里说的观测变换只是渲染管线中的一步,在最开始,是在顶点着色器中逐顶点操作的,我们得到了点在裁剪空间中的坐标,在接下来的渲染管线中,还要经过光栅化,采样,逐片元的片元着色器输出颜色,深度测试等等才会最终投影到二维平面上。

累比平时我们的拍照的方式,Model矩阵就像是我们找一个合适的拍照位置,View矩阵就是用摄像机找一个角度

我们这里不讲Model矩阵,因为他和View矩阵一样,就是从一个坐标系变为另一个坐标系,而Projection矩阵不太一样的地方是,观察空间是一个边长为1正方体的盒子,我们需要考虑缩放

View矩阵

View矩阵是将点从世界坐标变为观察空间,即从相对世界坐标系原点,变为相对于摄像机位置的坐标。

在经过Model变换之后,我们已经有了点在空间中的坐标,现在我们需要定义摄像机在空间的位置和方向:

这里摄像机的坐标和方向,都是相对于世界坐标系的。而我们的物体坐标目前也是相对于世界坐标系的。

现在我们要做的就是把相对于世界坐标系的物体坐标变为相对于摄像机坐标。

这里我们引入一个常见的物理概念——相对运动,即如果对摄像机和物体做相同的变换操作,二者相对位置不变。

那我们现在可以尝试把摄像机移动到原点,摄像机的观察方向朝向世界坐标系z轴的负方向,求出这个变换的矩阵,再对每个点应用这个矩阵,就相当于把物体移动到了观察空间。虽然从物体坐标来看,是在世界坐标系中做了一定的移动,但是这个移动没有改变物体和摄像机的相对位置,也成功把摄像机移动到了世界坐标的原点,所以结果就等于把物体移动到了观察空间。

那么怎么得到这个View矩阵呢?

这种方法比较复杂

我们这里可以利用一个比较好的性质,就是旋转矩阵其实是一个正交矩阵,正交矩阵的逆矩阵和转置矩阵是相同的,也就是说,我们可以求一下世界坐标轴变换到摄像机坐标轴的矩阵,然后求他的转置矩阵,即逆矩阵,就是摄像机坐标轴变换到世界坐标轴的矩阵

Projection矩阵

刚才通过View矩阵,我们摄像机和物体整体相对位置不变移动到了摄像机在世界坐标系原点的位置。

这样做的目的是什么呢?当然有一个好处是好理解,但是其实一点对于计算器来说没有意义,因为都是乘一个矩阵,计算量不会有差别。

这样做的另一个好处是,为了投影矩阵减少计算。

我们的投影矩阵分为两种,一种是平行投影,一种是正交投影:

平行投影

我们先看比较简单的平行投影

对于这个投影一个比较简单的理解方式,就是直接把z轴丢掉,就是最终这个点在屏幕上的坐标,然后x和y方向都通过平移和缩放到[-1,1]之间。这里我们可以直接丢掉z的原因就在于我们的摄像机被移动到了原点且朝向z轴负方向。

但是,丢掉z轴这个事情目前还不能做,我们还需要z的信息后续做深度测试,我们现在要做的是将x,y,z归一化到[1,1]3[-1,1]^3的正方体中

如下图所示,在平行投影中,我们一开始的观察空间是一个立方体,我们要将这个立方体的中心移动到原点,并且缩放为一个边长为1的正方体

正交投影

正交投影和平行投影不同点在于有一个近大远小的效果,它的初始观察空间不是一个立方体,而是一个梯台

我们在求这个梯台归一化矩阵可以分两步走:

  1. 将梯台缩放为立方体,这个时候的观察空间就相当于平行投影了
  2. 再利用平行投影的归一化矩阵

所以我们目前主要关注第一步就好

而又因为我们现在的摄像机是朝向z负方向的,所以缩放底并不会对z坐标造成影响,我们只需要关注x和y,我们以y坐标举例:

同样的,x左边变为x=nzxx' = \frac{n}{z}x

那么这个时候,我们的变换矩阵就可以写出来了,首先是,正交投影空间变平行投影空间的

Mpresportho[xyz1]=[nzxnzyz1]=[nxnyz2z]Mpresportho=[n0000n0000z00010]M_{presp-ortho}\begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} \frac{n}{z}x \\ \frac{n}{z}y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} nx \\ ny \\ z^2 \\ z \end{bmatrix} \\ 即 \\ M_{presp-ortho} = \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & z & 0 \\ 0 & 0 & 1 & 0 \\ \end{bmatrix}

那么正式的正交投影View矩阵就是MpresporthoM_{presp-ortho}再乘平行投影的View矩阵

至此,我们完成了将一个点从模型空间变换到了世界坐标系下的归一化空间中