【Cyril Crassin 2011】Interactive Indirect Illumination Using Voxel Cone Tracing

今天要介绍的是Cyril Crassin在2011年提出的实时全局光计算方案——Interactive Indirect Illumination Using Voxel Cone Tracing,这是一套业界非常出名的全局光实施方案,《怪物史莱克2》,UE4等渲染所使用的全局光照都是以此为基础展开。

这个方案在业界的名字是 Sparse Voxel Octree Global Illumination,简称为SVOGI。

GTX 480,25~70 fps,2-bounce

先给出文章开篇展示的效果图,从性能与表现来看还是非常吸引人的。

为了方便后续查阅,这里先对算法的基本逻辑做一个总结,其整体步骤可以总结为:

  1. 对场景进行Cell(体素)化
    1. Cell化的场景以稀疏化的方式存储在GPU上
      1. 八叉树结构存储
    2. 场景分成静态跟动态两部分,静态物件只构建一次,动态每帧构建
  2. 光照数据搜集
    1. 基于光源的光照方向,对场景进行光栅化,之后对光栅化中的每个像素进行光照数据的计算,并将这个数据转换到对应的前面Cell化后的八叉树的叶子节点上
      1. 光照数据包括Radiance跟Direction
    2. 对叶子节点进行混合,得到其父节点的光照数据,并不断递归,直到最高层
      1. Direction混合的结果是一个Gaussian-Lobes形式的数据结构
      2. 混合过程可以并行完成
  3. GI计算
    1. 基于当前待计算的屏幕像素的相关数据,向着四周发射光锥,并采集对应的数据
      1. 光锥的半径受像素属性如粗糙度的影响
    2. Diffuse跟Specular关注点不一样,因此使用的Cone Tracing逻辑也有所不同,会分开两套来计算

SVOGI的特点为:

  1. 优点
    1. 实现简单,且体素LOD的思路使得这个算法能够做到实时计算
  2. 缺点
    1. 难以找到符合需要的分辨率:过低的Cell分辨率会导致漏光,过高则会导致显存占用过大的问题
    2. 稀疏八叉树的数据结构对于GPU不是太友好

1. Abstract

由于场景的高复杂度以及BRDF计算的高额消耗,全局光照或者说间接光照实现的方案多采用离线或者混合式(将一些静态的信息在离线提取出来,在运行时用于完成对动态信息的计算)算法实现。此文给出了一种纯实时的全局光照计算方案,可以解放预计算导致的复杂编辑流程。整套算法是建立在一套体素八叉树(octree)结构之上的,体素数据的创建与更新都是在运行时根据常规的模型mesh(triangle数据)计算完成的,与这套结构相配套的还有一套可以用于实现对任意像素位置的可见性与能量数据进行快速计算评估的近似voxel cone tracing(简称VCT,下同)方案。

此文给出的VCT方案可以在可交互帧率(25~70 fps)下实现两次反射(2-bounce)的间接光计算,其计算的消耗基本上与场景复杂度无关,且由于可交互的八叉树体素化方案的使用,还可以实现对带有动态物体的复杂场景的间接光计算输出。此外,这套方案还可以用于对AO进行快速计算上面。

2. Introduction

全局光照效果好,但是消耗高,而消耗高的原因在于如下两点:

  1. 需要计算场景中任意两点之间的可见性,而这在基于光栅化的渲染路径下是非常困难的
  2. 需要计算出被渲染点各个方向的颜色输入数据
    基于这些原因,目前绝大部分全局光照方案都是离线的,而完美的离线方案在电影等特殊行业上可以得到不错的表现,但是却不适用于游戏等交互性强的实现上,而游戏行业常用的预计算方案效果表现比较一般。

本文给出了一种在GPU上实时完成的全局光照方案,此方案的优点在于不需要额外的预计算过程,可以给出高频光照效果(高光反射等),通过两次bounce完成,且不会出现噪声或者在时间上效果不连续等问题。这个方案的性能消耗不受场景复杂度影响,能够在极其复杂的场景中做到实时渲染表现。

此方案的核心在于一个预先filtered的层级voxel结构,这个层级结构是以一种动态稀疏voxel八叉树的形式存储在GPU上的,而这个八叉树则是从mesh数据中构建得到的。

这个结构会被用于估计任意两点之间的可见性,以及通过Voxel Cone Tracing算法累计从各个方向上过来的输入光线。

基于一个新的实时mesh体素化算法,这个方案能够很好的处理全动态的场景:整个八叉树结构的构建过程中,静态物件只进行一次,而动态物件数据更新则是每帧实时完成的。

3. Previous Work

全局光实现有很多方案,包括离线的,实时的,五花八门,这些方案都有着这样那样的缺陷,无法给出一种实时且令人满意的效果。

本文给出的方案虽然不如屏幕空间的全局光照方案,但相应的也就没有那么多假设条件,且能够得到更好的表现。

Algorithm Overview

Algorithm Overview

如上图所示,整个算法大致可以分成三步:

  1. 从光源出发,将当前voxel所接收到的输入radiance以及direction烘焙到八叉树对应的叶子节点中,这个过程是通过从各个光源出发,对场景进行光栅化来完成的。

在这个光栅化过程中,对于光源而言可见的mesh surface pixel,就会触发一次光照数据的计算与更新。

  1. 对八叉树节点中的irradiance以及direction数据进行滤波处理(filtering),这个滤波过程完成数据从叶子节点向更高层节点的混合,光照方向的混合结果用一个Gaussian-Lobes形式的数据结构来表达,这个过程是在屏幕空间中通过对四叉树的分析并行完成的,执行效率非常高。

这里有一个问题,前面第一步我们保存的是radiance,而第二步滤波则是针对irradiance的,为什么会发生变化?
radiance表示的是单位面积单位固体角接收到的光通量,这个通常在计算特定方向的输入光照时使用;而irradiance指的是单位面积接收到的光通量,移除了单位固体角的限制,指的是来自四面八方的输入光的累加效果,我们要求取全局光照,需要的自然是irradiance了。

整个滤波过程还会对BRDF以及NDF(Normal Distribution Function)进行处理,这个处理过程是view-dependent的,为什么要做成View-dependent?全局光不应该是无论能否看到入射点,都能够被玩家感知的数据吗?
BRDF本身就是会随着观察角度的变化而变化的,因此view-dependent符合需要

  1. 使用voxel cone tracing算法计算相机射线与场景的交点处的全局光(从前面的数据中获取到BRDF所需要的Diffuse&Specular数据)

对于diffuse与specular需要分开处理:
diffuse只需要少数几个cone(大概五个左右)就能够得到不错的效果
specular只需要根据视线方向与法线方向求取反射方向,在反射方向上用一个半径(此半径通常会根据Phong Lighting Model中的高光指数参数求得)比较小的cone就能够得到高光反射效果了。

4. Our hierarchical voxel structure

场景稀疏体素八叉树表示

出于实现效率的考虑,场景会以稀疏体素八叉树的形式进行储。使用这种层级结构对场景数据进行存储可以避免与实际的geometry mesh打交道,从而得到一个与场景复杂度无关的性能表现;此外,还可以根据需要调整实现的精度,比如近景处调高层级得到更优表现。

按照这种做法,可以根据视角以及光源配置对体素分辨率进行调整,从而在保证结果平滑的前提下实现实时渲染,而不会出现如Photon Mapping或者Path-Tracing方案中的采样不足导致的低质感。

4.1. Structure description

八叉树的根节点表示的是整个场景的boundingboxbox,其子节点则对应的是1/8的场景内容,以此类推。

GPU Representation
整个稀疏八叉树是基于指针实现的,八叉树节点在GPU中会被存储在一段线性空间中(连续空间),且节点是按照2x2x2作为一个tile group来进行组织的,通过这种组织关系,可以很方便的将某个指针赋予2x2x2中的每一个子节点,此外,每个节点会包含一个指向一个small voxel volume(这个在这里被称为brick)的指针,brick内容是存放在texture memory中的,且用于模拟此节点所对应的场景section内容。

按照这种组织逻辑,哪些空节点可以直接收缩来降低内存使用,且由于我们是使用贴图来存储数据,而非将数据直接写在节点中的,因此可以使用贴图的硬件三线性滤波插值来实现数据的插值与过滤。

由于场景中绝大部分volume都是空白的没有数据的,因此在对场景进行体素化的时候使用小尺寸的brick效率会更高,而数据的存储有两种模式,一种是存储中心点的数据,如下图右边小图所示,另一个则是存储顶点处的数据,这两种模式在数据存储量上区别不大,不过考虑到节点内的数据插值,使用顶点存储模式的会更占优势,因为使用中心点数据存储的话,要想实现brick内部的数据插值,还得引入相邻其他brick的数据,需要更多的数据存储空间,性能更差。

数据表达模式

4.2. Interactive voxel hierarchy construction and dynamic updates

在光照弹射计算中,前面提到的稀疏voxel结构会取代实际的场景数据的作用。为了能够实时完成对任意场景的稀疏八叉树构建,这里给出了一个自研的算法,这个算法会充分利用GPU的光栅化管线的能力来完成八叉树的构建与数据的混合处理。

为了能够做到实时,就需要做一些处理,简单来说就是跳过使用完整的regular grid中间数据来构建八叉树的步骤,这里直接使用triangle mesh场景完成八叉树构建,此外由于场景中绝大部分内容都是静态的,无需每帧更新的,只有少量物件(称为半静态物件)在交互的时候进行更新,以及一些动态物件就需要每帧更新。

半静态物件与动态物件会被单独存储在额外的一个八叉树结构中,以实现遍历与过滤。为了避免缺漏或者重复更新,这里还需要给出一个时间戳机制用于标注两种物件的类型以及其他关键信息。

整个八叉树构建算法可以分成两步:

  1. 八叉树构建
  2. 八叉树Mipmaping处理

4.2.1. Octree building

首先,这里会调用GPU的光栅化管线来构建八叉树。具体来说,由于场景是处于三维空间的,因此这里会需要对每个维度调用一次光栅化处理,光栅化时候的RT分辨率与最高层级(网格最密)的八叉树节点数目一致(比如最高层级是512x512x512个节点,那么每次光栅化时候的RT分辨率就为512x512)。此外,为了避免数据由于遮挡而被剔除,光栅化过程需要关闭深度测试。最终的属性(颜色法线材质等)写入是通过PS完成的,在PS执行时,会按照从上到下的顺序对八叉树进行遍历,对每个被mesh覆盖的叶子节点都会触发一个PS线程。

另外需要注意的是,传统光栅化实现逻辑中,判断一个三角形是否需要覆盖一个像素,是判断像素的中心是否在三角形内部,这里GI的光栅化逻辑则不太一样,更为保守:即只要覆盖到像素的任何区域都会认为三角形有覆盖到这个像素,都会创建对应的voxel。

在从上往下遍历细分的时候,当发现需要某个非叶子节点被mesh覆盖时,就会触发细分流程,将此节点往下细分成2x2x2个子节点,节点的数据都是存储在一个全局的共享node buffer(这个buffer会预先在显存中分配完成)中的,细分流程会触发在此buffer中的空间分配,分配完成后,第一个子节点的地址会被写入当前被细分的节点的child指针成员中。由于整个处理过程是通过多线程完成的,且可能会出现多个线程同时处理某个节点的细分的情况,因此为了避免冲突,这里的一些计算需要采用原子操作来完成。

为了避免过于昂贵的线程等待逻辑,这里采用了一种叫做"global thread list"的机制,当线程处理的过程中发现冲突时,就会将这些线程塞入到这个list中,等待其他所有线程处理完成之后,再启用一遍deferred pass在VS中对这个list中的线程进行重新运行,而这个过程可能还是会继续出现冲突线程,再按照之前的逻辑做同样的处理,直到global thread list为空为止。

在线程处理的过程中,各个叶子节点的属性数据是直接写入到与节点相关的brick中的,而brick的分配过程与节点的分配过程是类似的,不同的是,brick是分配在共享brick buffer中的。

这个部分的具体实现是借助于OpenGL的两个扩展来完成的:NV_shader_buffer_load 以及 NV_shader_buffer_store。

4.2.2 Dynamic update

动态物件由于需要每帧变化,因此其在八叉树中的数据也需要实时更新,整个更新逻辑与上面描述的静态物件的光栅化处理逻辑是相同的,唯一的区别在于,动态物件光栅化后输出的voxel数据不能也不应该覆盖此前已有的静态物件的相关数据,而为了避免覆盖,同时也是为了实现快速擦除,这些数据是被存储在buffer的末尾的(怎么做到避免覆盖与快速擦除的?通过索引指定动态数据存放位置)。

4.2.3 MIP-mapping

完成叶子节点相关属性的计算之后,下一步要做的就是根据叶子节点,通过插值与混合来得到非叶子节点的相关属性数据,而由于前面说过,这里是采用顶点数据存储方式的,因此在混合的时候,要考虑到顶点在插值中出现的频率,在平均计算过程中需要乘上顶点对应的权重,如下图所示:

顶点数据权重

上图给的是各个顶点相对于同级的其他顶点权重之间的倍率,而对于不同级的顶点,其权重是遵循高斯函数规律的,顶点的权重构成一个3x3的高斯filter kernel。

4.3 Voxel representation

下面来介绍一下voxel中的数据表达方式。

为了能够更好的表达方向信息,这里对于如法线以及光照方向等数据是使用一个分布函数而非单个数值来进行表达,而为了降低分布函数的内存消耗,这里直接使用了一个各向同性的高斯函数(lobe,高斯函数以角度为自变量,以长度为因变量,在极坐标曲线下就是一个lobe),这个高斯函数可以通过一个平均的方向D以及一个标准差\sigma来表示,与[Tok05]一致,为了简化插值,这里进一步用方向的长度来表示方差:

\sigma^2 = \frac{1- |D|}{|D|}

后面会进一步介绍光照数据是如何与这些数据进行交互的。

这里还需要存储用于表达光线可见性的遮挡数据,出于存储占用的考虑,这个数据就直接用一个平均数值(浮点数)来表达了,而这种表达方式的弊端在于缺少view-dependency info,从而对于一些大尺寸的稀薄物体而言,可能会导致瑕疵(拉远之后,一平均就没了)。材质颜色数据则是以提前预乘了透明度的颜色信息(alpha pre-multiplied)来表达,而为了将数据的可见度代入考虑,法线也同样需要预乘透明度数据。

5. Approximate Voxel Cone Tracing

全局光照之所以消耗高,在于需要对大量射线与场景进行相交测试,而实际上这些射线在空间上与方向上并不是完全无关的,很多方法比如packet ray-tracing正是利用这些射线之间的coherence实现性能的提升。同样,这里受[CNLE09]的启发,给出了一种voxel cone tracing的方法来利用这些射线之间的coherence实现性能加速。

原始VCT方案同样过于复杂且消耗高,这里尝试使用之前的filtered voxel结构来近似逼近VCT的结果,而在这种结构框架下,可以同时对处于同一个bundle中的多条射线的处理。

Voxel-based VCT

如上图所示,基本的做法就是沿着椎体的轴线往前采样,根据采样点所在位置的椎体半径,采取不同LOD级别的数据,为了保证数据的冰花,这里采用的是quadrilinera插值,使用这种方法可以大大加速VCT采样的过程,虽然在结果上看可能与真正的Cone Tracing会存在轻微差异。

在沿着轴线前进采样的过程中,这里使用的是[Max95, EHK*04]中的经典的emission-absorption光学模型来进行数据累加。即在这个过程中需要记录遮挡信息\alpha以及表示当前点打向椎体原点的光照的颜色的c。在每次采样中,都会从八叉树中得到对应的场景信息以及遮挡信息\alpha_2,并据此计算出一个新的出射颜色值c_2(这个过程会在后面详细介绍)。之后按照volumetric中经典的从前向后累加算法对前面的两个参数进行更新:
c:=\alpha \cdot c + (1-\alpha)\alpha_2 c_2 \\ \alpha := \alpha + (1-\alpha)\alpha_2

为了得到较好的整合质量,这里即使对于大尺寸(张角大)的椎体,两个连续采样点之间的距离d^{'}与当前voxel的尺寸d将保证不会重合,为了照顾到小步长情况,这里还添加了一个矫正(每太看懂,目的是?):
\alpha_s^{'} = 1-(1-\alpha_s)^{\frac{d^{'}}{d}}

6. Ambient Occlusion

为了对算法实现过程进行更为直观的描述,这里先给出了此算法应用的一个简单场景:AO。

某点p的AO信息用A(p)来表示,指的是点p上法线方向上半球面上各个方向的可见性的积分:
A(p) = \frac{1}{\pi}\int_{\Omega}V(p,\omega)cos(\omega)d\omega
V(p,\omega)指的是p点发射的方向为\omega的射线上的可见性,0跟1分别表示可见与不可见,通常来说,在实际情况中会给定一定长度作为半球的半径来进行可见性计算,但是这其实是一种近似模拟,正确的做法还是沿着射线继续前进直到发生碰撞,而这条射线的遮挡值\alpha则是一个碰撞点距离射线原点距离的函数f(r),这里使用:
f(r) = \frac{1}{1+\lambda r}
在这个公式作用下,修改后的遮挡值就变成了:
\alpha_f(p+r\omega) := f(r)\alpha(p+r\omega)

少数voxel cones下的AO计算

为了实现对AO积分的快速计算,这里可以将整个半球上的积分拆分成多个cone的积分:

A(p) = \frac{1}{N} \int_{i=1}^NV_c(p, \Omega_i) \\ V_c(p, \Omega_i) = \int_{\Omega_i}V_{p, \theta} \cdot cos\theta d\theta

如果将cos\theta从每个cone的积分中拆出来的话(这种做法只会对张角较大的cone或者接近于与法线垂直的cone有较大影响,通常情况下,对于整体AO的影响其实不是特别明显),剩下的V部分就可以通过对八叉树数据进行采样后用f(r)进行加权累计即可。

Final Rendering
AO的计算通常会放在PS中进行(如果考虑性能影响的话,放在VS中是否可以接受?),而为了进一步降低消耗,这个过程通常放在后处理(需要depth+normal,normal可以从depth重建)中进行,从而避免mesh overdraw导致的浪费。

7. Voxel Shading

AO只需要处理遮挡信息,但是全局光则需要处理更多的数据,比如颜色,方向等。[Fou92, HSRG07]等方案中表明,假设数据是按照lobe shapes保存的话,这些数据的计算可以通过转换成卷积计算来完成。在这里,我们需要处理的就是BRDF的卷积,NDF的卷积以及View Cone Span(这个是什么意思?)的卷积,而第一步要做的就是将数据用高斯lobe的方式进行表达。

比如这里,我们将Phong BRDF看成是一个大尺寸的diffuse lobe加上一个可以表达为高斯lobe的specular lobe,实际上,这里的光照模式可以轻易扩展到任意的lobe形式的BRDF。前面说过,NDF可以根据[Tok05]给出的方法从平均法线的长度中推导出来(\sigma_n^2= \frac{1-|N|}{|N|})。如下图所示,从视锥原点指向各个voxel的方向分布与从各个方向按照相同的视锥角指向voxel的方向分布是相同的,根据这个观察结果,我们可以用一个高斯lobe来表示视锥中的方向分布(什么逻辑?),这个高斯分布的标准差\sigma_v = cos(\Psi),其中\Psi指的是视锥的张角。

方向distribution

8. Indirect Illumination

间接光照处理比AO复杂,这里总共可以简化成两步处理过程:

  1. 从各个叶子节点上对光源进行capture,这个过程会将incoming数据存储在叶子节点中(存储incoming数据而非outgoing数据可以实现光滑表面的模拟(为什么outgoing不能用于模拟光滑表面?)),之后对叶子节点的数据进行filter处理用于完成非叶子节点数据的填充
  2. 通过voxel cone tracing来完成光线的传播的模拟

将incoming数据写入到叶子节点这个过程比较复杂,这里先略过,后面再详述。

8.1. Two-bounce indirect illumination

这里出于举例方便,使用的BRDF是Phong,但实际上此方案是用于任意的高低频BRDF。整个算法的实施步骤跟前面第6节中关于AO的实施步骤类似。

这里同样使用的是deferred shading(后处理)逻辑来确认哪些位置需要计算间接光,对于这些需要计算的位置点,这里会通过将数个cones的查询结果gathering起来来得到最终的结果。

通常来说,对于Phong BRDF而言,我们需要少数几个cones(通常是5)来评估diffuse部分,另外还需要一个小角度的cone来评估specular部分,specular cone的张角与高光公式中的指数相关。

8.2. Capturing Direct Illumination

这里来补上前面没有介绍的incoming数据的存储模式,整个算法是受[DS05]的启发设计出来的。

这里,会从光源视角,使用标准的光栅化算法来输出世界坐标,这个过程输出的每个像素就对应于一个在场景中bounce的photon,这个pass输出的贴图,我们称之为light-view map。

接下来,我们希望将这些photon数据按照direction distribution以及一个与光源视角下此像素的张角(subtended angle)成正比的energy的形式存储到八叉树中,这个过程是通过PS来完成的,由于light-view map的分辨率通常要高于最密的八叉树level,这里每个photon都可以被直接关联到八叉树的一个叶子节点上。此外,由于photon都是被写入到八叉树的最精细level中,因此在这一步还可以通过对empty节点进行折叠来得到稀疏八叉树结构。另外,由于可能出现多个photon对应同一个叶子节点的情况,因此这里的计算需要依赖原子操作。

整个计算过程遭遇了两个问题:

  1. 只有整数贴图才支持原子操作,这里直接使用了16位的归一化整数贴图格式进行数据存储,在使用的时候再对数据进行整数到浮点数的转换
  2. 相邻brick在边缘位置存在数据重复,这是前面为了插值方便将数据从voxel center移动到顶点上导致,是必要的,但是会导致数据在这些区域的写入上存在大量的冲突,极大的影响了程序执行的性能,因此需要一套更有效的计算模式。

Value Transfer to Neighboring Bricks
为了叙述方便,这里直接假设八叉树是完整的,而非稀疏的,在这种情况下,八叉树的每个叶子节点都对应于一个计算线程。

数据传输示意图

整个计算过程分成六个pass来完成,xyz每条轴对应两个pass,以x轴为例,在第一个pass中,每个线程会将当前节点右侧边界上的voxel数据累加到右侧节点左侧边界的voxel中,之后再第二个pass,则会将当前节点左侧边界的voxel数据赋值给左侧节点右侧边界的voxel中,经过这两步之后,在x方向上的边界上相邻节点的数据就一致了。将这个过程对于yz方向也进行一遍,在所有方向上边界数据的一致性就得到了保证。

由于我们可以通过相邻节点的指针对相邻节点的数据进行快速访问,因此这个算法的执行效率非常高,且不会存在线程冲突(甚至不需要原子操作)。

Distribution over levels
这一步主要是完成非叶子节点的数据填充,一个最简单的做法是为每个非叶子节点分配一个线程,之后在这个线程中取用对应的叶子节点的数据进行混合后输出。但这种做法有如下两种弊端:

  1. 某个节点可能被多个父节点共享,会导致此节点上的计算过程存在重复
  2. 不同线程的负载可能是不均衡的

这里设计了一个3 pass的算法来解决这两个问题,这个算法的特点是各个线程的负载十分均衡,且每个子节点的数据都只被处理了一次:

  1. 先计算voxel中心位置的父节点,如上图右图中黄色标记中,由于中心位置对应的子节点是完整的,因此需要对3x3x3个子节点的数据进行累加
  2. 再计算face中心(也就是2D表示中的edge中心)位置的父节点,如上图右图中的蓝色标记中,在不考虑相邻brick的情况下,此时父节点对应的子节点为2x3x3,对这些节点的数据进行累加即可
  3. 再计算corner位置的父节点,如上图右图中的绿色标记中,此时对应的子节点2x2x2个子节点

经过上述三个pass之后,这个层级的节点的布局方式就跟最底层子节点层级的布局完全一样了,后续level的计算就可以按照相同的方式进行累加得到,但是我们知道,face中心与corner位置的父节点存储的数据是不完整的,这时候还需要采用前面提到的“Value Transfer to Neighboring Bricks”方案进行补全。

Sparse Octree
由于前面为了叙述方便,假设八叉树是完整的,但实际上我们的八叉树是稀疏的。此外,我们前面的计算方法需要过多的线程资源:即使这些节点不包含任何photon,也需要分配对应的线程。由于直接光照只影响到一小部分场景,因此跳过非光照区域的处理就非常关键。这些问题的一个解决方案是为light-view map中的每个像素分配一个线程,之后查找到这个像素对应的节点,这种做法的结果是正确的,但是如果多个线程都对应于同一个节点的话(在高层次节点上,这种情况会非常常见),那么就会导致计算被重复多次。

这里给出的解决方案是2D node map,这个map是从light-view map中推导出来的,实际上是一个MIP-map金字塔。最低级(最高分辨率)的node map存储的是3D叶子节点(这些叶子节点包含了light-view map所对应的photon数据)的索引。更高级别的node-map像素存储的则是上一级别所有节点中的最底层共同祖先(如下图所示)

node-map示意图

在这种算法框架下,最低级MIP-map依然需要为每个像素分配一个线程,而后续级别的计算则是以判别是否与前面其他同级节点具有相同的祖先开始,当发现具有相同祖先,此节点就不再需要计算,直接共享前面同级节点的计算结果即可,通过这种方式来避免重复计算。根据测试,这种策略可以得到比前面计算框架至少两倍的速度。

9. Anisotropic voxels for improved cone-tracing

到目前为止,基本框架已经介绍的差不多了,不过在这个框架下依然会有一些质量问题。

第一个问题是two red-green wall问题,如下图所示:

two red-green wall

当两个不透明像素具有不同的颜色(比如红与绿)时,在混合到上层voxel的时候,就会使得结果呈现一种半透的异常效果,同样的问题还发生在不透明度Opacity的累加上面,比如不透明voxel与全透明voxel混合,会得到半透voxel,而这种情况会导致漏光问题。

为了得到更好的整合效果,这里提出了一种各向异性的Pre-integration方案,这个方案是在MIP-mapping构建过程中实施的。这里不同于之前每个voxel只存储一个无方向的数据,需要存储6个channel的数据,分别对应于6个方向(\pm x, \pm y, \pm z)。每个channel的数据是通过对此方向上的(两个)voxel数据沿着深度方向进行拷贝赋值,之后对这四个数据进行平均而求得(见上图描述)。在运行时,voxel的数值是通过对3个距离视线方向最近的方向的数值进行插值得到。

之所以选择这种方案而非更复杂的方案,是因为这种方案可以进行插值,且能够很容易实现数据在brick结构上的存储。此外,方向表达方案只需要用于那些在上一个八叉树层级上不存在的voxel上(即上一层级的子节点中存在空节点),因此存储所有属性的方向数据只是使得内存消耗增长到了1.5x而已。

10. Results and Discussion

系统硬件配置: NVIDIA GTX 480,Intel Core 2 Duo E6850 CPU

Sponza场景表现

在Sponza场景中,面片数为280K,八叉树分辨率是512x512x512,共计9级;屏幕分辨率为512x512。

时间消耗(ms)

上表给出了各个步骤的具体时间消耗,这些步骤分别为: Mesh rasterisation, Direct lighting, Indirect diffuse, Total direct+indirect diffuse, indirect specular, direct+indirect diffuse and specular

除了这些消耗之外,还有一项是动态物件的八叉树更新消耗,测试数据为16K面片的模型,其每帧更新消耗为5.5ms,而首次创建八叉树的时间消耗为280ms。光照注入与混合filter工作只在光照发生变化时执行,总共花费为16ms。

在Sponza场景中,如果不考虑Specular Cone的话,动态光源+动态场景能够达到30FPS,而加上Specular Cone,帧率就降到了20FPS(分辨率为512x512),如果将分辨率升到1024x768,帧率还会进一步下降到11.4FPS。

这里还比对了本文介绍的方法与当下执行效率最高的Light Propagation Volume(LPV)方法的实施效率跟输出效果,以Metal-Ray效果为比较基准,而不管在什么情况,本文介绍的方法不论是实施效率还是输出效果都要优于LPV。

本文方法的一个不足在于内存占用较高,在使用了稀疏结构的情况下,大致还需要1024MB的显存占用(虽然这个结果比LPV方案所需的内存要低)。

AO效果对比

这里对本文方案在AO实施效果上与Path-Tracing方案的比对,由于每个像素只分配了三个粗糙的cones,因此精度上会有所不足。下表给出了这个方案在不同的椎体张角下的时间消耗:

AO时间消耗(ms)

11. 总结

11.1 方案的特点

优点

  1. 在一些高配机型上,可以做到实时

不足

  1. 对于一些大型的,动态的场景,处理起来会有困难(性能)

参考文献

[1]【Siggraph 2012】The Technology Behind the “Unreal Engine 4 Elemental demo”
[2]. Voxel Cone Tracing and Sparse Voxel Octree for Real-time Global Illumination
[3]. Interactive Indirect Illumination Using Voxel Cone Tracing
[4]. Voxel-based Global Illumination in Wicked Engine

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容