Whitted Style Raytracing

从相机出发, 朝某个像素点出射光线, 直到接触到的第一个物体的位置, 从该位置与光源连线, 若连线上没有其他物体, 说明该点可以被照亮. 此时使用布林冯等着色模型进行着色即可.

image

同时考虑折射, 反射, 最后将所有点的贡献叠加, 作为当前像素的着色结果.

image

求光线与表面的交点

在这里说的光线应该指的是从相机射出的光线.

数学上的光线表示为: 从某点延申的一条射线.

球的表面表示为: 表面上的点到圆心的距离等于半径. 注意向量的平方是个标量.

image

代入解出t. 注意t必须是正的. 解的数量决定交点数量.

image

对于一般的隐式表面:

image

解出t之后, 那交点的位置其实就在o + td.

这里说下隐式表面和显示表面的区别. 前者代表满足某个方程的点. 后者则是给定参数, 用参数生成表面方程.

对于显示表面求交, 我们侧重的一般是与三角形面求交. 这个问题又能分解为先判断是否与三角形所在平面求交, 然后再判断是否与三角形求交.

一个平面, 我们只需要用一个法线和一个点即可定义. 这个被定义的平面与法线垂直, 并且过这个点.

image

联立方程求解.

image

解出交点后, 再判断是否在三角形内即可.

另一种, 能够一次性解出相交情况的算法: MT算法.

简单来说, 光线依旧是那个o + td, 但是我们用重心坐标形式的三角形平面表示法, 然后联立. 三个未知数, 而向量又是三维的因此有三个方程, 解线性方程组即可.

image

加速光线表面求交

上面提到的光线与表面求交, 使用在与模型的所有三角形面进行求交. 这样速度很慢, 我们希望提高计算速度.

包围盒(bounding volumes): 用简单的体积包围一个复杂的物体. 如果连包围盒都碰不到, 那么光线不可能与物体表面相交.

image

我们常用的是AABB(轴对齐包围盒). 一个包围盒认为是三个相对平面围成的长方体.

image

分别在x, y, z方向上, 一共六个平面, 用前面光线求交的方式, 可以求出六个t, 我们选择最大的tmin和最小的tmax, 就能得到光线经过这个包围盒的入射时间和出射时间.,

image

我们就在进入时间和退出时间上做文章. 如果退出时间为负的, 那么这个包围盒在光源背后, 没有交点; 如果退出时间为正而进入时间为负, 那么光源必定在包围盒之内, 必有交点. 所以相交当且仅当进入时间小于离开时间且离开时间大于等于0

image

最后, 在计算射线与包围盒交点的时候, 可以使用分量计算, 更快速简洁.

image

加速结构

基础的加速结构:

首先得到包围盒, 然后在包围盒内部创建网格, 存储每个物体覆盖的小网格.

image

然后光线遍历网格, 在达到被物体覆盖的网格后, 与物体进行求交计算.

光线行进时无需遍历所有网格, 会采用类似画线算法的思路来计算.

image

在物体分布均匀的场景中, 这种方法的效果比较好.

image

分布不均匀的场景, 效果欠佳. 有大部分网格被浪费掉.

image

空间划分

image

第一个是八叉树. 实际上只是示意图. 第一个图首先将空间切成八块, 然后每个子块继续切, 如果得到的子块中没有任何物体那就不需要再切了.

但是八叉树本身随着维度的升高, 指数级增长.

KD-tree则是一个不受维度影响的划分方式. 划分方式为: 给定一个网格, 先水平划分, 然后每个子块竖直划分, 然后每个子块水平划分, 以此类推. 当然这是二维的情况. 三维情况就是先沿着x, 再沿着y, 最后沿着z划分.

image

在划分之后, 具体的几何形体则会存储在叶子节点.

image

光线首先判断是否与最外层的包围盒有交点, 如果有, 那么看是否与子包围盒有交点, 遍历下去, 如果遍历到了叶子节点对应的网格后与该网格相交, 那么光线就会与网格内的所有物体求交. 在下面这张图中, 光线会与1234四个网格内的物体求交, 不会与5网格内的物体求交.

image

问题在于, 我们很难判定包围盒是否与三角形面有交集.

另外, 我们希望一个物体只属于一个包围盒, 而非多个包围盒共享一个物体.

所以另一个思路是基于物体划分, 而非基于空间划分.

物体划分

image

这种加速结构就是BVH. 现在基本上用的都是BVH.

将空间中的三角形进行划分, 然后求它们的包围盒, 这都很容易. 最后生成了多个包围盒.

image
image

注意一下每次划分之后都要重新计算一下当前节点的包围盒.

我们希望尽可能减少包围盒的重叠, 即重点在于三角形的划分方式.

为了让划分更加均匀. 有个方法是每次只沿着最长的轴划分. 也可以每次沿着xyz. 另外一个方法就是从中位数的物体划分. 这样能够让树更平衡.

找中位数的方法可以使用快速选择算法.

如果物体动了, 那必须重新计算BVH.

image

渲染方程

辐射度量学

基于几何光学. 不考虑光的波动性.

--radient energy: Q, 单位是J.

--radient flux: 功率. 单位是W.

image

--intensity: flux沿着每个立体角的分flux. 单位是cd(candela)

image

立体角的定义:

弧度制中, 定义弧长和半径的比值是一个角度.

image

三维中就是面积比上r平方.

image

我们想求单位立体角. 也就是单位面积下的立体角. 这里就是要求这个小矩形的面积

image

我们将这个矩形分为两个部分: 竖直方向和水平方向. 视竖直方向与z轴的角度为θ, 看作向量沿着竖直圆周移动. 那么由弧度的定义, 就能得到弧长为\(rdθ\). 此时, 水平方向视为在当前高度下沿着水平的圆周移动, 因此有效半径为\(rsinθ\). 因此弧长为\(rsinθΦ\). 所以面积如上公式所示.

那么单位立体角:

image

对着\(θ\)求0到\(Π\)的积分, 对着\(Φ\)求0到\(2Π\)的积分, 结果即为整个球的立体角.

image

回到intensity. 显然:

image

有一个需要强调的理解是: intensity对应的是沿着每个单位立体角的flux大小, 而单位立体角是空间中的"一块"角度, 其对应的是"一捆"光线.. 可以想象一个球体, 然后从圆心延伸出一块四棱锥. 四棱锥顶端的尖尖角就是立体角. 显然, 这个四棱锥可以含有非常多的光线.

--irradiance: 单位面积接收的从各个方向到达的flux. 注意, 这里所说的flux, 是指有效flux. 即垂直于当前表面方向的flux. 所以往往还要乘上一个cos.

image
image

从irradiance可以定义先前说的"衰减". 考察一个球, 光源在球心. 则intensity是光源朝某个单位立体角辐射的能量, 随着半径增大, 实际上对应的球面面积和半径都在增大, 最后得到的intensity是不变的. 而对于irradiance. 假设面积不变, 那么随着半径增大, 其对应的立体角在减小. 因此其对应的flux在减小. 所以最后的irradiance减小, 此即衰减.

image

上图中, 单位面积是1, 因此当半径为1的时候E是总的flux除以\(4Π\). 当半径为r的时候, E'是总的flux除以\(4Πr^2\). 衰减系数为\(r^2\).

--radiance: 可以理解为描述一束光线的属性. 这可以与我们先前的intensity概念相联系. intensity是"一捆"光线, 而radiance是一束, 或者说一条光线. 其定义就是: 单位面积单位立体角上的flux大小. 从计算的角度来讲, 可以由intensity对单位面积微分得到; 而另一方面, 也可以从irradiance对单位立体角微分得到. 这里注意, 不论是哪一种方式, 都要注意面积指的是有效面积. 基本上来说都要乘上一个cos.

image

radiance既可以用于接收的flux, 也可以用于辐射的flux. 其本身是对光线的描述, 物理意义取决于光线本身. 式中的Acosθ就是前面提到的有效面积了.

实际上, irradiance就是对当前表面的各个立体角方向的radiance做半球积分:

image

BRDF

简单来说, 就是一个用来定义材质表面的函数. 它决定了从某个方向入射的光将在吸收后朝哪个方向出射多少光.

比如, 对于一个光滑的表面, 其BRDF可能就是吸收了某个方向的radiance后, 往镜像方向辐射出一束大小几乎相同的radiance. 而对于一个粗糙表面, 其BRDF可能就是吸收了某个方向的radiance后, 朝所有方向均匀辐射出大小相同的radiance.

image

这里, E指的是入射flux. 我们让radiance乘上cos, 使其成为有效的, 然后乘以立体角的微分dw, 显然, 对此式求积分即为当前表面从各个方向接收的flux, 即irradiance. 我们的目的是求出特定方向的radiance. 而BRDF定义的是: 从某个方向射入的radiance, 最终会朝某个方向辐射多少radiance. 显然这是一个比例.

image

对于所有的入射radiance, 我们都通过当前表面的BRDF计算出每个入射radiance最终会往我们指定的方向辐射多少radiance. 所有入射radiance的贡献加起来就是指定方向的出射radiance了. 由此, 我们实际上就得到了反射方程: 比例 × 入射radiance × cos, 求积分.

image

而渲染方程, 实际上就是在此基础上加入一个自发光.

image

另外, 我们假定所有radiance都朝着表面往外的方向.

对于一个点光源, 我们就能视为其向当前点射出一条radiance. 而对于面光源, 我们视为点光源的集合. 对于从其他表面辐射出来的radiance, 同样视为点光源的集合. 因此, 用后半部分的积分实际上就统一了所有的光照计算.

而对渲染方程一个重要的理解是: 当前点朝指定方向辐射出的radiance, 也会成为其他点的入射radiance. 我们的渲染方程, 最终实质上是一个递推方程.

image

基于这个理解, 我们可以来求解这个递推方程. 由于我没有学过积分算子, 所以这里放一下ai给出的定义.

image

在视频中, 渲染方程被如下简化, 形似积分算子.

image

然后继续简化

image

这里看文字, 要将L和E理解为向量. 抽象地感受一下似乎有点懂了, 不过因为没有学过算子, 还是不可能完全看懂的. 有点将积分转为向量内积的意思. 然后向量维度无限延伸, 同时级数收敛所以两个L是相同的. 我也不知道我在讲什么, 但感觉到了就好, 主要是能够接收这个简化.

这之后就是求解这个矩阵方程的问题了, 涉及一点数值分析.

image

最后得到的结果, K的次方代表弹射次数,

image

蒙特卡洛路径追踪

概率密度函数 PDF

概率密度函数. 没学过概率论的话(我就是)初次接触很不好理解这个概念.

首先回忆一下高中的概率知识. 随机变量X是啥还记得吗? 比如骰子, 有六个取值可能, 这六个就是随机变量X的取值: 1, 2, 3, 4, 5, 6. 然后每个随机变量会对应一个概率. 显然在这个例子中概率都是1/6. 并且, 在这个例子中, 随机变量X是离散的. 显然, 会有X是连续的情况出现, 比如人群中的身高. (这里的"连续"并不严谨, 意思传达到了就行)

而PDF是这样一个曲线:

横坐标代表随机变量X, 纵坐标代表概率密度. 一个重要的点是: 曲线围成的面积是1.

这是概率密度函数的特征. 它是用来描述X的分布情况的. 有这个结论:

X取某个区间的时候, 这个区间对应的曲线围成的面积, 就是X取这个区间的概率.

比如上面这个图, X取-1到1的概率是曲线在-1到1之间围成的面积.

这里有一点要强调, 如果问X取某个点的概率, 该怎么算? 实际上, X取某个点的概率是0.

PDF只对区间有效. 这是因为如果X取某个点有概率, 那么在这个点的随便一个邻域可以取无穷多个点, 然后大家都有概率, 最后PDF的面积将会是无穷. 所以, PDF只对区间有效.

另一个理解是, 从纵坐标的意义来看. 概率密度, 描述的是一个密度. 一个没有长度, 没有面积, 没有体积的点, 密度对其也毫无意义.

那连续性的随机变量对应的PDF, 就像上面这个图这样, 如果随机变量离散呢? 比如骰子, 只有六个取值?

那结果就会是柱形图一样的效果:

与我们刚才讲的PDF并不矛盾, 柱形图不就是让PDF变成分段函数而已嘛. X取某个离散的值, 可以视为取某个区间.

再强调下, 笔者还没学过概率论, 讲的东西不太严谨, 讲究一个能用就行, 海涵.

希望看到这您能尽量完成对PDF的直观理解.

蒙特卡洛积分

求定积分. 用黎曼积分的方法就是取很多小的长方形, 最后加起来. 用蒙特卡洛的方法就是, 每次取a到b中间的一个值, 得到其函数值, 用这个函数值作为长方形的宽, 用b-a作为长方形的长, 算出长方形的面积. 采样足够多次, 把所有长方形的面积加起来取个平均即为结果.

蒙特卡洛积分常用于求表达式未知或复杂的积分.

这就遇到第一个问题, 为什么用蒙特卡洛方法, 然后平均起来就是正确结果.

实际上只要取得点足够多. 那么对应的函数值就足够多, 这时候取个平均, 相当于取了函数值的平均值. 所以对应的矩形面积当然就是定积分的值. 就像我们物理中求变力做功, 如果能够得到变力的平均值, 那么用平均乘以路程即可. 难点在于求平均, 而当我们采样点足够多的时候, 得到的平均值就能无限逼近函数整体的平均值.

"平均"这一点理解好以后, 明确一下我们做蒙特卡洛积分的步骤:

--生成随机数

--用这个随机数来采样

--将采样结果求期望(或者说平均)


假定我们的x均匀分布, 显然我们只需要采样足够多次, 然后取平均, 最后乘以b-a即可得到面积.

↑很多文章都像上面这样讲, 问题来了, 啥叫x均匀分布? 至少, 我们的目的是对函数求积分, 而似乎从没听说过求积分还分x均不均匀的. 所以我个人认为这个讲法是有点含糊不清的意味的. (当然, 这个式子肯定是对的, 就是正常地在a到b之间采样, 然后算平均值, 算面积.)

什么才是正确的理解呢? 其实, 以上面那个函数图为例, 我们只要在a和b之间取足够多的点, 是不是必定能够得到准确的平均值? 所以问题不在于所谓均不均匀分布, 而在于, 我们不可能取无数个点来得到准确值. 我们的结果是对精确值的估计, 我们要做的, 是最短时间内完成估计的收敛.

所以, 我们知道要采样, 要算平均; 我们希望的是尽可能少地采样, 尽快得到结果.

所以, 我们实际上是生成一系列满足某个分布的随机数, 用这些随机数来采样函数. 前面所谓"x均匀分布", 实际上指的是生成均匀分布的随机数, 然后用这些随机数采样, 而就像前面说的那样, 均匀采样需要非常多的点才能达到比较好的拟合效果. 我们希望有更快的收敛速度, 所以想用少量的随机数达到不错的拟合效果. 显然, 这些随机数需要满足某个特定分布, 显然, 这个分布与我们要求定积分的函数有关.

如何做呢? 首先为x指定一个PDF. 这个PDF描述了x的分布情况. 实际上这样讲依旧含糊不清, 先看式子吧:

对积分进行恒等变形, 这个p(x)就是我们指定的PDF. 重点看第三个项. 这个式子是最不好理解, 最关键的式子.

我们在干什么? 求期望. 随机采样之后, 求期望. 我们的恒等变形也是往这个方向在靠.

我们现在实际上在做的是: 指定"取这个x的时候, 这个x对应的概率是多少". 回忆一下求期望的公式, 是不是随机变量×概率, 然后求和? 对应我们的这个式子, "概率"部分指的是后半部分p(x)dx.

得益于微分dx的"微小区间"的性质, p(x)dx就是一个概率. 忘了的话可以回看PDF那部分内容.

剩下的就是解决"随机变量"这个部分了. 这部分也是我理解了半天的部分. 同样是其他文章几乎不讲的部分.

很显然, fx/px的大小与px成反比, 从直观上理解, 就是"这个x对应的概率"越低, 其对应的"随机变量"就要大. 当然, 由于fx/px不仅与px有关, 还跟fx有关, 所以应该是: 其对应的"fx"就要被"放大". 这个放大后的整体, 即fx/px, 就是"随机变量".

求期望就求期望, 为啥要放大随机变量呢? 当然可以说变形结果就是这样, 但我就是想直观理解它呢?

现在从整体的情况考虑, 这个问题先留着, 马上就清楚了.

我们为x指定了PDF, 下一步是: 我们按照这个PDF来生成随机数.

那是不是概率大的区间生成的随机数多, 概率小的区间生成的少? 这时候, 用这些数代入上面的式子. 结果就是定积分的估算值了. 而我们前面提到, 想要精确估计定积分, 需要采样足够多的点; 我们在这里生成的随机数并不够多, 并且大部分集中在概率大的区域, 此时fx/px就发挥作用了. 我们虽然只在概率小的区间生成了少量的采样点, 但是他们采样的结果, 我们将其贡献"放大"了. 而对于采样多的区间, 我们将其贡献"缩小"了. 这是一种"抓大放小"的思想, 被称为重要性采样.

有一个结论是: 使用的PDF越贴近fx, 拟合效果就越好. 为啥? 就是因为在fx大的地方, 我们的PDF也大, 那么就有更多的随机数分布在那块区域, 我们能够很快地"接近"fx的定积分的值, 而我们缩小它们的贡献, 能够让整体的值更加精确(每个采样点的权重更小, 对于采样点多的情况, 会获得更精确的结果); 同时对于少量分布在fx小的地方的随机数, 放大它们的贡献, "很快"地"调整"了估算值的大小.

所以这就是我们"放大随机变量"的目的/意义.

所以蒙特卡洛积分的结论:

这玩意就是个期望. 对于PDF的选择, 要选尽可能贴合fx的PDF. 而如果选择均匀采样, 所对应的结果也只是"需要更多的采样点才能够达成不错的拟合效果".

用法就是: 根据PDF生成随机数, 然后采样, 求期望, 结束.

很简短的一个结论, 只是一两步的恒等变形, 但确实是非常精妙的方法.

路径追踪

解渲染方程, 怎么解. 一个难点是积分, 积分是连续的; 另一个难点是, 渲染方程是个递归的方程.

计算积分的方法就是蒙特卡洛方法.

对于渲染方程, 我们先忽略其自发光项, 同时仅考虑直接光照. 对于一个面光源照射一个物体的场景.

则我们希望求解的积分, 需要明确PDF, 需要明确f(x). 最简单的就是均匀采样, 我们让PDF为\(1/2Π\), 这个值显然是半球对应的单位立体角. 至于为什么是1/2Π, 是因为我们的随机变量必定是在0到2Π之间, 为了让积分结果为1, 则PDF需要取倒数. 关于这一点的推导:

image
image
image
image

考虑间接光照. 假如打到了另一个物体, 那么相当于从当前点朝该物体看, 计算这条radiance.

image
image

光线指数级增长:

image

N=1时不会爆炸:

image

到此, 用N=1来做蒙特卡洛积分, 即为路径追踪. N!=1的时候称为分布式光线追踪.

显然, 只用一根光线会噪声非常大, 不过, 对于一个像素, 我们可以射出多根光线. 这样就人为指定了光线 的数量, 避免指数爆炸.

image
image

另一个问题, shade这个函数会一直递归, 不会停. 但如果人为设定一个弹射次数限制, 就会导致能量损失.

解决方法是:

image

我们用RR的方法, 用概率决定是否停止.

image
image

至此已经是正确的path tracing方法.

image
image
image

直接在光源上采样. 所以我们必须改成在光源上的积分. 这是因为积分域不同. 对光源采样, 积分域是A, 而对半球采样, 积分域是w. 基于积分换元的想法, 实际上我们只需要找到w和A之间的等式即可. 实际上, 回顾立体角的定义, 是球面上的面积除以半径的平方. 我们只需要让采样面积投影到球面上, 然后除以半径平方即可.

image
image
image
image

至此剩最后一个小问题...要判断是否被挡到.

image
image

一些问题:

--路径追踪不好处理点光源. 如果确实需要, 那将其设置成一个很小的面光源.

--如何均匀采样?(采样理论)

--PDF怎么选更好?(重要性采样)

--能否将光源采样和半球采样结合? 可以. 多重重要性采样

--对像素的着色采样, 最后平均起来是否正确? 还是说要加权?(pixel reconstruction filter)

--最后算出来的radiance不是个颜色, 如何得到颜色?(伽马矫正, 曲线, 颜色空间)