作业pa1对应的是GAMES101课程Lecture02到Lecture04这三节课的内容,主要是用于巩固空间中的物体投影到相机平面的整个过程。
说在前面,本文是在左手系下进行讨论的。
粗略地看一遍我们可以知晓main函数的流程:
①设定一些基本的初始参数并初始化源代码给出的光栅化类rasterizer
while循环:
②通过set_model设定被投影物体的位姿变换
☂通过ser_view设定相机系的位姿变换
④通过set_projection设定投影关系
⑤绘制显示投影结果
其中②☂④构成整个投影的过程。初学的小伙伴可能不理解这几个位姿变换的意义是什么,博主学过SLAM有一些三维旋转变换的基础,在我看来——
②中的model_matrix表示被投影的物体在世界坐标系下(即常说的xyz轴)的旋转和平移变换,他表示物体相对于我们最初给的位置(在本例中为三角形的三个顶点),经过了怎么样的运动:
m o d e l m a t r i x ∗ 物 体 原 始 坐 标 — — > 运 动 后 的 新 坐 标 modelmatrix*物体原始坐标——>运动后的新坐标 modelmatrix∗物体原始坐标——>运动后的新坐标
☂中的view_matrix表示相机所观察的角度,其往往包含:相机所在位置t+相机朝向的方向R,R和t是相对于世界坐标系而言的,即将世界坐标系做旋转和平移变化R、t后可变换成以相机建立的坐标系。这样可以将被投影物体的坐标转换到相机所建立的坐标系下:
M a t r i x ( R , t ) ∗ 世 界 坐 标 系 — — > 相 机 坐 标 系 M a t r i x ( R − 1 , − t ) ∗ 运 动 后 的 新 坐 标 ( 世 界 系 ) — — > 运 动 后 的 新 坐 标 ( 相 机 系 ) \begin{aligned} & Matrix(R,t)*世界坐标系——>相机坐标系 \\ & Matrix(R^{-1}, -t)*运动后的新坐标(世界系)——>运动后的新坐标(相机系) \end{aligned} Matrix(R,t)∗世界坐标系——>相机坐标系Matrix(R−1,−t)∗运动后的新坐标(世界系)——>运动后的新坐标(相机系)
注:这里为什么是 R − 1 R^{-1} R−1和 − t -t −t是因为坐标变换和坐标系变换其实是相反的,本质上是相对运动的关系。就像你静止不动,一个人离你而去,如果假设离去的人是静止的,那么就变成了你在以相反的方向离开那个人,在坐标变换中同理。
这里就是Games101课程中讲的核心内容啦。主要就是正交投影(Othographic Projection)和透视投影(Perspective Projection)两部分。
需要先介绍几个重要的形状,从左到右分别表示1、透视投影前的视角形状;2、透视投影后,正交投影前的视角形状;3、正交投影后的视角形状。
从这三张图可以很好地理解初学时课上可能存在的许多疑惑点,比如:
远近平面是什么?
图1中的粉色方形面为远平面,蓝色方形面为近平面,远近平面通过绿色的投影线包裹起来的部分会被投影到我们最后的屏幕上。图2中的红色方形面为透视投影后的远平面。
透视投影矩阵推导时两个用于计算第三行的等式关系是怎么来的?
观察图1图2就可以知道了。透视投影前后蓝色的近平面是不变的,此为等式1;粉色远平面投影后变成红色远平面,是一个挤压的过程,但是前后的中心点即粉色点和红色点是一样的,此为等式2。
即图1到图2的变换过程/变换矩阵,可以用下图表示。表示从粉红色所包裹的空间压缩为红色大红色包裹的空间这一过程。用矩阵可表示为下式,最左边的4*4矩阵 M p e r s p M_{persp} Mpersp为透视投影阵: M p e r s p ∗ [ x y z 1 ] = [ n 0 0 0 0 n 0 0 0 0 n + f − n ∗ f 0 0 1 0 ] ∗ [ x y z 1 ] = [ n x n y u n k o n w z ] M_{persp}* \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}= \begin{bmatrix} n & 0 & 0 & 0 \\ 0 & n & 0 & 0 \\ 0 & 0 & n+f & -n*f \\ 0 & 0 & 1 & 0 \\ \end{bmatrix}* \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}= \begin{bmatrix} nx \\ ny \\ unkonw \\ z \\ \end{bmatrix} Mpersp∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡n0000n0000n+f100−n∗f0⎦⎥⎥⎤∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡nxnyunkonwz⎦⎥⎥⎤
推导这个矩阵的条件有3个
条件①观察透视投影图可以知道任一点(x,y,z)在投影后的坐标 x ′ x^{'} x′和 y ′ y^{'} y′(z无法确定)都可以通过投影关系在近平面上确定为 x ′ = n z ∗ x x^{'}=\frac{n}{z}*x x′=zn∗x和 y ′ = n z ∗ y y^{'}=\frac{n}{z}*y y′=zn∗y,因为一条绿色投影线上的点在投影后的xy坐标就是投影线和近平面的交点的xy坐标
则可写出等式:
1 z ∗ M p e r s p ∗ [ x y z 1 ] = [ n x z n y z u n k n o w 1 ] M p e r s p ∗ [ x y z 1 ] = [ n x n y s t i l l u n k n o w z ] \frac{1}{z}*M_{persp}* \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}=\begin{bmatrix} \frac{nx}{z} \\ \frac{ny}{z} \\ unknow \\ 1 \\ \end{bmatrix} \qquad M_{persp}* \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}=\begin{bmatrix} nx \\ ny \\ stillunknow \\ z \\ \end{bmatrix} z1∗Mpersp∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡znxznyunknow1⎦⎥⎥⎤Mpersp∗⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡nxnystillunknowz⎦⎥⎥⎤
上式可推导出矩阵的1、2、4行。
此时假设仍然未知的第三行为 [ X Y A B ] \begin{bmatrix} X & Y & A & B\end{bmatrix} [XYAB]
条件②所有近平面上的点是不变的,原因上面已经解释。
M p e r s p ∗ [ x y n 1 ] n e a r = [ n x n y n 2 n ] M_{persp}* \begin{bmatrix} x \\ y \\ n \\ 1 \\ \end{bmatrix}_{near}=\begin{bmatrix} nx \\ ny \\ n^{2} \\ n \\ \end{bmatrix} Mpersp∗⎣⎢⎢⎡xyn1⎦⎥⎥⎤near=⎣⎢⎢⎡nxnyn2n⎦⎥⎥⎤
上式可以推出 X = Y = 0 X=Y=0 X=Y=0,因为式子对任意 [ x y n ] \begin{bmatrix} x \\ y \\ n\end{bmatrix} ⎣⎡xyn⎦⎤都成立,还可以推出 A n + B = n 2 An+B=n^2 An+B=n2。
条件☂远平面中点是不变的,原因上面已经解释。
M p e r s p ∗ [ 0 0 f 1 ] = [ 0 0 f 2 f ] M_{persp}* \begin{bmatrix} 0 \\ 0 \\ f \\ 1 \\ \end{bmatrix}=\begin{bmatrix} 0 \\ 0 \\ f^{2} \\ f \\ \end{bmatrix} Mpersp∗⎣⎢⎢⎡00f1⎦⎥⎥⎤=⎣⎢⎢⎡00f2f⎦⎥⎥⎤
上式可以推出 A f + B = f 2 Af+B=f^2 Af+B=f2
综上,可以解的 A = n + f A=n+f A=n+f, B = − n ∗ f B=-n*f B=−n∗f。
即图2到图3的变换过程/变换矩阵,可以用下图表示。表示从大红色表示的空间压缩成白色表示的空间的过程。
用矩阵可表示为下式,最左边的4*4矩阵 M o r t h o M_{ortho} Mortho为正交投影阵,可以拆分为尺度变换 M s c a l e M_{scale} Mscale和平移变换 M t r a n s M_{trans} Mtrans部分:
M o r t h o [ x y z 1 ] = M s c a l e ∗ M t r a n s [ x y z 1 ] = [ 2 r − l 0 0 0 0 2 t − b 0 0 0 0 2 n − f 0 0 0 0 1 ] ∗ [ 1 0 0 − r + l 2 0 1 0 − t + b 2 0 0 1 − n + f 2 0 0 0 1 ] [ x y z 1 ] M_{ortho} \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}= M_{scale}*M_{trans} \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix}= \begin{bmatrix} \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{bmatrix}* \begin{bmatrix} 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{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \\ \end{bmatrix} Mortho⎣⎢⎢⎡xyz1⎦⎥⎥⎤=Mscale∗Mtrans⎣⎢⎢⎡xyz1⎦⎥⎥⎤=⎣⎢⎢⎡r−l20000t−b20000n−f200001⎦⎥⎥⎤∗⎣⎢⎢⎡100001000010−2r+l−2t+b−2n+f1⎦⎥⎥⎤⎣⎢⎢⎡xyz1⎦⎥⎥⎤
这个就不解释了,知道前后(n,f)左右(l,r)上下(tb)可以很快写出上面的式子,注意注意注意 M s c a l e M_{scale} Mscale中第三行第三个为 n − f n-f n−f,因为是右手系,n的值是比f大的n的值是比f大的n的值是比f大的。
至此,设计到的知识基本讲完了,接下来看代码部分。
在这里实现物体的位姿变换,作业中要求的代码如下,即按照角度构建一个简单的旋转矩阵
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
Eigen::Matrix4f m;
float angle = rotation_angle/180.0f * PI;
float c = cosf(angle),s = sinf(angle);
m << c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
return m;
}
如果按照提高部分实现任意轴旋转变化如下,get_rotation中利用罗德里格斯公式实现轴角到旋转矩阵的变化,距离表现形式有两种,两个return的结果是一样的
Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
{
float a = angle*PI/180.0;
float cosa = cosf(a), sina = sinf(a);
Eigen::Matrix3f I = Eigen::Matrix3f::Identity();
axis = axis.normalized();
Eigen::Matrix3f nhat;
nhat << 0, -axis.z(), axis.y(),
axis.z(), 0, -axis.x(),
-axis.y(), axis.x(), 0;
// 两种罗德里格斯公式
return toMatrix4f(I + (nhat*nhat)*(1-cosa) + sina*nhat);
// return toMatrix4f(cosa*I + (1-cosa)*(axis*axis.transpose()) + sina*nhat);
}
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
Eigen::Matrix4f m;
Vector3f axis{1, 1, 1}; // 旋转轴
Eigen::Matrix4f t; // 位移矩阵,左右1列的三个0改成其他值表示三个方向的位移
t << 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
m = get_rotation(axis, rotation_angle);
return m*t;
}
源代码中已经给出,看一遍就可以知道在构建位姿变换矩阵view,正确理解上面将的“相机系的位姿变换”部分应该对其中的一些负号变换就不奇怪了。
在上面我们说到正交矩阵 M o r t h o M_{ortho} Mortho的计算需要知道前后(n,f)左右(l,r)上下(tb),但是我们目前还不知道上下左右是多少。我们可以通过垂直的可视角度fovY和长宽比aspect以及近平面距离n算出前后(n,f)左右(l,r)上下(tb),计算方式如图。这下我们就可以完全表示出所有矩阵啦。
get_projection_matrix函数的传入:垂直的可视角度fovY、长宽比aspect、远近平面距离zNear、zFar
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
Eigen::Matrix4f Mpersp, Mscale, Mtrans, MP2O;
float fovY = eye_fov*PI/180.0;
float t = abs(zNear)*tanf(fovY/2);
float r = t*aspect_ratio;
Mscale << 1/r, 0, 0, 0,
0, 1/t, 0, 0,
0, 0, 2/(zNear-zFar), 0,
0, 0, 0, 1;
Mtrans << 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, -(zNear+zFar)/2,
0, 0, 0, 1;
MP2O << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear+zFar, -zNear*zFar,
0, 0, 1, 0;
Mpersp = Mscale*Mtrans*MP2O;
return Mpersp;
}
由于本例中情况比较特殊,上下左右都为对称情况,所以 M s c a l e M_{scale} Mscale中的 2 2 − l \frac{2}{2-l} 2−l2和 2 t − b \frac{2}{t-b} t−b2简化成了 1 r \frac{1}{r} r1和 1 t \frac{1}{t} t1, M t r a n s M_{trans} Mtrans中的 − r + l 2 -\frac{r+l}{2} −2r+l和 − t + b 2 -\frac{t+b}{2} −2t+b简化成了0。
当然,不用分成三个矩阵,直接将结果计算好再码成代码也是可以的
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio,
float zNear, float zFar)
{
Eigen::Matrix4f Mpersp;
float fovY = eye_fov*PI/180.0;
float cota = 1.f/tanf(fovY/2);
float zD = zNear-zFar;
Mpersp << -cota/aspect_ratio, 0, 0, 0,
0, -cota, 0, 0,
0, 0, (zNear+zFar)/zD, -2*zNear*zFar/zD,
0, 0, 1, 0;
}
在网上找了一份C++中检测键盘输入的代码,在main函数最后加上实现,其中kbhit()为检测键盘输入的函数,有键入是返回1,对具体感兴趣的可以看一下我的源代码
if(kbhit()){
char action = getchar();
if(action=='A') angle += 3;
else if(action=='D') angle -=3;
}
本文讲了GAMES101作业1中涉及的知识和代码实现。源码可以去我的Github查看,为pa1对应的部分,感谢阅读!