乐趣区

关于前端:十天自制软渲染器DAY-04Zbuffering

如果你喜爱我的文章,心愿点赞???? 珍藏 ???? 评论 ???? 三连一下,谢谢你,这对我真的很重要!


在第三天的学习中,咱们学会了如何利用 重心坐标 算法画三角形,并使用 三角形绘制算法 把人头模型画了进去。尽管最初的渲染后果能看进去这是个脑袋,然而嘴巴处有很显著的穿帮。这一天咱们就学习一下,如何利用 Z-buffering(深度缓冲)来解决层叠问题。

本文源码 ????:toyRenderer-day04-Z-buffering

1. 画家算法

在正式解说 Z-buffering 问题之前,咱们先来理解一下画家算法。这个算法的思维极其简略,咱们能够联合下图简略剖析一下:

如果要画一个有山有草有树林的风景画,一个初学者画家能够按以下绘制程序画画:

  • 首先画 最远 处的山
  • 而后画 次远 处的草原
  • 最初画 最近 的树木

或者咱们用更程序员的形式形容一下:

  • 首先画 z-index=1 处的山
  • 而后画 z-index=2 处的草原
  • 最初画 z-index=3 的树木

在古代支流的 UI 渲染引擎中,各个元素的先后层级程序基本上都是用「画家算法」这种思路决定的:

  • 网页通过 CSS 的 z-index 管制层级程序
  • iOS 通过 layer.zPosition 管制层级程序
  • Android 通过 index 管制层级程序

平时画 UI 时,咱们能够简略粗犷的把各个 View 了解为一个一个的二维盒子,每个盒子在 z 轴上都是相互独立的,这样咱们就能够不便的用 z-index 动态控制盒子的层级;然而在渲染三维物体时,三维模型在 z 轴上是间断的,并且三维模型间还会相互组合交织,这种通过 z-index 管制层级的计划很难见效。

举个最简略的例子,下图中三个相互交织的三角形, 应用 z-index 是无奈辨别层级的,更不要说绘制了:

注:Newell 算法能够解决多边形重叠导致排序艰难的问题,感兴趣的同学能够自行查阅学习

为了解决这个问题,2020 年取得「图灵奖」的计算机图形学大佬——艾德文·卡特姆,提出了一个驰名的算法——Z-buffering。

2.Z-buffering

Z-buffering,中文名又为「深度图」「深度缓冲」,它是通过记录比拟 每个像素 的深度信息来解决层级问题。

Z-buffering 算法了解起来其实是十分直观的,咱们这里借用《虎书 4》里的一张插图(能够关注????️号「卤蛋实验室」后盾回复「图形学」支付本书)来解说一下 Z-buffering 的工作原理。

首先咱们假如要在一个 8*8 的屏幕上渲染两个相互遮挡的三角形,咱们在正式渲染前先开拓一块儿 8*8 的二维内存空间,这个空间的默认值均为 -∞

假如咱们已知两个三角形的每个像素的深度信息,红三角形的深度均为 5,紫三角形的深度区间为 [3, 8]。

咱们先遍历红色三角形的所有像素,和 Z-buffering 的默认值 -∞ 比拟,哪个值大,就保留哪个值。通过第一轮比拟后,咱们就记录了红色三角形的深度信息。

而后咱们遍历紫色三角形的所有像素。和最新的 Z-buffering 逐像素比拟,哪个值大,就保留哪个值。第二轮比拟后咱们就又记录了紫色三角形的深度信息。

最初咱们就失去了一份 深度缓冲,它记录了这张图片的层级程序,最终渲染时咱们按这个深度缓冲逐像素渲染三角形即可。

下面的思路写成伪代码就是这样的:

// 首先假如深度默认值都是负无穷 -∞(这里能够是无穷大,也能够是无穷小,依坐标系而定)
for (each triangle T)              // 遍历每个三角形
   for (each sample (x,y,z) in T)  // 遍历三角形里的每个像素
        if (z > zbuffer[x,y])        // 如果深度大于已有的值,framebuffer[x,y] = rgb;  // 则更新色彩,zbuffer[x,y] = z;        // 并更新 zbuffer
        else
            // do nothing            // 小于已有的值,就阐明这个像素点被遮挡不须要绘制了

3. 代码实现

了解了下面的伪代码,现成真正的代码就很容易了。

首先咱们定义一下 Z-buffering 的数据结构。按情理来说,咱们间接定义成一个二维数组是最合乎渲染场景的,第一维示意 ,第二维示意

// [[1, 2, 3],
//  [4, 5, 6],
//  [7, 8, 9]]

然而咱们并不需要这样写,咱们能够把二维数组拍平,而后通过 偏移量 进行拜访(能够联想一下 循环队列 最大堆 这两种数据结构的底层实现):

// [[1, 2, 3],       [1, 2, 3,
//  [4, 5, 6],   =>   4, 5, 6,
//  [7, 8, 9]]        7, 8, 9],

定义好构造后,咱们给 Z-buffering 的每个子元素都赋上 -∞ 的默认值:

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

for (int i=0; i < width * height; i++) {zbuffer[i] = -std::numeric_limits<float>::max();}

最初把下面的伪代码翻译为失常的 cpp 代码就能够了:

//......

Vec3f P;
for (P.x = boxmin.x; P.x <= boxmax.x; P.x++) {for (P.y = boxmin.y; P.y <= boxmax.y; P.y++) {Vec3f bc_screen = barycentric(pts, P); // bc 是 Barycentric Coordinates 的缩写

        //......
        
        // 计算以后像素的 zbuffer
        P.z = 0;
        for (int i = 0; i < 3; i++) {P.z += pts[i][2] * bc_screen[i];
        }
        
        // 更新总的 zbuffer 并绘制
        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-buffering 计算后,咱们渲染的模型就齐全失常了:

相应的,如果把 Z-buffering 渲染为一张图,则是上面这样的:

集体认为 Z-buffering 的概念还是很简略的,实践理解分明后代码很容易写进去。在理论利用中,Z-buffering 其实还有很多的问题,例如因为精度问题引起的 z-fighting,相应的也有一些解决方案。因为本系列教程指标只是构建一个 最小性能 的软渲染器,这些绝对深刻的问题就不探讨了,感兴趣的同学能够自行搜寻学习。


如果你喜爱我的文章,心愿点赞???? 珍藏 ???? 在看 ???? 三连一下,谢谢你,这对我真的很重要!

欢送大家关注我的微信公众号:卤蛋实验室,目前专一前端技术,对图形学也有一些渺小钻研。

原文链接 ???? day04-Z-buffering:更新更及时,浏览体验更佳

退出移动版