课程链接:GAMES101-现代计算机图形学入门-闫令琪
课程讲师:闫令琪
本系列笔记为本人根据学习该门课程的笔记,仅分享出来供大家交流,希望大家多多支持GAMES相关讲座及课程,如涉及侵权请联系我删除:albertlidesign@gmail.com
二维变换
Scale
在图形学中Scale变换是非常简单的,如果你想把一个物体Scale至它的s倍,那么只需要将这个物体上所有点的分量都乘以s,写成矩阵形式就是
[x′y′]=[s00s][xy]
Reflection
将一个物体的镜像也很简单,在二维中,这个物体想沿着哪个轴镜像就将另一个轴的分量乘以−1即可,即
[x′y′]=[−1001][xy]
Shear
在做切变的时候要注意,假设xy平面上左下角过原点的边长为1的正方形上方的两个点沿x轴平移a个单位,那么我们会发现:
(1) y=0上的点没有移动
(2) y=1上的点移动了a个单位,如点(0,1)移动至了(a,1)
(3) 垂直方向没有移动
(4) 如果假设y=0.5上有一个点(0,0.5),那么它应该移动至(2a,0.5)
那么也就是说,实际上所有的y都没变,而所有的x都变为x+ay
因此用矩阵表示如下:
[x′y′]=[10a1][xy]
Rotate
首先规定,任何时候我们说旋转都是默认绕着原点(0,0)进行旋转(绕其他点旋转可以看作是先将物体移动至原点,进行旋转操作后再移动回去),另外,如果不规定旋转方向,那么我们默认都是逆时针旋转。现有一个物体,假设让它旋转θ度,那么可以根据三角函数来求出旋转后的对应点的坐标。例如点(1,0)将会旋转至点(cosθ,sinθ),点(0,1)将会旋转至点(−sinθ,cosθ)。
这样我们就能很轻易地写成矩阵形式:
Rθ=[cosθsinθ−sinθcosθ]
那么如果向顺时针方向旋转该怎么表达呢?顺时针方向旋转其实就是旋转了−θ,将其代如旋转矩阵得到
R−θ=[cosθ−sinθsinθcosθ]
这里我们发现刚好R−θ=RθT,并且,旋转θ角和旋转−θ角正好是互逆的操作,因此还有R−θ=Rθ−1(逆变换的意义其实就是将矩阵变换的操作反过来,下文会继续提到),因此Rθ−1=RθT。这是因为旋转矩阵是一个正交矩阵。
求变换矩阵的方法
在变换中,无非就是将点(x,y)→(x′,y′),表示成矩阵形式就是
[x′y′]=[acbd][xy] 即 x′=Mx
我们所做的就是求a,b,c,d,那么既然一个变换矩阵会对这个物体的所有点都起作用,那么也一定对一些特殊点起作用,那么我们就可以利用几个简单的特殊点来进行问题的求解,例如上面旋转矩阵的例子中,有(1,0)→(cosθ,sinθ)和(0,1)→(sinθ,cosθ)
用矩阵表示就是
[cosθsinθ]=[acbd][10][−sinθcosθ]=[acbd][01]
通过第一个矩阵等式我们直接就能求出a和c,再代入第二个矩阵求出b和d即可,最终我们就能求得旋转矩阵为
Rθ=[cosθsinθ−sinθcosθ]
这也启发了我们,如果想求一个变换矩阵,只需要将变换前后的矩阵列出来,再代入特殊点求解即可。
Translation
平移操作我们可以很简单地将一个点操作前后的坐标写出来,即
x′=x+txy′=y+ty
但是我们会发现,我们不能将其表达成两个矩阵相乘的形式,这个操作的矩阵形式为
[x′y′]=[acbd][xy]+[txty]
这样一来,这里的变换就不是线性变换了,它就只能是一种特殊的变换了。
齐次坐标
在发现这件事之后人们就开始思考,有没有一种方法能将它表达成线性变换?答案是有,这就引入了齐次坐标(Homogeneous Coordinates) 的概念。人们引入了一种新的形式来表示物体的坐标,他们在二维坐标后面又加了一个分量w,规定
Point(2D)=(x,y,1)TVector(2D)=(x,y,0)T
这样,平移的变换就可以写成线性变换形式,即
⎣⎡x′y′w′⎦⎤=⎣⎡100010txty1⎦⎤⎣⎡xy1⎦⎤=⎣⎡x+txy+ty1⎦⎤
为什么在二维点的后面增加了1而在二维向量的后面增加了0呢?其实是有意义的。因为我们知道,在空间里,两向量和必为一个新的向量,如果坐标最后是0,那么相加后最后还是0;如果空间中的点,如果一个点减一个点,这样就形成了一个向量(末点-初点),我们发现最后的坐标是1时相减得到0,变成了一个向量,刚好也满足;一个点加一个向量表示为一个点沿着一个方向移动,移动到了一个新的点上,最后得到的还是一个点,最后的分量还是1,也可验证。因此第三个分量的引入,在点上加一个1,在向量上加一个0,保证了这些操作最后的结果是对的。
vector+vector=vectorpoint−point=vectorpoint+vector=pointpoint+point=??
那么最后一个,两点相加后,最后的分量是2,这是什么意思呢?人们也扩充了它的定义,即齐次坐标⎣⎡xyw⎦⎤表示的是二维点⎣⎡x/wy/w1⎦⎤,w=0。因此两点相加表达的是它们的中点。
在引入齐次坐标之前,我们做平移、旋转等操作可以起个名字叫做仿射变换(Affine Transformation),仿射(Affine map) = linear map + translation,即
[x′y′]=[acbd][xy]+[txty]
使用齐次坐标来表达就可以写成
⎣⎡x′y′1⎦⎤=⎣⎡ac0bd0txty1⎦⎤⎣⎡xy1⎦⎤
下面利用齐次坐标来重新书写我们前面所学的各种变换
Scale:S(sx,sy)=⎣⎡sx000sy0001⎦⎤
Rotation:R(α)=⎣⎡cosαsinα0−sinαcosα0001⎦⎤
Translation:T(tx,ty)=⎣⎡100010txty1⎦⎤
逆变换和组合变换
逆变换
逆变换的意义其实就是将矩阵变换的操作反过来,例如矩阵M通过旋转矩阵R得到M′,那么如果我们已知变换后的矩阵M′想求得M只需要左乘旋转矩阵R的逆R−1,即
M′=RMM=R−1M′
组合变换
复杂的变换可以通过多个变换组合来得到,其中,变换顺序至关重要,因为矩阵相乘不满足交换律。做一次变换可以理解为左乘一个矩阵,当然我们也可以将多次变换的矩阵的作用效果看作是一个矩阵的作用效果,因为矩阵相乘满足结合律。
那么如图所示,我们想求左侧矩阵变换到最右侧矩阵的效果,需要先旋转再平移,那么我们可以写成
M′=T(1,0)R45M
对于一个起点不在原点的物体做宣传操作该如何做呢?我们可以先把物体移动到原点,做旋转操作后再移动回原来的位置,即
M′=T(c)RαT(−c)M
三维变换
了解了二维变换之后,三维就变得很简单了,首先我们依然引入齐次坐标,得到三维点和三维向量:
Point(3D)=(x,y,z,1)TVector(3D)=(x,y,z,0)T
并且一般来说,齐次坐标⎣⎢⎢⎡xyzw⎦⎥⎥⎤,w=0表示的是三维点⎣⎡x/wy/wz/w⎦⎤。
当然,我们也可以用4×4的矩阵表达仿射变换,即
⎣⎢⎢⎡x′y′z′1⎦⎥⎥⎤=⎣⎢⎢⎡adg0beh0cfi0txtytz1⎦⎥⎥⎤⎣⎢⎢⎡xyz1⎦⎥⎥⎤
对应的三维中的各种变换表达如下:
Scale:S(sx,sy,sz)=⎣⎢⎢⎡sx0000sy0000sz00001⎦⎥⎥⎤
Translation:T(tx,ty,tz)=⎣⎢⎢⎡100001000010txtytz1⎦⎥⎥⎤
Rotation:Rx(α)=⎣⎢⎢⎡10000cosαsinα00−sinαcosα00001⎦⎥⎥⎤
Rotation:Ry(α)=⎣⎢⎢⎡cosα0−sinα00100sinα0cosα00001⎦⎥⎥⎤
Rotation:Rz(α)=⎣⎢⎢⎡cosαsinα00−sinαcosα0000100001⎦⎥⎥⎤
注意,绕y轴旋转时,左下角变为−sinα,右上角变为sinα,这是因为轴的顺序问题造成的,我们说轴的顺序是x→y→z,有x×y=z,y×z=x,但是根据右手定则,得到y=z×x,而不是x×z,因此这里是反的。
对于一般性旋转我们可以将其转化成三个轴的旋转,即
Rxyz(α,β,γ)=Rx(α)Ry(β)Rz(γ)
如果想绕着任意一个点旋转,我们可以先将物体沿着这个点平移至原点,做旋转操作后再移回该点。
如果想绕任意轴旋转,我们需要Rodrigues' Rotation Formula,给定角度α和轴n,我们有
R(n,α)=cos(α)I+(1−cos(α))nnT+sin(α)⎣⎡0nz−ny−nz0nxny−nx0⎦⎤
现实生活中,拍照片我们需要如下步骤:
(1) 找一个好的场地,集合所有人 (模型变换 Model Transformation)
(2) 找一个好的角度,放置相机 (视图变换 View Transformation)
(3) 拍照 (投影变换 Projection Transformation)
1. 定义相机
- 相机的位置 e
- 拍摄方向 (look-at / gaze direction) g^
- 向上方向 (up direction) t^ (垂直于拍摄方向)
在现实生活中,假如在摄影棚里拍照,相同的人,相同的相机,相同的相对摆放位置,不管在哪一个摄影棚,拍出来的效果是一样的。也就是说,如果相机和所有物体(包括前景背景)都一起移动时,拍出来的照片一定是一样的。更抽象地说,当我们移动物体和移动相机没有相对运动时,拍出来的照片是一样的。那么我们可以将相机永远放在原点这个固定的位置上,物体都可以移动,相机永远不动,并且相机的向上方向为Y方向,看向−Z。
通过变换将相机放到标准位置上
首先,相机原本在位置e,向g^看,并且向上方向为t^,现在要把它变成固定在原点,向−Z方向看,并且up方向为Y。那么我们可以先将相机从e移到原点,然后再把观原点察方向g^旋转到−Z上,再把向上方向t^旋转到Y,写成矩阵表达为Mview=RviewTview
其中,将相机从e移到原点很容易写出,将向量e的三个分量各减去他们本身即可,为
Tview=⎣⎢⎢⎡100001000010−xe−ye−ze1⎦⎥⎥⎤
但是如何把观原点察方向g^旋转到−Z上,再把向上方向t^旋转到Y呢?这件事并不容易做,但是反过来,将X旋转到(g^×t^),将Y旋转到t^,将Z旋转到−g^很容易实现,它和我们需要做的操作是一个互逆的操作,因此我们只需要求这一操作的矩阵的逆即可。
Rview−1=⎣⎢⎢⎡xg^×t^yg^×t^zg^×t^0xt^yt^zt^0x−g^y−g^z−g^00001⎦⎥⎥⎤Rview=⎣⎢⎢⎡xg^×t^xt^x−g^0yg^×t^yt^y−g^0zg^×t^zt^z−g^00001⎦⎥⎥⎤
需要注意的是,为了保证相对结果不变,我们要将场景中的所有物体都做这样的变换。
投影包含两种投影方式:正交投影 (Orthographic Projection) 和透视投影 (Perspective Projection)
正交投影 (Orthographic Projection)
正交投影很简单,不管物体的远近,我们只需将它“挤”到某个平面上即可。投影到XY平面的操作步骤如下:
- 先将相机放到标准位置上(原点,看向−Z,向上为Y)
- 移除掉物体的Z坐标
- 平移、缩放将结果映射到[−1,1]2 (约定俗称的方法,方便后续计算)
上述方法是一个简单的理解方式,但在图形学中,还有更方便的一种操作:
- 首先定义空间中的立方体[l,r]×[b,t]×[f,n],只需定义立方体的左右在X轴上的值,下上在Y轴上的值和前后在Z轴上的值(由于右手坐标系,远的值小于近),一共6个数
- 将这个方体映射到标准立方体[−1,1]3,将立方体的中心移动到原点,在将模型缩放至[−1,1]3
实现方法:首先通过平移,将立方体中心移动至原点,然后再缩放,以长宽高都缩放至2为例,则
Mortho=⎣⎢⎢⎡r−l20000t−b20000n−f200001⎦⎥⎥⎤⎣⎢⎢⎡100001000010−2r+l−2t+b−2n+f1⎦⎥⎥⎤
注意:
透视投影 (Perspective Projection)
透视投影是应用最广泛的投影,满足近大远小的性质,带来的视觉效果是平行线不再平行,相交于一点。
回顾一下我们之前关于齐次坐标的定义:
- (x,y,z,1),(kx,ky,kz,k=0),(xz,yz,z2,z=0)都表示三维空间中的一个相同点(x,y,z),因为z也属于任何一个数k
- 举个例子:(1,0,0,1)和(2,0,0,2)都表示点(1,0,0)
透视投影是从一个点(相机)开始,往外延伸出的一个四棱锥,我们定义近平面n和远平面f,称为Frustum,和正交投影的区别在于远平面f相对更大,这也是透视投影和正交投影的主要区别。
因此我们只需要在正交投影之前增加一步,将远平面f先挤压至与近平面n相同的尺寸。也就是说透视投影的过程为两个步骤:先将远平面挤压至近平面的尺寸,再进行正交投影(Mpersp→ortho(4×4))。在这一过程中,我们规定:
- 近平面n永远不变
- 远平面f上的点的z值不会变(因为是在平面内挤压,z值不变)
- 远平面的中心点挤压前后不变
从侧面看Frustum会发现远近平面与相机有着相似三角形的关系,从y坐标来看就有y′=zny,因此写成矩阵形式就有
⎣⎢⎢⎡xyz1⎦⎥⎥⎤⇒⎣⎢⎢⎡nx/zny/zunknown1⎦⎥⎥⎤==⎣⎢⎢⎡nxnyunknownz⎦⎥⎥⎤(同乘z后仍表示同一个点)
那么我们从
Mpersp→ortho(4×4)⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡nxnyunknownz⎦⎥⎥⎤
可知
Mpersp→ortho(4×4)=⎣⎢⎢⎡n0?00n?000?100?0⎦⎥⎥⎤
为了求第三行,我们需要另外两个条件:任意在近平面上的点都保持不变,任意在远平面上的点的z坐标保持不变。
(1)在近平面上,点坐标的z值其实是n,代入
Mpersp→ortho(4×4)⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡nxnyunknownz⎦⎥⎥⎤
有
⎣⎢⎢⎡xyn1⎦⎥⎥⎤⇒⎣⎢⎢⎡xyn1⎦⎥⎥⎤==⎣⎢⎢⎡nxnyn2n⎦⎥⎥⎤
第三行为n2,那么我们可以求出Mpersp→ortho(4×4)的第三行的前两项为0,即
[00AB]⎣⎢⎢⎡xyn1⎦⎥⎥⎤=n2
现在还剩下两个未知数A和B,接着我们再使用第二个条件:任意在远平面上的点的z坐标保持不变。
(2)在远平面上,点坐标的z值是f,代入
⎣⎢⎢⎡00f1⎦⎥⎥⎤⇒⎣⎢⎢⎡00f1⎦⎥⎥⎤==⎣⎢⎢⎡00f2f⎦⎥⎥⎤(同乘f后仍表示同一个点)
根据近平面我们得到的结果,将其展开得
[00AB]⎣⎢⎢⎡xyn1⎦⎥⎥⎤=n2⇒An+B=n2
根据远平面我们得到的结果,将其展开得
⎣⎢⎢⎡00f1⎦⎥⎥⎤⇒⎣⎢⎢⎡00f1⎦⎥⎥⎤==⎣⎢⎢⎡00f2f⎦⎥⎥⎤⇒Af+B=f2
于是将两个展开式联立
$An+B=n^2\ Af+B=f^2 $
得
A=n+fB=−nf
因此
Mpersp→ortho(4×4)=⎣⎢⎢⎡n0000n0000n+f100−nf0⎦⎥⎥⎤
透视投影的矩阵变换为
Mpersp=MorthoMpersp→ortho
视口变换
之前已经把如何将透视投影转化成正交投影说明白了,透视投影转化成正交投影需要保证近和远两个平面都是不变的,大小上远平面要变成和近平面一样大。在表示立方体上我们需要 左右前后远近(l,r,b,t,f,n) 6个值来表示立方体,既然远和近在正交投影和透视投影中都是一样的,就不用管它了,我们可以将Frustum变成一个长方体,问题在于我们如何定义这个Frustum?
如图所示,我们从摄像机出发看向某一个区域,如果假设看到的就是这个近的平面,那么我们可以定义一个宽度和高度,就好像我们在看一个显示器一样,我们需要定义一个宽高比 (aspect ratio),如4:3,16:9。我们还需要定义另外一个概念,称为field-of-view,即能看到的角度的范围。假如我们在看一个屏幕,我们可以分别从相机出发与屏幕顶边的中点和底边的中点连出两条红线,它们所形成的夹角就是垂直可视角度 (field-of-view Y, fovY)。因此,**定义一个视锥需要定义一个宽高比和垂直可视角度。**有些游戏里有水平可视角度,这个可以通过长宽比和垂直可视角度推出。
有了这两个概念,我们就可以将它们和之前定义的空间中的长方体转化成同一个概念。如图所示,从侧面来看这个视锥体可以看到一个三角形,如果我们取垂直可视角度的tangent,可以发现tan2fovY=∣n∣t,也就是说,如果我们知道这个近平面的距离,就能知道这个屏幕一半的高度是多少,屏幕的最高点对应的y值就是t,最低点对应的y值就是−t,也就是说,如果定义一个空间中的长方体,b=−t,根据宽高比aspect=tr即可求出水平方向上两边中点的坐标。
作业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;
}