[TinyRenderer] Lesson 3 深度缓冲(隐藏面移除)

翻译

原文地址
英语好的直接看原项目

1 简介

你好,让我给你介绍一下我的一个黑人朋友 z-buffer 。 他将帮助我们剔除上一课中隐藏面孔的视觉瑕疵。
在这里插入图片描述
顺便提一下,我在课程中大量使用的这个模型是由 Vidar Rapp 创建的。 他好心地允许我使用它来教授渲染基础知识,我破坏了它,但我保证你会把眼睛复原。
好吧,回到主题,理论上我们可以画出所有的三角形而不丢弃任何一个。 如果我们正确地从后到前开始,前面的刻面将擦除后面的刻面。 它被称为画家算法。 不幸的是,它伴随着高计算成本:对于每个摄像机移动,我们需要重新排序所有场景。 然后是动态场景……这甚至不是主要问题。 主要问题是并不总是可以确定正确的顺序。

2 让我们试图去渲染一个简单的场景

想象一个由三个三角形组成的简单场景:相机从上到下看,我们将彩色三角形投影到白色屏幕上:
在这里插入图片描述
渲染应如下所示:
在这里插入图片描述
蓝色面 - 它是在红色面的后面还是前面? 画家的算法在这里不起作用。 可以将蓝色面一分为二(一个在红色面前面,一个在后面)。 然后红色前面的那个被分成两部分——一个在绿色三角形前面,一个在后面……我想你明白了:在有数百万个三角形的场景中,计算起来真的很消耗资源。 可以使用 BSP 树 来完成它。 顺便说一句,这个数据结构对于移动相机来说是不变的,但它真的很乱。 而生命太短暂,不能让它乱七八糟。

3 更简单:让我们去掉一个维度。 Y缓冲区!

让我们暂时失去一个维度,然后沿着黄色平面切割上面的场景:
在这里插入图片描述
我的意思是,现在我们的场景由三个线段组成(黄色平面和每个三角形的交点),最终渲染的宽度正常,但高度为 1 像素:
在这里插入图片描述
与往常一样,有一个 commit 可用。 我们的场景是二维的,所以很容易使用我们在第一课中编写的 line() 函数来绘制它。

    { // just dumping the 2d scene (yay we have enough dimensions!)
        TGAImage scene(width, height, TGAImage::RGB);

        // scene "2d mesh"
        line(Vec2i(20, 34),   Vec2i(744, 400), scene, red);
        line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
        line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);

        // screen line
        line(Vec2i(10, 10), Vec2i(790, 10), scene, white);

        scene.flip_vertically(); // i want to have the origin at the left bottom corner of the image
        scene.write_tga_file("scene.tga");
    }

如果我们从侧面看,这就是我们的 2D 场景的样子:

在这里插入图片描述
让我们渲染它。 回想一下,渲染是 1 像素高度。 在我的源代码中,我创建了 16 像素高的图像,以便在高分辨率屏幕上阅读。 rasterize() 函数只写入图像的第一行 render

        TGAImage render(width, 16, TGAImage::RGB);
        int ybuffer[width];
        for (int i=0; i<width; i++) {
            ybuffer[i] = std::numeric_limits<int>::min();
        }
        rasterize(Vec2i(20, 34),   Vec2i(744, 400), render, red,   ybuffer);
        rasterize(Vec2i(120, 434), Vec2i(444, 400), render, green, ybuffer);
        rasterize(Vec2i(330, 463), Vec2i(594, 200), render, blue,  ybuffer);

所以,我声明了一个尺寸为 (width, 1) 的魔法数组 ybuffer。 该数组初始化为负无穷大。 然后我用这个数组和图像 render 作为参数调用 rasterize() 函数。 功能如何?

void rasterize(Vec2i p0, Vec2i p1, TGAImage &image, TGAColor color, int ybuffer[]) {
    if (p0.x>p1.x) {
        std::swap(p0, p1);
    }
    for (int x=p0.x; x<=p1.x; x++) {
        float t = (x-p0.x)/(float)(p1.x-p0.x);
        int y = p0.y*(1.-t) + p1.y*t;
        if (ybuffer[x]<y) {
            ybuffer[x] = y;
            image.set(x, 0, color);
        }
    }
}

这非常非常简单:我遍历 p0.x 和 p1.x 之间的所有 x 坐标并计算相应的段的 y 坐标。 然后我用当前的 x 索引检查我们在数组 ybuffer 中得到了什么。 如果当前 y 值比 ybuffer 中的值更接近相机,那么我将其绘制在屏幕上并更新 ybuffer
让我们一步一步来看看。 在第一个(红色)段上调用 rasterize() 后,这是我们的存储:
在这里插入图片描述
这里洋红色表示负无穷大,这些是与我们没有触摸的屏幕相对应的地方。 其余的都以灰色阴影显示:清晰的颜色靠近相机,深色远离相机。

然后我们绘制绿色部分。

在这里插入图片描述
最后是蓝色
在这里插入图片描述
恭喜,我们刚刚在 1D 屏幕上绘制了 2D 场景! 让我们再次欣赏渲染:
在这里插入图片描述

4 回到三维场景

因此,为了在 2D 屏幕上绘图,z 缓冲区必须是二维的:

int *zbuffer = new int[width*height];

将二维缓冲区打包成一维,转换很简单:

int idx = x + y*width;

和后面的:

int x = idx % width;
int y = idx / width;

然后在代码中,我简单地遍历所有三角形并使用当前三角形和对 z 缓冲区的引用调用光栅化器函数。

唯一的困难是如何计算我们要绘制的像素的 z 值。
让我们回顾一下我们如何计算 y 缓冲区示例中的 y 值:

        int y = p0.y*(1.-t) + p1.y*t;

t 变量的性质是什么? 原来 (1-t, t) 是该点的重心坐标
(x,y) 相对于 p0, p1 段:(x,y) = p0*(1-t) + p1*t。
所以想法是采用三角形光栅化的重心坐标版本,对于我们想要绘制的每个像素,只需将其重心坐标乘以我们光栅化的三角形顶点的 z 值:

triangle(screen_coords, float *zbuffer, image, TGAColor(intensity*255, intensity*255, intensity*255, 255));

[...]

void triangle(Vec3f *pts, float *zbuffer, TGAImage &image, TGAColor color) {
    Vec2f bboxmin( std::numeric_limits<float>::max(),  std::numeric_limits<float>::max());
    Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
    Vec2f clamp(image.get_width()-1, image.get_height()-1);
    for (int i=0; i<3; i++) {
        for (int j=0; j<2; j++) {
            bboxmin[j] = std::max(0.f,      std::min(bboxmin[j], pts[i][j]));
            bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
        }
    }
    Vec3f P;
    for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
        for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
            Vec3f bc_screen  = barycentric(pts[0], pts[1], pts[2], P);
            if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
            P.z = 0;
            for (int i=0; i<3; i++) P.z += pts[i][2]*bc_screen[i];
            if (zbuffer[int(P.x+P.y*width)]<P.z) {
                zbuffer[int(P.x+P.y*width)] = P.z;
                image.set(P.x, P.y, color);
            }
        }
    }
}

我们从上一课中对源代码进行了如此小的更改就可以移除隐藏的部分,这真是太棒了!
这是渲染的结果:
在这里插入图片描述

源代码可以在 这里 找到。

5 好的,我们只是插值了 z 值。 我们还能做什么?

纹理! 这将是我们的课后作业。
在 .obj 文件中,我们有以“vt u v”开头的行,它们给出了一个纹理坐标数组。
刻面线中间(斜线之间)的数字“f x/x/x x/x/x x/x/x”是这个三角形的这个顶点的纹理坐标。 将其插入三角形内,乘以纹理图像的宽度-高度,您将获得要放入渲染的颜色。

可以在 这里 获取漫反射纹理。

这是我对您的期望的示例:

在这里插入图片描述

实操效果(后补)

个人碎碎念

图形虽然难了一点,但它确实是兴趣,有了兴趣,就可以慢慢积累了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值