作业内容
• [5 分] 正确地提交所有必须的文件,且代码能够编译运行。
• [20 分] 正确实现三角形栅格化算法。
• [10 分] 正确测试点是否在三角形内。
• [10 分] 正确实现 z-buffer 算法, 将三角形按顺序画在屏幕上。
• [提高项 5 分] 用 super-sampling 处理 Anti-aliasing : 你可能会注意 到,当我们放大图像时,图像边缘会有锯齿感。我们可以用 super-sampling 来解决这个问题,即对每个像素进行 2 * 2 采样,并比较前后的结果 (这里 并不需要考虑像素与像素间的样本复用)。需要注意的点有, 对于像素内的每 一个样本都需要维护它自己的深度值,即每一个像素都需要维护一个 sample list。最后, 如果你实现正确的话,你得到的三角形不应该有不正常的黑边。
insideTriangle
// 检查点 (x, y) 是否位于三角形内部,使用向量叉积计算点与三角形三条边的方向关系
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]
Eigen::Vector3f p(x, y, 1.0f);
Eigen::Vector3f edge1 = _v[1] - _v[0];
Eigen::Vector3f edge2 = _v[2] - _v[1];
Eigen::Vector3f edge3 = _v[0] - _v[2];
Eigen::Vector3f c1 = p - _v[0];
Eigen::Vector3f c2 = p - _v[1];
Eigen::Vector3f c3 = p - _v[2];
float z1 = edge1.cross(c1).z();
float z2 = edge2.cross(c2).z();
float z3 = edge3.cross(c3).z();
// 如果点的方向一致,则在三角形内部
return (z1 > 0 && z2 > 0 && z3 > 0) || (z1 < 0 && z2 < 0 && z3 < 0);
}
rasterize_triangle
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
// 将三角形顶点转换为 4D 向量(裁剪空间坐标)
auto v = t.toVector4();
// Step 1: 计算三角形的包围盒
// 找到三角形顶点的最小和最大 x/y 值,构建包围盒
// 包围盒范围缩小了像素迭代的区域,提高光栅化效率
float minX = std::floor(std::min({v[0].x(), v[1].x(), v[2].x()}));
float maxX = std::ceil(std::max({v[0].x(), v[1].x(), v[2].x()}));
float minY = std::floor(std::min({v[0].y(), v[1].y(), v[2].y()}));
float maxY = std::ceil(std::max({v[0].y(), v[1].y(), v[2].y()}));
// Step 2: 遍历包围盒内的每个像素
// 使用整数循环确保效率,逐像素检查是否位于三角形内
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
// 使用像素中心点 (x+0.5, y+0.5) 判断是否在三角形内
if (insideTriangle(x + 0.5f, y + 0.5f, t.v)) {
// Step 3: 计算像素的重心坐标 (alpha, beta, gamma)
// 通过重心插值计算深度值和颜色值
auto [alpha, beta, gamma] = computeBarycentric2D(x + 0.5f, y + 0.5f, t.v);
// Step 4: 插值深度值 z
// w_reciprocal: 计算 w 的倒数,用于归一化重心坐标
float w_reciprocal = 1.0f / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
// 插值的 z 值,按权重计算,结合 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()) * w_reciprocal;
// Step 5: 深度测试
// 确保当前像素的 z 值比深度缓冲区中的值小
int index = get_index(x, y); // 获取像素对应的深度缓冲区索引
if (z_interpolated < depth_buf[index]) {
// 更新深度缓冲区的 z 值
depth_buf[index] = z_interpolated;
// Step 6: 绘制像素
// 构造像素位置和颜色
Vector3f point = Vector3f((float)x, (float)y, z_interpolated); // 像素位置
Vector3f color = t.getColor(); // 从三角形对象获取颜色
set_pixel(point, color); // 设置像素到帧缓冲区
}
}
}
}
}
解决三角形颠倒问题
将main.cpp文件中
r.set_projection(get_projection_matrix(45, 1, 0.1, 50));
改为
r.set_projection(get_projection_matrix(45, 1, -0.1, -50));
运行结果
放大图像后可以发现三角形边界有明显的锯齿。
附加题:MSAA Anti-aliasing
rasterizer.cpp
// 定义 MSAA 相关常量(8x MSAA)
// msaa_samples:每个像素的子采样点数量
// sample_weight:每个子采样点的权重
// sample_offsets:用于 MSAA 的子采样点偏移量(分布在 8 个不同位置)
constexpr int msaa_samples = 8; // 8x MSAA
constexpr float sample_weight = 1.0f / msaa_samples; // 每个子采样点的权重
constexpr std::array<std::pair<float, float>, msaa_samples> sample_offsets = {{
{0.125f, 0.125f}, {0.375f, 0.125f}, {0.625f, 0.125f}, {0.875f, 0.125f},
{0.125f, 0.875f}, {0.375f, 0.875f}, {0.625f, 0.875f}, {0.875f, 0.875f}
}};
//光栅化三角形
void rst::rasterizer::rasterize_triangle(const Triangle& t) {
auto v = t.toVector4();
// Step 1: 获取三角形的包围盒
float minX = std::floor(std::min({v[0].x(), v[1].x(), v[2].x()}));
float maxX = std::ceil(std::max({v[0].x(), v[1].x(), v[2].x()}));
float minY = std::floor(std::min({v[0].y(), v[1].y(), v[2].y()}));
float maxY = std::ceil(std::max({v[0].y(), v[1].y(), v[2].y()}));
// Step 2: 遍历包围盒中的每个像素
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
Vector3f final_color = Vector3f::Zero(); // 累积颜色
int covered_samples = 0; // 记录落在三角形内的采样点数
// Step 3: 遍历每个子采样点
for(size_t i = 0; i < sample_offsets.size(); ++i){
float dx = sample_offsets[i].first; // 获取第 i 个偏移点的 x 坐标
float dy = sample_offsets[i].second; // 获取第 i 个偏移点的 y 坐标
float sx = x + dx; // 子采样点的实际坐标x
float sy = y + dy; //子采样点的实际坐标y
// 子采样点的深度缓冲区索引
int depth_index = get_index(x, y) * msaa_samples + i;
if (insideTriangle(sx,sy,t.v)) {
// Step 4: 计算采样点的重心坐标
auto [alpha, beta, gamma] = computeBarycentric2D(sx, sy, t.v);
// 插值深度值
float w_reciprocal = 1.0f / (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;
// 更新深度缓冲区
if (z_interpolated < depth_buf[depth_index]) {
depth_buf[depth_index] = z_interpolated;
final_color += t.getColor(); // 累加采样点颜色
covered_samples++;
}
}
}
// Step 5: 平均采样点颜色,设置像素颜色
if (covered_samples > 0) {
final_color /= covered_samples; // 平均采样点颜色
final_color = final_color * (covered_samples / (float)msaa_samples) +
frame_buf[get_index(x, y)] * (1.0f - covered_samples / (float)msaa_samples); // 背景混合
Vector3f pixel_position = Vector3f(x, y, 0); // 像素位置
set_pixel(pixel_position, final_color); // 设置像素颜色
}
}
}
}
// 光栅化器构造函数
// 初始化帧缓冲区和深度缓冲区的大小
rst::rasterizer::rasterizer(int w, int h) : width(w), height(h)
{
// 初始化帧缓冲区
frame_buf.resize(w * h);
// 初始化 MSAA 深度缓冲区
depth_buf.resize(w * h * msaa_samples, std::numeric_limits<float>::infinity());
}
运行结果
三角形边缘相对顺滑,但是三角形交界处存在重叠情况,形成不正常的“黑边”。
黑边问题目前还未解决...