好康的:https://www.bilibili.com/video/BV1X7411F744
好玩的Latex编辑器:https://www.mathcha.io/editor
好玩的矩阵计算器:https://matrixcalc.org/zh/
好玩的Latex OCR:https://snip.mathpix.com/
旋转等线性变换和平移变换
线性变换
线性变换 = 矩阵M,使得a’ = Ma,也就是说可以写成矩阵点乘的形式:
旋转矩阵
其他略
平移变换和齐次坐标
引入概念
平移不能写成类似x’ = Mx的形式(平移不是线性变换),为了统一形式于是引入齐次坐标的概念:
齐次坐标下的2d点=
齐次坐标下的2d向量=
于是平移就可以表示成矩阵点乘的形式:
强行给向量平移不会改变向量
定义和性质
在齐次坐标的情况下,由于2d点最后是1,2d向量最后是0,于是也满足:
向量+向量=向量(末尾0-0=0)
点-点=向量(末1-1=0)
点+向量=点(末尾1+0=1)
点+点=无意义(末尾1+1=2)
但是在齐次坐标里,如果末尾不是0或1,则可以将他们归一化,即
这么一来,点+点就有意义了,点+点=两点的中点
齐次坐标下的变换矩阵
齐次坐标下的2d缩放矩阵:
齐次坐标下的2d旋转矩阵:
齐次坐标下的2d平移矩阵:
由于最后一行都是0 0 1,所以也储存的时候也可以将最后一行省略,节省空间
变换矩阵的叠加
变换矩阵可以叠加(结合律),多次的复杂的变换都可以变成多个简单变换的结合,也就都可以写成一个单独的变换矩阵
旋转变换矩阵和正交矩阵
对旋转矩阵而言
往相反反向旋转:
逆矩阵也就是复原的变换过程,而对旋转矩阵而言,其复原的变换过程就是往相反方向旋转,那么就可以得出旋转矩阵的逆矩阵=反向旋转矩阵=旋转矩阵的转制:
即旋转矩阵满足
3D下的齐次坐标和变换
3d和2d类似:
齐次坐标下的3d点=
齐次坐标下的3d向量=
于是齐次坐标下的3d变换就可以表示为:
(
3D下的旋转变换
沿轴旋转
3d旋转变换中绕x、y、z轴逆时针旋转(从箭头向原点看去的平面上的逆时针)的矩阵:
这里绕y轴旋转和其他两轴不太一样,因为从箭头向原点看去的平面实际上不是x-z平面(x横轴,z纵轴),而是z-x平面(z横轴,x纵轴)。或者也可以根据右手螺旋定则来解释,即x方向向量叉乘z方向向量得到的是-y方向向量,要得到y方向向量,则需要为z方向向量叉乘x方向向量,也就是z-x平面而非x-z平面。所以绕y轴逆时针旋转实际上是在z-x平面上逆时针旋转,也就是在x-z平面上的顺时针旋转
瞎几把旋转
以原点为中心沿任意方向旋转任意角度,可以使用罗德里格斯旋转公式:
沿向量n(默认起点为原点)为轴旋转α角度的旋转矩阵为:
(
关于旋转的补充
- 四元数一般用于求变换过程中的某个点,关键帧之间的中间帧。比如两个旋转矩阵(10度和20度)求中间值,结果并不等于两次旋转角度的中间旋转角度(15度),此时就需要用到四元数。具体略
- 如果要以非原点为中心旋转,则先进行平移变换,将旋转中心移至原点,再进行旋转,最后再平移回去
模型、视图、投影变换
MVP变换:模型变换(model transformation),视图变换(view transformation),投影变换(projection transformation)
模型/视图变换
求变换矩阵的计算方法:
-
定义相机位置为向量
(这里故意用向量表示点),相机朝向为方向向量 ,相机顶部朝向为方向向量 ,这三个变量定义了摄像机。 -
因为运动是相对的,所以用摄像机拍摄的物体运动来代替摄像机运动。那么首先需要将世界系转换为摄像机系,也就是需要让相机处于位置固定在原点(0,0,0),顶部朝向为Y轴方向,相机朝向为-Z方向的标准状态:
-
定义一个变换矩阵
来变换世界,以使得摄像机处于标准位置,它需要先将 平移至原点,然后将 旋转到-Z,将 旋转到Y,最后 就自然旋转到了X令
, 负责平移, 负责旋转 -
平移世界使
平移至原点: -
旋转世界使摄像机朝向和顶部朝向归于标准位置:
因为
不容易计算,先计算其逆 ,也就是将标准化的摄像机(-Z、Y、X)旋转至原位置( 、 、 ):由于旋转矩阵是正交矩阵(见前面#3D下的齐次坐标和变换),
,则可以得出 :
-
-
得到模型视图变换的矩阵
投影变换
分为正交投影(orthographic projection)和透视投影(perspective projection)
正交投影
易于理解的方法:
-
定义坐标系,相机位置为原点,朝向设为-Z轴,上方设为Y轴(也就是相机标准状态)
-
丢掉被观测物体的z轴坐标,使其成为x-y平面上的一个二维图形
-
缩放到
矩形内(约定俗成)
实际使用的求变换矩阵的计算方法:
-
定义一个在坐标轴上正着摆放的立方体作为视体,记录其左右面的x坐标(即左右面分别与y-z平面的距离)、下上面的y坐标(即下上面分别与x-z平面的距离)、远近面的z坐标(即远近面分别与x-y平面的距离),记为
注:这里f和n(远近)反过来记是因为摄像机朝向是-z,离镜头正面越远数值越小,也就是
,所以才故意记为 而不是 -
将其映射成标准正交立方体
,得到正交投影的矩阵缩 放 到 标 准 正 交 的 立 方 体 ( 长 宽 高 都 为 ) 平 移 立 方 体 中 心 到 原 点
注1:某些图形API用的左手系,有一些差别
注2:这里所写的正交投影矩阵实际上是包括了模型视图变换的,也就是要先平移再缩放。如果摄像机已经在原点为标准摄像机的话,就不需要
注3:在OpenGL中,视觉坐标系使用右手系而标准立方体(在OpenGL中叫做归一化的设备坐标,即NDC)使用左手系,所以给出的F和F都为正数(这里的N和F是NDC系中摄像机的近/远平面,在视觉坐标系中的n=-N,f=-F)。为了颠倒z轴,在上述正交投影的矩阵的基础上再乘上一个颠倒z轴方向的镜像矩阵,即:
透视投影
热知识:
求变换矩阵的计算方法:
-
定义一个方平截头体作为视体(视锥),该体具有n和f两个面,可以将n看作视口(投影平面),整个物体看作视野(其实只是为了得出投影矩阵而设置的一个“视野”),不在n和f之间物体的就不再显示
-
将整个方平截头体压缩至长方体,得到“压缩”矩阵
-
三个原则:n平面上点的坐标不变,f平面上点的z坐标不变,z平面的中心点的坐标不变
-
根据相似三角形:
可知方平截头体内任意一点会进行如下变换:
则可初步写出“压缩”矩阵:
-
然后再特殊值法,根据“n平面上点的坐标不变”、“f平面上点的z坐标不变”、“z轴上的点的坐标不变”写出其变化后的式子,可计算得:
-
-
将长方体映射成标准立方体,也就是前面的正交投影的内容,矩阵为
-
得到透视投影矩阵
注1:个人理解这里所写的透视投影矩阵实际上是针对摄像机已经在原点为标准摄像机的情况的(否则相似三角形那一步就不正确了),但是变为立方体后就需要将立方体中心移动至原点的,也就是乘上的
注2:虽然有一点违反直觉,这里的“压缩”并不能保证三维体上点的z坐标不变。假设t=1,可以将z代入,得z坐标为
注3:这里的第3步,也就是将长方体映射成标准立方体时所用的点的齐次坐标不是
也就是说,最终完成透视投影后的点的齐次坐标实际上并没有全部归一化,还差了一步除w。这个w分量的值为透视投影前的z坐标。当然,有的时候也会将负值的z坐标取反得正以便于后面对比深度信息(比如OpenGL中就是如此,w=-z)
注4:因为不同的图形api的标准立方体使用的摄像机系有左右手系之分(比如作业框架中就是左手系),而未经过任何变换的视觉坐标系也有左右手系之分(比如我们之前设定的视觉坐标系就是左手系)。如果两者不同的话,那就需要像正交透视中的注3那样,最后乘上一个将z轴颠倒的镜像矩阵。下面注5是直接用了正交透视中注3推导出的矩阵
注5:在OpenGL中,视觉坐标系使用右手系而标准立方体(在OpenGL中叫做归一化的设备坐标,即NDC)使用左手系,所以给出的N和F都为正数(这里的N和F是NDC系中摄像机的近/远平面,在视觉坐标系中的n=-N,f=-F)。且其在额外的w储存的不是原本变换前负数的z,而是化为正值后的-z(因此后面在取插值时所用的深度信息也就为正数的-z)。所以任意一点
然后再乘上OpenGL的正交投影矩阵,最终得到其透视投影的完整变换矩阵:
一般来说,这里还有l=-r,b=-t,所以还可以继续约掉一些值,懒得写了,写latex真几把麻烦。
整个推导稍微有点绕,主要因为是这里的N、F是在NDC的左手系中的,而n、f是在右手系的。具体解释可见OpenGL Projection Matrix(相关的中文翻译:3D图形学中的矩阵变换(三)、OpenGL投影变换矩阵)
注6:理论上来讲,经过透视投影后的空间应该被称为裁剪空间而不是ndc空间(见注3),我们费劲心思搞出来这么一个四不像空间(因为他只是一个数值意义上的“空间”,而不容易用一张图来解释),单纯是为了做裁剪,去除不在空间内的点。如果有三角形部分被裁减,我们还需要在交界处生成新点,进而建立新的三角形。
为什么不直接合并这个过程,直接从透视投影一步到位变换到ndc空间(正如我们本来所设想的那样),然后在ndc空间内裁剪呢?一种说法是因为ndc空间内x,y,z都已经除w了,三角形的重心关系已经改变,如果在这时候裁剪,然后有三角形部分被裁减并在交界处生成新点,那么这个生成的新点的位置就不能通过线性插值来计算了。所以才选择在摄像机空间->ndc空间的之间硬生生插入一个所谓的“裁剪空间”,在这个裁剪空间内,重心关系还没有被扭曲,还可以做线性插值,在这个时候裁剪刚好。
那么问题来了,w分量实际上并没有作用,如果在裁剪空间->ndc空间的时候,只对x、y、z做齐次除法,各自除w,但不对w做,那么就可以保留这个w值,这个w值本质是摄像机空间里的z值(前面说过,取-z也行),对三角形三顶点的w来找重心关系是不会被扭曲的。那么就可以将裁剪的过程直接在ndc内完成,同时也方便之后的插值。我在后面的作业中也是这么做的,但是实际上流水线是不是这么做的我确实不太清楚…
既然想要在ndc空间做裁剪这么麻烦,在裁剪空间做裁剪又有额外的心智负担,那能不能在摄像机空间做裁剪呢?其实我个人觉得也是可以的…大概是为了计算方便才选择了现在的方式吧,毕竟裁剪空间内判断顶点是否在范围内只需要判断x、y、z是否在w到-w内即可(这其实是从判断是否在ndc空间内的方法反推出来的,即-1<x/w<1 => -w<x<w),而在摄像机空间判断起来会很麻烦。
作业
Assignment1
因为最近wineQQ无法登录了,所以从kubuntu转到了win,实在不想折腾vbox的拖拽了,打算用windows来做。
首先用vcpkg把eigen3和opencv(编译好慢…)装了,然后clion配置使用visual studio工具链,cmake选项加上-DCMAKE_TOOLCHAIN_FILE="[vcpkg_path]/scripts/buildsystems/vcpkg.cmake"
,最后把CMakeLists.txt改一下:
cmake_minimum_required(VERSION 3.10)
project(Rasterizer)
find_package(OpenCV REQUIRED)
set(CMAKE_CXX_STANDARD 17)
add_executable(Rasterizer main.cpp rasterizer.hpp rasterizer.cpp Triangle.hpp Triangle.cpp)
target_link_libraries(Rasterizer ${OpenCV_LIBRARIES})
作业基本就是照抄笔记上的公式,透视投影矩阵因为包括一步正交投影所以需要top、bottom、right、left,都可以通过eye_fov算出来。
看了下评论区,有得到上下颠倒的三角形的问题。其实是因为作业给的n和f是正值,后面作业中也提到为了方便计算深度,深度信息也是正值,所以基本可以断定作业是按照OpenGL的那一套来的。所以直接用前面透视投影的注5
中推导的式子即可:
代码如下:
Eigen::Matrix4f get_model_matrix(float rotation_angle) {
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
// Create the model matrix for rotating the triangle around the Z axis.
// Then return it.
Eigen::Matrix4f translate;
translate << cos(rotation_angle / 180 * MY_PI), -sin(rotation_angle / 180 * MY_PI), 0, 0,
sin(rotation_angle / 180 * MY_PI), cos(rotation_angle / 180 * MY_PI), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1;
model = translate * model;
return model;
}
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection;
Eigen::Matrix4f M_persp_to_ortho;
Eigen::Matrix4f M_ortho_scale;
Eigen::Matrix4f M_ortho_trans;
Eigen::Matrix4f M_z_mirror;
// 因为传入的n和f用了正值(OpenGL约定),这里将它们恢复成负的
float near = -zNear;
float far = -zFar;
float top = abs(near * tan(eye_fov / 2 / 180 * MY_PI));
float bottom = -top;
float right = top * aspect_ratio;
float left = -right;
// 按照OpenGL规则来计算变换矩阵
M_persp_to_ortho << -near, 0, 0, 0,
0, -near, 0, 0,
0, 0, -near - far, near * far,
0, 0, -1, 0;
M_ortho_trans << 1, 0, 0, -(right + left) / 2,
0, 1, 0, -(top + bottom) / 2,
0, 0, 1, -(near + far) / 2,
0, 0, 0, 1;
M_ortho_scale << 2.0f / (right - left), 0, 0, 0,
0, 2.0f / (top - bottom), 0, 0,
0, 0, 2.0f / (near - far), 0,
0, 0, 0, 1;
M_z_mirror << 1, 0, 0, 0,
0, 1, 0, 0,
0, 0, -1, 0,
0, 0, 0, 1;
projection = M_z_mirror * M_ortho_scale * M_ortho_trans * M_persp_to_ortho;
return projection;
}