500行C++代码实现软件渲染器 - 3.隐藏面消隐(Z缓冲区)

4 篇文章 0 订阅
4 篇文章 0 订阅

引入

您好,我来介绍一下我的朋友z缓冲区,一个黑色的伙计。 他将帮助我们避免上一课中隐藏面移除的视觉效果。

顺便说一句,我想提一下,我在课程中大量使用的这个模型是由Vidar Rapp创建的。 他授予了我使用许可,以便我可以教授关于渲染的基础知识。虽然我对它进行了破坏,但我保证我会把眼睛还给那个人。

好吧,回到主题,理论上我们可以绘制所有三角形而不丢弃任何一个。 如果我们正确地从后到前开始绘制三角形,前面将擦除后面。,这被称为画家算法。 不幸的是,它伴随很高的计算成本:对于每一次摄像机的运动,我们需要对所有场景进行重新排序。 而且还有动态场景,复杂程度就更不必说了......这甚至还不是主要问题。 主要问题是我们并不总是能够确定正确的顺序。

我们尝试渲染一个简单的场景

想象一下由三个三角形组成的简单场景:摄像机从上到下看,我们将彩色三角形投影到白色屏幕上:

渲染结果应该如下所示:

蓝色小平面 - 它是红色背后还是前方呢?可见, 画家的算法在这里不起作用。 可以将蓝色小平面分成两个(一个在红色小平面前面,一个在后面)。 然后红色前面的那个被分成两个 - 一个在绿色三角形前面,一个在后面......我想你可能意识到了问题:在有数百万个三角形的场景中,计算起来真的代价很高。 可以使用BSP树来解决它。 顺便说一下,这个数据结构对于移动相机来说是不变的,但它确实非常混乱。 生命太短暂,不能让它变得凌乱。

简化问题:丢弃一个维度。Y缓冲区。

让我们暂时丢弃一个维度并沿着黄色平面切割上面的场景:

我的意思是,现在我们的场景由三个线段组成(黄色平面和每个三角形的交线),最终渲染具有正常宽度但是1个像素高度:

与往常一样,提交代码在此。 我们的场景是二维的,所以使用我们在第一课中编写的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()函数仅在图像的第一行中写入。

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);

所以,我声明了一个数组ybuffer,尺寸为(width,1)。 该数组初始化为负无穷大。 然后我调用rasterize()函数,并将数组和图像render作为参数。这个函数如下:

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坐标。然后我检查了ybuffer数组中序号为x的值。如果当前y值比ybuffer中的值更接近相机,那么我将绘制当前的像素,并更新对应ybuffer的值。

让我们一步步来看。在第一个红色线段调用rasterlize()之后,我们的内存是这样的。

屏幕:

ybuffer:

在这里,品红色表示无穷小,那些堤防表示我们无法触及的屏幕。其他的表示为灰色:灰度值比较低的堤防更接近相机,灰度值更高的地方离相机远。

然后我们绘制绿色线段。

屏幕:

ybuffer:

最后绘制蓝色线段。

屏幕:

ybuffer:

恭喜,我们在一维屏幕上绘制了二维场景。让我们再次欣赏我们的渲染结果:

回到三维

对于在二维屏幕上绘制三维场景,我们需要一个二维的Z缓冲区z-buffer。

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

我将二维缓冲区打包成一维的,转换方式也是极简单的:

int idx = x + y*width;

z缓冲序号到x、y坐标的转换如下:

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);
            }
        }
    }
}

我们对上一课的源代码进行了少量更改,以丢弃隐藏的部分。 这是渲染,效果是多么显著啊!

源代码在这里。

我们只是插入了Z值,还有什么可以做的吗?

答案是添加纹理! 这将是我们的家庭作业。

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

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

这是我期望你渲染出来的效果:

 

感谢原作者Dmitry V. Sokolov的授权,原文链接:https://github.com/ssloy/tinyrenderer/wiki/Lesson-3:-Hidden-faces-removal-(z-buffer)

 

 

 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
gcc-c -7.3.1-5.fc27.aarch64.rpm 是一个用于在 Fedora 27 操作系统上提供 gcc 编译器的软件包。aarch64 表示此软件包适用于基于 ARM 架构的 64 位操作系统。gcc 是 GNU Compiler Collection 的缩写,它是一套包含编译器、链接器和库的工具集,用于开发各种编程语言的软件。gcc-c 是一个特定版本的 gcc 编译器的 C/C++ 开发库和头文件的软件包。 在 Fedora 27 上安装 gcc-c -7.3.1-5.fc27.aarch64.rpm,可以享受到 gcc 编译器和与之相关的开发库和头文件的功能。这对于开发者来说非常重要,因为 C/C++ 是常用的编程语言,用于开发各种应用和系统软件。使用 gcc 编译器,开发者可以编译和构建他们的 C/C++ 代码,并将其转换为可执程序或库文件。开发人员还可以使用 gcc 提供的各种选项和优化来改进代码的性能和效率。 安装这个软件包还为开发者提供了许多额外的功能,如调试工具,性能分析工具,静态代码分析工具等。这些工具可以帮助开发者定位和修复代码中的错误和性能瓶颈。同时,软件包还包含了一些其他工具,如 make 和 autotools,它们可以帮助开发者更有效地管理和构建他们的项目。 总之,gcc-c -7.3.1-5.fc27.aarch64.rpm 是一个在 Fedora 27 上提供 gcc 编译器及其相关开发库和头文件的软件包。它为开发者提供了一系列强大的工具和功能,帮助他们编译、构建和调试 C/C++ 代码。这对于开发各种应用和系统软件的开发者来说非常有价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值