作业内容
- 目标:这一次我们继续推进一步——在屏幕上画出一个实心三角形, 换言之,栅格化一个三角形。上一次作业中,在视口变化之后,我们调用了函数 rasterize_wireframe(const Triangle& t)。
- 这一次,你需要自己填写并调用函数rasterize_triangle(const Triangle& t)。
- 该函数的内部工作流程如下:
- 创建三角形的2维bounding box。
- 遍历此 bounding box 内的所有像素(使用其整数索引)。然后,使用像素中心的屏幕空间坐标来检查中心点是否在三角形内。
- 如果在内部,则将其位置处的插值深度值(interpolated depth value) 与深度 缓冲区(depth buffer) 中的相应值进行比较。
- 如果当前点更靠近相机,请设置像素颜色并更新深度缓冲区(depthbuffer)。 你需要修改的函数如下:
- rasterize_triangle(): 执行三角形栅格化算法 • static bool insideTriangle(): 测试点是否在三角形内。你可以修改此函 数的定义,这意味着,你可以按照自己的方式更新返回类型或函数参数。 因为我们只知道三角形三个顶点处的深度值,所以对于三角形内部的像素, 我们需要用插值的方法得到其深度值。我们已经为你处理好了这一部分,因为有 关这方面的内容尚未在课程中涉及。插值的深度值被储存在变量z_interpolated 中。
- 请注意我们是如何初始化depthbuffer 和注意zvalues 的符号。为了方便 同学们写代码,我们将z进行了反转,保证都是正数,并且越大表示离视点越远。 在此次作业中,你无需处理旋转变换,只需为模型变换返回一个单位矩阵。最后,我们提供了两个hard-coded 三角形来测试你的实现,如果程序实现正确,你将看到理想的输出图像。(这里放到后面去实现)
实现
准备
- 根据要求,我们预先解决一下视口变换对应的方法进行移植,对应的矩阵和推导见之前的内容。
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
// 根据作业要求我们将上一节中写好的视口变化函数转移到这里。
// Create the projection matrix for the given parameters.
// Then return it.
// 将视场角从度转换为弧度,这一步一定要从头贯彻到尾,在这一对应的CPP库当中,要注意所有的角度都是采用弧度制的!
float fovy_in_radians = eye_fov * MY_PI / 180.0f;
// 计算半角的正切值
float tan_half_fovy = std::tan(fovy_in_radians / 2.0f);
// 设置透视投影矩阵的元素
projection(0, 0) = 1.0f / (aspect_ratio * tan_half_fovy); // x 缩放
projection(1, 1) = 1.0f / tan_half_fovy; // y 缩放
projection(2, 2) = (zFar + zNear) / (zFar - zNear); // z 缩放
projection(2, 3) = (2.0f * zFar * zNear) / (zFar - zNear); // z 偏移
projection(3, 2) = -1.0f; // z 坐标变换
projection(3, 3) = 0.0f; // w 坐标变换
return projection;
}
实现方法
rasterize_triangle()的实现
我们首先需要完成对三角形的绘制以及渲染。根据作业要求中的相关内容,我们需要根据流程实现这一算法。
- 当前三角形的包围盒计算:通过min,max方法计算相应的坐标值。
//包围盒计算:
float x_min = std::min({ v[0].x(), v[1].x(), v[2].x() });
float y_min = std::min({ v[0].y(), v[1].y(), v[2].y() });
float x_max = std::max({ v[0].x(), v[1].x(), v[2].x() });
float y_max = std::max({ v[0].y(), v[1].y(), v[2].y() });
int x_start = static_cast<int>(std::floor(x_min));
int y_start = static_cast<int>(std::floor(y_min));
int x_end = static_cast<int>(std::ceil(x_max));
int y_end = static_cast<int>(std::ceil(y_max));
- 对包围盒内每个像素进行计算以及着色,以及在着色前进行适当的深度更新。
- 这里的深度测试以及插值深度的获得已经计算好,根据逻辑进行相应的深度测试并且更新深度出测试区域即可;这里需要注意的是设置像素颜色的时候虽然我们计算的是遍历出的x,y对应值,但是实际传入参数是Eigen::Vector3f。
for (int x = x_start; x < x_end; ++x) {
for (int y = y_start; y < y_end; ++y)
{
//enhance the strength to avoid error:the range out of array.
if (x < 0 || x >= width || y < 0 || y >= height)
{
continue; // skip it
}
// use relevant api to calculate its core place.‘
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;
// Z-Buffer Test
if (depth_buf[get_index(x, y)] >z_interpolated)
{
// 更新深度缓存
depth_buf[get_index(x, y)] = z_interpolated;
// 设置像素颜色
Eigen::Vector3f point(x,y, z_interpolated);
set_pixel(point, t.getColor());
}
}
}
效果展示:

- 这里可以发现,这是三角形所在的包围盒,被整个渲染了出来!原因是我们在进行渲染的时候只考虑了对应矩形内所有点可能的渲染和遮挡关系而没有判断像素点在不在三角形内!也就是说,我们的任务刚刚完成了一半。
static bool insideTriangle()的实现
- 使用位置:考虑刚才的函数中Step2的相关介绍部分,我们发现在片元剔除的时候只是粗略的判定了三角形顶点是否位于视锥体内(只是出于robust考虑,并没有更加深层的含义),而真正需要判定考虑时其实需要考虑其对应像素是否在三角形内。这里使用法向量叉乘法用以判定是否在三角形内部。原理:用该点和顶点以及顺时针的某条边叉乘,做三次判断对应的三个方向是否同号,若同号说明在三条封闭线的内部。
//1.完善对应的方法insidetriangle:
static bool insideTriangle(int x, int 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]
Eigen::Vector3f v0 = _v[0];
Eigen::Vector3f v1 = _v[1];
Eigen::Vector3f v2 = _v[2];
// 计算边向量
Eigen::Vector3f ab = v1 - v0;
Eigen::Vector3f bc = v2 - v1;
Eigen::Vector3f ca = v0 - v2;
// 计算点相对于顶点的位置向量
Eigen::Vector3f ap = Eigen::Vector3f(x - v0.x(), y - v0.y(), 0.0f);
Eigen::Vector3f bp = Eigen::Vector3f(x - v1.x(), y - v1.y(), 0.0f);
Eigen::Vector3f cp = Eigen::Vector3f(x - v2.x(), y - v2.y(), 0.0f);
// 计算叉乘
Eigen::Vector3f cross1 = ab.cross(ap);
Eigen::Vector3f cross2 = bc.cross(bp);
Eigen::Vector3f cross3 = ca.cross(cp);
// 检查叉乘结果的z分量的符号是否一致
return (cross1.z() >= 0 && cross2.z() >= 0 && cross3.z() >= 0) ||
(cross1.z() <= 0 && cross2.z() <= 0 && cross3.z() <= 0);
}
效果展示:

- 完成了三角形的正确判断和绘制
- Weakness:
- 与官方例程相比遮挡关系反了,需要调整。再次阅读作业文档发现z值被反转为正值且值越大表示距离越远。
- 蓝色三角形斜边处有十分明显的锯齿走样,需要进行抗锯齿方法的实现解决。
提高:用 super-sampling 处理 Anti-aliasing
拓展要求:你可能会注意 到,当我们放大图像时,图像边缘会有锯齿感。我们可以用 super-sampling 来解决这个问题,即对每个像素进行 2 * 2 采样,并比较前后的结果 (这里 并不需要考虑像素与像素间的样本复用)。需要注意的点有,对于像素内的每 一个样本都需要维护它自己的深度值,即每一个像素都需要维护一个 sample list。最后,如果你实现正确的话,你得到的三角形不应该有不正常的黑边。
// 对每个像素进行 2x2 采样,每个位置都保留原点位置左上,左下,右上,右下像素点再平均值。
Eigen::Vector3f color_sum(0.0f, 0.0f, 0.0f);
float depth_sum = 0.0f;
int sample_count = 0;
for (int sy = 0; sy < 2; ++sy) {
for (int sx = 0; sx < 2; ++sx) {
float sub_x = x + (sx + 0.5f) / 2.0f;
float sub_y = y + (sy + 0.5f) / 2.0f;d
利用细分对应的点并且加权取平均数的方式使得边界处的采样在理论上更加平滑。
效果:

- 坦率的说,提高一倍的采样率的纯粹MSAA方法计算量大了很多但是效果并不尽如人意,但能够看出一定的成效。
后记:Debug:我的三角形为什么和预期渲染顺序相反?
De了半天,发现之前的p变换是按-z方向看决定的视锥体正则化,而现在应该按正向重新绘制矩阵,更改完之后发现绘制顺序返回正常状态。。。感受到了渲染管线在修改的时候的破防感觉,有一种初现端倪的感觉2333. 修改后的深度测试:(其实就是把对应z值乘了个相反)
Eigen::Matrix4f get_projection_matrix(float eye_fov, float aspect_ratio, float zNear, float zFar)
{
Eigen::Matrix4f projection = Eigen::Matrix4f::Identity();
float fovy_in_radians = eye_fov * MY_PI / 180.0f;
float tan_half_fovy = std::tan(fovy_in_radians / 2.0f);
// 标准透视投影矩阵
projection(0, 0) = 1.0f / (aspect_ratio * tan_half_fovy);
projection(1, 1) = 1.0f / tan_half_fovy;
projection(2, 2) = -(zFar + zNear) / (zFar - zNear);
projection(2, 3) = -2.0f * zFar * zNear / (zFar - zNear);
projection(3, 2) = -1.0f;
projection(3, 3) = 0.0f;
return projection;
}
最终成果
这下终于出现了作业中的场景!不枉费我期末周闹心学习图形学,起码有了成果。
