目录
作业概述
在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是那么的有趣。所以这一次我们继续推进一步——在屏幕上画出一个实心三角形,换言之,栅格化一个三角。上一次作业中,在视口变化之后,我们调用了函数rasterize_wireframe(const Triangle& t)。但这一次,你需要自己填写并调用函数 rasterize_triangle(const Triangle& t) 。该函数的内部工作流程如下:1. 创建三角形的 2 维 bounding box 。2. 遍历此 bounding box 内的所有像素(使用其 整数索引)。然后,使用像素中心的屏幕空间坐标来检查中心点是否在三角形内。3. 如果在内部,则将其位置处的 插值深度值 (interpolated depth value) 与深度缓冲区 (depth buffer) 中的相应值进行比较。4. 如果当前点更靠近相机,请设置像素颜色并更新深度缓冲区 (depth buffer) 。你需要修改的函数如下:• rasterize_triangle(): 执行三角形栅格化算法• static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。因为我们只知道三角形三个顶点处的深度值,所以对于三角形内部的像素,我们需要用 插值的方法得到其深度值。我们已经为你处理好了这一部分,因为有关这方面的内容尚未在课程中涉及。插值的深度值被储存在变量 z_interpolated中。请注意我们是如何初始化 depth buffer 并 注意 z values 的符号。为了方便同学们写代码,我们将 z 进行了反转,保证都是正数,并且越大表示离视点越远。在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。最后,我们提供了两个 hard-coded 三角形来测试你的实现。提高部分:用 super-sampling 处理 Anti-aliasing : 你可能会注意到,当我们放大图像时,图像边缘会有锯齿感。我们可以用 super-sampling来解决这个问题,即对每个像素进行 2 * 2 采样,并比较前后的结果 (这里并不需要考虑像素与像素间的样本复用 )。需要注意的点有,对于像素内的每一个样本都需要维护它自己的深度值,即每一个像素都需要维护一个 sample list 。最后,如果你实现正确的话,你得到的三角形不应该有不正常的黑边。
如上所示即为本次作业需要完成的各个任务,下面我就各个问题分享一下我的思路。
Bouding box
最开始我一直有些不能理解为什么可以直接由一个三维空间中的三角形变成二维的三角形,为什么可以直接进行“正交投影”而忽略Z轴呢?现在的三角形是哪个阶段的三角形?我虽然知道只需要也只能取出来当前三角形三个顶点的X,Y坐标用做平面化,但是不明白其中的道理。在我半知半解往下做的过程中,我逐渐理解了。
如作业中所说,我们是在完成视口变化之后找出Bounding box,而视口变化即是在我们已将Z轴某处的model投影到当前Z处画布的后变换回原来的大小的变化。我之所以把视口变化解释这么多,重点在于说明在视口变化之前,我们已经进行了投影变化,也就是说我们已经把原模型Z轴的坐标“解决”了,所以在视口变化时,我们已经不再处理Z轴,其变化矩阵如下,目的为将的模型挪到。故在视口变化之后的寻找BoudingBox就更无须考虑Z轴了。此时Z轴的数据记录的是原本模型的Z轴值,作用是为了后续进行z-buffer的深度差值,判断遮挡性。
也就是现在可以认为当前三角形的坐标已经是在此范围,故只需要分别得到最小的X、Y坐标和最大的X、Y坐标即可划分出Bouding Box,那么Z轴的值有什么用呢?它是用来判断各点的深度情况,以此得出遮挡的对应关系。
判断点是否在三角形内
这一步用到了之前叉乘的性质,即按某一确定方向得到三角形的各个边向量后,分别让三个边向量与同起点、终点在待确定点的向量做叉乘,如果得到的结果都向外或都向内(这里认为三角形和点都是二维的,向内向外是相对于纸面/三角形所在平面而言)。
更详细具体的理论可以参考别人的博客或百度,我在这里说两点编码实现中可能会有的问题。
1、闫老师给出的函数形式是:static bool insideTriangle(float x, float y, const Vector3f* _v)(这里我已经修改了x,y的类型为了服务于拓展功能,原本为int),如果不改动第三个参数,则默认应该传入三角形的三个顶点坐标,注意!不能传入原本在rasterize_triangle函数中的v,因为此v为齐次变换后的坐标,是Vector4f的。如果你仔细查看了Triangle.hpp文件,会发现其中定义了三角形的三个坐标点,命名也为v,故传入的应该是t.v。
2、判断方法是把整个空间当做二维的处理的(不然也没有点在三角形内这一说了),所以其实只需要取出三角形顶点坐标的X,Y进行判断即可,也可利用三维坐标做叉积但只需判断Z轴方向是否一致即可。
具体代码如下:
static bool insideTriangle(float x, float y, const Vector3f* _v)
{
// TODO : Implement this function to check if the point (x, y) is inside
// the triangle represented by _v[0], _v[1], _v[2]
Vector2f point(x,y);
Vector2f AP = point-_v[0].head(2);
Vector2f BP = point-_v[1].head(2);
Vector2f CP = point-_v[2].head(2);
Vector2f AB = _v[1].head(2)-_v[0].head(2);
Vector2f BC = _v[2].head(2)-_v[1].head(2);
Vector2f CA = _v[0].head(2)-_v[2].head(2);
float a = AB[0]*AP[1] - AB[1]*AP[0];
float b = BC[0]*BP[1] - BC[1]*BP[0];
float c = CA[0]*CP[1] - CA[1]*CP[0];
if(a>0 && b>0 && c>0)
return true;
else if(a<0 && b<0 && c<0)
return true;
return false;
}
Z-buffer
这一部分其实反而比较简单,闫老师代码中已经给出了线性插值得出当前像素点所在深度的代码,重点在于关注rasterizer.hpp中对于相关数据结构(depth_buf、frame_buf)的定义,以便较好的利用,如果当前像素的深度值比depth_buf中的浅,则将depth_buf更新,并设置此像素值的颜色。注意理解使用depth_buf的下标,作为一个数组,下标肯定是一个整型数字,如何把二维的点和整型数字对应?代码框架里给每个像素点都设置了一个index,代码框架中还有get_index函数,注意利用哦~
提高部分
综上,就可以完成所有的基础功能了,你便可以得到下方这个图:
不难看出,图中锯齿化问题很明显,如何解决?提高部分,使用MSAA。MSAA的思想也很简单,把一个像素分成若干份,看有几份在三角形内部,若一共N份、有n份在三角形,则把该像素的颜色设置为,但是如果你真的这么处理会导致出现一条黑线:
黑线解决方案
关于黑线的产生我认为比较有道理的解释是:4个小像素只有1个被靠前的三角形覆盖,1/4*color得到的rgb值太小了,显示接近黑色。
我利用吸色得到前面的三角形的RGB值大约为(217,238,185)除以4后得到(54,60,46),如下图所示,确实接近黑色(我也真是绝了哈哈哈哈):
所以其实不能单纯通过对靠前的三角形的像素平均以实现模糊去锯齿的目的,还应考虑另一个三角形在一个像素中的各个小块的作用。故应记录下各个小块中的depth_buf和frame_buf,最终对一个大格子内的每一个像素点的颜色求和平均即可。
因此我其实修改了对应的.hpp文件,并在初始化时给出两个vector的对应内存,关键代码如下所示:
//Screen space rasterization
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
// 将三维向量变为齐次坐标
auto v = t.toVector4();
// TODO : Find out the bounding box of current triangle.
// iterate through the pixel and find if the current pixel is inside the triangle
float minx = std::min(v[0][0],std::min(v[1][0],v[2][0]));
float miny = std::min(v[0][1],std::min(v[1][1],v[2][1]));
float maxx = std::max(v[0][0],std::max(v[1][0],v[2][0]));
float maxy = std::max(v[0][1],std::max(v[1][1],v[2][1]));
int min_x = floor(minx);
int min_y = floor(miny);
int max_x = round(maxx);
int max_y = round(maxy);
bool MSAA=true;
// 利用MSAA进行模糊,抗锯齿
for(int x=min_x;x<=max_x;x++){
for(int y=min_y;y<=max_y;y++){
//MSAA
if(MSAA){
const float axis_x[4] = {+0.25,+0.25,+0.75,+0.75};
const float axis_y[4] = {+0.25,+0.75,+0.25,+0.75};
for(int pix =0;pix<4;pix++){
float X = x + axis_x[pix];
float Y = y + axis_y[pix];
int samplePid = get_index(x,y) * 4 + pix;
if(insideTriangle(X,Y,t.v)){
// If so, use the following code to get the interpolated z value.
auto[alpha, beta, gamma] = computeBarycentric2D(X, Y, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// compare z to find the most front pixel whose z is the min
if(z_interpolated < sample_depth_buf[samplePid]) {
sample_depth_buf[samplePid] = z_interpolated;
sample_frame_buf[samplePid] = t.getColor() / 4.0;
depth_buf[get_index(x,y)] = std::min(depth_buf[get_index(x,y)],z_interpolated);
}
}
}
set_pixel(Eigen::Vector3f(x,y,depth_buf[get_index(x,y)]),
sample_frame_buf[get_index(x,y)*4] + sample_frame_buf[get_index(x,y)*4+1] +
sample_frame_buf[get_index(x,y)*4+2] + sample_frame_buf[get_index(x,y)*4+3]);
}
//no MSAA
else{
//三角形t的v记录了三个顶点的三维坐标
if(insideTriangle(x+0.5,y+0.5,t.v)){
// If so, use the following code to get the interpolated z value.
auto[alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
float w_reciprocal = 1.0/(alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float z_interpolated = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
z_interpolated *= w_reciprocal;
// TODO : set the current pixel (use the set_pixel function) to
// the color of the triangle (use getColor function) if it should be painted
if(z_interpolated < depth_buf[get_index(x,y)]){
depth_buf[get_index(x,y)] = z_interpolated;
set_pixel(Eigen::Vector3f(x,y,z_interpolated), t.getColor());
}
}
}
}
}
}
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
frame_buf.resize(w * h);
depth_buf.resize(w * h);
//额外初始化
sample_depth_buf.resize(w * h * 4);
//可以完善在clear,我的处理方式是写在了初始化里
std::fill(sample_depth_buf.begin(), sample_depth_buf.end(), std::numeric_limits<float>::infinity());
sample_frame_buf.resize(w * h * 4);
std::fill(sample_frame_buf.begin(), sample_frame_buf.end(), Eigen::Vector3f{0, 0, 0});
}
rasterizer.hpp:
std::vector<Eigen::Vector3f> frame_buf;
// 补充
std::vector<Eigen::Vector3f> sample_frame_buf;
std::vector<float> depth_buf;
// 补充
std::vector<float> sample_depth_buf;