4.1 概述
游戏中的实时体积云通常为了提高渲染效率而降低质量。最成功的方法仅限于低空蓬松半透明的层状云。我们提出了一种体积解决方案,可以使用不断变化并且逼真的结果填充天空,来描绘高海拔卷云和所有主要的底层云类型,包括厚厚的、像波浪一样翻滚的积云。

此外,我们的方法近似于实现了几种实时云渲染中尚未出现过的体积光照效果。最后,我们的这种解决方案在内存和GPU的消耗上足够低,可以运用在3A主机游戏中。
4.2 介绍
在3A主机游戏中渲染云的标准解决方案中涉及了一些类型的资源,例如2D billboards、极地天空穹顶图像或在渲染时实例化的一些体积库。对于那些需要天空不断变化并且允许玩家穿越巨大距离的游戏中,例如开放世界游戏,存储和访问多相机角度、一天中的时间和光照等条件下的数据消耗使高细节的资源不再有优点(个人理解是这样的)。此外,云系统的模拟技术的发展仅在利用错觉和伪造方面,例如旋转天空穹顶或者使用2D噪声扭曲图像。
许多程序化云系统技术不依赖使用资源。在ShaderToy.com上有几个很好的例子可以免费获取到,例如“Clouds”[Quilez 13]。Evolution工作室在游戏《Drive Club》中使用了叫做TrueSky的插件制作了令人印象深刻的大气天气效果。
然而这些方法有几个局限性:
- 它们都只描绘了低空的层云,而不是蓬松的、起伏的层积云或者积云。
- 当前的体积化方法没有实现在云上的真实的光照效果。
- 实时体积云的消耗非常大,并且不值得为所表现出的效果质量,花费那么大的消耗。
为了《Horizon: Zero Dawn》这款游戏,我们开发了一种新的解决方案来解决这些问题。我们提交了建模、照明和渲染的新算法,这些算法可以保持在20M内存预算和2ms的性能目标的同时提供逼真并不断发展的结果。
4.3 云形状建模
图4.2展示了各种云类型及其所在的高度范围。我们以体积方式渲染两个层级的云:在1.5km到4km之间的低层云和横跨1.5km到8km之间低层大气的积雨云。alto和cirro类的云的高度通常非常薄,可以通过2D纹理查找这种低消耗的方式进行渲染。

随着一天中时间的推移,太阳向地球传递热量,水蒸气从地表上升,穿过这些大气层。每一层都有自己的风向和温度。水蒸气的温度随着上升的高度而降低。随着温度的降低,水蒸气在遇到的尘埃颗粒周围凝结成水或冰(有时会以雨或雪的形式出现)。在蒸汽流动中的大量不稳定引起了湍流。当云上升的时候,它们往往会形成翻腾的形状。当它们散开时,它们像雾一样伸展和消散。
云确实是流体动力学中一个令人惊奇的例子,并且对这种行为进行建模,需要设计人员在某种程度上近似与以基础物理的方式去处理云(很多渲染效果都是以物理学为基础展开设计)。有了这些概念,我们定义了几种技术,这些技术将在使用ray march(ray march方式渲染体积效果)方法时对云进行建模。4.3.1到4.3.3章节详细介绍了一些用于云的建模的概念,4.3.4章节会讲解如何将它们放在一起使用。
4.3.1 改进的分型布朗运动
实时渲染体积云系统的标准建模方法涉及到了ray march和一种叫做分形布朗运动的技术,简称FBM(可以参考这里)。FBM是一些列频率较高,振幅较低的噪声的八度之和。
柏林噪声通常用于此目的。虽然这是形成雾状层云的可靠模型,但它无法描述积云中的圆形、波浪状,也无法给它们一种隐隐的运动感,如图4.4所示。柏林噪声可以在其范围中间进行翻转以创建一些蓬松的形状,但由于它只是一种噪声波,它仍然无法在云中形成像花菜一样形状的效果。图4.5展示了柏林噪声,abs(perlin*2+1)的结果,以及在云中发现的分形波浪图案的摄影作品参考。





另一种类型的噪声,Worley噪声,由Steven Worley在1996年引入【Worley 96】,经常用于渲染焦散和水的效果,如图4.6所示。
如果将其反向并用于FBM中,Worley噪声近似于一个不错的分形波浪图案。他也可以用来为低频柏林噪声的低密度区域添加细节(见图4.7左边和中间)。我们通过使用Worley噪声FBM作为原始范围的最小值,重新映射柏林噪声来做到这一点(通过下面公式可以更好地理解这句话)。
OldMin = Worley_FBM
PerlinWorley = NewMin + (((Perlin − OldMin)/(OldMax − OldMin)) ∗ (NewMax − NewMin))
这种结合两种噪声类型的方法为柏林噪声产生的连通性增加了一点起伏感,并产生了一个更自然的结果。


我们将此称为低频 Perlin-Worley 噪声,它是我们建模方法的基础。(参考图 4.7,右侧。)
我们不是使用每个八度音程读取一个纹理来构建FBM,而是预编译FBM,因此我们只需要读取两个纹理。图4.8展示了我们的第一个3D纹理,它由Perlin-Worley噪声的FBM和三个八度Worley 噪声的FBM组成。图4.9展示了我们的第二个3D纹理,它由另外三个八度的Worley噪声组成。
第一个3D纹理定义了我们的基础云的形状。第二个频率更高,用于侵蚀基础云形状的边缘并添加细节,如4.3.4所述。
4.3.2 密度-高度函数
该领域以前的工作是通过根据高度偏移或缩放云密度值来创建特定的云类型 [Quilez 13]。


我们通过使用三个这样的函数来扩展这种方法,一个用于三种主要的低层云类型中的每一种:层云、层积云和积云。 图4.10展示了我们使用的梯度函数。图4.11展示了使用这些函数改变云密度随高度变化的结果。在运行时,我们计算三个函数的加权和。我们使用天气纹理改变权重,或多或少地添加各种类型的云,详细信息在下一节中。
4.3.3 天气纹理
在我们的云系统中的任意一点中,我们需要知道三件事:
云覆盖量:天空中云覆盖的百分比。
降水:头顶的云层产生降雨的几率。
云类型:0.0表示层云,0.5表示层积云,1.0表示积云。

这些属性都可以表示为介于0到1之间的概率,这使得它们易于使用和在2D纹理中进行预览。可以对该缓冲区进行采样,以获取世界空间中任意点上每个属性的值。
图 4.12 将该场景的天气图分解为多个组件。该图的比例尺为6万 × 6万米,箭头表示摄像机方向。
实际上,雨云总是在下雨的地方出现。 为了模拟这种行为,我们将云类型偏向积雨云,云覆盖率至少为70%,而下雨的可能性为100%。
此外,我们允许美术重写天气纹理,为过场动画或其他定向体验制作艺术导向的天空 [Schneider 15, slide 47]。
4.3.4 云采样器
建立了云密度函数的组件后,我们现在将继续讨论云模型。
与迄今为止所有其他体积云解决方案一样,我们使用ray march方案。光线步进穿过一个区域并采样密度值,用于照明和密度的计算。这些数据用于构建体积主体的最终图像。我们的云密度采样函数完成了解释采样的位置和天气数据的大部分工作,从而为我们提供了给定的点上的云密度值。
在我们开始使用函数之前,我们计算一个归一化的标量值,它表示云层中当前采样位置的高度。 这将用于建模过程的最后一部分。
// 在云层中采样的坐标值。
float GetHeightFractionForPoint ( float3 inPosition,
float2 inCloudMinMax )
{
// 获取云区域中的世界空间坐标值
float height_fraction = (inPosition.z − inCloudMinMax.x)/(inCloudMinMax.y−inCloudMinMax.x);
return saturate (height_fraction);
}
我们还定义了一个重新映射函数来将值从一个范围映射到另一个范围,在组合噪声以制作我们的云时使用。
// 将一个值从一个范围映射到另一个范围的功能函数。
float Remap(float original_value, float original_min ,
float original_max, float new_min, float new_max ) {
return new_min + (((original_value − original_min) / (original_max − original_min)) ∗ (new_max − new_min))
}
我们采样算法的第一步是利用我们第一个 3D 纹理中的低频 Perlin-Worley 噪声构建基本的云形状。 过程如下:
- 第一步是检索构建基本云形状所需的四个低频噪声值。 我们对包含低频八度音阶的第一个 3D纹理进行采样。
- 我们将使用包含Perlin-Worley噪声的第一个通道来建立我们的基础云形状。
- 虽然基本的Perlin-Worley噪声提供了合理的云密度函数,但它缺乏真实云的细节。我们使用重映射函数将其他三个低频噪声添加到Perlin-Worley噪声的边缘。这种组合噪声的方法可以防止Perlin-Worley云形状的内部变得不均匀,并确保我们只在我们可以看到的区域添加细节(总结:云厚的地方用采样一次噪声,薄的地方再采样一次)。
- 为了确定我们正在绘制的云的类型,我们根据天气纹理中的云类型属性计算我们的密度高度函数。
- 接下来,我们将基础云形状乘以密度高度函数,以根据天气数据创建正确的云类型。
下面是代码:
float SampleCloudDensity ( float3 p , float3 weather_data ) {
// 获取低频 Perlin-Worley和Worley噪声。
float4 low_frequency_noises=tex3Dlod(Cloud3DNoiseTextureA,Cloud3DNoiseSamplerA,float4 (p,mip_level)).rgba;
// 从低频Worley噪声中构建FBM,可用于为低频Perlin-Worley噪声添加细节。
float low_freq_FBM = (low_frequency_noises.g ∗ 0.625)+(low_frequency_noises.b∗0.25)+(low_frequency_noises.a * 0.125);
// 通过使用由Worley噪声制成的低频FBM对其进行膨胀来定义基本云形状。
float base_cloud = Remap(low_frequency_noises.r, −(1.0 − low_freq_FBM), 1.0, 0.0, 1.0);
// 使用4.3.2节中解释的密度高度函数获得密度-高度梯度。
float density_height_gradient = GetDensityHeightGradientForPoint(p, weather_data);
// 将高度函数应用于基础云形状。
base_cloud ∗= density_height_gradient;

在这一点上,我们已经有了一些类似于云的东西,尽管它的细节很低(图 4.13)。
接下来,我们应用天气纹理中的云覆盖属性,以确保我们可以控制云覆盖天空的程度。此步骤涉及两个操作:
- 为了使云在我们为覆盖属性设置动画时真实地增长,我们使用重映射函数中的云覆盖属性扩展了前面步骤生成的基本云形状。
- 为了确保密度以美观的方式随着覆盖范围的增加而增加,我们将此结果乘以云覆盖属性。

下面是代码:
float cloud_coverage = weather_data.r ;
// 使用重新映射来应用云覆盖属性。
float base_cloud_with_coverage = Remap(base_cloud,
cloud_coverage, 1.0, 0.0, 1.0) ;
// 将结果乘以云覆盖属性,使较小的云更轻且更美观。
base_cloud_with_coverage ∗= cloud_coverage ;
这些步骤的结果如图4.14所示。基础云的细节仍然很低,但它开始看起来更像一个系统,而不是一个噪声场。接下来,我们通过添加逼真的细节来完成云的处理,从上升的水蒸气中的不稳定性产生的小波浪到由大气湍流引起的纤细扭曲(详见图4.15中的示例)。

我们使用三个步骤对这些效果进行建模:
- 我们使用动画卷曲噪声(curl noise)来扭曲云底的样本坐标,模拟使用扭曲的样本坐标对高频 3D 纹理进行采样时的湍流效果。
- 我们利用高频Worley噪声构建FBM,以便为云的边缘添加细节。
- 我们使用高频FBM收缩基础云形状。在云的底部,我们反转Worley噪声以在该区域产生纤细的形状。在顶部使用Worley噪声收缩产生翻滚细节。
下面是代码:
//在云底添加一些湍流。
p.xy += curl_noise.xy ∗ (1.0 − height_fraction);
//采样高频噪声。
float3 high_frequency_noises = tex3Dlod(Cloud3DNoiseTextureB,Cloud3DNoiseSamplerB,float4(p∗0.1, mip_level)).rgb;
//构建高频Worley噪声FBM。
float high_freq_FBM = (high_frequency_noises.r ∗ 0.625)+ (high_frequency_noises.g ∗0.25)+(high_frequency_noises.b ∗ 0.125);
//获取用于在高度上混合噪声类型的高度分量。
float height_fraction = GetHeightFractionForPoint(p,inCloudMinMax);
//从纤细的形状过渡到高度的波浪形状。
float high_freq_noise_modifier = mix(high_freq_FBM,1.0 − high_freq_FBM, saturate(height_fraction ∗ 10.0));
//用扭曲的高频Worley噪声侵蚀基础云形状。
float final_cloud = Remap(base_cloud_with_coverage,high_freq_noise_modifier ∗ 0.2, 1.0, 0.0, 1.0);
return final_cloud;
}

这些步骤的结果如图 4.16 所示。这一系列操作是我们的采样器用来在ray march中创建云景的框架,但是,我们采取了额外的步骤来增加隐隐约约的运动感,这是传统基于噪声的云景解决方案所缺乏的。
为了模拟云从一个大气层上升到另一个大气层时的剪切效应,我们在风向的高度上偏移了采样坐标。此外,两个 3D 纹理的采样都在风向上进行偏移,并且随着时间的推移略微向上,但速度不同。为每个噪声赋予它自己的速度可以使云的运动看起来更加逼真。随着时间的流逝,云似乎在向上生长。
//风
float3 wind_direction = float3(1.0, 0.0, 0.0);
float cloud_speed = 10.0;
//cloud_top_offset将云顶沿风向推了这么大单位的偏移量。
float cloud_top_offset = 500.0;
//风向偏斜。
p += height_fraction ∗ wind_direction ∗ cloud_top_offset;
//以风向为云设置动画,并为风向添加一个小的向上偏差。
p+=(wind_direction + float3 (0.0, 0.1, 0.0)) ∗ time ∗ cloud_speed;
此代码必须位于CloudDensitySample()函数中的任何3D纹理采样之前。

4.3.5 成果
图 4.17 显示了使用不同天气设置创建的一些体积云景。
这种建模方法使我们能够雕刻出许多独特的云景。当雨信号沿风向接近相机时,它会产生接近风暴前沿的效果[Schneider 15, slide 43-44]。
4.4 云的光照
体积云照明是计算机图形学的一个很好的研究领域。不幸的是,对于游戏开发者来说,最好的结果来自于大量的采样次数。这意味着我们必须找到方法来模拟制作影视级质量的云时产生的复杂且昂贵的过程。


我们的方法通过近似解决了三种特别的效果:云中的多重散射和定向照明,当我们看向太阳时云周围的白光,以及当我们远离太阳时云上的暗边。图 4.18 显示了这三种效果的照片参考。
4.4.1 体积散射
当光线进入云层时,大部分光线会花时间折射通过云层内的水滴和冰,然后散射到我们的眼睛 [Van De Hulst 57]。 光子进入云层会发生三件事(另请参见图 4.19):
- 它可以被水或云中的非参与颗粒物(如灰尘)吸收; 这是消光或吸收。
- 它可以从云层中射向眼睛; 这是内散射。
- 它可以离开云层,远离眼睛; 这是外散射。
Beer定律是一种标准方法,用于估计这三种结果中的每一种的概率。
4.4.2 Beer's Law
Beer 定律最初被设想为一种化学分析工具,它模拟了光在穿过材料时的衰减 [Beer 52]。(参考见图4.20)

在体积的情况下,它可以根据光学厚度可靠地计算透光率[Wrenninge 13]。如果我们参与的介质是非均匀的,就像云一样,我们必须利用ray march方法来沿着光线累积光学厚度。该模型已广泛用于电影视觉效果,它构成了我们照明模型的基础。
下面是代码:
light_energy = exp(−density_samples_along_light_ray);
4.4.3 Henyey-Greenstein 相位函数
在云中,光向前散射的概率更高 [Pharr 和 Humphreys 10]。这就是云周围的光(见图 4.21)。

1941年,Henyey-Greenstein 相位函数被用来模拟小粒子对光散射的角度依赖性,用于描述星际尘埃云对光的散射 [Henyey 和 Greenstein 41]。在体积渲染中,该函数用于模拟参与介质中光的散射的概率。我们使用偏心率(方向分量)g 为 0.2 的单一 Henyey-Greenstein 相位函数,以确保云中更多的光向前散射:
下面是它在代码中的实现:
float HenyeyGreenstein(float3 inLightVector, float3 inViewVector, float inG){
float cos_angle = dot(normalize(inLightVector),
normalize(inViewVector));
return ((1.0 − inG ∗ inG) / pow((1.0 + inG ∗ inG − 2.0 ∗ inG ∗ cos_angle), 3.0 / 2.0)) / 4.0 ∗ 3.1415;
}
结果如图 4.22 所示。

4.4.4 内散射概率函数(糖粉效应)
Beer定律是一种消光模型,这意味着它关注的是光能如何随深度衰减。这无法近似与云面向太阳侧的内散射相关的重要照明效果。当视线接近光线方向时,这种效果会在云层上呈现为暗边。成堆的糖粉也有类似的效果,这是我们对这种效果的昵称的来源。 请参见图 4.23 中的说明。

这种效果在圆形密集的云区域中最为明显,以至于每个凸起处之间的褶皱,看起来比更靠近太阳的凸起处本身更亮。 这些结果似乎与Beer定律模型完全相反。回想一下,内散射是一种效果,其中云内的光线反弹,直到它们变得平行,然后离开云并传播到我们的眼睛。 这种现象甚至在我们观察云的阳光照射的一面时也会发生(图4.24)。
还记得由于前向散射,更多的光沿着原始光线方向向前散射。但是,必须存在相对较大的光学深度才能使光子有合理的机会转动180度。绕云边缘的路径不会通过足够大的光学深度,从而完全转动相当一部分光子。光学深度大到足以使光子旋转180度的路径几乎总是在云中,所以Beer消光定律会在光子离开云中进入我们的眼睛之前消除这种贡献。(简单地说就是云越厚的地方,光越可能以光入射方向的反方向照射回去)。空隙和裂缝是个例外; 它们为云体积内部提供了一个窗口,那里有光学深度相对较大的光子路径,为光子逃逸提供了一条低密度捷径,使裂缝处比周围的凸起处更亮。

我们选择用概率来表达这种现象。想象一下,你正以与你身后太阳发出的一组光线相同的角度看着云层上的一个凸起区域(图4.25)。
如果我们在其中一个凸起的表面下方对一个点进行采样,并将其与其中一个裂缝中相同深度的点进行比较,则裂缝中的点将具有更多可能有助于内散射的潜在云物质(图4.26)。 就概率而言,折痕应该更亮。



以这个思想实验为指导,我们提出了一个新函数来解释这种效应。 因为这个结果实际上与Beer定律相反,所以我们将其表示为原始函数的倒数(图4.27)。
就我们的目的来说,这是这一现象的一个足够精确的近似值,它不需要任何额外的抽样。
我们将这两个函数组合成一个新函数:Beer's-Powder。请注意,我们将整个结果乘以2,使其更接近原始归一化范围(图4.28)。

下面是它在代码中的实现:
powder_sugar_effect = 1.0 − exp(−light_samples ∗ 2.0);
beers_law = exp(−light_samples);
light_energy = 2.0 ∗ beers_law ∗ powder_sugar_effect;
图4.29显示了一些独立测试用例和游戏内部解决方案的结果。

4.4.5 雨云
我们的系统还模拟了较暗的雨云底部。 雨云比其他低空云更暗,因为水滴凝结得如此之多,以至于大部分光线在到达我们的眼睛之前就被吸收了。
因此,由于我们已经有了采样点的降水属性,我们可以使用它来人为地“加厚”云物质。 通过增加Beer’s-Powder函数的采样密度可以轻松完成此任务;见图4.30,变量p代表降水。

图4.31显示了一些效果。

4.4.6 照明模式总结
回顾一下,我们的照明模型是四个组件的组合:
- Beer定律(August Beer,1852 年)(模拟光穿过云时的衰减),
- Henyey-Greenstein 相位函数 (Henyey and Greenstein, 1941)(模拟光向前散射的概率),
- 内散射概率函数(糖粉效应)(模拟光在云中折射,最终旋转180度的概率),
- 雨云吸收增益。
以 E 为光能,d 为照明采样的密度,p 为雨水的吸收乘数,g 为我们在光线方向上的偏心率,θ 为视角与光线之间的角度,我们可以完整地描述我们的照明模型 :
4.5 云渲染
选择在哪里对数据进行采样以构建图像对于性能和图像质量非常重要。我们的方法试图将巨大消耗的工作限制在可能需要的情况下进行。
4.5.1 球面大气
使用ray march进行渲染的第一部分是决定从哪里开始。当观众位于海洋等看似“平坦”的表面时,地球的曲率显然会导致云层下降到地平线上。 这是因为地球是圆形的,云层是球形的而不是平面的。(见图 4.32)

为了重现这一特征,我们的ray march发生在距地球表面1.5公里处的3.5公里厚的球壳中。我们使用球体相交测试来确定光线行进的起点和终点。 当我们向视界看时,射线长度显着增加,这需要我们增加潜在样本的数量。 在玩家的正上方,我们走多达 64 步,在地平线上,我们走多达 128 步。 在我们的 ray-march 循环中有几个优化,允许它提前退出,所以平均样本数比这个低得多。
4.5.2 Ray March 优化
我们不是每次都评估完整的云密度函数,而是只评估云密度函数的低频部分,直到我们接近云。回想一下,我们的密度函数使用低细节的Perlin-Worley噪声来建立我们的云的基本形状和更高频率的Worley噪声,它用作从该基本云形状边缘的侵蚀。 仅评估密度函数的低频部分意味着读取一个3D纹理而不是两个,这大大节省了带宽和指令数。图4.33说明了使用“廉价”的采样通过空气的步骤,然后在靠近云时切换到昂贵的采样。一旦几个样本返回零,我们就返回到“廉价”采样(简单地说就是先计算出云的边界,再计算出需要在云内采样的距离,只有在云内时,进行复杂的噪声贴图采样)。
为了在代码中实现这一点,我们从零的cloud_test值开始,并使用我们的采样器的布尔值true 在循环中累积密度。 只要cloud_test为0.0,我们就会继续搜索云边界。一旦我们得到一个非零值,我们就抑制该步骤的行进积分并继续使用完整的云密度样本。在连续六个返回0.0的完整云密度样本后,我们切换回云边界搜索。这些步骤确保我们已经退出了云边界并且不会触发额外的工作。

float density = 0.0;
float cloud_test = 0.0;
int zero_density_sample_count = 0;
//启动主ray-march循环。
for (int i = 0; i < sample_count; i++)
{
//云测试从零开始,所以我们总是从头开始评估第二种情况。
if(cloud_test > 0.0)
{
//采样密度通过将最后一个参数设置为false的昂贵方式,表示一个完整的采样。
float sampled_density = SampleCloudDensity(p, weather_data, mip_level,false);
//如果我们进行采样值为0,则递增计数器。
if(sampled_density = 0.0)
{
zero_density_sample_count++;
}
//如果我们正在做一个仍然可能在云中的昂贵的采样:
if(zero_density_sample_count != 6)
{
density += sampled_density;
p += step;
}
//如果不是,则将云测试设置为零,以便我们回到廉价的采样案例。
else
{
cloud_test = 0.0;
zero_density_sample_count = 0;
}
}
else
{
//采样密度廉价的方法,只使用低频噪声。
cloud_test = SampleCloudDensity(p, weather_data, mip_level, true);
if(cloud_test == 0.0)
{p += step;}
}
}

该算法将3D纹理调用的数量减少了一半,以实现最佳情况,即我们在空旷的天空中步进采样。
为了计算光照,需要在每个ray march步骤中对光照进行更多的采样。这些样本的总和将在光照模型中使用,然后通过每个视图光线行进步骤的沿视图光线的当前密度总和进行衰减。图4.34说明了光线行进中的基本光样本集成行进。
因为我们的目标是在支持许多其他GPU计算密集型任务的游戏引擎中使用,所以我们被限制为每个ray-march步骤不超过六次采样。
减少光采样次数的一种方法是仅在ray march进入云层时执行它们。这是一个重要的优化,因为光的采样非常昂贵。通过这种优化,视觉结果没有变化。
...
density += sampled_density;
if(sampled_density != 0.0)
{
//SampleCloudDensityAlongRay只是从起点沿给定方向走,并获取X个光照样本。
density_along_light_ray = SampleCloudDensityAlongRay(p);
}
p += step;
...
4.5.3 锥形采样照明
找到太阳光照量的明显方法是测量查询点和太阳之间的云的透光率。然而,云中任何一点的光都受到其周围区域在光源方向上的光的很大影响。把它想象成一个光能的漏斗,在我们的采样位置达到顶点。为了确保我们的照明模型的Beer定律部分受到这种影响,我们在一个向光源扩散的圆锥体中取六个光照样本,从而通过包括云的相邻区域来加权Beer定律衰减函数。请参见图4.35。

由于采样次数少,条带状的伪影会立即出现。锥形采样有助于稍微打破条带状,但为了进一步平滑它,我们以较低的mip级别对密度进行采样。
为了计算锥体偏移,我们使用了介于-(1, 1, 1)和+(1, 1, 1)之间的六个噪声结果组成的核心算法,并随着我们远离采样位置而逐渐增加其幅度。如果沿着视图行进的累积密度超过了一个阈值,则其光贡献可以用一个固定的数值代替(我们使用 0.3),我们将采样切换到低细节模式以进一步优化ray march。在此阈值上几乎没有视觉差异。
static float3 noise_kernel[] =
{
一些噪声向量
}
//锥体的宽度。
float cone_spread_multplier = length(light_step);
//一种收集锥形密度的功能,用于云的照明。
float SampleCloudDensityAlongCone(p, ray_direction)
{
float density_along_cone = 0.0;
//光照的ray-march循环。
for (int i=0; i<=6; i++)
{
//将采样位置加上当前步距。
p += light_step + (cone_spread_multiplier ∗ noise_kernel[i] ∗ float(i));
if(density_along_view_cone < 0.3)
{
//以昂贵的方式进行云密度采样。
density_along_cone += SampleCloudDensity(p, weather_data, mip_level + 1, false);
}
else
{
//云密度廉价的采样方式,只使用一个水平的噪声。
density_along_cone += SampleCloudDensity(p,
weather_data, mip_level + 1, true);
}
}
}
此外,为了考虑从远处的云层投射到我们正在计算光照的部分云层上的阴影,我们在锥体长度的三倍处采集一个长距离样本。(见图 4.36)

4.5.4 高海拔云
我们的方法仅以体积方式渲染低层云。 高海拔云以滚动纹理表示。 然而,为了将它们与体积云集成,它们将在ray march结束时进行采样。 对于带有三个通道的5122纹理,这个纹理读取的成本可以忽略不计。 我们让它们在不同的风向下运动,这个风向与我们天气系统中的风向不同,以模拟不同云层中的不同风向。(见图 4.37)

4.5.5 效果
图4.38显示了说明一天中时间变化的一系列照明结果。

4.7 致谢
I would like to thank Nathan Vos, Michal Valient, Elco Vossers, and Hugh Malan for assistance with our development challenges. I would also like to thank Jan-Bart van Beek, Marijn Giesbertz, and Maarten van der Gaag for their assistance in accomplishing our look goals for this project.
Additionally, I would like to personally thank colleagues whose work has greatly influenced this: Trevor Thomson, Matthew Wilson, and Magnus Wrenninge.
参考:
Unity项目源码:
最后,附上我自己在unity中写的一个体积云效果,该工程参考了这篇文章和其它一些网上的案例。在这个工程里,你可以找到这篇文章中大部分的知识点的代码实现,所以最好将该项目与文章结合起来学习。