体素化、有向距离场、光线追踪阴影分析

发布时间 2023-11-26 09:03:14作者: 吴建明wujianming

体素化、有向距离场、光线追踪阴影分析

体素化

对于任意连续的函数f(x,y,z),隐式地将体积定义为f(x,y,z)>0,表面是f(x,y,z)=0的水平集。

 

只需要一个连续的函数,任意的代数函数、有向距离场(CSG树,在网格三线性采样)、密度函数(在网格三线性采样)。

 

使用密度(Density)要容易得多(局部更改),但在距离场上的一些有用操作(如放大、缩小体积、更高质量的渐变计算)上会失败,可以将密度视为距离场,夹紧距离约为一个采样单元格。

将隐式曲面或参数化网格体素化的过程:

1、在网格上采样。

2、近似每个单元格中的表面。

3、确保表面与单元边界对齐。

体素化的理想特征是易于实现、局部独立、平滑、自适应/适合LOD、最小化三角形条形、保留锐利和薄的特征。

对于简单的立方体,在每个网格单元的中心采样f(x,y,z),在具有不同符号的单元格之间绘制一个面。

 

这种表达方式在体素化的理想特征的优劣如下表:

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

++

+

-

-

+

-

-

步进立方体(Maring Cube):在每个单元格的角落采样f(x,y,z),在三角形拓扑中使用角的符号,沿着边缘在插值的0点处定位顶点。

 

这种表达方式在体素化的理想特征的优劣如下表:

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

+

+

+

-

-

-

-

超体素(transvoxel)算法:在每个单元格的角落采样f(x,y,z),允许细分一次单元格的边(以缝合相邻的LOD级别),为三角形拓扑使用采样点的符号,沿边在插值的0点处定位顶点。

 

Transvoxel是一种允许行进立方体跨越不同LOD级别的方法。总共71种拓扑方式,用于处理任意边组合的细分。

这种表达方式在体素化的理想特征的优劣如下表:

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

+

+

+

+

-

-

-

双重轮廓(Dual Contour):在每个单元格的角落采样f(x,y,z),在每个边交叉点位置采样f′(x,y,z),在轮廓上的每个单元格内找到一个理想点,连接相邻单元格的两点(支持多个LOD分辨率)。

 

这种表达方式在体素化的理想特征的优劣如下表:

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

-

-

+

+

+

+

~

双重行进立方体(Dual Marching Cube):在精细网格上采样f(x,y,z),找出误差最小的点(QEF),如果误差 > ε,则在该点细分八叉树。重复前面的步骤,直到索引的误差 < ε。构造此八叉树的拓扑对偶(topological dual),当成双重轮廓曲面细分之。

 

这种表达方式在体素化的理想特征的优劣如下表:

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

-

-

+

+

+

+

+

立方体行进正方体(Cubical Marching Square):构建一个带有误差细分的八叉树(类似于DMC),对于任何体素(在任何八叉树级别):展开体素,分别观察每一侧;使用行进立方体创建曲线,如果出现误差,则进行细分;将两边折叠在一起,形成三角形。

 

易于实现

局部独立

平滑

自适应LOD

最小化三角形

锐利

-

+

+

+

+

+

+

Windborne的体素化概览:F(x,y,z) = 合成的、轴对齐的密度网格,每个块(chunk)都有重叠的特征列表,在计算时,使用布尔运算累积到块。

 

组合时,每个特征都是一个层,每一层要么减去,要么叠加,Alpha混合密度:αa+αb(1−αa)。

 

除了组合,还有减去、叠加、并集、轮廓指示器、网格密度(可用于动态光照,如光流、AO)、暴露参数、利用特征等等操作或应用。

17.2.6 有向距离场

有向距离场(SDF)是函数SDF(P)到P处最近表面的有符号距离,有解析距离函数体积纹理两种形式。

  • 解析距离函数流行于场景demo中,巨大的着色器,很多数学知识,没有数据。
  • 体积纹理存储距离函数,使用三线性过滤。游戏Claybook将体积纹理与mip贴图结合使用,世界SDF的分辨率=1024x1024x512,格式=8位有符号,大小=586 MB(5 mip级别),[-4,+4]体素的距离,256个值/8个体素,1/32体素精度,每mip级别最大步进(世界空间)翻倍。

Claybook在GPU上生成世界SDF的步骤:

  • 生成SDF笔刷网格。64x64x32的dispatch,4x4x4的线程组。
    • 在分块中心T处采样刷子体积。如果SDF>光栅分块边界+4个体素,则剔除。如果接受,原子添加+存储到GSM。
    • 通过GSM中的笔刷循环。细胞中心C处的样本[i],如果接受,存储到网格(线性),局部+全局原子压缩。

 

  • 生成调度坐标。64x64x32的调度,4x4x4的线程组。
    • 读取刷子网格单元。
    • 如果不是空的:原子加法(L+G)得到写索引,将单元格坐标写入缓冲区。

 

  • 生成Mip掩码。4x调度(mips),4x4x4的线程组。
    • 分组:加载1个更宽的体素网格L-1邻域,下采样1=0掩码并存储到GSM。
    • 将掩码放大1个体素(3x3x3)。
    • 掩码=0则写入网格单元坐标。
  • 在8x8x8的tile中生成0级(稀疏)。间接调度,8x8的线程组。
    • 分组:读取网格单元坐标(SV_GroupId)。
    • 从网格读取笔刷并存储到GSM。
    • 通过GSM中的笔刷循环,采样[i],执行exp平滑最小/最大操作。
    • 将体素写入WorldSDF的级别0。
  • 生成mips(稀疏)。4倍间接调度(mips),8x8的线程组。
    • 分组:加载更宽的L-1邻域的4个体素。2x2x2下采样(平均值)并在GSM中存储为123123,+-4体素带变成+-2体素阶(band)。
    • 分组:在GSM中运行3步eikonal方程(下图),扩展阶:2个体素变成4个体素。
    • 存储8x8x8邻域的中心。

球体追踪算法:

  • D = SDF(P)。
  • P += ray * D。
  • if (D < epsilon) break。

 

多层体纹理追踪:

Loop

    D = volume.SampleLevel(origin + ray*t, mip)

    t += worldDistance(D, mip)

    IF D == 1.0 -> mip += 2

    IF D <= 0.25 -> mip -= 2; D -= halfVoxel

    IF D < pixelConeWidth * t -> BREAK

// 如果曲面位于像素内边界圆锥体内,则中断,获得完美的LOD!

最后一步:球体追踪需要无限步才能收敛,假设我们碰到一个*面,三线性过滤=分段线性曲面,几何级数,使用最后两个样本,Step=D/(1−(D−D−1))Step=D/(1−(D−D−1))。

 

锥体追踪解析解:

 

粗糙锥体追踪Prepass:

 

锥形追踪可以跳过大面积的空白空间,大幅缩短步长,体积采样更多缓存局部性。Mip映射改善缓存的局部性,Log8数据缩放:100%、12.5%、1.6%、0.2%...测量(1080p渲染),访问8MB数据(512MB),99.85%的缓存命中率。存在的问题有过步进(Overstepping)、加载平衡等,它们有各自的缓解方案。

17.3 光线追踪技术

在几何光学中,可以忽略光线的波动性而直接简化成直线,从而研究光线的物理特性。同样地,在计算机图形学,也可以利用这一特点,以简化光照着色过程。

 

此外,人类的眼睛接收到的光照信息是有限的像素,大多数人的眼睛在5亿像素左右。人类接收到的图像信息可以分拆成5亿个像素,也就是说,可以分拆成5亿条非常微小的光线,以相反的方式去逆向追踪这些光线,就可以检测出这些光线对应的场景物体的信息(位置、朝向、表明材质、光照颜色和亮度等等)。

 

光线追踪技术就是利用以上的物理原理衍生出来。将眼睛抽象成摄像机,视网膜抽象成显示屏幕,5亿个像素简化成屏幕像素,从摄像机位置与屏幕的每个像素连成一条射线,去追踪这些射线与场景物体交点的光照信息。当然,实际的光线追踪算法会更加复杂,光线追踪的伪代码:

for each pixel do

    compute ray for that pixel

    for each object in scene do

        if ray intersects object and intersection is nearest so far then

            record intersection distance and object color

    set pixel color to nearest object color (if any)

 

每个像素都会发出一条光线,该算法计算出哪个物体首先被光线击中,以及光线击中物体的确切点。这个点被称为第一个交点,算法在这里做了两件事:

  • 估计交点处的入射光。要估计入射光在第一个交点处的样子,算法需要考虑该光从何处反射或折射。
  • 将入射光的信息与被击中物体的信息相结合。关于每个对象的特定信息很重要,因为对象并不都具有相同的属性——它们以不同的方式吸收、反射和折射光:
    • 不同的吸收方式导致物体具有不同的颜色(例如,叶子是绿色的,因为它吸收了除绿光以外的所有光)。
    • 不同的反射率会导致一些物体发出镜面反射,而其他物体会向各个方向散射光线。
    • 不同的折射率导致某些物体(如水)比其他物体更扭曲光线。

通常,为了估计第一交叉点处的入射光,算法必须将该光追踪到第二交叉点(因为击中物体的光可能已被另一物体反射),甚至更远。有时发出的光线不会击中任何东西,这就是第一种边缘情况,我们可以通过测量光线传播的距离来轻松覆盖,这样我们就可以对传播太远的光线进行额外的处理。第二种边缘情况涵盖了相反的情况:光线可能会反弹太多,从而减慢算法速度,或者无限次,导致无限循环。该算法追踪光线在每一步后被追踪的次数,并在一定次数的反射后终止。我们可以证明这样做是合理的,因为现实世界中的每个物体都会吸收一些光,甚至是镜子。这意味着光线每次被反射时都会失去能量(变得更弱),直到它变得太弱而无法察觉。因此,即使我们可以,追踪光线任意次数也没有意义。

与传统的光栅化渲染技术相比,光线追踪的算法过程还是比较明晰的。以视点为起点,向场景发射N条光线,然后根据碰撞点的材质进行BXDF、BRDF的运算,然后再进行漫反射、镜面反射或者折射,如此递归循环直到光线逃离场景或者到达最大反射次数,最后对N条光线进行蒙特卡洛积分即可获得结果。

 

结合上图,可以将光线追踪的算法过程抽象成以下伪代码:

遍历屏幕的每个像素 {

  创建从视点通过该像素的光线

  初始化 最近T 为 无限大,最近物体 为 空值

 

  遍历場景中的每个物体 {

     如果光线与物体相交 {

        如果交点处的 t 比 最近T 小 {

           设置 最近T 为交点的 t 值

           设置 最近物体 为该物体

        }

     }

  }

 

  如果 最近物体 为 空值{

     用背景色填充该像素

  } 否则 {

     对每個光源射出一条光线来检测是否处在阴影中

     如果表面是反射面,生成反射光,并递归

     如果表面透明,生成折射光,并递归

     使用 最近物体 和 最近T 来计算着色函数

     以着色函数的结果填充该像素

  }

}

上述伪代码中涉及的着色函数可采用任意光照模型,可以是Lambert、Phong、Blinn-Phong、BRDF、BTDF、BSDF、BSSRDF等等。若是更近一步,用计算机语言形式的伪代码描述,则光线追踪的计算过程如下:

-- 遍历图像的所有像素

function traceImage (scene):

    for each pixel (i,j) in image S = PointInPixel

         P = CameraOrigin

        d = (S - P) / || S – P||

        I(i,j) = traceRay(scene, P, d)

    end for

end function

 

-- 追踪光线

function traceRay(scene, P, d):

    (t, N, mtrl) ← scene.intersect (P, d)

    Q ← ray (P, d) evaluated at t

     I = shade(mtrl, scene, Q, N, d)

     R = reflectDirection(N, -d)

     I ← I + mtrl.kr ∗ traceRay(scene, Q, R) -- 递归追踪反射光线

   

    -- 区别进入介质的光和从介质出来的光

    if ray is entering object then

         n_i = index_of_air

         n_t = mtrl.index

    else n_i = mtrl.index

         n_i = mtrl.index

        n_t = index_of_air

    end if

   

    if (mtrl.k_t > 0 and notTIR (n_i, n_t, N, -d)) then

        T = refractDirection (n_i, n_t, N, -d)

        I ← I + mtrl.kt ∗ traceRay(scene, Q, T) -- 递归追踪折射光线

    end if

 

    return I

end function

 

-- 计算所有光源对像素的贡献量(包含阴影)

function shade(mtrl, scene, Q, N, d):

    I ← mtrl.ke + mtrl. ka * scene->Ia

     for each light source l do:

         atten = l -> distanceAttenuation( Q ) * l -> shadowAttenuation( scene, Q )

         I ← I + atten*(diffuse term + spec term)

     end for

    return I

end function

 

-- 此处只计算点光源的阴影,不适用其它类型光源的阴影

function PointLight::shadowAttenuation(scene, P)

    d = (l.position - P).normalize()

    (t, N, mtrl) ← scene.intersect(P, d)

    Q ← ray(t)

    if Q is before the light source then:

         atten = 0

    else

         atten = 1

    end if

     return atten

end function

上述distanceAttenuation的接口中,通常还涉及到BRDF的光照积分,但是在实时渲染领域,要对每个相交点做一次积分是几乎不可能的。于是可以引入蒙特卡洛积分和重要性采样(可参看《由浅入深学习PBR的原理及实现》的章节5.4.2.1 蒙特卡洛(Monte Carlo)积分和重要性采样(Importance sampling)),以局部采样估算整体光照积分。

当然,引入这个方法,如果采样数量不够多,会造成光照贡献量与实际值偏差依然会很大,形成噪点。随着采样数量的增加,局部估算越来越接近实际光照积分,噪点逐渐消失(下图)。

 

从左到右分别对应的每个象素采样为1、16、256、4096、65536。

 

在每个像素内部,可以使用偏移来生成追踪像素,从而获得更准确且带抗锯齿的渲染效果。

结合了蒙特卡罗积分和重要性采样的光线追踪技术,也被称为路径追踪(Path tracing)

17.3.1 光线追踪方式

17.3.1.1 递归光线追踪

当光线击中具有镜面反射或折射的表面时,计算那里的颜色可能需要追踪更多光线——分别称为反射光线和折射光线。这些光线可能会击中其他镜面反射表面,导致更多光线被追踪,由此有了术语——递归光线追踪(Recursive ray tracing)。下图显示了反射光线的递归“树”,这种技术也被称为经典光线追踪或惠特式光线追踪,因为它是由特纳·惠特于1980年引入的。

 

递归式的光线追踪通常在最后阶段需要一个最终收集(Final gathering)——从粗略的GI解决方案中读取辐射度(Radiosity)或光子映射。

17.3.1.2 蒙特卡洛光线追踪

蒙特卡洛光线追踪(Monte Carlo ray tracing)也称为随机光线追踪(Stochastic Ray Tracing),其中光线原点、方向或时间使用随机数计算。蒙特卡罗射线追踪通常分为两类:分布光线追踪(Distribution ray tracing)路径追踪(Path tracing)

分布光线追踪从每个曲面点向采样区域灯光、光泽和漫反射以及许多其他效果发射多条光线。下图显示了用于分布光线追踪的反射和折射光线树。如图所示,分布光线追踪在经过几次反射后,光线数量易于爆炸;为了避免这种情况,通常在几级反射后减少光线的数量。使用分布光线追踪,很容易确保反射点处光线方向的良好分布,例如通过分层方向。

 

用于分布光线追踪的反射和折射树。

路径追踪是分布光线追踪的一种变体,其中每个点仅发射一条反射和折射光线,避免了光线数量的爆炸,但简单的实现会导致非常明显噪点的图像。为了补偿这一点,通过每个像素追踪许多可见性光线。路径追踪的一个优点是,由于每个像素拍摄许多可见性光线,因此可以以很少的额外成本合并景深和运动模糊等相机效果。

另一方面,与分布光线追踪相比,更难确保反射光线的良好分布(例如通过分层)。简而言之,分布光线追踪会在光线树中向更深的位置发射最多光线,而路径追踪会发射最多可见性光线。

17.3.2 场景加速结构

光线追踪涉及的数据结构包含边界体积层次结构(Bounding Volume Hierarchy,BVH)、无栈边界体积层次结构(Stackless Bounding Volume Hierarchy,SBVH)、KD树、边界区间层次(Bounding Interval Hierarchy,BIH)等。

 

堆栈和无栈数据结构和内存布局对比图。

测试相同场景采用不同数据结构的时间曲线如下:

 

BVH优势是可以矢量化测试,更好地处理空白空间,堆栈不是瓶颈。

17.3.2.1 BVH

对于复杂场景,测试每一个对象与每一条光线的交集将是毫无希望的低效。因此,我们将对象组织成一个层次结构,以便快速拒绝大部分对象。

加速度数据结构最重要的特征是构造时间、内存使用和光线遍历时间。根据应用,可能会对这些特性中的每一个给予不同的强调。对于图像序列的渲染(例如,用于交互式视觉化或用于电影的“快照”渲染),还需要选择可以随着增量几何变化而有效更新的加速数据结构。

有一系列令人困惑的加速度数据结构:边界体积层次结构、均匀网格、层次网格、BSP树、kd树、八叉树、5D原点方向树、边界区间层次结构等。在这里,我们将仅详细描述一种加速度数据结构,即边界体积层次。

光线追踪场景使用了大量的射线检测,需要一种高效的场景加速结构。在实时光线追踪中,使用最广的的加速结构是层次包围盒(Bounding volume hierarchy,BVH)。BVH将对象及其边界体积组织成一棵树,树的根是包含整个场景的边界体积,最常用的边界体积是轴对齐框,因为这样的框易于计算和组合。

 

 

BVH树的示例。

例如,茶壶场景的BVH具有五层边界框,顶层由整个场景的单个边界框组成,下一层包含两个茶壶和正方形的边界框。每个茶壶由四部分组成:壶身、壶盖、壶柄和壶嘴,每个零件都有一个边界框,茶壶主体由八个贝塞尔面片组成,每个面片都有自己的边界框。对于曲面细分的Bezier面片,每组四边形可以有一个边界框,用于有效的光线相交测试。

可以直接使用场景建模层次,如茶壶场景示例。另一种策略是分割几何体,使每个部分的表面积近似相等。

当光线需要与场景中的对象进行交集测试时,第一步是检查与整个场景的边界框的交集。如果光线击中边界框,将测试子对象的边界框,依此类推。当到达层次结构的某个叶时,必须对该叶表示的对象进行交集测试。

这些加速度数据结构中没有一个始终比另一个更快。对于给定场景,哪一个是最佳的取决于场景特征,以及重点是快速构建、快速更新、快速光线遍历还是紧凑内存使用。

坦克世界在实现光追的部分特性(如软阴影)时,分为CPU侧和GPU侧逻辑。其中CPU侧包含两级加速结构:

  • BLAS(底层加速结构)BVH。适用于所有坦克模型,在网格加载期间构造一次并上传到GPU,网格中的硬蒙皮部分拆分为多个静态BVH,跳过软蒙皮部分。
  • TLAS(顶层加速结构)BVH。多线程,使用Intel Embree和Intel TBB,重建每帧并上传到GPU。

 

TLAS BVH(左)和BLAS BVH(右)可视化。

实时光线追踪中的基于可见性的算法和加速结构图例如下:

 

加速结构的双层加速结构,不透明(实现定义的)数据结构,高效的构建和更新:

 

RTX构建、更新、使用加速结构示意图:

 

KD-Tree

通过KD-Tree结构体可以避免栈遍历,下图是一个示例场景在拆分平面后构成的一个树形结构:

 

遍历时,通过树形结构可以快速检测到相交物体避免栈遍历:

 

提出了一种高度并行、线性可伸缩的kd树构造技术,用于动态几何的光线追踪。其使用与高性能算法(如MLRTA或截头体追踪)兼容的传统kd树,提供了卓越的构建速度,为渲染阶段保持了合理的kd树质量。该算法从每帧开始构建kd树,因此不需要运动/变形或运动约束的先验知识。对于具有200K动态三角形、1024x1024分辨率和阴影和纹理的模型,实现了7-12fps的几乎实时性能。

使用高质量的kd树对于实现交互式光线追踪性能至关重要。因此,目标是尽可能快地构建kd树,以最小化其质量退化。典型的kd树构造以自顶向下的方式进行,通过使用以下任务序列将当前节点递归地拆分为两个子节点。

1、在某些位置生成分裂平面候选。

2、在每个位置使用SAH评估成本函数。

3、选择最佳候选(成本最低),并将其拆分为两个子节点。

4、跳过几何图形,将其分配给子节点。

5、递归重复。

该文着眼于前三个阶段。在快速估计SAH期间,使用三角形AABB作为三角形的代理。成本函数是分段线性的,因此只需要在位于当前节点内的AABB边界处进行评估,这些位置也称为拆分候选位置。

在第2个步骤,对于大量几何图元,由于其积分形式,成本函数可以在离散化设置中计算。为了克服算法复杂性,使用概念上类似的技术,尽管此方法适用于大型和小型对象。不在每个容器中存储对象引用,而是用一个对象计数器替换一个可变大小的列表(或数组)。构建这样的结构需要对几何体进行单一且廉价的通道,而不是排序。

最初,针对点提出了装箱算法(鸽子洞排序、桶排序)。其思想是将1D间隔分割为给定数量的大小相等的容器,形成规则网格。对象所属的bin索引可以直接从其位置计算。使用一个单一的线性传递几何体,可以计算箱中的三角形数量,并更新箱的候选分割值(最接近箱边界),如下图所示。当一个三角形表示为一个点时,如果算法在整个三角形范围内工作,则更新该点所在的箱,或更新与该三角形重叠的每个箱。该数据随后用于非常不精确的快速SAH近似。

 

(a) 传统的装箱算法;(b)使用该算法评估SAH。

最小-最大装箱算法的思想是追踪每个三角形AABB在两组单独的装箱中的开始和结束位置(下图)。每个箱子只是一个柜台,对于每个图元的AABB,在第一个集合(AABB开始的地方)和第二个集合(AAABB结束的地方)中只更新一个bin。因此,完全消除了对容器总数的依赖。算法的这一特性对于初始聚类任务至关重要,且使用最小-最大装箱算法估计SAH。

 

(a) 最小-最大装箱算法;(b)使用该算法评估SAH。

该方法易于扩展到多线程并行构造kd树,并行运行任务需要将整个任务划分为分配给线程的较小部分(作业)。

一种简单的方法是在每个步骤中利用数据并行性。事实上,当每个线程被赋予相等数量的图元时,装箱和几何拆分过程完美地并行运行。内存管理也很简单:每个线程都有自己的上述池集,适用于大量图元。另一种方法是每个线程构建子树,需要对几何体进行某种初始分解。然而,迄今为止的初始分解是按顺序进行的,实际上,这个阶段也可使用并行解决方案。

最简单的分解是在可用线程之间均匀分布图元,如4个线程中的每个线程处理场景中1M个三角形中的250K个三角形。尽管具有良好的内存局部性,但这种几何分解具有明显的缺点,即不同线程构建的Kd树将在空间上重叠,没有已知的方法可以合并重叠的kd树,而使用光线遍历多个树会导致渲染速度减慢。空间分割而不是几何分割导致不重叠的kd树很容易合并为一棵树。常规的空间分区会导致负载平衡不良。因此,并行处理空间区域需要使用几何分布信息进行区域选择。

该文使用了混合并行化方案,对数据进行并行初始分解(聚类),以创建独立处理的作业。

 

且使用了初始聚类平衡分解:

 

使用优化后的KD-Tree,在不同的场景的加速比如下图:

 

由此可见,KD-Tree的构建实际大幅度提升,但渲染性能有所下降。

光线追踪阴影

光线追踪的第一个附加用途是阴影计算:我们可以通过追踪从点到光源的光线来确定点是否处于阴影中。如果光线沿途击中不透明对象,则该对象处于阴影中;如果没有,它将照亮。当计算不透明阴影的光线对象交点时,我们只关心命中或不命中;不是交点和法线。对于点光源和聚光灯,我们追踪曲面点和光源位置之间的光线。对于定向光源,我们沿着光的方向追踪来自表面点的平行光线。

 

(a)阴影射线;(b)带有光线追踪阴影的茶壶。

如果对象是不透明的,任何命中都足以确定阴影。但是如果物体是半透明的(例如彩色玻璃),我们需要获得点和光源之间所有相交表面的透射颜色,然后通过乘以每个颜色分量来合成透射颜色。

区域光源导致柔和阴影,完全阴影和完全照明之间的区域称为半影。软阴影可以通过向区域光源表面上的随机点发射阴影光线来计算。下图(a)显示了从三个表面点到三角形区域光源的阴影光线,一些光线击中物体;图(b)显示了熟悉的茶壶场景中的软阴影。在该图像中,光源是球形的,软阴影是通过分布光线追踪计算的。

 

(a) 将光线投影到区域光源。(b) 有柔和阴影的茶壶。

在表面和光源之间发射光线:

  • 如果光线击中任何东西,则什么都不做(区域被阴影和未照明)。
  • 如果光线到达光线而没有击中任何物体,则照亮该像素。

 

不是为每个表面点发射一条光线,而是发射多条光线。每个光线的行为与硬阴影情况相同,平均每个像素的所有光线的结果:

  • 如果所有光线都被遮挡,则表面完全被遮挡。
  • 如果所有光线到达光源,则表面将完全照亮。
  • 如果一些光线被遮挡,一些光线到达光线,则表面处于半影区域。

 

如果区域中的光源为灯光,则将光线分布在从表面可见的光源的横截面上。要使用无限远的平行光近似日光,请从表面选择一个光线锥:为了表示完全晴朗的一天,圆锥体的立体角为零;为了表示多云的日光,立体角变大。正在估计到达表面点的入射光,要获得良好的估计,样本应均匀覆盖域。

 

需要大量光线来精确采样软阴影,但此过程尽量保证GBuffer的连续性,避免多余的光线。对于大多数图像,从一个像素到其相邻像素,表面属性变化很小。因此,从G缓冲区的一个像素发送的光线很可能与从相邻像素发送的相同光线击中同一对象。当然有一种方法可以利用这个事实来减少光线计数,但保持视觉精度?

可以尝试交错采样(Interleaved Sampling),以利用来自相邻像素的阴影光线数据。在帧缓冲区上分块N2�2个光线方向的正方形2D数组,基于网格发射阴影光线,得到的图像具有临界特性,即对于图像的任何NxN区域,表示整个$数组。因此,使用方框滤波器从图像中去除噪声。每个输出像素是N2个相邻输入像素的平均值,必须处理图像中的不连续性。

 

传统的边界体积层次结构可以跳过许多光线三角形命中测试,需要在GPU上重建层次结构,对于动态对象,树遍历本身就很慢。

存储用于光线跟追踪的图元,而无需构建边界体积层次!对于阴影贴图,存储来自光源的深度,简单而连贯的查找。同样地存储图元,一个深层图元图,逐纹素存储一组正面三角形。深度图元图绘制(N x N x d)包含3个资源:

  • 图元数量图(Prim Count Map):纹理中有多少个三角形,使用一个原子来计算相交的三角形。
  • 图元索引图(Prim index Map):图元缓冲区中三角形的索引。
  • 图元缓冲区(Prim Buffer):后变换的三角形。

 

d够大吗?可视化占用率——黑色表示空的,白色表示满了,红色则超出限制,对于一个已知的模型,很容易做到这一点。

 

GS向PS输出3个顶点和SV_PrimitiveID:

 

[maxvertexcount(3)]

void Primitive_Map_GS( triangle GS_Input IN[3], uint uPrimID : SV_PrimitiveID, inout TriangleStream<PS_Input> Triangles )

{

    PS_Input O;

    [unroll]

    for( int i = 0; i < 3; ++i )

    {

        O.f3PositionWS0 = IN[0].f3PositionWS; // 3 WS Vertices of Primitive

        O.f3PositionWS1 = IN[1].f3PositionWS;

        O.f3PositionWS2 = IN[2].f3PositionWS;

        O.f4PositionCS = IN[i].f4PositionCS; // SV_Position

        O.uPrimID = uPrimID; // SV_PrimitiveID

        Triangles.Append( O );

    }

    Triangles.RestartStrip();

}

PS哈希了使用SV_PrimitiveID的绘制调用ID(着色器常量),以生成图元的索引/地址。

float Primitive_Map_PS( PS_Input IN ) : SV_TARGET

{

    // Hash draw call ID with primitive ID

    uint PrimIndex = g_DrawCallOffset + IN.uPrimID;

    // Write out the WS positions to prim buffer

    g_PrimBuffer[PrimIndex].f3PositionWS0 = IN.f3PositionWS0;    

    g_PrimBuffer[PrimIndex].f3PositionWS1 = IN.f3PositionWS1;

    g_PrimBuffer[PrimIndex].f3PositionWS2 = IN.f3PositionWS2;

    // Increment current primitive counter uint CurrentIndexCounter;

    InterlockedAdd( g_IndexCounterMap[uint2( IN.f4PositionCS.xy )], 1, CurrentIndexCounter );

    // Write out the primitive index

    g_IndexMap[uint3( IN.f4PositionCS.xy, CurrentIndexCounter)] = PrimIndex; return 0;

}

需要使用保守的光栅来捕捉所有与纹素接触的图元,可以在软件或硬件中完成。硬件保守光栅化——光栅化三角形接触的每个像素,在DirectX 12和11.3中启用:D3D12_RASTERIZER_DESC、D3D11_RASTERIZER_DESC2。

 

软件保守光栅化——使用GS在裁减空间中展开三角形,生成AABB以剪裁PS中的三角形,参见GPU Gems 2-第42章。

 

光线追踪时,计算图元坐标(与阴影贴图一样),遍历图元索引数组,对于每个索引,取一个三角形进行射线检测。

float Ray_Test( float2 MapCoord, float3 f3Origin, float3 f3Dir, out float BlockerDistance )

{

    uint uCounter = tIndexCounterMap.Load( int3( MapCoord, 0 ), int2( 0, 0 ) ).x;

    [branch]

    if( uCounter > 0 )

    {

        for( uint i = 0; i < uCounter; i++ )

        {

            uint uPrimIndex = tIndexMap.Load( int4( MapCoord, i, 0 ), int2( 0, 0 ) ).x;

            float3 v0, v1, v2;

            Load_Prim( uPrimIndex, v0, v1, v2 );

            // See “Fast, Minimum Storage Ray / Triangle Intersection“

            // by Tomas Möller & Ben Trumbore

            [branch]

            if( Ray_Hit_Triangle( f3Origin, f3Dir, v0, v1, v2, BlockerDistance ) != 0.0f )

            {

                return 1.0f;

            }

        }

    }

    return 0.0f;

}

 

左:3k x 3k的阴影图;右:3k x 3k的阴影图 + 1K x 1K x 64的PM。

为了抗锯齿,使用额外的光线可行吗?开销太大了!可使用简单技巧——应用屏幕空间AA技术(如FXAA、MLAA等)。

混合方法——将光线追踪阴影与传统的软阴影相结合,使用先进的过滤技术,如CHS或PCS,使用阻挡体距离计算lerp系数,当阻挡体距离->0时,光线追踪结果普遍存在。插值因子可视化:

 

L = saturate( BD / WSS * PHS )

L: Lerp factor

BD: Blocker distance (from ray origin)

WSS: World space scale – chosen based upon model

PHS: Desired percentage of hard shadow

FS = lerp( RTS, PCSS, L )

FS: Final shadow result

RTS: Ray traced shadow result (0 or 1)

PCSS: PCSS+ shadow result (0 to 1)

使用收缩半影过滤,否则,光线追踪结果将无法完全包含软阴影结果,将导致在两个系统之间执行lerp时出现问题。

 

效果对比:

 

不同图元复杂度的效果、消耗及性能如下:

 

目前仅限于单一光源,不能扩大到适用于整个场景,存储将成为限制因素,但最适合最接近的模型:当前的焦点模型、最近级联的内容。总之,解决传统的阴影贴图问题,AA光线追踪硬阴影的性能非常好,混合阴影结合了这两个世界的优点,无需重新编写引擎,游戏速度足够快!

在2017年,坦克世界就已经通过各种优化手段在DirectX 11及以上的图形平台实现了光线追踪阴影。他们实现了实时光线追踪物理正确的软阴影,不需要硬件RT Core,使用了用于构建BVH的Intel Embree,使得坦克世界成为第一款在D3D11中使用实时RT阴影的游戏。

 

坦克世界开启(左)和关闭(右)光线追踪软阴影的对比图。

在实现光追软阴影时,分为CPU侧和GPU侧逻辑。其中CPU侧包含两级加速结构:

  • BLAS(底层加速结构)BVH。适用于所有坦克模型,在网格加载期间构造一次并上传到GPU,网格中的硬蒙皮部分拆分为多个静态BVH,跳过软蒙皮部分。
  • TLAS(顶层加速结构)BVH。多线程,使用Intel Embree和Intel TBB,重建每帧并上传到GPU。

CPU BVH占CPU帧时间的2.5%,使用TBB线程,SSE 4.2(比原始WoT内部BVH builder快5.5倍),每帧更新高达约5mb的GPU数据,高达72mb的静态GPU数据。下图是CPU侧的各个阶段消耗:

 

GPU侧执行像素着色或计算着色:

  • 基于均匀锥分布的时间射线抖动。
  • BVH遍历和射线三角形交点。
  • 时间积累。
  • 降噪器(基于SVGF)。
  • 时间抗锯齿(TAA)。

下图是GPU侧的各个阶段消耗:

坦克世界对光追阴影进行了优化:RT阴影只能由坦克投射,不支持alpha测试的几何体,BLAS使用LOD,每像素只发射1根射线。如果出现以下情形之一,则不追踪光线的像素:

  • NdotL <= 0。
  • 如果像素已被阴影贴图遮挡。
  • 距离摄像机300米以上。

利用此法实现的实时光追阴影的性能参数如下:

 

Northlight Engine实现的光追阴影和常规的Shadow Map阴影对比如下:

 

1080p上的每像素单根光线只耗费小于4ms,下图是每像素单根光线的局部放大图:

 

Claybook使用了软阴影球体追踪,用柔和的半影扩大阴影,沿光线步进SDF近似最大圆锥体覆盖率,Demoscene圆锥体覆盖近似:

c = min(c, light_size * SDF(P) / time);

 

并且对软阴影进行了改进,即三角测量最近距离,Demoscene=单个样本(最小),三角测量cur和prev样本,更少条带。抖动阴影光线,UE4时间累积,隐藏剩余的带状瑕疵,较宽的内半影。

 

改进前后对比:

 

以往的LTC并不能处理遮挡的光照,但更真实的光影应该具备:

 

之前有文献提出了仅光追的软阴影,做法是平均可见性:

 

但如果使用BRDF获得直接光,再乘以光追的平均可见性的软阴影,将得到错误的结果:

 

正确的做法应该如下图右边所示:

 

也可以采用随机化的方式,但必须强制BRDF的所有项都是随机化的:

 

随机化的结果是过多噪点和过于模糊:

所以仅光追的软阴影和完全随机化的两种方案都将获得错误或不良的结果。正确的软阴影算法应该如下所示:

 

从数学上讲,我们可以看到事情显然是正确的:a⋅b/a=b:

 

对应的正确随机化公式:

 

更加准确的方法推导如下:


正确降噪的各个频率的函数如下:

 

降噪图例:

 

在采样方面,使用了多重要性采样:

 

对于电介质(非金属),使用了电解质多重要性采样:

 

最终效果对比:

 

渲染通道和流程如下:

 

总之,比率估计器:无噪声有偏分析+无偏噪声随机,作为稳健噪声估计的总变化(非方差),由分析着色驱动的阴影多重要性采样。实时光线追踪GPU的注意事项包含活动状态、延迟和占用率、多重要性采样的分支、波前与内联,混合的光线+光栅图形示例。