大白话聊体积云——软光栅下体积云理论与实现(二)

Yuri
发布于

分而治之

在上一章 软光栅下体积云理论与实现(一) 中我们聊到了怎么去定义云这种材质,通过使用体素对于问题进行分化,把非标准的云划分成了一个个标准的体素,这对于接下来的渲染降低了难度。

这回我们聊聊怎么设计光线在云中的传播,从而完成渲染任务。我们一直说“云这种材质”,有点拗口,“云”作为参与到了光线传播的媒介之一,我们后面在通用的一些地方就叫它: participating media.

首先回忆一下最简单一种光线传播方式:光线在bling-phong模型下的传播
光线在bling-phong模型下的传播

可以分为四步:

  1. 构建一条射线作为光路
  2. 看场景中是否存在交点
  3. 获得直接光照的能量
  4. 根据材质选择一条光路,计算间接光照的能量

伪代码大致是

获得radiance(一条射线){
    构建光线
    判断场景中的交点
    if (存在交点) {
         直接光照的能量 = 根据材质、光照位置计算得到
         
         在半球分布上构建下一条光路   
         间接光照 = 获得radiance(下一条光路)
    } else {
        return 无能量
    }
    根据RR、bounce次数等条件 判断停止
}

这样就构成了一幅图像,blingphong的物体会展示出来,并把后面的物体挡住。

对于光线在participating media中的传播,和普通的表面材质非常相似,只有一个地方改变了
“在半球分布上构建下一条光路 ” -> "在整个球分布上构建下一条光路 "。
这里的光路就变成了:

为了方便理解,这里定义两个假设(后面会打开这两个限制)

  1. 光线每次就只前进一个体素的距离,就一定要bounce一次
  2. 光在传播过程中就像在真空一样,没有任何折损(先不考虑任何光的散射效果)

就像blingphong模型下的表面反射的能量 由直接光照和间接光照组成,在participating media中的散射也可以按照“直接”+“间接”的思路来理解。

光线每次在体素中bounce一次,都可以看做接受了一次间接光照的能量贡献(上图虚线部分),最后光线从云中出射到背景的物体,也会作为“间接”光照的能量参与到整体能量的共享中。

伪代码可以是

获得radiance(一条射线){
    构建光线
    if (在 “云” 材质中) {
        判断材质中的 体素 的交点
    } else {
        判断场景中的交点
    }
    
    
    if (存在交点) {
         直接光照的能量 = 根据材质、光照位置计算得到
         if (在 “云” 材质中) {
             在整个球分布上构建下一条光路   
             间接光照 = 获得radiance(下一条光路)
         } else {
             在半球分布上构建下一条光路   
             间接光照 = 获得radiance(下一条光路)
         }
         
    } else {
        return 无能量
    }
    根据RR、bounce次数等条件 判断停止
}

根据上面提到的两点限制,我们在伪代码中增加了两个判断,

  1. 在 “云” 材质中,由于我们要求每次只能前进一个体素,所以对于场景物体的求交改为了体素中的求交
  2. 在 “云”材质中,散射不局限于半球范围,而是整个球

思路上这样就没问题了,不过读起来着实有点吃力,if套if不是好的工程实践,我们稍微改一下伪代码:

获得radiance(一条射线){
    构建光线
    for (bounce次数 < 最大bounce次数) {
    
        //tips:间接光照 (例如第2次bounce)时 光线方向已经改变
        if (在 “云” 材质中) {
            判断材质中的 体素 交点  
        } else {
            判断场景中的 表面 交点 
        }
        
        if (存在体素交点) {
            计算光照能量
            在整个球分布上构建下一条光路 
            更新光线方向
        } else if (存在表面交点) {
            计算光照能量
            在半球分布上构建下一条光路
            更新光线方向
        } else {
            return 无能量
        }
        
        if (命中RR) {
            break;
        }
    }
    return 光照能量
    
}

这样问题就解决了…吗?

表面与次表面

并没有。
这里存在一个问题。

如果按照上面这样设计光路,那不就和一块玻璃一样了吗?
就像做Whitted style ray tracing 也会根据fresnel公式来计算整个球的反射方向,难道云就是多bounce了几次的玻璃?

云材质效果

玻璃材质效果

从感性的认知来看肯定是不对的,云和玻璃显然表现是非常不一样的。差在哪里呢?

上面的光线传播提到了两个假设

  1. 光线每次就只前进一个体素的距离,就一定要bounce一次
  2. 光在传播过程中就像在真空一样(先不考虑任何光的散射效果)

这里要打破第二个假设。
在玻璃中,玻璃内部的光线不会受到任何影响,其方向、光的能量都不会有改变。所以如果也用体素的思路去构建一块立方体玻璃也是没有问题的。

光线在玻璃内部同等于在真空传播

但光并不能在云中像在真空一样无所影响的传播,上一节聊到了云的材质其中有无数的水滴或冰晶,即使我们已经把云划分成了一个个的相同小体素,体素内部也会对光线造成影响。
这个影响分为两个部分,一个是光的能量,一个是光的方向。

散射效果对能量的影响

根据上一章 软光栅下体积云理论与实现(一) 已经了解到了光在体素中会有4种散射效果,接下来就来看他们是如何影响到光的能量,是会增加还是减少,具体又改变了多少。

只要获得了光在每个体素中是如何变化,上一节所说的光的传播线路就可以被计算出来。

我们可以朴素的建立一套伪代码:

改变的光能量 = Absorption的能量 + Emission的能量 + in-scattering能量+ outscattering能量
  1. Absorption 效果
    每一个体素中有若干个分子,这些分子有的会参与到散射效果,有的不会,这种不确定的问题,照旧用我们的老办法:概率统计。
    对于每个体素可以继续拆分为更小的体素,可以假设:
    在大体素中每走过的每个小体素,都有确定影响,例如:光的能量减少1%,那么走过的小体素越多,能量少的就越多,最终可以得到大体素的absorption效果

可以得到一套伪代码:

Absorption的能量 = 入射的光能量 * 每个小体素的减少比例 * 光路的距离
  1. Emission 效果
    同样使用概率统计的思路,伪代码为
emission的能量 = 入射的光能量 * 每个小体素的释放比例 * 光路的距离

接下来的工作就都是相同的了,in-scattering和out-scattering的效果也是如此,这样可以得到新的伪代码,我们把所有小体素的效果比例称之为“系数”就可以得到:

改变的光能量 = 四种效果的系数和 * 光路距离 * 入射的光能量

其中,in-scattering和out-scattering会根据方向所调整,从而合并得到一个scattering系数。
而absorption作为负向影响,可以与正向的scattering再次合并,从而得到一个attenuation efficient记作-σ_t;

而类似渲染方程会把自发光项提出来一样,对于participating media中的散射效果的影响也会如此处理。

最终可得:

改变的光能量 = 自发光 + attenuation_efficient * 光路距离 * 入射的光能量

tips:
这里为了方便理解,有许多隐式假设,将会在后面打开

从数学上来说,上面的式子是没问题的,但是从工程上来说,还可以再优化一下,显而易见有几个要求:

  1. 改变的能量需要小于总能量且大于零
  2. 光路的距离,也就是participating media的“厚度”thickness ,与光的能量变化成正相关

这两个要求翻译一下,对于工程的限制就是:
限制一:改变的能量 / 入射能量 的值在 [0,1] 的范围内
限制二:thickness即使无限增加,也应该满足限制一

结合上面的限制,可以设计一个用于描述光能量衰减的比例 beam transmittance 记为TR

以e为底的指数函数就可以满足上面的要求,e^{-x} 的图像如下:

其中的连续性与可导性非常适合我们的工程要求,并且指数衰减也符合电磁波的物理规律。

结合工程限制以及考虑散射效果,现在就可以计算出光在体素内一次bounce会改变多少radiance了:

L_o = L_i * Tr

单个体素的计算完成,就可以计算整个participating media中的能量情况了,对于整条光路,亦是单个体素的叠加:

所以结合各个体素情况可得:

Tr_0n = Tr_01 * Tr_12 * Tr_2n // Tr_01 代表 从p0到p1 的TR,下同
L_o = Li * Tr_0n

最终就可以得到出射光线的radiance。

散射效果对方向的影响

文章开头聊到了“ ‘在半球分布上构建下一条光路’改变为 ‘在整个球分布上构建下一条光路’ ”。
散射对于光的影响不仅是上一节提到的能量衰减,同时还是散射方向的改变。

可以进行一下对比:
反射仅在法线方向上进行的分布;散射会在整个球的范围进行分布。
反射有BRDF描述分布情况;散射也必然有对应的模型来进行描述。
这就是phase function。

Phase function

Phase function在职责上与BRDF相比,不能说毫无区别,也可以称为一模一样。都负责两件事:

  1. 确定出射方向
  2. 确定强度分布

不过相对于BRDF一般由严谨的物理性质推导而来,phase function更多是经验模型和概率统计而来;
类比与BRDF有很多模型:phong、microfact,同样的phase function也有很多模型,这里介绍广为使用的“The Henyey-Greenstein Phase Function”。

对于出射方向与强度分布,最简单的一种情况是在整个球上随机出射、整个球上均匀分布,这种情况下,伪代码为:

w_i =  randomSoildAngle();
f = 1/4 * INV_PI;

也有一种可能是phasefunction也像BRDF一样,不同的材质有不同的lobe,类似于lobe会有自己的倾向,不一定是均匀的

diffuse module 的lobe 分布

Glossy specular 的lobe 分布

这种情况下能量并不会均匀的分布在整个球上,而是有明显的方向倾向性,可能会沿着入射方向散射,也有可能向着相反方向散射,于是我们就可以使用“The Henyey-Greenstein Phase Function” 来描述这种倾向性:

这里引入了参数 g (asymmetry parameter) ,是一个根据材质预先设计好的常数,表示光的前向散射倾向性:

图1 “The Henyey-Greenstein Phase Function” 公式

g=-0.7 光线向入射方向的反方向散射

g=0.7 光线向入射方向的同方向散射

g越大的情况下,散射方向就会越靠近入射方向。根据入射与出射的cos夹角,可以得出对应方向的强度分布。

对于出射的方向,我们当然可以继续均匀的进行全球采样,phase function自然会给出对应强度情况。
只是这样会不会有什么问题?
这个后面再讨论,我们先把最小流程跑通再说。

综上可以得出伪代码:

w_i =  randomSoildAngle();
f = PHG(Dot(w_i,W_o),g);// PHG即为图1公式

次表面散射光路

这下终于完成了所有前置条件,我们可以把散射对于能量和方向的影响都加入到path tracing的逻辑中了,伪代码为:

获得radiance(一条射线){
    // 构建光线
    ray = Ray();
    L = Vector(0.f);// radiance
    f = 1.f; // 散射率
    
    for (bounce次数 < 最大bounce次数) {
    
        //tips:间接光照 (例如第2次bounce)时 光线方向已经改变
        if (在 participating media 中) {
            判断材质中的 体素 交点  
        } else {
            判断场景中的 表面 交点 
        }
        
        if (存在体素交点) {
            // 计算当前点的“直接光照”radiance
            L =  光照到达体素交点的radiance();
            // 根据 beam transmittance 计算达到能量的损失情况
            L = L * f * tr;
            // 在整个球分布上构建下一条光路 
            w_i = randomSoildAngle();
            // 根据participating media 预设的g 计算能量比例
            f = PHG();
            // 根据出射方向更新光线方向
            ray = swapRay(w_i);
        } else if (存在表面交点) {
            计算光照能量
            在半球分布上构建下一条光路
            更新光线方向
        } else {
            return 无能量
        }
        
        if (命中RR) {
            break;
        }
    }
    return 光照能量
    
}

tips: 上面的模型都存在一个前提,participating media是各向同性的,一旦是各向异性的,不仅Tr的计算需要考虑入射方向,PHG的计算也要考虑,将在后续下文中迭代处理。

这样一来在participating media下的次表面散射效果的基本框架就建立了出来,逻辑上也很通顺。


真的会如此顺利吗?

均匀的散射分布到底会不会有问题?
数学上没问题,工程上有大问题到底是什么挑战?
单个的散射组件怎么集成到复杂的piccolo系统中?


如果你觉得看伪代码不方便,也可以直接看楼主写的项目代码:Github

后续越来越复杂啦,争取一周内更新。

连载不易,原创费时,各位认可的求求给个免费的赞再走吧~

如有错误之处,恳请大家批评指正,学习交流。

5
评论 1
收藏
  • Yuri
    Yuri
    目前网站似乎没有支持Latex公式,所以都使用了伪代码代替。同时由于图片没办法内嵌,直接贴阅读体验不好,所以尽可能减少了直接贴公式图片。 大家觉得目前的阅读体验如何,希望留下你的反馈,这会帮助我下一篇写的让大家看起来更舒服。