大气散射(七) 大气散射的shader

一、采样视线方向

让我们回顾一下我们最近推导出的大气散射方程:

<math xmlns="http://www.w3.org/1998/Math/MathML"> I = I S ∑ P ∈ A B ‾ S ( λ , θ , h ) T ( C P ‾ ) T ( P A ‾ ) d s I= I_S \sum_{P \in \overline{AB}} {S\left(\lambda, \theta, h\right) T\left(\overline{CP}\right) T\left(\overline{PA}\right) ds } </math>I=IS∑P∈ABS(λ,θ,h)T(CP)T(PA)ds

我们接收到的光量等于来自太阳的光量 <math xmlns="http://www.w3.org/1998/Math/MathML"> I S I_S </math>IS,乘以 <math xmlns="http://www.w3.org/1998/Math/MathML"> A B ‾ \overline{AB} </math>AB 中每个点 P 的单独贡献的总和。

我们可以直接在我们的着色器中实现这个函数。然而,有一些优化可以做。在之前的教程中,曾经暗示过可以进一步简化这个表达式。我们可以将散射函数分解为其两个基本组成部分:

<math xmlns="http://www.w3.org/1998/Math/MathML"> S ( λ , θ , h ) = β ( λ , h ) γ ( θ ) = β ( λ ) ρ ( h ) γ ( θ ) S \left(\lambda, \theta, h\right ) = \beta \left(\lambda, h \right ) \gamma\left(\theta\right) = \beta \left(\lambda\right )\rho\left(h\right) \gamma\left(\theta\right) </math>S(λ,θ,h)=β(λ,h)γ(θ)=β(λ)ρ(h)γ(θ)

相位函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> γ ( θ ) \gamma\left(\theta\right) </math>γ(θ) 和海平面处的散射系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> β ( λ ) \beta \left(\lambda\right ) </math>β(λ) 是与求和无关的常数,因为角度 <math xmlns="http://www.w3.org/1998/Math/MathML"> θ \theta </math>θ 和波长 <math xmlns="http://www.w3.org/1998/Math/MathML"> λ \lambda </math>λ 不依赖于采样点。因此,它们可以被提取出来:

<math xmlns="http://www.w3.org/1998/Math/MathML"> I = I S   β ( λ ) γ ( θ ) ∑ P ∈ A B ‾ T ( C P ‾ ) T ( P A ‾ ) ρ ( h ) d s I = I_S \, \beta \left(\lambda\right ) \gamma\left(\theta\right) \sum_{P \in \overline{AB}} { T\left(\overline{CP}\right) T\left(\overline{PA}\right) \rho\left(h\right) ds } </math>I=ISβ(λ)γ(θ)∑P∈ABT(CP)T(PA)ρ(h)ds

这个新表达式在数学上等价于以前的表达式,但计算效率更高,因为一些最重要的部分已经被提取出来。

我们现在可以开始实现它了。我们应该考虑无限多个点 P。对 I 的一个合理近似是将 <math xmlns="http://www.w3.org/1998/Math/MathML"> A B ‾ \overline{AB} </math>AB 分成几个长度为 ds 的较小段,并累积每个单独段的贡献。这样做的时候,我们假设每个段都足够小,以至于其密度是恒定的。一般来说,这并不是真实情况,但如果 ds 足够小,我们仍然可以得到一个相当好的近似。

<math xmlns="http://www.w3.org/1998/Math/MathML"> A B ‾ \overline{AB} </math>AB 中的段数称为视线采样点,因为所有段都位于视线射线上。在着色器中,这将是 _ViewSamples 属性。通过将其作为属性,它可以从材质检视器中访问。这允许我们在性能方面减少着色器的精度。

下面的代码段允许循环遍历大气中的所有段。

c 复制代码
// 数值积分以计算
// AB 中每个点 P 的光贡献
float3 totalViewSamples = 0;
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
    // 点的位置
    //(在视线采样段的中间采样)
    float3 P = O + D * (time + ds * 0.5);
    // T(CP) * T(PA) * ρ(h) * ds
    totalViewSamples += viewSampling(P, ds);
    time += ds;
}
// I = I_S * β(λ) * γ(θ) * totalViewSamples
float3 I = _SunIntensity *  _ScatteringCoefficient * phase * totalViewSamples;

变量 time 用于跟踪我们离起始点 O 有多远,并在每次迭代后增加 ds。

二、光学深度PA

沿着视线 (\overline{AB}) 的每个点都对我们绘制的像素的最终颜色贡献了自己的部分。从数学上讲,这个贡献是求和符号中的数量:

<math xmlns="http://www.w3.org/1998/Math/MathML"> I = I S   β ( λ ) γ ( θ ) ∑ P ∈ A B ‾ T ( C P ‾ ) T ( P A ‾ ) ρ ( h ) d s ⏟ light contribution of   L ( P ) I = I_S \, \beta \left(\lambda\right ) \gamma\left(\theta\right) \sum_{P \in \overline{AB}} \underset{\text{light contribution of}\,L\left(P\right)}{\underbrace{T\left(\overline{CP}\right) T\left(\overline{PA}\right) \rho\left(h\right) ds}} </math>I=ISβ(λ)γ(θ)∑P∈ABlight contribution ofL(P) T(CP)T(PA)ρ(h)ds

像在上一段中所做的那样,让我们尝试进一步简化它。我们可以通过用其实际定义替换 T 来进一步扩展上述表达式:

<math xmlns="http://www.w3.org/1998/Math/MathML"> T ( X Y ‾ ) = exp ⁡ { − β ( λ ) D ( X Y ‾ ) } T\left(\overline{XY}\right) =\exp\left\{ - \beta\left(\lambda\right) D\left(\overline{XY}\right) \right\} </math>T(XY)=exp{−β(λ)D(XY)}

<math xmlns="http://www.w3.org/1998/Math/MathML"> C P ‾ \overline{CP} </math>CP和 <math xmlns="http://www.w3.org/1998/Math/MathML"> P A ‾ \overline{PA} </math>PA 上的透射率的乘积变成了:

<math xmlns="http://www.w3.org/1998/Math/MathML"> T ( C P ‾ ) T ( P A ‾ ) = T\left(\overline{CP}\right) T\left(\overline{PA}\right)= </math>T(CP)T(PA)=

<math xmlns="http://www.w3.org/1998/Math/MathML"> = exp ⁡ { − β ( λ ) D ( C P ‾ ) } ⏟ T ( C P ‾ )   exp ⁡ { − β ( λ ) D ( P A ‾ ) } ⏟ T ( P A ‾ ) = =\underset{T\left(\overline{CP}\right) }{\underbrace{ \exp\left\{- \beta\left(\lambda\right) D\left(\overline{CP}\right)\right \} }} \, \underset{T\left(\overline{PA}\right) }{\underbrace{ \exp\left\{- \beta\left(\lambda\right) D\left(\overline{PA}\right) \right \} }}= </math>=T(CP) exp{−β(λ)D(CP)}T(PA) exp{−β(λ)D(PA)}=

<math xmlns="http://www.w3.org/1998/Math/MathML"> = exp ⁡ { − β ( λ ) ( D ( C P ‾ ) + D ( P A ‾ ) ) } = \exp\left\{- \beta\left(\lambda\right) \left( D\left(\overline{CP}\right) + D\left(\overline{PA}\right) \right) \right \} </math>=exp{−β(λ)(D(CP)+D(PA))}

联合透射率被建模为指数衰减,其系数是光线( <math xmlns="http://www.w3.org/1998/Math/MathML"> C P ‾ \overline{CP} </math>CP 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> P A ‾ \overline{PA} </math>PA)的路径上的光学深度之和,乘以海平面处的散射系数( <math xmlns="http://www.w3.org/1998/Math/MathML"> β \beta </math>β,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> h = 0 h=0 </math>h=0)。

我们首先要计算的量是段 <math xmlns="http://www.w3.org/1998/Math/MathML"> P A ‾ \overline{PA} </math>PA 的光学深度,它从进入大气的点穿过大气,直到我们在 for 循环中当前正在采样的点。让我们回顾一下光学深度的定义:

<math xmlns="http://www.w3.org/1998/Math/MathML"> D ( P A ‾ ) = ∑ Q ∈ P A ‾ exp ⁡ { − h Q H }   d s D\left( \overline{PA}\right)=\sum_{Q \in \overline{PA}} { \exp\left\{-\frac{h_Q}{H}\right\} } \, ds </math>D(PA)=∑Q∈PAexp{−HhQ}ds

如果要天真地实现这一点,我们将在一个循环中对 P 和 A 之间的点进行采样。这是可能的,但效率非常低下。实际上, <math xmlns="http://www.w3.org/1998/Math/MathML"> D ( P A ‾ ) D\left( \overline{PA}\right) </math>D(PA) 就是我们已经分析的外层 for 循环中当前段的光学深度。如果我们计算当前以 P 为中心的段的光学深度(opticalDepthSegment),并在 for 循环中持续累积它(opticalDepthPA),我们可以节省大量计算。

cpp 复制代码
// 光学深度累加器
float opticalDepthPA = 0;
// 数值积分以计算
// AB 中每个点 P 的光贡献
float time = tA;
float ds = (tB-tA) / (float)(_ViewSamples);
for (int i = 0; i < _ViewSamples; i ++)
{
    // 点的位置
    //(在视线采样段的中间采样)
    float3 P = O + D * (time + viewSampleSize*0.5);
    // 当前段的光学深度
    // ρ(h) * ds
    float height = distance(C, P) - _PlanetRadius;
    float opticalDepthSegment = exp(-height / _ScaleHeight) * ds;
    // 累加
    // 累加光学深度 opticalDepthPA += opticalDepthSegment; ...  
    time += ds;
}

三、光线采样

如果我们回顾一下点P的光贡献的表达式,我们会发现唯一需要的量是线段(\overline{CP})的光学深度:

<math xmlns="http://www.w3.org/1998/Math/MathML"> L ( P ) = exp ⁡ { − β ( λ ) ( D ( C P ‾ ) + D ( P A ‾ ) ) } ⏟ 合并透射率   ρ ( h ) d s ⏟ 线段的光学深度 L\left(P\right) = \underset{\text{合并透射率}}{\underbrace{\exp\left\{- \beta\left(\lambda\right) \left( D\left(\overline{CP}\right) + D\left(\overline{PA}\right) \right) \right \}}} \, \underset{\text{线段的光学深度}}{\underbrace{\rho\left(h\right) ds}} </math>L(P)=合并透射率 exp{−β(λ)(D(CP)+D(PA))}线段的光学深度 ρ(h)ds

我们将计算线段(\overline{CP})的光学深度的代码移到一个名为lightSampling的函数中。这个名称来自于光线,它是从P开始并指向太阳的线段。我们称它穿出大气层的点为C。

然而,lightSampling函数不仅仅计算(\overline{CP})的光学深度。到目前为止,我们只考虑了大气层的贡献,忽略了实际行星的作用。我们的方程没有考虑到从P向太阳发出的光线可能会击中行星。如果这种情况发生,到目前为止所做的所有计算都必须被丢弃,因为没有光实际上会到达相机。

在上面的图中,很容易看出应该忽略 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 P_0 </math>P0的光贡献,因为太阳光没有照到 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 0 P_0 </math>P0。在循环遍历从P到C之间的点时,lightSampling函数还会检查行星是否被击中。这可以通过检查点的高度是否为负来完成。

c# 复制代码
bool lightSampling
(    float3 P,    // Current point within the atmospheric sphere
    float3 S,    // Direction towards the sun
    out float opticalDepthCA
)
{
    float _; // don't care about this one
    float C;
    rayInstersect(P, S, _PlanetCentre, _AtmosphereRadius, _, C);

    // Samples on the segment PC
    float time = 0;
    float ds = distance(P, P + S * C) / (float)(_LightSamples);
    for (int i = 0; i < _LightSamples; i ++)
    {
        float3 Q = P + S * (time + lightSampleSize*0.5);
        float height = distance(_PlanetCentre, Q) - _PlanetRadius;
        // Inside the planet
        if (height < 0)
            return false;

        // Optical depth for the light ray
        opticalDepthCA += exp(-height / _RayScaleHeight) * ds;

        time += ds;
    }

    return true;
}

该函数首先使用rayIntersect计算点C。然后,它将线段 <math xmlns="http://www.w3.org/1998/Math/MathML"> P A ‾ \overline{PA} </math>PA分成长度为ds的_LightSamples个片段。光学深度的计算与最外层循环中使用的计算相同。

如果行星被击中,该函数返回false。我们可以使用这一点来更新最外层循环中缺失的代码,将"...."替换为以下代码:

cpp 复制代码
// D(CP)
float opticalDepthCP = 0;
bool overground = lightSampling(P, S);
if (overground)
{
    // Combined transmittance
    // T(CP) * T(PA) = T(CPA) = exp{ -β(λ) [D(CP) + D(PA)]}
    float transmittance = exp
    (
        -_ScatteringCoefficient *
        (opticalDepthCP + opticalDepthPA)
    );
    // Light contribution
    // T(CPA) * ρ(h) * ds
    totalViewSamples += transmittance * opticalDepthSegment;
}

现在,我们已经考虑了所有要素,我们的着色器已经完成。

相关推荐
qbbmnnnnnn8 分钟前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹10 分钟前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪12 分钟前
uni-app环境搭建
前端·uni-app
安冬的码畜日常16 分钟前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨16 分钟前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小199218 分钟前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
前端没钱38 分钟前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠43 分钟前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax
叫我:松哥1 小时前
基于机器学习的癌症数据分析与预测系统实现,有三种算法,bootstrap前端+flask
前端·python·随机森林·机器学习·数据分析·flask·bootstrap
让开,我要吃人了1 小时前
HarmonyOS鸿蒙开发实战(5.0)网格元素拖动交换案例实践
前端·华为·程序员·移动开发·harmonyos·鸿蒙·鸿蒙开发