Assignment 2
投影矩阵的重新定义
GAMES101 在 Lab 2 中调整了深度值的约定:
请注意我们是如何初始化 depth buffer 和注意 z values 的符号。为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。
我的理解:
输入阶段:原点仍为相机位置,物体位于 z 负半轴,相机朝向 -z 方向。
投影之后:深度被映射到 [-1, 1],并且数值越大表示距离相机越远。
因此需要重新计算投影矩阵。下面几个参数含义保持不变:
- :半宽;
- :半高;
- :近平面与原点距离;
- :远平面与原点距离;
- ;
- :视野角。
那么,视锥拉正矩阵 保持不变,表示为:
但是第二步的变换,在 方向上,需要把近平面映射到 ,远平面映射到 ,所以可以列出方程:
得到变换矩阵 :
然后:
insideTriangle 函数
Lab 2 在 Lab 1 画线框三角形的基础上,加入了三角形的栅格化。课程给出的基本思路是:遍历像素,并判断像素点是否位于三角形内部。
对于像素点 和三角形 ,按顺序计算三组叉乘:
如果三者同号,说明点 位于三角形三条有向边的同侧,也就位于三角形内部。这里把落在三角形边上的点也视为内部点。
draw 函数
draw函数虽说是代码框架提供的,但是也详细读读
- 首先接受了main函数传来的三个关于三角形的pos、ind、cols的 map
- 定义了
f1``f2,用于将后续被映射到[-1,1]的z坐标再映射到正值(深度值) - 进入循环,实例化三角形,对于每一个三角形,先将三个点分别通过
MVP矩阵,把模型自己的局部坐标,一步步变换到裁剪空间 - 经过mvp转换后,模型已经映射到了这个正方体,但是我们最后显示的屏幕为width * height大小,因此需要下一步操作
vert.x() = 0.5*width*(vert.x()+1.0); // 转为[0,width]
vert.y() = 0.5*height*(vert.y()+1.0); // 转为[0,height]
vert.z() = vert.z() * f1 + f2; // 转为[zNear , zFar]
- 简而言之,这里的代码将NDC坐标[-1,1]
Normalized Device Coordinates归一化设备坐标,转为屏幕坐标 - 接着通过
setVertex和setColor,对于前面实例化的三角形设置参数,然后调用rasterize_triangle函数
rasterize_triangle 函数
Bounding Box
为了避免每次渲染都遍历整个屏幕,可以先计算三角形的 bounding_box,用一个最小矩形框住当前三角形。
具体做法是分别找出三角形三个顶点的最小、最大 和 坐标,然后只对这个矩形范围内的像素做逐点判断。
判断与绘制
绘制一个像素前,需要进行两步判断:
- 像素是否在三角形内部;
- 像素是否足够靠近相机,能够覆盖当前已经绘制的颜色。
第一个判断已经由 insideTriangle 完成。
第二个判断使用代码框架提供的 z-buffer 算法。这里维护了两个一维数组:
frame_buf:存储每个像素最终输出的颜色;depth_buf:存储当前像素对应图元的深度值。
由于 Lab 2 中深度值越大表示离相机越远,所以深度更小的像素应该覆盖深度更大的像素。
代码中使用下面的公式把二维坐标转换为一维数组下标:
(height - 1 - y) * width + x
具体流程如下:
- 遍历当前三角形的
bounding_box; - 对每个像素点调用
insideTriangle; - 如果像素位于三角形内部,就计算插值得到的深度
z_interpolated; - 调用
get_index(int x, int y)得到当前像素在缓冲区中的下标; - 比较
z_interpolated和depth_buf[idx]; - 如果当前像素更近,就更新
frame_buf和depth_buf。
由于此时还没有实现更复杂的着色算法,所以直接使用三角形对象的 getColor() 返回颜色即可。
完成这些步骤后,三角形已经可以正常显示,并且前后遮挡关系也正确。
Bonus 篇
从生成结果可以看出,三角形边缘存在明显锯齿。这是因为一个像素只能显示一种颜色,而三角形边界又不一定刚好穿过像素边界,最终就会产生生硬的阶梯状边缘。
因此,Bonus 部分需要实现 Super-Sampling Anti-Aliasing,也就是超采样抗锯齿。这里采用 采样:把原本的一个像素划分成四个子采样点,分别判断它们是否被三角形覆盖,最后再把四个采样点的颜色平均为当前像素的最终颜色。
首先定义四个子采样点相对于当前像素左下角的偏移量:
std::vector<Eigen::Vector2f> pixel_offsets ={
{0.25f , 0.25f},
{0.25f , 0.75f},
{0.75f , 0.25f},
{0.75f , 0.75f}
};
这些 offset 分别指向四个子采样区域的中心。与此同时,还需要额外维护两个一维数组:
ssaa_frame_buf:长度为原本frame_buf的四倍,用于存储每个子采样点的颜色;ssaa_depth_buf:长度为原本depth_buf的四倍,用于记录每个子采样点对应的深度。
不过,最终绘制到屏幕上的仍然是原始分辨率的像素。因此,frame_buf 仍然需要保留,用来存储四个子采样点平均之后的最终颜色;而深度测试则可以交给 ssaa_depth_buf 完成。
具体修改并不复杂。对于原本的每一个像素 ,之前只需要判断像素中心是否在三角形内部;现在则要遍历四个 offset,分别判断四个子采样点是否位于三角形内部。
每个子采样点都执行原本的深度测试与颜色更新逻辑,只是不再直接调用 set_pixel。等四个子采样点都处理完成后,再对它们的颜色求平均,并写入当前像素对应的 frame_buf。
运行结果
之前:

之后:

可以看到,开启超采样之后,三角形边缘柔和了很多,锯齿感明显减弱。
同时也能观察到,frame_count 的递增速度大约变成了原来的 。这是因为每个像素都需要额外进行四次采样和深度测试,也正体现了超采样抗锯齿在效果之外所付出的性能代价。