Aspect Ratio
我们已经知道在正交投影中我们是怎么定义那个立方体了,但是在透视投影中我们应该怎么定义这个视锥体(4棱锥)呢?
我们从摄像机出发,看向某个方向,会有一个近平面,我们给这个近平面定义一个宽高比Aspect Ratio $Aspect Ratio = \frac{width}{height}$ 这就相当于我们得显示器
Field of view
视场角 FOV 描述的就是你在某一瞬间能看到的视野范围的大小(角度),下面图片中红色虚线表示的角度就是 Vertical FOV,当然还有Horizontal FOV,它们之间可以通过 Aspect Ratio 转换

Perspective Projection
我们可以使用 FOV 和 Aspect Ratio 来表示我们之前的参数

Screen
我们掌握了 MVP 变换后把所有的物体都变换到了 NDC 中,最后我们需要将其中的物体都显示到屏幕上,什么是屏幕呢
- 屏幕是一个二维数组,数组的元素是像素(piexl)
- 数组的大小是屏幕的分辨率
- 屏幕是光栅成像设备
- 像素简单来说是一个一个小方块
- 不同的像素使用3个数 (red,green,blue) 表示颜色,(255,0,0)表示红色
在这门课中我们这么定义屏幕中像素的坐标
原点位于屏幕的左下角,像素的索引(index)坐标我们写为(x,y),例如这幅图中蓝色的像素的索引坐标位(2,1)。如果我们屏幕大小是 $width \times height$ 那么我们屏幕的像素索引范围就是 $(0,0) to (width-1,height-1)$,我们是使用索引表示一个像素的位置的,但实际上某个像素$(x,y)$它的中心坐标是$(x+0.5,y+0.5)$
我们需要将我们 NDC 映射到屏幕空间
- 对于NDC的z分量我们先不去管它,它在其中有其他的用处
- 对于NDC的x,y我们映射到屏幕空间这个操作非常简单,我们使用一个缩放矩阵和平移矩阵就能做到
Rasterization
光栅化是将数学描述的几何图形转换为像素的这么一个过程
可以看到这只老虎(模型)是由很多个平面组成的,但是这些平面怎么使用屏幕的像素显示出来呢?光栅化的作用就是将这些几何图形转化为像素的方法。
这里与Games101不同的点是这里步会介绍屏幕的成像方式,你唯一要知道的就是现代显示器通过将屏幕像素数据存储在内存中在需要显示的时候读取内存中的数据通过显示器的解码器再点亮对应不同的像素就行了
光栅化三角形
我们知道我们的目标是将平面或者多边形显示到屏幕上,这些多边形可以被分为多个三角形,三角形是最最基础的多边形。我们可以通过三角形组成各种模型,所以我们只需要学习如何光栅化一个三角形,那么我们就能光栅化任何模型。
三角形有诸多非常好的性质
- 三角形是最基础的多边形
- 三角形的内部是一个平面
- 三角形很好定义内外
- 我们可以通过三角形的三个顶点属性进行插值得到三角形内部的点的属性
那么我们现在的问题就是如何光栅化一个三角形
(注:这图中定义的坐标系与我们不同)
我们需要让被三角形覆盖住的像素设置为我们三角形的颜色,但是你会发现一个问题,在三角形的边上有些像素只覆盖了一半或者只覆盖了一点点,这是我们后面需要考虑的。我们需要判断一个像素的中心点是否在三角形的内部。
最简单的做法就是——采样,采样用老师的话来讲就是:给你一个连续函数,在不同的离散的地方,去询问这个函数的值是多少,采样是将一个连续函数离散化的过程。
在这里我们使用像素的中心对屏幕空间进行采样,我们的屏幕空间是$[0,0]\times [width,height]$这么一个区域,我们使用离散的像素索引整数值$[0,0]\to [width-1,height-1]$进行采样。
我们需要对这么一个在屏幕空间这个连续空间中对这么一个$inside(tri,x,y)$函数进行采样
我们可以对所有的像素采样这个函数,每遍历一个像素,问一下这个函数,这个像素的中心坐标(x+0.5,y+0.5)代入$\text{inside}$函数获取一个采样值(函数值),如果是1则在三角形内部,如果是0则这个像素在三角形外部
伪代码
1for(int x = 0; x < xmax; ++x)
2 for(int y = 0; y < ymax; ++y)
3 screen[x][y] = inside(tri,x+0.5,y+0.5);
现在唯一的问题就是$inside$这个函数(算法)怎么使用代码去实现,还记得我们之前介绍向量的叉积运算吗,我们之前说过叉积可以判断一个点是否在三角形内部,具体来说
- 如果一个点在三角形内部,那么三角形的三条边构成的向量分别与这个点与三角形顶点够成的向量按顺序叉乘得到的方向应该是一致的
- 那么如果这个点在三角形外部,按照上述的方式会得到不同的方向
具体是这么做的
如图:这个点在三角形外部,我们逆时针(顺时针也行)看三角形的边,我们依次计算 - $P_OP_1 \times P_0Q$它的方向是垂直于屏幕指向屏幕外的
- $P_1P_2 \times P_1Q$它的方向是垂直于屏幕指向屏幕外的
- $P_2P_0 \times P_2Q$它的方向是垂直于屏幕指向屏幕内的
可以看到方向不全相同,所以该点在三角形外部,如果一个点在三角形内部则是全部相同的方向,你可以验证一下。
还有一种特殊情况,如果一个点在三角形上,我们不做处理
优化
我们之前是使用屏幕上所有的像素对屏幕空间进行采样,不过这是不必要的,如果你的三角形相对于屏幕非常的小,那么这么做非常的浪费时间,实际上我们可以通过三角形的顶点找出这个三角形的Bounding Box(包围盒),来优化这个采样空间,下面图片使用的是AABB包围盒,它被定义为一个矩形,边始终平行于坐标轴

还有其他更快的优化方法,比如扫描线法,详细的同学可以去搜索了解一下,可能未来会在blog中更新。

锯齿(Aliasing)现象
我们使用像素的中心点去采样屏幕空间,然后判断是否在三角形内,然后我们最后根据函数值,我们填回屏幕像素,我们会得到下面这样的结果
你会发现这个三角形有点丑陋,这是因为采样将连续函数离散化的过程导致的锯齿问题以及我们的像素本身就是有一定的大小,在下一节中我们会给出优化锯齿的方式

作业1解析
作业1是Games101中的第一个正式作业。由于我们没有按照作业0的要求使用虚拟机进行写作业,而是直接在windows系统上写,所以我们需要修改原代码框架以及cmake配置,我们在作业0中已经配置好了eigen,但是还没有配置opencv(但是已经安装了),作业1需要使用到opencv。
1.修改作业框架
作业中使用了Eigne与OpenCV,我们先修改CMkae 原CMakeLists是
1cmake_minimum_required(VERSION 3.10)
2project(Rasterizer)
3
4find_package(OpenCV REQUIRED)
5
6set(CMAKE_CXX_STANDARD 17)
7
8include_directories(/usr/local/include)
9
10add_executable(Rasterizer main.cpp rasterizer.hpp rasterizer.cpp Triangle.hpp Triangle.cpp)
11target_link_libraries(Rasterizer ${OpenCV_LIBRARIES})
我们将其改为
1cmake_minimum_required(VERSION 3.10)
2project(Rasterizer)
3
4set(CMAKE_CXX_STANDARD 17)
5
6set(OpenCV_DIR "F:/libs/opencv/build")
7find_package(OpenCV REQUIRED)
8
9
10set(EIGEN3_INCLUDE_DIR "F:/libs/eigen")
11include_directories(${EIGEN3_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS})
12
13add_executable(Rasterizer main.cpp rasterizer.hpp rasterizer.cpp Triangle.hpp Triangle.cpp)
14target_link_libraries(Rasterizer ${OpenCV_LIBRARIES})
如果你和我一样在windows下使用Clangd编译器,那么你可以这么设置,因为opencv的这个库是专门为msvc编译的,并且使用clang-cl编译器
1cmake_minimum_required(VERSION 3.10)
2project(Rasterizer)
3
4set(CMAKE_CXX_STANDARD 17)
5
6set(OPENCV_ROOT "F:/libs/opencv/build")
7set(OpenCV_INCLUDE_DIRS "${OPENCV_ROOT}/include")
8set(OpenCV_LIBRARIES "F:/libs/opencv/build/x64/vc16/lib/opencv_world4120d.lib")
9
10
11set(EIGEN3_INCLUDE_DIR "F:/libs/eigen")
12include_directories(${EIGEN3_INCLUDE_DIR} ${OpenCV_INCLUDE_DIRS})
13
14add_executable(Rasterizer main.cpp rasterizer.hpp rasterizer.cpp Triangle.hpp Triangle.cpp)
15
16target_link_libraries(Rasterizer ${OpenCV_LIBRARIES})
并且和作业0一样将#include <eigen3/Eigen/Eigen>改为#include <Eigen/Eigen>,并且将~\opencv\build\x64\vc16\bin添加到Path环境遍历,做完这些就可以开始写作业了
2.开始做作业
第一个任务就是完成TODO

- 实现绕z轴旋转矩阵,更具z轴旋转矩阵的公式,我们有
1Eigen::Matrix4f get_model_matrix(float rotation_angle)
2{
3 Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
4
5 Eigen::Matrix4f rotate;
6 float angle = rotation_angle / 180.0 * MY_PI;
7 rotate << cos(angle), -sin(angle), 0, 0,
8 sin(angle), cos(angle), 0, 0,
9 0, 0, 1, 0,
10 0, 0, 0, 1;
11
12 model = rotate * model;
13
14 return model;
15}
- 使用给定的参数返回一个投影矩阵,结合上节的知识以及这一节开头的知识我们有
整理进代码
1Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
2{
3 Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
4
5 float n = -zNear;
6 float f = -zFar;
7
8 float angle = eye_fov * MY_PI / 180.0;
9 float t = tan(angle / 2) * fabs(zNear);
10 float r = aspect_ratio * t;
11 float l = -r;
12 float b = -t;
13
14
15 Eigen::Matrix4f persp_to_ortho;
16 persp_to_ortho << n, 0, 0, 0,
17 0, n, 0, 0,
18 0, 0, n + f, -n * f,
19 0, 0, 1, 0;
20
21 Eigen::Matrix4f ortho;
22 ortho << 2 / (r - l), 0, 0, -(r + l) / (r - l),
23 0, 2 / (t - b), 0, -(t + b) / (t - b),
24 0, 0, 2 / (n - f), -(n + f) / (n - f),
25 0, 0, 0, 1;
26
27 projection = ortho * persp_to_ortho;
28
29 return projection;
30}
此时我们运行编译好的程序就会得到如下效果,并且按下A或者D键会旋转

3. 提高作业
要求我们构建一个可以绕绕任意过原点的轴旋转angle角度的矩阵,我们可以参考第4节中的这个公式

1Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
2{
3 Eigen::Matrix4f rotation = Eigen::Matrix4f::Identity();
4
5 Vector3f n = axis.normalized();
6
7 float alpha = angle * MY_PI / 180.0;
8
9 Eigen::Matrix3f I = Eigen::Matrix3f::Identity();
10
11 Eigen::Matrix3f nnT = n * n.transpose();
12
13 Eigen::Matrix3f N;
14 N << 0, -n.z(), n.y(),
15 n.z(), 0, -n.x(),
16 -n.y(), n.x(), 0;
17
18 Eigen::Matrix3f R = cos(alpha) * I + (1 - cos(alpha)) * nnT + sin(alpha) * N;
19
20 // 5. 将 3x3 矩阵填入 4x4 矩阵的左上角
21 rotation.block<3, 3>(0, 0) = R;
22
23 return rotation;
24}
我们还需要将
1 r.set_model(get_model_matrix(angle));
改为
1r.set_model(get_rotation(Eigen::Vector3f(1, 0, 0), angle));
并且将点的坐标从
1std::vector<Eigen::Vector3f> pos{{2, 0, -2}, {0, 2, -2}, {-2, 0, -2}};
改为
1std::vector<Eigen::Vector3f> pos{{2, 0, 0}, {0, 2, 0}, {-2, 0, 0}};
这样你三角形就会绕着x轴旋转了
