目录
- GAMES101 学习笔记 Lecture 1~6
- 往期作业汇总帖
- Lecture 01 Overview of Computer Graphics
- Lecture 2: Review of Linear Algebra
- Lecture 3: Transformation
- Lecture 4: Transformation Cont
- 作业 0
- Lecture 5: Rasterization 1 (Triangles)
- 作业 1
- Lecture 6: Rasterization 2 (Antialiasing and Z-Buffering)
GAMES101 学习笔记 Lecture 1~6
往期作业汇总帖
https://games-cn.org/forums/topic/allhw/
Lecture 01 Overview of Computer Graphics
笔记参考
https://zhuanlan.zhihu.com/p/548671972
其他教程
Learn OpenGL CN
https://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/
怎么判断一个画面是否优秀
看画面是否足够“亮”,也就是全局光照是否做的好
应用场景
Video Games 游戏
Movie 电影
Animation 动画
Design 设计:CAD等软件相关
Visualization 显示:B超等
Virtual Reality 虚拟现实:全是虚拟的场景
Augmented Reality 增强现实:部分现实的、部分虚拟的
Simulation 仿真
Graphical User Interfaces 人机界面
Typography 字体排版:字体渲染等
课程主题
-
Rasterization 光栅化
把三维空间的几何形体显示在屏幕空间上,就是光栅化
-
Curves and Meshed 曲线与网格
如何表示曲线和曲面
如何用简单曲面表示复杂曲面
物体变形时,物体的面该如何变化,怎么保持物体的拓扑结构
-
Ray Tracing 光线追踪
从摄像机的每个像素点发出射线,计算相交和阴影,直到这个射线击中光源
-
Animation / Simulation 动画与仿真
关键帧动画
质量-弹簧 系统
GAMES101 is NOT about 课程不会涉及
具体的图形API:OpenGL等
具体的shader语言:GLSL等
具体的3D建模软件 / 具体的游戏引擎:Maya、Unreal等
Computer Vision / DeepLearning 视觉、深度学习
视觉与CG的区别
视觉:2D图片转为3D模型
CG:由3D模型转为2D图片
Lecture 2: Review of Linear Algebra
向量
向量的模
向量的加减运算
向量之间的点乘:计算向量间夹角
点乘可以用来
-
衡量两个方向之间的接近程度
因为一般给的两个方向都是单位向量,或者说我们应该转化为单位向量
那么点乘的值其实就是 cos(夹角)
-
分解向量
比如 a = x i + y j \pmb{a} = x \pmb{i} + y \pmb{j} a=xi+yj
那么对 a \pmb{a} a 乘 i \pmb{i} i 就得到了 x,乘 j \pmb{j} j 就得到了 y
-
确定是前向还是后向
就是与一个 forward 向量做点乘,结果大于 0 说明这个方向属于前向,反之为后向
向量之间的叉乘:计算两向量平面的法线向量
正交坐标系:两坐标轴的点乘为 0,叉乘为第三个坐标轴
叉乘的大小可以写为矩阵形式
叉乘的作用:
-
判定左与右
-
判定内与外
例:判断一个点是否在三角形内
将三角形三个点逆时针(或顺时针)相连,得到 AB BC CA 向量
再取 AB 向量与 AP 向量叉乘,BC 向量与 BP 向量叉乘,CA 向量与 CP 向量叉乘
三次叉乘结果应该都为垂直纸面向外,那么说明该点在三条边的“左边”,也就是点在三角形内
如果有一次叉乘结果是垂直纸面向里,那么说明点在三角形外
矩阵
矩阵的乘积
矩阵的加减运算
矩阵的转置
单位矩阵
矩阵的逆
Lecture 3: Transformation
Why Transformation 为什么要变换
模型从局部坐标最终转换到屏幕坐标的过程中需要进行的一系列矩阵变换操作。
局部坐标(3D)->…->屏幕坐标(2D)
变换类型
Scale 缩放
Rotate 旋转
Translation 平移
怎么理解旋转矩阵
把旋转矩阵设为待求,自己用 (1,0) 和 (0,1) 旋转 θ,代入这个矩阵变换式,就可以求出旋转矩阵每一个元素的值
这里也可以方便理解旋转矩阵的每一个元素值是怎么来的,为什么一个 sin 是正,一个 sin 是负(负是因为 (0,1) 转到了第二象限)
齐次坐标
为什么要用齐次坐标
一般的缩放与旋转可以写成一个矩阵与向量相乘
而平移的话是向量与向量相加
如果我们不希望缩放、旋转与平移之间在运算上之间被区分开的话,就要想办法把它们统一起来
统一它们的方法就是更改坐标的定义
齐次坐标的定义
以 2d 为例,一般的 2d 的点或者向量的定义是 (x,y)
现在添加第三个坐标,称为齐次坐标
然后 2d point 就是 (x,y,1)
,2d 向量是 (x,y,0)
齐次坐标的用法
那么现在点的平移就可以用矩阵乘法来表示
弹幕里面说的好啊,假设代入一个原点,这种投影会让 (0,0)
投影到 (tx,ty)
,其实也就是移动了坐标系的原点
同时,可以观察到,如果对向量使用这个平移矩阵,得到的向量时不变的
因为这里的点和向量是不一样的,这个结果就表明了向量的平移不变性
齐次坐标的适用
再更深一层,这个添加的维度上的 0 1 的设置,可以对应
向量 + 向量 = 向量
点 - 点 = 向量
点 + 向量 = 向量
点 + 点 = 点/2,这里是扩充定义
可见,两个点相加得到的这个点的中点
使用齐次坐标的仿射变换
没有使用齐次坐标的仿射变换:
使用了齐次坐标的仿射变换:
逆变换
M 是从 A 到 B 变换
M 的逆变换就是从 B 到 A 变换
变换的组合
变换的顺序
也就是说矩阵运算中 AB 不一定等于 BA,也就是矩阵运算不满足交换律
因此,一般的矩阵运算规定是,先调用的变换在矩阵连乘式的右边,后调用的在矩阵连乘式的左边
虽然矩阵没有交换律,但是矩阵有结合律
因此,一个矩阵可以表示一个很复杂的变换
变换的分解
绕着点 C 旋转一个物体 α 度,可以分解为
先把这个物体平移,使点 C 移动到原点
然后将这个物体绕着原点旋转 α 度
最后将这个物体移回点 C
那么三维空间中绕着任意轴的变换也是这样
三维点的齐次坐标
也是在最后加一个维度
仿射变换内的顺序
仿射变换 = 线性变换 + 平移
那么其实是先线性变换,然后再平移
Lecture 4: Transformation Cont
三维齐次坐标系中的旋转
关于这个矩阵是怎么来的,我自己的理解就是以二维的旋转矩阵为基础,绕着哪个轴旋转向量 a,就是相当于在这个轴垂直的平面 p1 内旋转向量 a 平行于 p1 的分量。因此绕这个轴的旋转矩阵在这个轴的位置为 1,在其他两个轴可以填入二维旋转矩阵的相应元素
例如,绕着 x 轴旋转某个向量,假设按照右手法则确定旋转方向,那么就相当于在 yz 平面内旋转这个向量平行于 yz 平面的分量
已经知道从 x 轴向 y 轴的二维转动的公式是怎么样的,那么在名称不同的二维坐标系中,只是把已知的二维坐标系的名字改成新名字就好了,这样建立起对应关系就方便写新的公式
写完之后,由于一个向量平行于 yz 平面的分量其实就是 (y,z),那么对应到三维旋转矩阵中,其实就是把修改后的二维旋转矩阵的项填入三维旋转矩阵中的相应位置
可以发现,之所以绕 y 轴的旋转矩阵不太一样,是因为他是从 z 轴旋转到 x 轴的,从二维旋转矩阵填到三维旋转矩阵是反着的
欧拉角
α roll 滚转角
β pitch 俯仰角
γ yaw 偏航角
罗德里格旋转公式
绕轴 n 右手螺旋旋转 α 度
一般来说的话,如果说物体要绕某个轴旋转,还需要指明这个轴过哪个点,过不同的点得到的结果是不一样的
公式这里是默认这个旋转轴是过原点的
那么如果我们需要这个旋转轴过特定点呢?
就用到之前的知识,那就是把所有的东西都移到原点,然后再旋转,然后再把所有的东西都移动回去
公式后面那个 N 是叉乘的形式
观测变换
物体上某一点的坐标变换顺序:M->V->P
1.Model Transformation 模型变换
场景中每个物体上的某一点,从局部坐标系变换到世界坐标系,每个物体有自己独有的模型变换矩阵,代表物体独有的在世界坐标系中的位置。
2.View / Camera Transformation 视图变换
相机在世界坐标系中移动。对同一个相机,所有模型共用一个视图变换矩阵
3.Projection Transformation 投影变换
正交投影:投影区域为一个长方体,远近物体最终在屏幕上显示同等大小
透视投影:投影区域为一个梯台,近、远平面为两个缩放的长方体,四侧边线延迟交于相机,最终显示到屏幕为近大远小
View / Camera Transformation 视图变换
需要确定的参数
需要知道相机的位置,相机看向的方向,还需要知道相机的上方向
确定相机的上方向,相当于确定相机在手中拍照时旋转的角度
在变换之前的约定
因为相机与物体之间的相互位置不变时,相机与物体这对组合放在空间中的哪里是无关紧要的
比如相机与物体的位置组合是 (a1, b1) 和 (a2, b2) 这两组,只要 a1 与 b1 之间的相对位置与 a2 与 b2 之间的相对位置是相等的,那么 (a1, b1) 与 (a2, b2) 在相机拍摄出来的效果是一样的
然后有一个约定就是,相机始终在原点,相机始终指向 -Z
因为既然你这对组合在空间中是自由的嘛,所以我给其中一个顶死了,另外一个也定死了
我个人是感觉,为了方便一直处理一个数据对象,可以假设相机始终在
视图变换的步骤
-
将摄像机的中心移动到世界坐标原点
-
将摄像机的前方向 g 转动到世界坐标系的 -Z 方向
-
将摄像机的上方向 t 转动到世界坐标系的 Y 方向
此后,摄像机的右方向 g x t 自然是世界坐标系的 X 方向
要计算这个变换矩阵的具体形式时,分为两个矩阵来写
第一个矩阵是平移矩阵,写在连乘式的最左边
第二个矩阵是旋转矩阵,是 g 到 -Z,t 到 Y,g x t 到 X 的旋转矩阵
平移矩阵很好写,但是这个旋转矩阵不好写
因为是三个任意的轴旋转到三个固定的轴
虽然旋转矩阵不好写,但是这个旋转矩阵的逆矩阵好写
这个旋转矩阵的逆矩阵的意义是,-Z 到 g,Y 到 t,X 到 g x t
红字说了这个原理:那么我这里把几个基底按顺序横着写,相当于在乘的适合把原来的向量直接投影到新的基底上去
好吧,看了一下,这个矩阵似乎写错了
应该是
x1 y1 z1 0
x2 y2 z2 0
x3 y3 z3 0
0 0 0 1
这样排
那么其实这个逆矩阵就是将 (X, Y, Z) 坐标系的向量投影到 (g x t, t, -g) 的坐标系
所以基地按照这么排列就可以完成这个变换
写完逆矩阵之后,由于这个逆矩阵是一个基底的列向量矩阵,每一列之间两两正交,所以 A T A = E A^TA = E ATA=E,所以这是一个正交矩阵,所以它的逆就等于它的转置
至此,相机变换的矩阵就求出来了
因为相机与其他物体之间的相对距离不能变,所以其他物体就会应用这个相机变换的矩阵
正交投影 Orthographic
因为在前面已经把相机的前方向指向了 -Z,那么只要将物体的 Z 坐标丢掉,得到的就是物体在 XY 面上的投影
最后得到的投影结果,约定要缩放到 [-1, 1]
一般性的正交投影的描述
约定要将一个长方体投影到正方体
Todo: 为什么要将一个长方体映射到一个正方体
然后之后我也不知道为什么要将一个长方体映射到一个正方体?
感觉上应该是跟之前的约定缩放到 [-1, 1] 是类似的?
搜了一下,是因为要投射到 NDC Normalized Device Coordinate 标准设备坐标
我直觉感觉应该是因为,设备不管你之前是怎么变换的,反正设备的尺寸是千奇百怪的,所以设备首先需要定一个标准,然后再根据自己的设备的独特的尺寸做一些处理
那么设备相关的这个约定比较方便的就是 [-1, 1]
https://www.youtube.com/watch?v=EqNcqBdrNyI
各个面的描述
l 左边 left
r 右边 right
b 底部 bottom
t 顶部 top
n 近处 near
f 远处 far
在图示的右手系中,与一般的常识不同的是,距离我们比较近的 n 面,z 值比较大,距离我们比较远的 f 面,z 值比较小
opengl 在窗口空间(屏幕空间)中使用的左手系,就是为了方便符合 n 面的 z 值小,f 面的 z 值大,这个直觉
正交投影的变换式
首先是平移矩阵
要计算两个面之间中心,然后在这个面的方向上移动到原点
例如 (r+l)/2 是两个 x 方向上两个面之间的中心,然后 -(r+l)/2 就是把中心移动到 x = 0
然后是缩放矩阵
因为要缩放到的正方体的长度为 2,要覆盖 [-1, 1]。而原来的边长,也就是两个面之间的距离是两个面的坐标值相减
例如 x 方向上,原来的边长是 r-l,现在要缩放到 2,那么就是乘一个 2/(r-l)
Todo: 正交变换的时候,为什么不考虑物体的旋转
之后再说
Todo: 正交变换的时候,物体不会被拉伸吗?
会
在之后的视口变换中会考虑
Todo: 正交变换的块是怎么来的?
变换的块是事先规定的,规定好要让它显示什么
Todo: 精度上会有问题吗?
精度上可能会有问题,之后可能会说
透视投影 Perspective Projection
背景
欧式几何中的平行线在透视投影之后会相交
在齐次坐标中,(x,y,z,1), (kx,ky,kz,k) 表示同一个点
因此 (x,y,z,1), (xz,yx,z^2,z) 也表示同一个点
约定
约定与正交投影中类似的近面 n 远面 f
挤压视锥体到长方体的思路
直接写透视投影的公式不好理解
好理解的是,首先知道透视投影和正交投影的目标都是类似的,都是将一个区域投影到一个边长为 2 的正方体
这一点在视频中没有明确说,即使从来不知道渲染管线,只是说靠你简单的逻辑就知道,嗯,应该是要达成相同效果的
然后基于相同的结果的这个目标,我们再去看透视投影的这个方块,就会发现,他这个梯形的远面,只要远面的四个顶点压缩一下,压缩成大小跟近面一样,就会得到一个长方形,而得到长方形之后就只需要做正交投影就好了
这个挤压的过程,自然是视锥体中的每一个与 XY 面平行的面都会相应挤压
然后我们约定挤压的时候,近面和远面的 Z 值不会发生改变,这是很自然的,不然物体就偏了
再约定远面的中心点也不会发生变化,那么物体就彻底不会偏了
物体偏不偏这个是我的直观感受吧,视频没有这么讲
挤压中的 x 和 y 的变化
现在需要知道,挤压过程中视锥体中每个点的变化
因为我们现在的目标是压缩到与近面相同,所以我们要看近面
然后我们再同时观察我们的目标的与 XY 平行的面,就可以看到一个相似三角形
根据相似三角形我们可以知道,这个近面的 Z 值 n 是可以利用的,然后就有这个 y’ = n/z*y 的关系
x 也是类似的
也就是我们可以根据压缩前的 x 和 y 值获得压缩之后的 x’ 和 y’ 值
观察得到旧向量和新向量
新向量三个值都是已知的
为什么不是新向量的,也就是压缩之后的向量,的第三个维度的值不是保持为 z?
这是一种错觉,之前约定了 near 和 far 面的 z 值不变,就给人了一种感觉是,好像中间的点的 z 在压缩的时候也不会变,实际上是会变的
这个错觉的一个另一个可能的来源是,在计算压缩之后的 x 和 y 的值的时候,我们是使用旧面的 z 来参与相似三角形的计算
但是我使用旧面的 z 只是因为我在 x 和 y 上的变换值与近面是有这个联系的,不代表变换之后旧面不会动
然后我们保持这个新向量的第三个维度的值不知道
就已经可以知道这个变换矩阵的其他值了,就差变换矩阵的第三行不知道了
这一行不知道,所以一般的想法就是待定系数法来解
然后我们已知的条件是,近面和远面都不变,所以我们将近面和远面的点代入这个变换式,就可以解出
问题:视锥体中间的点在变换之后的 z 的变化?
变换之后变成 n+f-nf/z
那么这个很简单,就是相减一下就好了
F ( z ) = n + f − n f z − z F(z) = n + f - \dfrac{nf}{z} - z F(z)=n+f−znf−z
F ′ ( z ) = + n f z 2 − 1 F'(z) = +\dfrac{nf}{z^2}-1 F′(z)=+z2nf−1
这里有一个问题是,nf 不一定是为正,那么分类讨论:
i) 假设 nf >= 0
可见, F ′ ( z ) F'(z) F′(z) 是单调下降,而 F ′ ( z ) F'(z) F′(z) 的零点在 n f z 2 − 1 = 0 \dfrac{nf}{z^2}-1 = 0 z2nf−1=0 => z = n f z = \sqrt{nf} z=nf
那么在 n < z < n f n < z < \sqrt{nf} n<z<nf 时 F ′ ( z ) > 0 F'(z) > 0 F′(z)>0, F ( z ) F(z) F(z) 单调递增;
n f < z < f \sqrt{nf} < z < f nf<z<f 时 F ′ ( z ) < 0 F'(z) < 0 F′(z)<0, F ( z ) F(z) F(z) 单调递减
所以 F ( z ) F(z) F(z) 在 z = n f z = \sqrt{nf} z=nf 处取到极大值 n + f − 2 n f n+f-2\sqrt{nf} n+f−2nf
根据算术平均值大于等于几何平均值,得到 n + f − 2 n f > 0 n+f-2\sqrt{nf} > 0 n+f−2nf>0
然后再看 F ( n ) = 0 , F ( f ) = 0 F(n) = 0, F(f) = 0 F(n)=0,F(f)=0,那么其实这个时候也不用看这个极大值的符号了,因为在两个零点之间先增后减,中间的极大值一定是为正
那么也就是 F ( z ) F(z) F(z) 在 n < z < f n < z < f n<z<f 上是恒大于等于 0 的,也就是缩放之后的 z 值会变大
ii) nf < 0
最直接的考虑就是:
F ′ ( z ) F'(z) F′(z) 是单调递增
F ′ ( z ) F'(z) F′(z) 的单调性也无所谓,主要是 F ′ ( z ) F'(z) F′(z) 恒小于 0
那么 F ( z ) F(z) F(z) 单调递减,但是却在区间端点 F ( n ) = 0 , F ( f ) = 0 F(n) = 0, F(f) = 0 F(n)=0,F(f)=0 这就有问题了
问题就出现在,nf < 0 说明 n 和 f 一正一负,然后 z 可能取到 0,那么在 F(z) 中 z 在 0 是无定义的
我感觉这样的话,应该是会强行规定 n 和 f 的符号相同的,比如我强行规定我的摄像机在 z = 0
哦……这就是为什么之前的相机变换要把相机移动到 z = 0,原来别人已经移动好了
那没事了,很神奇
诶,等等,不一定啊,就是,嗯,物体似乎也是能在相机之后的
合理的做法就是,相机根本看不到后面的物体嘛,所以即使物体在后面,n 也不会跟着物体放在后面,n 是我人为设定的嘛
然后即使如此也没有办法避免 n 为 0
但是我似乎记得,unity 中相机都是有近裁面的,不会让 n = 0 的
太合理了,太完美了
最后我们证明了 z 会变大,因为我们现在是在右手系中看的,z 越大,实际上就是距离 n 面越近,也就是距离相机越近
也就是,透视变换之后,物体距离相机变近了
作业 0
配置 Eigen
直接下载 Eigen 并且解压之后,在项目的属性-C+±附加包含目录中加入 Eigen 源码根目录就好了
旋转矩阵和平移矩阵
很简单的旋转矩阵和平移矩阵
#include <iostream>
#include <Eigen/Dense>
#define _USE_MATH_DEFINES
#include <math.h>
using namespace Eigen;
using namespace std;
int main()
{
// 点 P 的齐次坐标
Vector3f P(2.0f, 1.0f, 1.0f);
Matrix3f R, T;
R << cos(M_PI / 4), -sin(M_PI / 4), 0,
sin(M_PI / 4), cos(M_PI / 4), 0,
0, 0, 1;
T << 1, 0, 1,
0, 1, 2,
0, 0, 1;
P = T * R * P;
cout << P << endl;
}
Lecture 5: Rasterization 1 (Triangles)
光栅化流程:处于MVP变换之后的流程,用于最终显示到屏幕
透视投影的另一个定义
定义近裁面,可以使用 fovY 和 aspect ratio
也就是视角和宽高比
感觉还需要一个高度或者宽度
视角和宽高比的定义
这里是因为默认近裁面的中心就在 z 轴,所以认为宽度是 2r 高度是 2t
屏幕像素约定
屏幕在宽度上有 width 个像素,在高度上有 height 个像素
每一个像素的长度定为 1
那么屏幕在宽度上的长为 width,在高度上的长为 height
像素下标从 (0,0) 到 (width-1, height-1)
下标为 (x,y) 的像素的中心位于 (x + 0.5, y + 0.5)
视口变换:投影空间到屏幕空间
投影完之后,可见方块是一个边长为 2 的,中心位于原点的立方体
现在将它的 XY 面变为宽为 width,高为 height 的长方体,长方体的左下角是屏幕的左下角,Z 方向不用动
写成一个变换矩阵的话,因为是先变换后平移,所以先变成 宽为 width,高为 height,那么接下来要移动 width/2 height/2
这个变换称为视口变换
显示设备
Oscilloscope 示波器
阴极射线管(Cathode Ray Tube,CRT)
逐行扫描
=> 隔行扫描
LCD
LED
墨水屏
三角形的优势
三角形是最基础的多边形,其他多边形都可以拆成三角形
三角形一定能生成一个平面,其他多边形(或者说多个点的连线)不一定能构成一个平面
三角形的内外定义很简单,更高边数的多边形还有凹凸之分等等
判断点在三角形内部的四种方法:
https://www.cnblogs.com/kyokuhuang/p/4314173.html
三角形内部的插值很简单
加速判定屏幕像素点是否在三角形内
(1)使用 AABB 包围盒,首先获取三角形的 xy 的最小/最大的坐标形成 AABB 包围盒,在包围盒外的像素绝对不会在三角形内,因此可以剔除屏幕上大部分像素。
(2)获取每一行像素在三角形内的起始点和终止点,则中间的像素均在三角形内,适合斜45°狭长的三角形,这种三角形的 AABB 盒较大,但是实际覆盖的像素少
人眼对绿色更敏感
因此可以在绿色中添加更多像素点,如右图
加色混合 减色混合
copy 定义
加色混合就是诸如电视机或者智能手机屏幕发光的设备。在大多数设备中,发出三种不同颜色的光(红绿蓝为三原色),并且在使用时把它们加在一起合成其他不同的颜色和不同的亮度,而这也就是大家常常听说到的RGB。
减色混合是指每种颜料或者染料都具有反射或透射一定色光的能力,而某些颜料在混合中,其波长的光线会被吸收而造成一定的颜色混合,从而出现其他颜色。
光栅化后的问题
锯齿严重,所以后面需要抗锯齿Anti-Aliasing
理解抗锯齿
抗锯齿其实并没有让像素面积更细,而是让像素的颜色不是非0即1,而是取中间的某一个值,从而从宏观上看不会锯齿明显。抗锯齿后,边界看上去会更模糊。
作业 1
OpenCV 安装
https://blog.csdn.net/cocapop/article/details/127515873
模型矩阵和投影矩阵
这之前好像没有说过模型变换矩阵是怎么样的
但是既然它已经这么说了,就先这么写吧
Eigen::Matrix4f get_model_matrix(float rotation_angle)
{
Eigen::Matrix4f model = Eigen::Matrix4f::Identity();
// TODO: Implement this function
// Create the model matrix for rotating the triangle around the Z axis.
// Then return it.
rotation_angle = rotation_angle / 180.0f * MY_PI;
Eigen::Matrix4f translate;
translate << cos(rotation_angle), -sin(rotation_angle), 0, 0,
sin(rotation_angle), cos(rotation_angle), 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)
{
// Students will implement this function
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
// TODO: Implement this function
// Create the projection matrix for the given parameters.
// Then return it.
eye_fov = eye_fov / 180.0f * MY_PI;
float height = tan(eye_fov / 2.0f) * abs(zNear) * 2.0f;
float width = height * aspect_ratio;
float length = zNear - zFar;
Eigen::Matrix4f p2o;
p2o << zNear, 0, 0, 0,
0, zNear, 0, 0,
0, 0, zNear + zFar, -zNear * zFar,
0, 0, 1, 0;
Eigen::Matrix4f orthoT;
// 这里已经假设了视锥体的平行于 XY 面的面的形心是在 Z 轴上
// 或者说因为相机摆放是放在原点的,所以相机看到的视锥体的 XY 面的形心自然就在 Z 轴上
// 所以只移动了 z 方向
orthoT << 1.0f, 0, 0, 0,
0, 1.0f, 0, 0,
0, 0, 1.0f, -(zNear + zFar) / 2.0f,
0, 0, 0, 1.0f;
Eigen::Matrix4f orthoS;
orthoS << 2.0f / width, 0, 0, 0,
0, 2.0f / height, 0, 0,
0, 0, 2.0f / length, 0,
0, 0, 0, 1;
projection = orthoS * orthoT * p2o * projection;
return projection;
}
然后还有一个地方对源代码要改的是
有两个地方的 get_projection_matrix
传入的参数要改
r.set_projection(get_projection_matrix(45, 1, -0.1, -50));
因为在我们的公式的推导中,摄像机是向 -Z 看的,所以得到的 zNear 和 zFar 都是负的
对任意轴旋转的代码
对任意轴旋转的直接就抄公式就好了
Eigen::Matrix4f get_rotation(Vector3f axis, float angle)
{
// 罗德里格旋转公式
Eigen::Matrix4f rot = Eigen::Matrix4f::Identity();
angle = angle / 180.0f * MY_PI;
Eigen::Matrix3f cross;
cross << 0, -axis[2], axis[1],
axis[2], 0, -axis[0],
-axis[1], axis[0], 0,
0, 0, 0;
Eigen::Matrix3f translate3f;
translate3f << cos(angle) * Eigen::Matrix3f::Identity()
+ (1 - cos(angle)) * (axis * axis.transpose())
+ sin(angle) * cross;
Eigen::Matrix4f translate4f;
translate4f.block<3, 3>(0, 0) = translate3f;
translate4f(0, 3) = 0.0f;
translate4f(1, 3) = 0.0f;
translate4f(2, 3) = 0.0f;
translate4f(3, 3) = 1.0f;
rot = translate4f * rot;
return rot;
}
要测试这个函数的话,在 main 函数中把光栅化的流程中,模型变换的那一步改成 get_rotation
用绕 y 轴旋转来做测试的话:
while (key != 27) {
r.clear(rst::Buffers::Color | rst::Buffers::Depth);
r.set_model(get_rotation(Eigen::Vector3f(0.0f, 1.0f, 0), angle));
//r.set_model(get_model_matrix(angle));
结果:
可以看到这个东西……问题还挺大……?
这个三角形的中心怎么总是在画面中心……
绕任意轴旋转时的报错
绕其他轴旋转的时候可能会报错
https://games-cn.org/forums/topic/zuoye1-raoxzhouxuanzhuanhuishuzuyuejie/#post-16598
出错在数组越界
出错文件在 rasterizer.cpp
其中 set_pixel
应改为
void rst::rasterizer::set_pixel(const Eigen::Vector3f& point, const Eigen::Vector3f& color)
{
//old index: auto ind = point.y() + point.x() * width;
if (point.x() <= 0 || point.x() >= width ||
point.y() <= 0 || point.y() >= height) return;
// if point.x() == 0 and point.y() == 0
// then get ind == height * width
// but size of frame_buf == height * width
// so available index of frame_buf is [0, height * width - 1]
// so don't allow point.x() == 0 and point.y() == 0
auto ind = (height-point.y())*width + point.x();
frame_buf[ind] = color;
}
主要是不能允许 point.x() == 0 and point.y() == 0
我也看到另外一个帖子在讲这个
https://games-cn.org/forums/topic/zuoye1kuangjiadexiaowenti/
draw 函数的作用
看到有人问 draw 函数是什么意思
我也挺想问的……
https://games-cn.org/forums/topic/guanyuzuoye1-3zhongdeviewport-transformation/
不能列表初始化结构体的报错
看到有人遇到了这个问题,我没遇到
https://games-cn.org/forums/topic/zuoye1dewenti/#post-16603
搜了一下,感觉是 C++ 版本的问题……?
取三角形然后渲染的顺序
看到有人问,刚好我就梳理了一下
https://games-cn.org/forums/topic/zuoye1daimamain-cppzhongind-0-1-2-dezuoyong/#post-16605
在 main 函数中,一开始定义了 pos
和 ind
,然后 load 之后得到 pos_id
和 ind_id
之后的循环之中没有更改 pos_id
和 ind_id
,那么显然,一开始得到的 pos_id
和 ind_id
就是三角形的 pos
和 ind
再看具体是怎么用这个 pos_id
和 ind_id
的,他是在光栅化器的 draw 函数中用的,用在 pos_buf
和 ind_buf
auto& buf = pos_buf[pos_buffer.pos_id];
auto& ind = ind_buf[ind_buffer.ind_id];
然后之后的功能都是根据这个取出来的 buf
和 ind
那么再看位置缓冲和顶点缓冲的定义:
std::map<int, std::vector<Eigen::Vector3f>> pos_buf;
std::map<int, std::vector<Eigen::Vector3i>> ind_buf;
map
里面一个 id
对应一个 vector
对于 pos_buf
是一个 id
对应一个 std::vector<Eigen::Vector3f>
,根据常识以及之前 main 函数中的初始化写法可以知道,这里就是给了三个点的坐标的值,坐标用 Eigen::Vector3f
表示
对于 ind_buf
同理,就是一个 id
对应三个顶点的序号
这就很容易理解了。对于只有一个三角形的情况,这里取出来的 buf
和 ind
就是第一个三角形三个点的坐标的值,三个顶点的序号
然后我们就知道渲染主体是 draw 函数,我也写了注释
void rst::rasterizer::draw(rst::pos_buf_id pos_buffer, rst::ind_buf_id ind_buffer, rst::Primitive type)
{
if (type != rst::Primitive::Triangle)
{
throw std::runtime_error("Drawing primitives other than triangle is not implemented yet!");
}
auto& buf = pos_buf[pos_buffer.pos_id];
auto& ind = ind_buf[ind_buffer.ind_id];
float f1 = (100 - 0.1) / 2.0;
float f2 = (100 + 0.1) / 2.0;
Eigen::Matrix4f mvp = projection * view * model;
for (auto& i : ind)
{
Triangle t;
// 对每一个点应用 MVP
Eigen::Vector4f v[] = {
mvp * to_vec4(buf[i[0]], 1.0f),
mvp * to_vec4(buf[i[1]], 1.0f),
mvp * to_vec4(buf[i[2]], 1.0f)
};
// 对齐次坐标下的点,将齐次坐标缩放到第四个维度为 1
// 那么缩放后的 xyz 就是这个齐次坐标所代表的点的实际坐标
for (auto& vec : v) {
vec /= vec.w();
}
// 这里对点的处理不知道是什么意思
for (auto & vert : v)
{
vert.x() = 0.5*width*(vert.x()+1.0);
vert.y() = 0.5*height*(vert.y()+1.0);
vert.z() = vert.z() * f1 + f2;
}
// 对三角形每个点设置位置
for (int i = 0; i < 3; ++i)
{
t.setVertex(i, v[i].head<3>());
// 这里不知道为什么源代码写了三遍,只写一遍就行了吧
//t.setVertex(i, v[i].head<3>());
//t.setVertex(i, v[i].head<3>());
}
// 对三角形每个点设置颜色
// 在 Triangle.cpp 中可以看到,暂时还没有三角形内部颜色插值相关的实现
// 因此这里应该只是先占坑写了设置顶点颜色
t.setColor(0, 255.0, 0.0, 0.0);
t.setColor(1, 0.0 ,255.0, 0.0);
t.setColor(2, 0.0 , 0.0,255.0);
// 绘制线框
rasterize_wireframe(t);
}
}
Lecture 6: Rasterization 2 (Antialiasing and Z-Buffering)
6.1 Undersampling introduces aliasing
采样不足(欠采样)导致的各种现象:
-
锯齿
-
摩尔纹
-
旋转的车轮在视觉上倒转的现象
等等
采样不足就是采样频率低于信号频率
6.2 pre-filtering then sampling antialiasing
先预过滤(模糊)后采样,可以实现抗锯齿
模糊的过程就是降频的过程
信号频率被降低了,采样频率能跟上了,结果就会更好看
先模糊后采样还是先采样后模糊
由上面的讨论可知,先采样后模糊达不到抗锯齿的目的
因为根本目的要降低信号频率,降低信号频率必须是先模糊
高频信息需要高频采样
一个信号可以使用傅里叶变换分解成高频信息和低频信息
采样频率比较低的时候,会丢失掉高频信息
如图所示,对高频的 f5(x) 采样时,得到的结果反而是低频的
走样
同一个采样方法,对两个不同的函数采样,得到的结果之间无法区分,就叫走样
例如图中,空心点是采样点,两个颜色不一样的线是两个函数,这些采样点对这两个函数的采样结果是一样的
图像中的频率信息
左边是原图,右边是傅里叶变换之后的
傅里叶变换是时域到频域
图像上虽然没有时间,但是图像上不同点的位置可以代表时间
然后定义输出一个频率图像,图像的中心是低频,图像的四周是高频,某一点的亮度代表这个原图像在这个频率上的强度
可以看到,这个原图像在低频信息上比较丰富,自然界中的图像大都如此
一般认为图像在水平和竖直方向上重复自身,然后又因为很少有图像的左右边界之间是一样的,上下边界之间是一样的
因此图像在左右边界之间,上下边界之间的变化就会很剧烈,也就是频率很高
所以右边的频率图中可以到横竖两条亮线
那么其实这么讲的话,图像上的频率其实就是一个微小距离上像素变化的剧烈程度……
高通滤波
只有高频信号可以通过,叫作高通滤波
得到的结果是图像的边界
因为图像的边界上像素变化更剧烈嘛,所以是边界
低通滤波
只有低频信号可以通过,叫作低通滤波
去掉图像的边界,得到的结果就是原图像的模糊
所以到这里,就已经证明了对图像先模糊后采样为什么可以抗锯齿,因为对图像的模糊就是消掉了边界,也就是消掉了高频信息,也就是让采样频率跟上信号频率
低通滤波中的水波纹
后面解释
去掉高频和低频信息
去掉高频和低频信息,得到的是类似边界的图像
卷积定理
时域上的卷积是频域中的乘积
时域上的乘积是频域中的卷积
那么卷积有两种方式:
-
在空间上使用卷积核对原图像进行卷积
-
先使用傅里叶变换将原图像和卷积核变换到频域,然后对频域中的原图像和卷积核相乘,最后把相乘结果傅里叶逆变换回去
上图中,第一行的图像是第一种方法
第二行的图像是第二种方法
可见,乘一个卷积核的过程就像一个
Box 卷积核
全 1 的卷积核,9 个 1 * 原图像对应像素之和的能量显然大于一个像素的能量,因此除 9 做归一化
一个卷积核相当于一个低通滤波
如果卷积核变大,就相当于模糊得越厉害,也就是低通滤波越强,保留的频率更低
可以外推到一个很大的 box,那么就相当于卷积后的图像处处相等
一个很小的 box,一个像素,那就相当于没卷积
采样
采样相当于被采样函数乘以冲击函数,如 e 图
被采样函数在频域上是 f 图中的第一幅图
冲击函数在频域上是 f 图中的第二幅图
时域上的乘积对应频域上的卷积
因此两者在频域上的卷积是 f 图中的第三幅图
可以看到,采样就是在频域上把原始函数的频谱复制粘贴了很多份
采样就是在重复原始信号的频谱
走样在频谱上的表现
如果采样的频率够快,那么复制粘贴原函数的频谱之间的间隔就大
如果采样的频率慢,复制粘贴之间的间隔就小
间隔比较小,复制粘贴的频谱之间就会有重叠
这个重叠就是走样
反走样
由以上分析得到反走样的方法
1.增大采样频率
对于游戏来说不现实,因为硬件设备的分辨率是固定的
2.先做模糊后做采样
就是先把高频信息拿掉,再采样
这里可以发现,去掉高频信号就是把原始函数的频谱的两边砍掉一部分,那么这就允许复制粘贴的间隔可以小
怎么模糊一个三角形
最常用的是使用一个像素大小的卷积核
这里应该是默认,卷积之后输出的是单通道的图
因此这里的一个像素大小的卷积核的作用就是平均每一个像素点的三个通道?
但是问题是,最后我还是要采出一个 rgb 颜色的呀……?那我最后不可能对一个单通道的图做采样啊……?
他之后说了不是这个意思,而是计算原图形在这个像素点的覆盖面积,然后平均到这个像素
比如这里如果原图形覆盖了某一个像素点的 1/8,那么这个像素点就会有原图形 1/8 的颜色,如果覆盖了 1/2 就会有原图形 1/2 的颜色
MSAA
Multi Sample 多重采样
精确地计算原图形覆盖了一个像素点的百分比比较困难
但是我们又想知道这个百分比
因此我们把一个像素拆分成若干个子像素,通过计算
原图形覆盖的子像素的数目/一个像素块的子像素的数目
来计算这个像素的百分比
这一步只是解决了模糊的这一步,而不是采样的这一步
或者说他也隐含了采样的步骤,但是采样的像素块的大小始终是原始的像素的大小,而不是在 MSAA 中划分的子像素的大小
所以 MSAA 不是提高了采样率
MSAA 的代价
很显然,如果是一个像素分成 4 * 4 的子像素,计算代价就是 16 倍
因此其他方法可以不均匀地分布子像素的图案
其他抗锯齿方案
FXAA
先得到锯齿,再对锯齿做特殊操作
但是我们一开始也知道,先采样后模糊,得到的是锯齿的模糊
而 FXAA 先采样,得到锯齿之后,通过寻找锯齿边界的方法对锯齿做处理
TAA
复用上一帧的信息
在时间上用 MSAA
Temporal 重音在前 表示”暂时的“
TAA 的 Temporal 重音在后,与”暂时的“这个意思区分
超分辨率
相似的概念
图的分辨率很高,采样率却很小
DLSS