有趣的深度图:可见性问题的解法

0x00 前言

提及深度,朋友们必定都不陌生。为了解决渲染场景时哪部分可见,哪部分不可见的问题(便可见性问题,也被称为隐藏面移除问题,hidden surface removal problem,从术语这个角度看,技术的发展有时也会带动心态向积极的方向的变化),计算机图形学中常使用画家算法或深度缓冲的方式。html

这也是在处理可见性问题时的两个大方向上的思路:Object space方式和Image space方式。在后文的描述中,各位应该可以体验到这两种方式的异同。
下图就是在Unity引擎中将深度缓冲的数据保存成的图片。
QQ截图20170529165355.png算法

QQ截图20170529125238.png
而利用深度图咱们又能够实现不少有趣的视觉效果,例如一些颇有科幻感的效果等等。编程

QQ截图20170602193326.png

不过在说到这些有趣的效果以前,咱们先来看看所谓的可见性问题和深度图的由来吧。学习

0x01 人类的本能和画家算法

在计算机图形学中,有一个很重要的问题须要解决,即可见性问题。由于咱们要将一个3D模型投影到2D的平面上,这个过程当中哪些多边形是可见的,哪些是不可见的必需要正确的处理。
按照人类的天性,一个最简单的解决方案就是先绘制最远的场景,以后从远及近,依次用近处的场景覆盖远处的场景。这就比如是一个画家画画同样。
Painter's_algorithm.png
(图片来自维基百科)
而计算机图形学中的画家算法的思想即是如此:测试

  • 首先将待渲染的场景中的多边形根据深度进行排序。spa

  • 以后按照顺序进行绘制。code

这种方法一般会将不可见的部分覆盖,这样就能够解决可见性问题。
可是,世界上就怕可是二字,使用画家算法这种比较朴素的算法的确能解决简单的可见性问题,不过遇到一些特殊的状况就无能为力。例以下面这个小例子:htm

QQ截图20170529174402.png
在这个例子中,三个多边形A、B、C互相重叠,那么到底如何对它们进行排序呢?此时咱们没法肯定哪一个多边形在上,哪一个多边形在下。在这种状况下,多边形做为一个总体进行深度排序已经不靠谱了,所以必须用一些方法对这些多边形进行切分、排序。对象

咱们能够看到,这种方式是以场景中的对象或者说多边形为单位进行操做的。于是经常被称为Object space 方法或者称为Object precision 方法,我我的更喜欢后者这个称呼,由于这是一个关于操做精度的区别。这种方式主要是在对象或多边形这个级别的,即对比多边形的先后关系。除了画家算法以外,背面剔除也是Object Space的方法。它经过判断面的法线和观察者的角度来肯定哪些面须要被剔除。排序

0x02 切分多边形的Newell算法

既然做为总体互相重叠致使难以排序,那么是否能够对多边形进行切分呢?Newell算法早在1972年就已经被提出了,因此算不得是什么新东西。可是它的一些思路仍是颇有趣的,倒也值得咱们学习。

和画家算法同样,Newell算法一样会按照深度对场景内的对象进行排序并对排序后的多边形从远及近的依次绘制,不过有时会将场景内的多边形进行切割成多个多边形,以后再从新排序。

简单来讲,首先咱们能够将参与排序的结构定义为各个多边形上顶点的最大Z值和最小Z值[Zmax,Zmin]。

咱们会以多边形上距离观察者最远的顶点的Z值对场景内的多边形进行一个粗略的排序(由于此时只是依据每一个多边形距离观察者最远的那一个顶点的Z值进行排序),这样咱们就得到了一个多边形列表。

QQ截图20170601000402.png

以后,取列表中的最后一个多边形P(它的某个顶点是距离观察者最远的顶点)和P以前的一个多边形Q,以后经过对比来肯定P是否能够被写入帧缓冲区。
这个对比简单的说就是是否符合下面这个条件:

多边形P的Zmin > 多边形Q的Zmax

若是符合该条件,则P不会遮盖Q的任何部分,此时能够将P写入帧缓冲区。

tmp3189325_thumb (1).png
即使答案是否,P和Q也有可能不发生遮盖。例如它们在x、y上并没有重叠。可是,Q仍是有可能会被分割成若干个多边形{Q1,Q2...}。此时有可能会针对下面的几条测试结果,对最初的多边形列表进行从新排序(也有可能生成新的多边形,将新的多边形也归入最初的列表中)并决定渲染的顺序。

  • 多边形P和多边形Q在X轴上是否可区分?

  • 多边形P和多边形Q在Y轴上是否可区分?

  • 多边形P是否彻底在多边形Q的后方?

  • 多边形Q是否彻底在多边形P的前方?

  • 判断两个多边形的投影是否重叠?

若是这几条测试所有都没有经过,则须要对Q或P进行切割,例如将Q切割成Q一、Q2,则Q1和Q2将被插入多边形列表代替Q。

可是,咱们能够发现,这种对深度进行排序后再依次渲染的方式会使得列表中多边形的每一个点都被渲染,即使是不可见的点也会被渲染一遍。所以当场景内的多边形过多时,画家算法或Newell算法会过分的消耗计算机的资源。

0x03 有趣的Depth Buffer

正是因为画家算法存在的这些缺点,一些新的技术开始获得发展。而深度缓冲(depth buffer或z-buffer)就是这样的一种技术。Depth Buffer技术能够看做是画家算法的一个发展,不过它并不是对多边形进行深度排序,而是根据逐个像素的信息解决深度冲突的问题,而且抛弃了对于深度渲染顺序的依赖。

于是,Depth Buffer这种方式是一种典型的Image space 方法,或者被称为Image precision方法,由于这种方式的精度是像素级的,它对比的是像素/片元级别的深度信息。

QQ截图20170602192314.png

这样,除了用来保存每一个像素的颜色信息的颜色缓冲区以外,咱们还须要一个缓冲区用来保存每一个像素的深度信息,而且两个缓冲区的大小显然要一致。

图片5.png

该算法的过程并不复杂:

    • 首先,须要初始化缓冲区,颜色缓冲区每每被设置为背景色。而深度缓冲区则被设为最大深度值,例如通过投影以后,深度值每每在[0,1]之间,所以能够设置为1。

    • 通过光栅化以后,计算每一个多边形上每一个片元的Z值,并和对应位置上的深度缓冲区中的值做比较。
      若是z <= Zbufferx(即距离观察者更近),则须要同时修改两个缓冲区:将对应位置的颜色缓冲区的值修改成该片元的颜色,将对应位置的深度缓冲区的值修改成该片元的深度。即:Colorx = color; Zbufferx = z;

    下面是一个小例子的图示,固然因为没有通过标准化,所以它的各个坐标和深度值没有在[0-1]的范围内,不过这不影响:

    图片6.png
    第一个多边形,深度都为5。

    图片7.png
    第二个多边形,它的三个顶点的深度分别为二、七、7,所以通过插值,各顶点之间的片元的深度在[2-7]之间,具体如右上角。咱们还能够看到右下角是最后结果,紫色的多边形和橘色的多边形正确的互相覆盖。

    0x04 来算算顶点的深度值

    众所周知,渲染最终会将一个三维的物体投射在一个二维的屏幕上。而在渲染流水线之中,也有一个阶段是顶点着色完成以后的投影阶段。不管是透视投影仍是正交投影,最后都会借助一个标准立方体(CVV),来将3维的物体绘制在2维的屏幕上。
    咱们就先来以透视投影为例,来计算一下通过投影以后某个顶点在屏幕空间上的坐标吧。

    QQ截图20170603095703.png

    因为咱们使用左手坐标系,Z轴指向屏幕内,所以从N到F的过程当中Z值逐渐增大。依据类似三角形的知识,咱们能够求出投影以后顶点V在屏幕上的坐标。
    QQ截图20170603095916.png

    咱们能够经过一个实际的例子来计算一下投影后点的坐标,例如在一个N = 1,v的坐标为(1,0.5,1.5),则v在近裁剪面上的投影点v'的坐标为(0.666,0.333)。

    可是,投影以后顶点的Z值在哪呢?而在投影时若是没有顶点的深度信息,则两个不一样的顶点投影到同一个二维坐标上该如何断定使用哪一个顶点呢?

    QQ截图20170603100051.png
    (v1,v2投影以后都会到同一个点v')

    为了解决保存Z值的信息这个问题,透视变换借助CVV引入了伪深度(pseudodepth)的概念。

    QQ截图20170603100212.png

    即将透视视锥体内顶点的真实的Z值映射到CVV的范围内,即[0,1]这个区间内。须要注意的是,CVV是左手坐标系的,所以Z值在指向屏幕内的方向上是增大的。

    为了使投影后的z'的表达式和x’、y‘的表达式相似,这样作更易于用矩阵以及齐次坐标理论来表达投影变换,咱们都使用z来作为分母,同时为了计算方便,咱们使用一个z的线性表达式来做为分子。

    QQ截图20170603101114.png

    以后,咱们要作的就是计算出a和b的表达式。

    在CVV中处于0时,对应的是透视视锥体的近裁剪面(Near),z值为N;

    0 = (N * a + b) / N

    而CVV中1的位置,对应的是视锥体的远裁剪面(Far),z值为F;

    1 = (F * a + b) / F

    所以,咱们能够求解出a和b的值:

    a = F / (F - N)
         b = -FN / (F - N)

    有了a和b的值,咱们也就求出来视锥体中的Z值映射到CVV后的对应值。
    QQ截图20170603101914.png

    0x05 Unity中的深度

    最后来讲说Unity中的Depth,它的值在[0,1]之间,而且不是线性变化的。QQ截图20170603101720.png
    所以有时咱们须要在Shader中使用深度信息时,每每须要先将深度信息转化成线性的:

    float linearEyeDepth = LinearEyeDepth(depth);

    float linear01Depth = Linear01Depth(depth);

    咱们根据Unity场景中的深度信息渲染成一张灰度图,就获得了本文一开头的深度图。
    QQ截图20170529125238.png

    -分割线-
    最后打个广告,欢迎支持个人书《Unity 3D脚本编程》

    相关文章
    相关标签/搜索