一、采样视线方向
让我们回顾一下我们最近推导出的大气散射方程:
<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;
}
现在,我们已经考虑了所有要素,我们的着色器已经完成。