大气散射(七) 大气散射的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;
}

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

相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^2 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic3 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具4 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test5 小时前
js下载excel示例demo
前端·javascript·excel
Yaml46 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事6 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro