第十五章 全局光照

  • 渲染管线主要形式:
    1. 光栅化:高效、易行,但却牺牲了渲染质量,难以处理软阴影、镜面反射、间接光照等全局现象
    2. 光线追踪:法善于解决上述困难,而其代价则是渲染时间的成倍提升

光线投射

  • 在渲染一幅画面的过程中,屏幕上任意一个点所对应的颜色应该来自于场景中的哪一个物体?从人眼 (摄像机) 向屏幕上的点连一条射线,该射线在场景中击中的第一个物体将决定该点的颜色

  • 发出射线、击中物体的过程,也就是所谓的光线投射。算法流程:

    1. 对于每一个像素点 (x, y),从观察者的眼睛发出一条穿过 (x, y) 的光线
    2. 找出这条光线与场景中的物体第一次发生相交的位置
    3. 将这个交点与每一个光源相连,分别形成一根 shadow ray(这根光线也可能被障碍物遮挡)
    4. 通过 shadow ray 和我们一开始发出的光线,我们得到了入射方向和出射方向,结合交点的几何信息,我们也已知了法线方向,从而可以运用之前介绍过的着色模型计算出交点的颜色
    5. 将算出的颜色作为像素点 (x, y) 的颜色写入

image.png

  • 利用光线投射法来渲染场景的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Raycast ()
/∗ generate a picture ∗/
for each pixel x , y
color ( pixel ) = Trace ( ray_through_pixel (x , y ))

Trace ( ray )
/∗ fire a ray , return RGB radiance of light traveling backward along it ∗/
object_point = Closest_intersection ( ray )
if object_point
return Shade( object_point , ray )
else
return Background_Color

Closest_intersection ( ray )
for each surface in scene
calc_intersection ( ray , surface )
return the closest point of intersection to viewer
( also return other info about that point , e . g . , surface normal , material properties , etc . )

Shade( point , ray )
/∗ return radiance of light leaving point in opposite of ray direction ∗/
calculate surface normal vector
use Phong illumination formula ( or something similar )to calculate contributions of each light source

光线追踪

  • 光线追踪就是在光线投射的基础上,进一步考虑光线的折射和反射,找出这个过程中与物体的每一个交点

  • 然后分别计算颜色,加权相加写入到对应的像素当中

  • 在光线追踪算法中,我们只对可以反射和折射的材质进行递归处理,这些材质的颜色的权重将由其反射率和折射率决定;而当遇到其他材质时递归就会停止,此时它们的颜色将获得全部的权重

image.png

  • 光线追踪的伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Trace ( ray )
/∗ fire a ray , return RGB radiance of light traveling backward along it ∗/
object_point = Closest_intersection ( ray )
if object_point
return Shade( object_point , ray )
else
return Background_Color

Shade( point , ray )
/∗ return radiance along ray ∗/
radiance = black ; /∗ initialize color vector ∗/
for each light source
shadow_ray = calc_shadow_ray( point , light )
if !in_shadow(shadow_ray , light )
radiance += phong_illumination ( point , ray , l i g h t )
if material is specularly reflective
radiance += spec_reflectance ∗ Trace ( reflected_ray ( point , ray ))
if material is specularly transmissive
radiance += spec_transmittance ∗ Trace ( refracted_ray ( point , ray ) )
return radiance

数学: 射线与几何体相交

  • 在光线追踪算法中,我们需要反复求解光线与物体的交点

  • 首先,我们将光线定义为一条从原点发出,沿着某个方向延展的射线,可以用如下方程表示:o 表示光线的原点,d 表示光线射出方向的单位向量,这样我们只要将这个方程与表示物体表面的方程联立,就可以解出交点

image.png

球面

  • 设球心的位置为 c,球面的半径为 R,那么球面上任意一点 p 的位置满足方程:

image.png

  • 与光线方程联立,我们就可以得到交点满足:

image.png

  • 们可以将这个式子展开得到一个一元二次方程组:at2 + bt + c = 0,其中 a =d|²,b = 2(o − c) · d,c = |o − c|² − R2,而最后解出交点:

image.png

  • 这里 b² − 4ac < 0、b² − 4ac = 0 以及 b² − 4ac > 0 分别对应光线与球面相离、相切和相割的三种情况

长方体

  • 为了简化光线与物体的求交计算,通常会对物体创建一个规则的几何外形将其包围

  • 其中,最常见的创建包围盒的方法就是 AABB(axis-aligned bounding box),即轴对称包围盒,它要求我们构建的长方体每条边都平行于一个坐标平面,且恰好能将物体包围

  • Slabs method解决光线与长方体求交的问题:

    1. 首先不失一般性地考虑长方体中平行于 xy 平面的一对表面,设表面所在平面的方程分别为 z = z1 和 z = z2
    2. 接着结合我们的光线方程我们可以得到光线与这两个平面相交时满足:
      image.png
    3. 解出 t1 和 t2 后,取其中的较小值记为 tnear,较大值记为 tfar,那么我们就可以得到光线在这两个平面内时 t 的取值为 [tnear, tfar]
    4. 同样的,我们可以求出光线在另外两对平面内时 t 的取值范围
    5. 如果这三个取值范围有交集的话,那就说明光线在 t 取这个交集范围之内时同时处在这三对平面之间,也就是出现在了长方体内,此时就可以判定光线与长方体相交
  • 判断三个取值范围是否有交集是不难做到的,我们只要取三组 tnear 的最大值和三组 tfar 的最小值进行比较,如果前者小于后者就说明存在交集,从而说明了光线会与长方体相交

三角面片

  • Möller Trumbore Algorithm算法:
    1. 设三角形三个顶点的坐标分别为 V0、V1 和 V2,对于三角形上的任意一点 T(u, v),它满足:
      image.png
    2. 这里的 (u, v) 为三角形的重心坐标,满足 u ≥ 0,v ≥ 0 和 u + v ≤ 1,与光线方程联立,我们可以得到交点满足:
      image.png
    3. 用矩阵的形式表示上式我们可以得到:
      image.png
    4. 我们设 E1 = V1 − V0,E2 = V2 − V0 和 T = O − V0,运用克拉默法则可以解得:令 P = D × E2 和 Q = T × E1,通过这些变量代换,计算效率将得到提高
      image.png
    5. 算出了 t, u, v 后,就可以通过判断是否满足 0 ≤ t < ∞ 来得知交点是否在光线上,以及是否满足 u ≥ 0,v ≥ 0 和 u + v ≤ 1 来得知交点是否在三角形内

加速结构

  • 虽然光线追踪算法可以为我们提供高质量的渲染结果,但是渲染时间会成倍地增加.其中最耗费时间的是需要不断地去求光线与物体的交点

网格化

  • 网格化是指将空间切分为等大的体素。对于每个体素,我们会记录在体素内存在顶点的所有图元

  • 这样在光线追踪时,我们可以从光线原点开始,由近及远地依次查询光线是否与穿过的体素内的图元相交

  • 如果在某个体素内发生相交,则只需找出其中最近的交点返回即可

  • 缺点:网格化在作为加速结构时对于网格的大小往往会有比较高的要求,同时当场景中的物体分布比较不均匀时表现也会比较差

层次包围盒

image.png

  • 层次包围盒(Bounding Volume Hierarchies,BVH)是一种基于图元(即构成场景的基本元素,如三角形、球面等)划分的空间索引结构

  • 实现层次包围盒时我们会构建一棵层次包围盒树,图元被存储在叶节点上,每一个节点都存储了一个包围了其所有子节点内图元的包围盒

  • 每一个图元虽然在整个结构中只会出现一次.但是同一块空间区域可能会被多个节点所包含

  • 由于我们构建的层次包围盒树是一棵二叉树,在这棵二叉树中,所有的内部节点都有两个子节点,所以当有 n 个叶节点时,我们构建的层次包围盒树会有 n − 1 个内部节点,其消耗的内存是确定的

  • 层次包围盒的优点:

    1. 层次包围盒相比于 KD 树构建效率更高,但是遍历效率略低
    2. 层次包围盒相较于KD 树拥有更好的数值鲁棒性,更不容易因为浮点数舍入误差而导致弃真(实际相交,但是计算结果为不相交)情况的出现

层次包围盒的构建

  • 层次包围盒的构建大致可以分为两步:

    1. 计算每个图元的包围盒(如 AABB 包围盒)和质心位置并存储
    2. 按照划分策略构建树索引结构
  • 整个构建过程是递归的:

    1. 每次递归都会处理一组图元,当图元数量小于某一阈值时(叶节点允许的最大图元数),递归就会终止,返回一个叶节点
    2. 不然,我们就需要考虑将图元按照一定的策略划分为两部分,形成两个子节点,并递归地对这两部分进行处理
  • 最重要的问题就是如何对图元进行划分.这个策略应该尽可能减少划分后两部分包围盒重叠的体积。因为重叠的体积越大,光线穿过重叠区域的可能性也越大,遍历两棵子树的可能性就越高,计算消耗也越多

  • 为此,我们一般会在图元跨度最大的坐标轴上进行划分.图元的跨度可以用图元的包围盒或图元的质心位置来进行衡量

  • 两种简单的划分策略:

    1. 根据坐标轴跨度的中点进行划分,将位于中点左侧的图元划分到左节点,而将位于中点右侧的图元划分到右节点
    2. 进行等量划分,将最左边的一半图元划分到左节点,剩下的一半则划分到右节点
  • 但是这两种划分策略在图元分布不均匀的时候会得到很差的结果

  • 基于表面积的启发式评估划分方法(Surface AreaHeuristic,SAH):通过对求交代价和遍历代价进行评估,给出了每一种划分的代价,而我们的目的便是去寻找代价最小的划分

    1. 假设当前节点中存在 n 个物体,设对每个物体求交的代价为 t(i),如果不做划分依次求交的总代价为:
      image.png
    2. 如果我们将他们分为 2 组,这两组物体分别有包围盒 A 和包围盒 B.设光线击中它们的概率分别为 p(A) 和 p(B),设 ttrav 为遍历这层树结构(即判断是否会与包围盒 A 和包围盒B 相交)的时间开销,那么划分后对每个物体求交的代价就变为:
      image.png
    3. 假设对于每个物体求交的代价都近乎相同,所以设对于任意 i 有 t(i) = 1,又考虑到遍历树结构的开销会低于与物体求交的代价,所以设 ttrav = 0.125。再设包围盒 A 中的图元数量为 a,包围盒 B 中的图元数量为 b,我们就可以得到:
      image.png
    4. 最后我们需要对 p(A) 和 p(B) 进行估计,值得注意的是,由于包围盒 A 和包围盒 B 可能有重叠,同时它们也可能并不会占据当前节点包围盒的全部空间,所以 p(A) + p(B) 并不一定严格等于 1.但是,当包围盒的表面积越大时,它也就越有可能被光线击中,所以我们从这一点出发,取子包围盒表面积与父包围盒表面积的比值来估计 p(A) 和 p(B),从而有:
      image.png
    5. 设包围盒 A 和包围盒 B 以及当前节点的包围盒的表面积分别为 S(A),S(B)和 S(C).而 SAH 就是选择这其中代价最小的划分作为最终划分
  • 在实际应用中会将当前节点的包围盒空间沿着跨度最长的坐标轴的方向均匀地划分为若干个桶,考虑的划分只可能出现在桶与桶之间

image.png

  • 这样,如果桶的数量为 n,那么可能的划分就只有n − 1 种

  • 以按照图元质心的位置将它们分别放入到对应的桶中,接着就可以算出每个桶中所有图元构成的包围盒.最后,对于每一种 mid ∈ [1, n] 的划分的代价就可以表示为:∪S(i) 代表了各个桶 i 的包围盒一起形成的大包围盒的表面积.我们从中选出代价最小的一种划分方案进行划分即可

image.png

  • 如果当前节点中所有图元质心的位置都相同的话,就需要采取特殊处理,我们可以直接建立一个叶节点,让这个叶节点包含所有图元.因为如果他们的质心位置已经相同,再进行分割将没有意义,最终都需要对它们逐个求交

层次包围盒的遍历

  • 对于每一个节点:

    1. 首先检查光线和该节点的包围盒是否相交
    2. 如果不相交则不再需要对该节点的子节点进行遍历
    3. 若相交且该节点为内部节点则对其子节点进行遍历
    4. 若相交且该节点为叶节点,则对该叶节点下的每一个图元进行相交测试
  • 一般情况下需要遍历完整个层次包围盒才能得到最近的交点,因为各节点之间是可能存在重叠区域的

  • 如果光线是在对光源进行遮挡测试时发射的 Shadow Ray,则不需要遍历整个层次包围盒,因为此时只需要确认是否存在交点即可,一旦发现有一个图元和Shadow Ray 相交即可返回

  • 同时,为了剪枝,每当发现一个光线与图元的交点时,都需要进行记录更新,在之后的搜索中只需要搜索比这个点更近的交点即可

  • 同时在遍历内部节点时,我们也可以记录光线的传播方向,优先对距离光线较近的节点进行遍历

KD 树

  • 二元空间分割(binary space partitioning,BSP)树是一种使用平面对空间进行自适应划分的空间索引结构

  • BSP 树的根节点是一个包裹整个场景的大包围盒

  • 如果包围盒中图元的数目大于一个特定的阈值,该包围盒就会从中被一个平面分为两半.图元则会被关联到与其相重叠的半边包围盒上

  • 若一个图元同时与两边的包围盒相重叠,则会同时被关联到两个包围盒上,而在层次包围盒中一个图元最终只会被关联到左右两个的包围盒的其中一个之上

  • 正是由于这样的特性使得在层次包围盒在构建开始之前就能够知道需要消耗多少内存并提前分配,而 BSP 树则没有办法做到这一点

  • KD 树(KD-Trees)和八叉树(Octrees)是两种常见的 BSP 树的变种

  • KD 树与 BSP 树的区别在于它限定了划分平面必须与某一个坐标轴垂直:

    1. 好处:它可以使得遍历和构建的效率得到极大提高
    2. 代价:在空间划分时失去了一定的的灵活度

image.png

  • 八叉树则是使用三个分别与 x,y 和 z 轴垂直的平面同时将整个包围盒划分为八部分

KD 树的构建

  • KD 树的构建与层次包围盒类似,同样是一个自顶向下的递归过程:

    1. 或将当前节点包围盒所包围的区域划分为两个子区域
    2. 或当前节点中的图元数目低于阈值,将其转化为叶节点并将与包围盒重叠的图元与该节点相关联,终止递归
    3. 需要规定一个 KD 树的最大深度,因为在某些极特殊的情况下,KD 树可能会无限递归下去,在 PBRT[2] 中建议的最大深度为 8 + 1.3 log(N),N 代表图元的数目
  • 在对 KD 树进行划分的时候,我们同样可以用我们之前在介绍层次包围盒时提到的SAH 方法来估计代价

  • 但一个不同的地方在于此时我们更加偏向于划分出的两个子节点中存在一个空节点,这样当光线通过该节点所包围区域的时候就能不经过求交快速地舍弃该节点并前进至下一个节点

  • 里需要对代价函数进行一定的修改,增加一个奖励项be,仅当两个待划分节点中的任意一个为空节点时 be = 1:

image.png

  • 如果分割平面不是出现在包围盒的某一个面上,那么其代价一定会不低于在面上进行分割的代价,所以我们只需要比较所有在包围盒面上进行划分的情况,从中选出代价最小的即可

  • 如果在构建过程中,遇到了无法找到划分的位置;或求出的最优划分的代价高于不进行划分的代价时,我们只能放弃对当前节点的划分直接将当前节点变为叶节点

KD 树的遍历

  • KD 树遍历的流程大致如下:

    1. 首先光线与 KD 树的根节点的包围盒求交,若不相交则函数可以直接返回;若相交则继续遍历
    2. 对于每一个内部节点,首先计算光线和划分平面的交点,据此可以判断光线是与该节点的一个子节点相交还是同时与两个子节点相交,若同时相交于两个子节点需要按照远近顺序由近及远地依次遍历子节点
    3. 对于叶节点只需要对与其关联的图元进行逐一地求交即可
  • 这里与层次包围盒不同的是,因为 KD 树是根据空间进行自适应划分的空间索引结构,我们可以保证优先与较近的物体进行求交,所以并不需要遍历完整个 KD 树就可以找到交点

  • 关于判断光线是与几个子节点相交,我们可以通过是否满足 tsplit ∈ [max(0, tmin), tmax]来判断,这里 tmin 表示光线相交的较近点,tmax 表示光线相交的较远点,tsplit 表示光线与分隔平面的相交点

image.png


第十六章 高级渲染

辐射度量学

  • 辐射度学 (Raidometry) 提供了一系列思想和数学工具,来描述光的传播和反射

  • 光的偏振等效应并不自然地适应于这个框架

  • 辐射传输研究的是辐射能量的传递,它基于辐射度学原理,在几何光学层面上运作

  • 其中光的宏观性质足以描述光与远大于光波长的物体互作用的方式。通过这种方式,可以描述光与与光波长近似相同大小的物体的相互作用,从而模拟色散和干涉等效应

  • 在章中我们将假设几何光学是描述光和光散射的足够模型.这导致了有关光行为的几个基本假设,这些假设将在整个系统中隐含地使用:

    1. 线性性:光学系统的两个输入的联合效应总是等于每个输入单独效应的总和
    2. 能量守恒:当光从表面或参与介质中散射时,散射事件永远不会产生比起始时更多的能量
    3. 无偏振:我们将忽略电磁场的偏振;因此,光的唯一相关属性是其波长 (或等效地是频率) 的分布
    4. 无荧光或磷光:光在一个波长处的行为完全独立于光在其他波长或时间的行为.与偏振一样,包含这些效应并不太困难,但它们对系统的实际价值相对较小
    5. 稳态:假定环境中的光已经达到平衡状态,因此其辐射分布随时间不会变化.这在现实场景中几乎会在光瞬间到达平衡,因此在实践中不会有限制.请注意,磷光也违反了稳态假设
  • 采用几何光学模型最重要的损失是很难考虑衍射和干涉效应

基本物理量

  • 以下是有关渲染的四个辐射度量:通量、辐照度/辐射出射度、强度和辐射率.它们可以通过逐步限制时间、面积和方向来从能量 (以焦耳为单位) 派生出来

  • 所有这些辐射度量通常都依赖于波长(重点需要记住)

能量

  • 能量的单位是焦耳 (J).光源向四面八方发出光子,每个光子处于一个特定的波长并携带了特定一部分能量

  • 辐射度量学的基本物理量都是从不同方面描述一个光子.一个波长为 λ 的光子携带的能量是:其中 c 是光速,为 299 472 458 m/s,h 是普朗克常量,h ≈ 6.626 × 10−34 m²kg/s

image.png

通量 (Flux)

  • 能量描述了一段时间内的功.尽管我们假设了光在环境中每一时刻都是准静态,我们更加关心某一时刻光的状态

  • 辐射通量,亦作辐射功率 (power),是单位时间内通过某一表面的总能量.辐射通量可以通过对单位时间能量的变化求极限得到:单位是焦耳每秒 (J/s) 或者说瓦特 (W)

image.png

  • 给定通量关于时间的变化,我们可以积分得到给定时间段内的总能量:

image.png

  • 光源的总发射通常用通量来描述。下图显示了通过光周围假想球体的总能量来测量点光源的通量

image.png

  • 两个球体上测量到的总通量是相同的——虽然通过大球体局部的能量比通过小球体局部的能量少,但大球体的面积更大,这意味着总通量是相同的

辐照度 (Irradiance) 与辐射出射度 (Radiant exitance)

  • 对于通量的度量需要一块给定的表面计算单位时间内通过的光子

  • 给定一块面积为 A 的有限表面,我们可以定义面上单位面积的功率密度为 E = Φ/A.我们定义进入单位面积的通量为辐照度 (E),以及离开单位面积的通量 (M).它们的单位是 W/m²

  • 假设光源像四面八方辐射出的能量是相同的,那么半径为r 的球面上一点的辐照度为:这个现象也解释了为什么从一点收到的能量总量与距离的平方成反比

image.png

  • 可以定义点 p 的辐照度与辐射出射度为面积微元上通量变化的极限:

image.png

  • 也可以对辐照度进行积分得到通量:

image.png

  • 辐照度方程还可以帮助我们理解 Lambert 定律,即到达表面的光能与光照方向和表面法线之间夹角的余弦成正比

image.png

强度 (Intensity)

  • 考虑一个发射光子的无限小光源,假设这个光源在一个单位球的球心,可以计算单位立体角上的辐射功率称之为辐射强度 (I).它的单位是 W/sr.对整个球面来说球面,我们有:

image.png

  • 更一般地,我们感兴趣的是某一特定方向,取该方向锥形微元的极限:

image.png

  • 可以通过对强度进行积分来恢复功率.给定强度作为方向 I(ω) 的函数,我们可以对一组有限的方向 Ω 上进行积分来恢复功率:

image.png4

  • 强度描述了光的方向分布,但仅对点光源有意义

辐射率 (Radiance)

  • 辐照度和辐射出射度提供了在某一点的微分功率与微分面积的关系,但它们不能区分功率的方向分布

  • 辐射率将辐照度或辐射出射度与固体角度相关联。定义如下:

image.png