https://haqr.eu/tinyrenderer/z-buffer/

Painter's algorithm (画家算法)

不丢弃任何一个三角形,将其全部绘制出来。不过按照从后到前的顺序来绘制,那么前面的三角形会覆盖掉后面的三角形。这种技术被称为“画家算法”。

缺点

  1. 计算成本非常高,每次都要排序

  2. 结果的正确性无法得到保证,例如三角形彼此重叠

    painter.svg

(其实解决的方案很简单,我们没有必要按照三角形排序,直接单像素处理就可以了。

Depth Interpolation (深度插值)

遍历所有的三角形,然后涂色每个三角形中的所有像素。由于这些三角形没有经过正确的排序,因此会出现视觉上的瑕疵。叫深度插值的原因是只有三角形的顶点有深度值 (z),三角形内的需要使用上一节学到的重心坐标的方式插值。

for each triangle t:
    for each pixel p that t covers:
        compute its depth z
        update the depth buffer with z
        paint the pixel

我先直接绘制插值了的深度(不做额外处理),这里很简单,就只是额外创建一幅 ppm 图,然后里面使用重心坐标插值的 z 作为颜色就可以。观察左图可以看到 “混乱” 的深度图。本应该越近越亮,说明了我们前后关系的错误 (例如尾巴的正面在腿前面)。

// Random color to framebuffer image
set_pixel(framebuffer, x, y, color);

// Depth interplotion to zbuffer image
depth := (alpha * a.z + beta * b.z + gamma * c.z) / 255.0;
set_pixel(zbuffer, x, y, Vector4.{depth, depth, depth, 1});

040_depth.png

040_color.png

BUG 小插曲:意外得到的 故障效果。

我计算三角形面积的时候乘 0.5 的时候忘了加括号。导致AABB 光栅化三角形时候出错。

image.png

bug.png

接下来实现正确的 z-buffer,不过首先给我们自己写的 ppm.jai 加一个 get_pixel 返回 [x, y] 位置的像素颜色。这非常简单。

PPM_Image :: struct {
    width:  int;
    height: int;
    pixels: [] Vector4;
}

get_pixel :: (image: *PPM_Image, x: int, y: int) -> Vector4 {
		// 我这里分开写 assert,如果不小心越界了,是编译期错误,
		// 我可以清楚知道到底是哪一个,到底是怎么越界的。
    assert(x >= 0);
    assert(x < image.width);
    assert(y >= 0);
    assert(y < image.height);

    index := y * image.width + x;
    return image.pixels[index];
}

041_depth.png

041_color.png

Z-Buffer 的实现非常简单,只需要做一个判断。我这里的 Z 是颜色,值越大深度图颜色越浅 (因为 0xFFFFFF 是白色, 0x000000 是黑色嘛,RGB 什么光都没有,就黑啦,白光就是所有频率混合一起(老实说,身体,或者说大脑的这种视觉处理方式挺有趣的,就像是水是无味道一样。))。

draw_triangle :: (framebuffer: *PPM_Image, zbuffer: *PPM_Image, a: Position, b: Position, c: Position, color : Vector4) {
    aabb := get_aabb(a, b, c);

    abc_area := signed_triangle_area(a, b, c);
    if abc_area == 0 return;

    for y: aabb.y_min .. aabb.y_max-1{
        for x: aabb.x_min .. aabb.x_max-1 {
            p := Position.{x, y, 0};
            alpha := signed_triangle_area(p, a, b) / abc_area;
            beta  := signed_triangle_area(p, b, c) / abc_area;
            gamma := signed_triangle_area(p, c, a) / abc_area;

            // negative barycentric coordinate => the pixel is outside the triangle
            if alpha < 0 || beta < 0 || gamma < 0 {
                continue;
            }

            z := (alpha * a.z + beta * b.z + gamma * c.z) / 255.0; // Depth interplotion
            if z <= get_pixel(zbuffer, x, y).z continue;
            set_pixel(zbuffer, x, y, Vector4.{z, z, z, 1});
            set_pixel(framebuffer, x, y, color);
        }
    }
}