Games101(1): Transformation

课程链接:GAMES101-现代计算机图形学入门-闫令琪
课程讲师:闫令琪
本系列笔记为本人根据学习该门课程的笔记,仅分享出来供大家交流,希望大家多多支持GAMES相关讲座及课程,如涉及侵权请联系我删除:albertlidesign@gmail.com

二维变换

Scale

图1 Rotation

在图形学中Scale变换是非常简单的,如果你想把一个物体Scale至它的ss倍,那么只需要将这个物体上所有点的分量都乘以ss,写成矩阵形式就是

[xy]=[s00s][xy]\left[\begin{matrix}x'\\y'\end{matrix}\right] = \left[\begin{matrix}s & 0\\ 0& s\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right]

Reflection

图2 Reflection
将一个物体的镜像也很简单,在二维中,这个物体想沿着哪个轴镜像就将另一个轴的分量乘以1-1即可,即

[xy]=[1001][xy]\left[\begin{matrix}x'\\y'\end{matrix}\right] = \left[\begin{matrix}-1 & 0\\ 0& 1\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right]

Shear

图3 Shear

在做切变的时候要注意,假设xyxy平面上左下角过原点的边长为11的正方形上方的两个点沿xx轴平移aa个单位,那么我们会发现:

(1) y=0y=0上的点没有移动

(2) y=1y=1上的点移动了aa个单位,如点(0,1)(0,1)移动至了(a,1)(a,1)

(3) 垂直方向没有移动

(4) 如果假设y=0.5y=0.5上有一个点(0,0.5)(0,0.5),那么它应该移动至(a2,0.5)(\frac{a}{2},0.5)

那么也就是说,实际上所有的yy都没变,而所有的xx都变为x+ayx+ay

因此用矩阵表示如下:

[xy]=[1a01][xy]\left[\begin{matrix}x'\\y'\end{matrix}\right] = \left[\begin{matrix}1 & a\\ 0& 1\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right]

Rotate

图4 Rotate

首先规定,任何时候我们说旋转都是默认绕着原点(0,0)(0,0)进行旋转(绕其他点旋转可以看作是先将物体移动至原点,进行旋转操作后再移动回去),另外,如果不规定旋转方向,那么我们默认都是逆时针旋转。现有一个物体,假设让它旋转θ\theta度,那么可以根据三角函数来求出旋转后的对应点的坐标。例如点(1,0)(1,0)将会旋转至点(cosθ,sinθ)(cos\theta,sin\theta),点(0,1)(0,1)将会旋转至点(sinθ,cosθ)(-sin\theta,cos\theta)

这样我们就能很轻易地写成矩阵形式:

Rθ=[cosθsinθsinθcosθ]R_\theta = \left[\begin{matrix}cos\theta & -sin\theta\\ sin\theta& cos\theta\end{matrix}\right]

那么如果向顺时针方向旋转该怎么表达呢?顺时针方向旋转其实就是旋转了θ-\theta,将其代如旋转矩阵得到

Rθ=[cosθsinθsinθcosθ]R_{-\theta} = \left[\begin{matrix}cos\theta & sin\theta\\ -sin\theta& cos\theta\end{matrix}\right]

这里我们发现刚好Rθ=RθTR_{-\theta} = R_{\theta}^T,并且,旋转θ\theta角和旋转θ-\theta角正好是互逆的操作,因此还有Rθ=Rθ1R_{-\theta} = R_\theta^{-1}(逆变换的意义其实就是将矩阵变换的操作反过来,下文会继续提到),因此Rθ1=RθTR_\theta^{-1} = R_\theta^T。这是因为旋转矩阵是一个正交矩阵

求变换矩阵的方法

在变换中,无非就是将点(x,y)(x,y)(x,y)\rightarrow(x',y'),表示成矩阵形式就是

[xy]=[abcd][xy]\left[\begin{matrix}x'\\y'\end{matrix}\right] = \left[\begin{matrix}a & b\\ c& d\end{matrix}\right]\left[\begin{matrix}x\\y\end{matrix}\right]x=Mxx' = Mx

我们所做的就是求a,b,c,da,b,c,d,那么既然一个变换矩阵会对这个物体的所有点都起作用,那么也一定对一些特殊点起作用,那么我们就可以利用几个简单的特殊点来进行问题的求解,例如上面旋转矩阵的例子中,有(1,0)(cosθ,sinθ)(1,0)\rightarrow(cos\theta,sin\theta)(0,1)(sinθ,cosθ)(0,1)\rightarrow(sin\theta,cos\theta)

用矩阵表示就是

[cosθsinθ]=[abcd][10][sinθcosθ]=[abcd][01]\left[\begin{matrix}cos\theta\\sin\theta\end{matrix}\right] = \left[\begin{matrix}a & b\\ c& d\end{matrix}\right]\left[\begin{matrix}1\\0\end{matrix}\right]\\\left[\begin{matrix}-sin\theta\\cos\theta\end{matrix}\right] = \left[\begin{matrix}a & b\\ c& d\end{matrix}\right]\left[\begin{matrix}0\\1\end{matrix}\right]

通过第一个矩阵等式我们直接就能求出aacc,再代入第二个矩阵求出bbdd即可,最终我们就能求得旋转矩阵为

Rθ=[cosθsinθsinθcosθ]R_\theta = \left[\begin{matrix}cos\theta & -sin\theta\\ sin\theta& cos\theta\end{matrix}\right]

这也启发了我们,如果想求一个变换矩阵,只需要将变换前后的矩阵列出来,再代入特殊点求解即可。

Translation

图5 Translation

平移操作我们可以很简单地将一个点操作前后的坐标写出来,即

x=x+txy=y+tyx'=x+t_x \\ y'=y+t_y

但是我们会发现,我们不能将其表达成两个矩阵相乘的形式,这个操作的矩阵形式为

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

这样一来,这里的变换就不是线性变换了,它就只能是一种特殊的变换了。

齐次坐标

在发现这件事之后人们就开始思考,有没有一种方法能将它表达成线性变换?答案是有,这就引入了齐次坐标(Homogeneous Coordinates) 的概念。人们引入了一种新的形式来表示物体的坐标,他们在二维坐标后面又加了一个分量ww,规定

Point(2D)=(x,y,1)TVector(2D)=(x,y,0)TPoint(2D) = (x,y,1)^T \\Vector(2D) = (x,y,0)^T

这样,平移的变换就可以写成线性变换形式,即

[xyw]=[10tx01ty001][xy1]=[x+txy+ty1]\left[\begin{matrix}x'\\y'\\w'\end{matrix}\right] = \left[\begin{matrix}1 & 0 & t_x\\ 0& 1 & t_y \\ 0 & 0 &1\end{matrix}\right]\left[\begin{matrix}x\\y\\1\end{matrix}\right]=\left[\begin{matrix}x+t_x\\y+t_y\\1\end{matrix}\right]

为什么在二维点的后面增加了11而在二维向量的后面增加了00呢?其实是有意义的。因为我们知道,在空间里,两向量和必为一个新的向量,如果坐标最后是00,那么相加后最后还是00;如果空间中的点,如果一个点减一个点,这样就形成了一个向量(末点-初点),我们发现最后的坐标是11时相减得到00,变成了一个向量,刚好也满足;一个点加一个向量表示为一个点沿着一个方向移动,移动到了一个新的点上,最后得到的还是一个点,最后的分量还是11,也可验证。因此第三个分量的引入,在点上加一个11,在向量上加一个00,保证了这些操作最后的结果是对的。

vector+vector=vectorpointpoint=vectorpoint+vector=pointpoint+point=??vector + vector = vector\\ point-point=vector\\point+vector = point\\point+point =??

那么最后一个,两点相加后,最后的分量是22,这是什么意思呢?人们也扩充了它的定义,即齐次坐标[xyw]\left[\begin{matrix}x\\y\\w\end{matrix}\right]表示的是二维点[x/wy/w1],w=0\left[\begin{matrix}x/w\\y/w\\1\end{matrix}\right],w\not =0。因此两点相加表达的是它们的中点。

在引入齐次坐标之前,我们做平移、旋转等操作可以起个名字叫做仿射变换(Affine Transformation),仿射(Affine map) = linear map + translation,即

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

使用齐次坐标来表达就可以写成

[xy1]=[abtxcdty001][xy1]\left[\begin{matrix}x'\\y'\\1\end{matrix}\right] = \left[\begin{matrix}a & b & t_x\\ c& d & t_y \\ 0 & 0 &1\end{matrix}\right]\left[\begin{matrix}x\\y\\1\end{matrix}\right]

下面利用齐次坐标来重新书写我们前面所学的各种变换

Scale:S(sx,sy)=[sx000sy0001]Scale: S(s_x,s_y) = \left[\begin{matrix}s_x & 0 & 0\\ 0& s_y & 0\\0 & 0 &1\end{matrix}\right]

Rotation:R(α)=[cosαsinα0sinαcosα0001]Rotation: R(\alpha) = \left[\begin{matrix}cos\alpha & -sin\alpha & 0\\ sin\alpha& cos\alpha & 0\\0 & 0 &1\end{matrix}\right]

Translation:T(tx,ty)=[10tx01ty001]Translation: T(t_x,t_y) = \left[\begin{matrix}1 & 0 & t_x\\ 0& 1 & t_y\\0 & 0 &1\end{matrix}\right]

逆变换和组合变换

逆变换

逆变换的意义其实就是将矩阵变换的操作反过来,例如矩阵MM通过旋转矩阵RR得到MM',那么如果我们已知变换后的矩阵MM'想求得MM只需要左乘旋转矩阵RR的逆R1R^{-1},即

M=RMM=R1MM' = RM \\ M = R^{-1}M'

组合变换

图6 组合变换

复杂的变换可以通过多个变换组合来得到,其中,变换顺序至关重要,因为矩阵相乘不满足交换律。做一次变换可以理解为左乘一个矩阵,当然我们也可以将多次变换的矩阵的作用效果看作是一个矩阵的作用效果,因为矩阵相乘满足结合律。

那么如图所示,我们想求左侧矩阵变换到最右侧矩阵的效果,需要先旋转再平移,那么我们可以写成

M=T(1,0)R45MM' = T_{(1,0)}R_{45}M

图7 先将物体移动至原点,做旋转操作,最后再移动回原位置

对于一个起点不在原点的物体做宣传操作该如何做呢?我们可以先把物体移动到原点,做旋转操作后再移动回原来的位置,即

M=T(c)RαT(c)MM' = T_{(c)}R_{\alpha}T_{(-c)}M

三维变换

了解了二维变换之后,三维就变得很简单了,首先我们依然引入齐次坐标,得到三维点和三维向量:

Point(3D)=(x,y,z,1)TVector(3D)=(x,y,z,0)TPoint(3D) = (x,y,z,1)^T \\Vector(3D) = (x,y,z,0)^T

并且一般来说,齐次坐标[xyzw],w=0\left[\begin{matrix}x\\y\\z\\w\end{matrix}\right],w\not =0表示的是三维点[x/wy/wz/w]\left[\begin{matrix}x/w\\y/w\\z/w\end{matrix}\right]

当然,我们也可以用4×44×4的矩阵表达仿射变换,即

[xyz1]=[abctxdeftyghitz0001][xyz1]\left[\begin{matrix}x'\\y'\\z'\\1\end{matrix}\right] = \left[\begin{matrix}a & b & c & t_x\\ d& e &f & t_y \\ g&h&i&t_z\\0& 0 & 0 &1\end{matrix}\right]\left[\begin{matrix}x\\y\\z\\1\end{matrix}\right]

对应的三维中的各种变换表达如下:

Scale:S(sx,sy,sz)=[sx0000sy0000sz00001]Scale: S(s_x,s_y,s_z) = \left[\begin{matrix}s_x & 0 & 0 & 0\\ 0& s_y & 0 & 0\\0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1\end{matrix}\right]

Translation:T(tx,ty,tz)=[100tx010ty001tz0001]Translation: T(t_x,t_y,t_z) = \left[\begin{matrix}1 & 0 & 0 & t_x\\ 0& 1 & 0 & t_y\\0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1\end{matrix}\right]

Rotation:Rx(α)=[10000cosαsinα00sinαcosα00001]Rotation: R_x(\alpha) = \left[\begin{matrix}1 & 0 & 0 & 0\\ 0& cos\alpha & -sin\alpha & 0\\ 0& sin\alpha& cos\alpha & 0\\0 &0& 0 &1\end{matrix}\right]

Rotation:Ry(α)=[cosα0sinα00100sinα0cosα00001]Rotation: R_y(\alpha) = \left[\begin{matrix}cos\alpha & 0 & sin\alpha & 0\\ 0& 1 & 0 & 0\\ -sin\alpha & 0 & cos\alpha & 0\\0 &0& 0 &1\end{matrix}\right]

Rotation:Rz(α)=[cosαsinα00sinαcosα0000100001]Rotation: R_z(\alpha) = \left[\begin{matrix}cos\alpha & -sin\alpha & 0 & 0\\ sin\alpha& cos\alpha & 0 & 0\\ 0& 0& 1 & 0\\0 &0& 0 &1\end{matrix}\right]

注意,绕yy轴旋转时,左下角变为sinα-sin\alpha,右上角变为sinαsin\alpha,这是因为轴的顺序问题造成的,我们说轴的顺序是xyzx\rightarrow y\rightarrow z,有x×y=zx×y =zy×z=xy×z=x,但是根据右手定则,得到y=z×xy = z×x,而不是x×zx×z,因此这里是反的。

对于一般性旋转我们可以将其转化成三个轴的旋转,即

Rxyz(α,β,γ)=Rx(α)Ry(β)Rz(γ)R_{xyz}(\alpha,\beta,\gamma) = R_x(\alpha)R_y(\beta)R_z(\gamma)

如果想绕着任意一个点旋转,我们可以先将物体沿着这个点平移至原点,做旋转操作后再移回该点。

如果想绕任意轴旋转,我们需要Rodrigues' Rotation Formula,给定角度α\alpha和轴nn,我们有

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

观测变换 (Viewing Transformation)

现实生活中,拍照片我们需要如下步骤:

(1) 找一个好的场地,集合所有人 (模型变换 Model Transformation)

(2) 找一个好的角度,放置相机 (视图变换 View Transformation)

(3) 拍照 (投影变换 Projection Transformation)

1. 定义相机

  • 相机的位置 e\vec e
  • 拍摄方向 (look-at / gaze direction) g^\hat g
  • 向上方向 (up direction) t^\hat t (垂直于拍摄方向)

2.视图变换(View Transformation)

在现实生活中,假如在摄影棚里拍照,相同的人,相同的相机,相同的相对摆放位置,不管在哪一个摄影棚,拍出来的效果是一样的。也就是说,如果相机和所有物体(包括前景背景)都一起移动时,拍出来的照片一定是一样的。更抽象地说,当我们移动物体和移动相机没有相对运动时,拍出来的照片是一样的。那么我们可以将相机永远放在原点这个固定的位置上,物体都可以移动,相机永远不动,并且相机的向上方向为YY方向,看向Z-Z

通过变换将相机放到标准位置上

首先,相机原本在位置e\vec e,向g^\hat g看,并且向上方向为t^\hat t,现在要把它变成固定在原点,向Z-Z方向看,并且up方向为YY。那么我们可以先将相机从e\vec e移到原点,然后再把观原点察方向g^\hat g旋转到Z-Z上,再把向上方向t^\hat t旋转到YY,写成矩阵表达为Mview=RviewTviewM_{view} = R_{view}T_{view}

其中,将相机从e\vec e移到原点很容易写出,将向量e\vec e的三个分量各减去他们本身即可,为

Tview=[100xe010ye001ze0001]T_{view} = \left[\begin{matrix}1 & 0 & 0 & -x_e\\ 0& 1 & 0 & -y_e\\0 & 0 & 1 & -z_e \\ 0 & 0 & 0 & 1\end{matrix}\right]

但是如何把观原点察方向g^\hat g旋转到Z-Z上,再把向上方向t^\hat t旋转到YY呢?这件事并不容易做,但是反过来,将XX旋转到(g^×t^)(\hat g×\hat t),将YY旋转到t^\hat t,将ZZ旋转到g^-\hat g很容易实现,它和我们需要做的操作是一个互逆的操作,因此我们只需要求这一操作的矩阵的逆即可。

Rview1=[xg^×t^xt^xg^0yg^×t^yt^yg^0zg^×t^zt^zg^00001]Rview=[xg^×t^yg^×t^zg^×t^0xt^yt^zt^0xg^yg^zg^00001]R_{view}^{-1} = \left[\begin{matrix}x_{\hat g×\hat t} & x_{\hat t} & x_{-\hat g} & 0\\ y_{\hat g×\hat t}& y_{\hat t} & y_{-\hat g} & 0\\z_{\hat g×\hat t} & z_{\hat t} & z_{-\hat g} & 0 \\ 0 & 0 & 0 & 1\end{matrix}\right]\quad R_{view} = \left[\begin{matrix}x_{\hat g×\hat t} & y_{\hat g×\hat t}& z_{\hat g×\hat t} & 0 \\ x_{\hat t} & y_{\hat t} & z_{\hat t}& 0\\ x_{-\hat g} & y_{-\hat g} & z_{-\hat g} & 0\\ 0 & 0 & 0 & 1\end{matrix}\right]

需要注意的是,为了保证相对结果不变,我们要将场景中的所有物体都做这样的变换。

3. 投影变换(Projection Transformation)

投影包含两种投影方式:正交投影 (Orthographic Projection) 和透视投影 (Perspective Projection)

图8 正交投影与透视投影(1)
图9 正交投影与透视投影(2)

正交投影 (Orthographic Projection)

正交投影很简单,不管物体的远近,我们只需将它“挤”到某个平面上即可。投影到XYXY平面的操作步骤如下:

  • 先将相机放到标准位置上(原点,看向Z-Z,向上为YY
  • 移除掉物体的ZZ坐标
  • 平移、缩放将结果映射到[1,1]2[-1,1]^2 (约定俗称的方法,方便后续计算)
图10 正交投影(1)

上述方法是一个简单的理解方式,但在图形学中,还有更方便的一种操作:

  • 首先定义空间中的立方体[l,r]×[b,t]×[f,n][l,r]×[b,t]×[f,n],只需定义立方体的左右在XX轴上的值,下上在YY轴上的值和前后在ZZ轴上的值(由于右手坐标系,远的值小于近),一共66个数
  • 将这个方体映射到标准立方体[1,1]3[-1,1]^3,将立方体的中心移动到原点,在将模型缩放至[1,1]3[-1,1]^3
图11 正交投影(2)

实现方法:首先通过平移,将立方体中心移动至原点,然后再缩放,以长宽高都缩放至2为例,则

Mortho=[2rl00002tb00002nf00001][100r+l2010t+b2001n+f20001]M_{ortho} = \left[\begin{matrix}\frac{2}{r-l} & 0 & 0 & 0\\ 0& \frac{2}{t-b} & 0 & 0\\0 & 0 & \frac{2}{n-f} & 0 \\ 0 & 0 & 0 & 1\end{matrix}\right]\left[\begin{matrix}1 & 0 & 0 & -\frac{r+l}{2}\\ 0& 1 & 0 & -\frac{t+b}{2}\\0 & 0 & 1 & -\frac{n+f}{2} \\ 0 & 0 & 0 & 1\end{matrix}\right]

注意:

  • 由于我们定义相机看向Z-Z,所以近>远,这也是为什么在OpenGL中使用左手系,但左手系意味着X×Y=ZX×Y \not =Z

  • 也可以用变换坐标系的方式来理解,道理是一样的,但是不直观、不好理解。

透视投影 (Perspective Projection)

图12 透视投影(1)

透视投影是应用最广泛的投影,满足近大远小的性质,带来的视觉效果是平行线不再平行,相交于一点。

回顾一下我们之前关于齐次坐标的定义:

  • (x,y,z,1),(kx,ky,kz,k=0),(xz,yz,z2,z=0)(x,y,z,1),(kx,ky,kz,k\not =0),(xz,yz,z^2,z \not =0)都表示三维空间中的一个相同点(x,y,z)(x,y,z),因为zz也属于任何一个数kk
  • 举个例子:(1,0,0,1)(1,0,0,1)(2,0,0,2)(2,0,0,2)都表示点(1,0,0)(1,0,0)

透视投影是从一个点(相机)开始,往外延伸出的一个四棱锥,我们定义近平面nn和远平面ff,称为Frustum,和正交投影的区别在于远平面ff相对更大,这也是透视投影和正交投影的主要区别。

图13 透视投影(2)

因此我们只需要在正交投影之前增加一步,将远平面ff先挤压至与近平面nn相同的尺寸。也就是说透视投影的过程为两个步骤:先将远平面挤压至近平面的尺寸,再进行正交投影(Mpersportho(4×4)M_{persp\rightarrow ortho}^{(4×4)})。在这一过程中,我们规定:

  • 近平面nn永远不变
  • 远平面ff上的点的zz值不会变(因为是在平面内挤压,zz值不变)
  • 远平面的中心点挤压前后不变
图13 透视投影(3)

从侧面看Frustum会发现远近平面与相机有着相似三角形的关系,从yy坐标来看就有y=nzyy' = \frac{n}{z}y,因此写成矩阵形式就有

[xyz1][nx/zny/zunknown1]==[nxnyunknownz](z)\left[\begin{matrix}x\\y\\z\\1\end{matrix}\right] \Rightarrow \left[\begin{matrix}nx/z\\ny/z\\unknown\\1\end{matrix}\right] == \left[\begin{matrix}nx\\ny\\unknown\\z\end{matrix}\right](同乘z后仍表示同一个点)

那么我们从

Mpersportho(4×4)[xyz1]=[nxnyunknownz]M_{persp\rightarrow ortho}^{(4×4)} \left[\begin{matrix}x\\y\\z\\1\end{matrix}\right] = \left[\begin{matrix}nx\\ny\\unknown\\z\end{matrix}\right]

可知

Mpersportho(4×4)=[n0000n000010]M_{persp\rightarrow ortho}^{(4×4)} = \left[\begin{matrix}n& 0 & 0 & 0\\0 & n & 0 & 0\\?& ? & ? & ?\\0& 0 & 1 & 0\end{matrix}\right]

为了求第三行,我们需要另外两个条件:任意在近平面上的点都保持不变,任意在远平面上的点的zz坐标保持不变。

(1)在近平面上,点坐标的zz值其实是nn,代入

Mpersportho(4×4)[xyz1]=[nxnyunknownz]M_{persp\rightarrow ortho}^{(4×4)} \left[\begin{matrix}x\\y\\z\\1\end{matrix}\right] = \left[\begin{matrix}nx\\ny\\unknown\\z\end{matrix}\right]

[xyn1][xyn1]==[nxnyn2n]\left[\begin{matrix}x\\y\\n\\1\end{matrix}\right] \Rightarrow \left[\begin{matrix}x\\y\\n\\1\end{matrix}\right]== \left[\begin{matrix}nx\\ny\\n^2\\n\end{matrix}\right]

第三行为n2n^2,那么我们可以求出Mpersportho(4×4)M_{persp\rightarrow ortho}^{(4×4)}的第三行的前两项为00,即

[00AB][xyn1]=n2\left[\begin{matrix}0& 0&A&B\end{matrix}\right] \left[\begin{matrix}x\\y\\n\\1\end{matrix}\right] = n^2

现在还剩下两个未知数AABB,接着我们再使用第二个条件:任意在远平面上的点的zz坐标保持不变。

(2)在远平面上,点坐标的zz值是ff,代入

[00f1][00f1]==[00f2f]f\left[\begin{matrix}0\\0\\f\\1\end{matrix}\right]\Rightarrow \left[\begin{matrix}0\\0\\f\\1\end{matrix}\right] == \left[\begin{matrix}0\\0\\f^2\\f\end{matrix}\right](同乘f后仍表示同一个点)

根据近平面我们得到的结果,将其展开得

[00AB][xyn1]=n2An+B=n2\left[\begin{matrix}0& 0&A&B\end{matrix}\right] \left[\begin{matrix}x\\y\\n\\1\end{matrix}\right] = n^2 \Rightarrow An+B=n^2

根据远平面我们得到的结果,将其展开得

[00f1][00f1]==[00f2f]Af+B=f2\left[\begin{matrix}0\\0\\f\\1\end{matrix}\right]\Rightarrow \left[\begin{matrix}0\\0\\f\\1\end{matrix}\right] == \left[\begin{matrix}0\\0\\f^2\\f\end{matrix}\right] \Rightarrow Af+B=f^2

于是将两个展开式联立

$An+B=n^2\ Af+B=f^2 $

A=n+fB=nfA = n+f\\B=-nf

因此

Mpersportho(4×4)=[n0000n0000n+fnf0010]M_{persp\rightarrow ortho}^{(4×4)} = \left[\begin{matrix}n& 0 & 0 & 0\\0 & n & 0 & 0\\ 0 & 0 & n+f & -nf\\0& 0 & 1 & 0\end{matrix}\right]

透视投影的矩阵变换为

Mpersp=MorthoMpersporthoM_{persp} = M_{ortho}M_{persp\rightarrow ortho}

视口变换

之前已经把如何将透视投影转化成正交投影说明白了,透视投影转化成正交投影需要保证近和远两个平面都是不变的,大小上远平面要变成和近平面一样大。在表示立方体上我们需要 左右前后远近(l,r,b,t,f,nl,r,b,t,f,n) 66个值来表示立方体,既然远和近在正交投影和透视投影中都是一样的,就不用管它了,我们可以将Frustum变成一个长方体,问题在于我们如何定义这个Frustum?

图14 视口变换(1)

如图所示,我们从摄像机出发看向某一个区域,如果假设看到的就是这个近的平面,那么我们可以定义一个宽度和高度,就好像我们在看一个显示器一样,我们需要定义一个宽高比 (aspect ratio),如4:3,16:94:3,16:9。我们还需要定义另外一个概念,称为field-of-view,即能看到的角度的范围。假如我们在看一个屏幕,我们可以分别从相机出发与屏幕顶边的中点和底边的中点连出两条红线,它们所形成的夹角就是垂直可视角度 (field-of-view Y, fovY)。因此,**定义一个视锥需要定义一个宽高比和垂直可视角度。**有些游戏里有水平可视角度,这个可以通过长宽比和垂直可视角度推出。

有了这两个概念,我们就可以将它们和之前定义的空间中的长方体转化成同一个概念。如图所示,从侧面来看这个视锥体可以看到一个三角形,如果我们取垂直可视角度的tangenttangent,可以发现tanfovY2=tntan\frac{fovY}{2} = \frac{t}{|n|},也就是说,如果我们知道这个近平面的距离,就能知道这个屏幕一半的高度是多少,屏幕的最高点对应的yy值就是tt,最低点对应的yy值就是t-t,也就是说,如果定义一个空间中的长方体,b=tb=-t,根据宽高比aspect=rtaspect = \frac{r}{t}即可求出水平方向上两边中点的坐标。
图15 视口变换(2)

作业0

/*
给定一个点 P=(2,1), 将该点绕原点先逆时针旋转 45◦,再平移 (1,2), 计算出
变换后点的坐标(要求用齐次坐标进行计算)。
*/

#include <iostream>
#include<cmath>
#include <Eigen/Dense>

#define _USE_MATH_DEFINES
using namespace Eigen;

int main()
{
    // define a 2d point (2,1)
    Vector3f vec = Vector3f(2, 1, 1);
    std::cout << "The point is: \n" << vec << std::endl;

    // build a rotation matrix
    Matrix3f R45(3, 3);
    float theta = 45.0f / 180.0f * M_PI;
    R45 << 
        cos(theta), -sin(theta), 0,
        sin(theta), cos(theta), 0,
        0, 0, 1;
    std::cout << "Rotation Matrix: \n" << R45 << std::endl;

    // build a translation matrix
    Matrix3f T12(3, 3);
    T12 << 
        1, 0, 1,
        0, 1, 2,
        0, 0, 1;
    std::cout << "Translation Matrix: \n" << T12 << std::endl;

    // compute the result
    Vector3f result = T12 * R45 * vec;
    std::cout << "The result is: \n" << result << std::endl;
}

作业1

/*
填写一个旋转矩阵和一个透视投影矩阵。给定三维下三个点
v0(2.0, 0.0, -2.0), v1(0.0, 2.0, -2.0), v2( 2.0, 0.0, 2.0), 你需要将这三个点的坐
标变换为屏幕坐标并在屏幕上绘制出对应的线框三角形 (在代码框架中,我们已
经提供了 draw_triangle 函数,所以你只需要去构建变换矩阵即可)。简而言之,
我们需要进行模型、视图、投影、视口等变换来将三角形显示在屏幕上。在提供
的代码框架中,我们留下了模型变换和投影变换的部分给你去完成。

get_model_matrix(float rotation_angle): 逐个元素地构建模型变换矩阵并返回该矩阵。
在此函数中,你只需要实现三维中绕 z 轴旋转的变换矩阵,而不用处理平移与缩放。

get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar): 使用给定的参数逐个元素地构建透视投影矩阵并返回该矩阵。

[Optional] main(): 自行补充你所需的其他操作。
*/
#include <iostream>
#include<cmath>
#include <Eigen/Dense>

#define _USE_MATH_DEFINES
using namespace Eigen;

Matrix4f get_model_matrix(float rotation_angle);
Matrix4f get_rotation(Vector3f axis, float angle);      // build a rotation matrix about any axis through orgin
Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar);

Matrix4f get_model_matrix(float rotation_angle)
{
    // build a matrix rotating about the z-axis  
    Matrix4f RZ(4, 4);

    float theta = rotation_angle / 180.0f * M_PI;

    RZ <<
        cos(theta), -sin(theta), 0.0f, 0.0f,
        sin(theta), cos(theta), 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f;
    return RZ;
}

// build a rotation matrix about any axis through orgin
Matrix4f get_rotation(Vector3f axis, float angle)
{
    Matrix3f N(3, 3);

    float theta = angle / 180.0f * M_PI;

    N <<
        0.0f, -axis.z(), axis.y(),
        axis.z(), 0.0f, -axis.x(),
        -axis.y(), axis.x(), 0.0f;

    Matrix3f R = cos(theta) * Matrix3f::Identity(3,3) + (1 - cos(theta)) * axis * axis.transpose() + sin(theta) * N;
    Matrix4f RN(4, 4);
    RN <<
        R(0, 0), R(0, 1), R(0, 2), 0.0f,
        R(1, 0), R(1, 1), R(1, 2), 0.0f,
        R(2, 0), R(2, 1), R(2, 2), 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f;
    return RN;
}

Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
    float fovY = eye_fov / 180.0f * M_PI;
    float t = tan(fovY / 2) * abs(zNear);
    float r = aspect_ratio * t;
    float l = -r;
    float b = -t;
    float n = zNear;
    float f = zFar;

    // build a matrix of orthographic projection
    Matrix4f orthoA(4, 4);
    orthoA <<
        2.0f / (r-l), 0.0f, 0.0f, 0.0f,
        0.0f, 2.0f / (t-b), 0.0f, 0.0f,
        0.0f, 0.0f, 2.0f / (n - f), 0.0f,
        0.0f, 0.0f, 0.0f, 1.0f;
    Matrix4f orthoB(4, 4);
    orthoB <<
        1.0f, 0.0f, 0.0f, -(r + l) / 2.0f,
        0.0f, 1.0f, 0.0f, -(t + b) / 2.0f,
        0.0f, 0.0f, 1.0f, -(n + f) / 2.0f,
        0.0f, 0.0f, 0.0f, 1.0f;
    Matrix4f ortho = orthoA * orthoB;

    // build a matrix from perspective to orthographic
    Matrix4f pto(4, 4);
    pto <<
        n, 0.0f, 0.0f, 0.0f,
        0.0f, n, 0.0f, 0.0f,
        0.0f, 0.0f, n + f, -(n * f),
        0.0f, 0.0f, 1.0f, 0.0f;

    // compute the projection matrix
    Matrix4f proj = ortho * pto;

    return proj;
}