免责声明:此文章为记录自主学习图形学机制而留,并没有提交到BSS论坛,请勿直接复制粘贴,后果自负。
HomeWork1:MVP矩阵
作业的最终效果如下:就是让我们画一个三角形,作业包已经完成了大部分的内容,所以我们最后只用填入模型的变换矩阵M和投影矩阵P

什么是MVP矩阵?
在计算机图形学中,我们用向量表示一个点,用向量表示一个虚拟世界,而最后这些模型有些会进入我们的屏幕而有的不会,这就是经历了mvp矩阵变换的结果。 顾名思义:m代表模型矩阵,用来表示模型的空间位置;v代表观测变换,表示我们用照相机产生的一个视野,为了方便实现,挪动相机实际上就是挪动整个世界而相机不动;P代表投影,类似于最后成像的方式
仿射变换
实际上M和V矩阵都是让世界物体做仿射变换,那什么是仿射变换? 简单来说就是平移+旋转+放缩三种变换的统称,我们用矩阵表示就是:
$$\begin{bmatrix}x' \\y' \\z' \\1\end{bmatrix}=\begin{bmatrix}R & \mathbf{t} \\\mathbf{0}^T & 1\end{bmatrix}\begin{bmatrix}x \\y \\z \\1\end{bmatrix}$$其中$R$表示旋转/缩放的部分,$t_x,t_y,t_z$是平移。 为了更直观,我们把三种基本变换拆开写: 平移矩阵:
$$ T(\Delta x,\Delta y,\Delta z)= \begin{bmatrix} 1 & 0 & 0 & \Delta x \\ 0 & 1 & 0 & \Delta y \\ 0 & 0 & 1 & \Delta z \\ 0 & 0 & 0 & 1 \end{bmatrix} $$绕$z$轴旋转:
$$ R_z(\theta)= \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$绕$x$轴旋转:
$$ R_x(\theta)= \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$绕$y$轴旋转:
$$ R_y(\theta)= \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$实际上还有绕任意轴旋转的矩阵(罗德里格斯旋转公式):
$$ R_{\mathbf{u}}(\theta)=I\,c+(1-c)\,\mathbf{u}\mathbf{u}^T+s\,[\mathbf{u}]_\times $$其中$\mathbf{u}=(u_x,u_y,u_z)$为单位向量,$c=\cos\theta,s=\sin\theta$,
$$ [\mathbf{u}]_\times= \begin{bmatrix} 0 & -u_z & u_y \\ u_z & 0 & -u_x \\ -u_y & u_x & 0 \end{bmatrix} $$缩放矩阵:
$$ S(s_x,s_y,s_z)= \begin{bmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$这三种矩阵按顺序相乘就是模型矩阵$M$。注意矩阵相乘是有顺序的,例如先旋转再平移和先平移再旋转效果不一样。
观测矩阵V
将三维的点集由世界坐标变换为相机坐标; 相机有如下几种属性:位置$\vec{e}$ ,视线方向$\hat{g}$ ,向上方向$\hat{t}$ (假设与视线方向垂直) 我们将原坐标系变换为相机坐标系,并符合如下规则:相机在原点、向上方向为 $y+$ ,视线方向为 $z-$ ($x+$ is $\hat{g}\times\hat{t}$, $y+$ is $\hat{t}$, $z+$ is $-\hat{g}$); 假设变换矩阵为 $M_{view}$ ,考虑其的形式($M_{view}=R_{view}T_{view}$):首先需要将相机点移到原点($T_{view}=T(-\vec{e})$),再进行坐标轴变换; 考虑 $R_{view}^{-1}$ 即从相机坐标系变换回原坐标系:参考
$$ R_{view}^{-1}= \begin{bmatrix} x_{\hat{g}\times\hat{t}} & x_{\hat{t}} & x_{-\hat{g}} & 0 \\ y_{\hat{g}\times\hat{t}} & y_{\hat{t}} & y_{-\hat{g}} & 0 \\ z_{\hat{g}\times\hat{t}} & z_{\hat{t}} & z_{-\hat{g}} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$得:
$$ R_{view}= \begin{bmatrix} x_{\hat{g}\times\hat{t}} & x_{\hat{t}} & x_{-\hat{g}} & 0 \\ y_{\hat{g}\times\hat{t}} & y_{\hat{t}} & y_{-\hat{g}} & 0 \\ z_{\hat{g}\times\hat{t}} & z_{\hat{t}} & z_{-\hat{g}} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} $$投影矩阵P
投影的作用是把3D的物体压到2D平面上。可以分成两步:透视到正交,再做正交投影。
具体来说:正交投影就是把将$z$坐标写为0之后把$(-\infty,+\infty)^2$的物体写入$[-1,+1]^3$的标准立方体中,投影出来的效果不符合现实世界观察近大远小的规律
而透视投影就是模拟真实世界观测的效果(近大远小,平行线看着不平行)具体做法就是将一个以原点为锥点的截锥体变换成立方体,再进行正交投影,就完成了透视投影的过程.

- 透视到正交的变换: $$ M_{persp\to ortho}= \begin{bmatrix} z_{near} & 0 & 0 & 0 \\ 0 & z_{near} & 0 & 0 \\ 0 & 0 & z_{near}+z_{far} & -z_{near}z_{far} \\ 0 & 0 & 1 & 0 \end{bmatrix} $$
- 正交投影把视锥体挤到标准立方体里。设视角为$\text{fov}$,宽高比为$\text{aspect}$,近远平面为$z_{near},z_{far}$, 则上/下/左/右边界为: $$ t=|z_{near}|\tan\left(\frac{\text{fov}}{2}\right),\quad b=-t,\quad r=t\cdot \text{aspect},\quad l=-r $$ 正交投影矩阵: $$ M_{ortho}= \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{2}{z_{near}-z_{far}} & -\frac{z_{near}+z_{far}}{z_{near}-z_{far}} \\ 0 & 0 & 0 & 1 \end{bmatrix} $$ 最终的投影矩阵就是: $$ P=M_{ortho}M_{persp\to ortho} $$
MVP合成
在代码里我们把三个矩阵按顺序乘起来,就是:
$$ MVP=P\,V\,M $$最后顶点经过$MVP$变换,再做一次齐次除法(除以$w$)就可以得到屏幕上的点了。 在了解这些前置知识之后,我们就可以开始作业1了!

get_model_matrix与get_projection_matrix这两个函数一个对应模型的位移,另一个则对应透视投影,我们直接将上文中的公式代入,得到: | |
关于提高项:

| |
调用也很简单:我们只需要:Eigen::Matrix4f transformer=get_rotation(Eigen::Vector3f(0,0,1),rotation_angle);就可以实现绕z轴旋转
Homework2:采样和z-buffering
第二个作业的最终效果如下图:


视口变换(Viewport Transformation)
经过MVP变换后,顶点位于$[-1,1]^3$的立方体中。为了将这些坐标映射到实际的屏幕像素坐标,我们需要进行视口变换。 视口变换将NDC坐标映射到屏幕坐标系,其中屏幕左下角为$(0,0)$,右上角为$(width, height)$。视口矩阵$M_{viewport}$定义为:
$$ M_{viewport} = \begin{bmatrix} \frac{w}{2} & 0 & 0 & \frac{w}{2} + x \\ 0 & \frac{h}{2} & 0 & \frac{h}{2} + y \\ 0 & 0 & \frac{1}{2} & \frac{1}{2} \\ 0 & 0 & 0 & 1 \end{bmatrix} $$其中:
- $w$ 是视口的宽度
- $h$ 是视口的高度
- $x, y$ 是视口左下角在屏幕上的坐标(通常为0) 这样,正方体中的点$(x_{ndc}, y_{ndc}, z_{ndc})$经过视口变换后变为屏幕坐标$(x_{screen}, y_{screen}, z_{screen})$。
采样和光栅化
采样是将连续的几何体转换为离散的像素的过程。这个过程称为光栅化。
基本步骤
边界框计算:为每个三角形计算其在屏幕上的轴对齐边界框(Axis-Aligned Bounding Box, AABB),以减少需要检查的像素数量。
像素采样:对于边界框内的每个像素,检查像素中心是否位于三角形内部。课里面教的是直接顺时针的方向和三角形里的每个点进行叉乘,若全部同号则在三角形内部,但是不太好处理叉积=0的情况,所以在这里介绍使用重心坐标来判断:
- 三角形三个顶点为$\mathbf{v}_0, \mathbf{v}_1, \mathbf{v}_2$
- 对于像素中心点$\mathbf{p}$,计算重心坐标$(\alpha, \beta, \gamma)$
- 如果$\alpha \geq 0, \beta \geq 0, \gamma \geq 0$,则点在三角形内
重心坐标可以通过叉积计算:
$$ \alpha = \frac{ (\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{p} - \mathbf{v}_0) }{ (\mathbf{v}_1 - \mathbf{v}_0) \times (\mathbf{v}_2 - \mathbf{v}_0) } $$我们可以用同样的方式计算$\beta, \gamma$。
属性插值:对于在三角形内的像素,使用重心坐标插值顶点属性,如颜色、纹理坐标等。
深度插值:同样使用重心坐标插值深度值,用于z-buffering。
Z-Buffering
Z-Buffering是一种解决可见性问题的算法,用于确定哪个像素在前。
- 维护一个深度缓冲区,大小与屏幕像素相同,初始值为无穷大(表示最远)。
- 对于每个像素,如果其插值深度小于缓冲区中的值,则更新缓冲区并绘制该像素。
- 这样确保了近的物体覆盖远的物体。
反走样
如果我们直接按照上述的朴素算法渲染图形,最后打在画面上的结果就会这样:

- 超采样(SSAA):对每个像素做多次子像素采样并平均,简单但计算量大;
- 多重采样(MSAA):对深度/覆盖做多样本采样、对颜色按片元计算,降低开销;
- 屏幕空间后处理(FXAA/SMAA):基于像素图像做边缘检测与滤波,速度快但可能模糊细节。
- 时域抗锯齿(TAA):类似于在时域上进行多重采样。
简单2×2SSAA
对每个像素采样 4 个子样本,常用位置(以像素左下为 (0,0))为: $(0.25,0.25),(0.75,0.25),(0.25,0.75),(0.75,0.75)$。
MSAA与后处理
- MSAA:为每像素维护 N 个样本的深度/覆盖,着色器通常按片元执行一次,覆盖到被占据的样本;
- FXAA/SMAA:基于最终图像的边缘检测与滤波,代价低、易集成,但属于近似方法,会影响锐利细节。
了解这些前置知识之后,我们来到作业2:我们直接实现提高项:实现一个2x2的ssaa:
我们先实现判断一个点是否在三角形内部的函数
insideTriangle参考上文,我们使用重心坐标法实现:
| |
在正式实现光栅化之前,我们要先修改hpp的对象结构,我们需要额外的维护一个std::vector< std::vector<std::pair<float, Eigen::Vector3f>> > depColor来维护深度测试当前最前面的颜色,为了代码美观,我还额外定义了一个N=2来表示当前ssaa的分割数
参照上文,我们要先判断这个三角形的AABB包围盒来减少像素计算数量,然后再对内部进行细分,每个点进行一次采样和深度测试,更新depcolor的值,在做完ssaa之后,记得调用set_pixel函数来设置像素值
| |
致此,我们把3D图形的变换以及把图形打到屏幕上的过程走通了一遍,但这还不是完整的渲染管线,实际上,我们一直没有考虑到着色的情况,这也是接下来的课程和作业所关注的