【GPU Pro 7】Real-Time Volumetric

2020-11-25  本文已影响0人  离原春草

本文是GPU Pro7(首版发行时间为2016年)中的体积云相关文章的学习总结,原文作者为Andrew Schneider,即Siggraph 2015中《地平线零》体积云方案的作者,文中内容增加了自己的理解,并对原文进行了删减,如对其中内容有任何疑义,欢迎评论发问。

1. 简介

在地平线零体积云方案之前,标准的3A大作云效是通过模型实现的,这种方案效果好,但是如果要实现Time of Day效果以及兼顾超远视角的话,就需要消耗较高的资源,实现起来成本就太高了。

与模型云效相对的是渐进式程序生成方案(Procedural Cloud System),在ShaderToy上有大量的相关实现,比如Quilez 13年在ShaderToy上的方案,比如Evolution工作室使用的中间件TrueSky方案(被部署在了《Drive Club》上),不过这些方案都有着各自的局限性:

而这些问题都是《地平线零》体积云方案所重点需要解决的。

2. 云层建模

云层分布图

上面这张图给出了各种云层与对应的海拔分布,体积云方案主要用于实现对低空的层云(stratus,分布在1.5KM~4KM的高空)以及低海拔的积雨云(cumulonimbus, 分布在1~8 KM的高空),而较高海拔的alto&cirro云效由于比较稀薄,常常会选择使用2D贴图来模拟。

云层是水汽蒸发后在空气动力学的作用下的结果,在实现这个效果的时候要想得到更加正确的表现就需要将这一点考虑进去。

2.1 Modified Fractal Brownian Motion

实时体积云实现的标准方案是Raymarching+Fractal Brownian Motion (FBM),所谓的FBM指的是将多个频率依次增高而幅度依次减弱的多个噪声叠加到一起得到一种具有特定规律噪声输出的技术。

体积云FBM常用的噪声是Perlin噪声,Perlin噪声在模拟云层的雾状效果上有着非常不错的表现,不过如果要模拟菜花状的云层翻涌效果,Perlin噪声就显得不够了。

Perlin FBM与翻转Perlin FBM

如果将Perlin噪声沿着中值进行翻转,还可以得到一种非常松软的绒状效果,如上图所示,左图表示的是常规的Perlin FBM,右图表示的则是abs(-2 * Perlin + 1)的效果,虽然松软的效果看起来更好,但是依然无法用于实现前面说的云层翻涌效果。

另一种可以用于实现云层效果的噪声是Steven Worley在1996年引入的Worley噪声,这个噪声通常用于实现水面折射效果比如焦散之类的模拟。

Worley噪声模拟焦散 Worley噪声(左),Worley FBM(中),Worley-Perlin FBM(右)

如果将Worley噪声翻转并按照FBM进行累加,可以得到一种类似云层翻涌的效果,这个效果可以用于为低频的Perlin噪声的低密度区域添加一些细节表现,也就是说,我们常说的Perlin-Worley噪声实际上是通过用WorleyFBM对Perlin噪声进行Remap而得到,这个过程是通过如下的公式实现的(OldMin与NewMin,NewMax等参数按照后面的使用方法,通常会设定NewMin为0,OldMax为1):

OldMin = Worley_FBM
PerlinWorley = NewMin + ((( Perlin − OldMin )/( OldMax − OldMin ) )
∗ ( NewMax − NewMin ) )

通过这种编码方式,可以得到一种非常自然的云层效果。《地平线零》的云层方案建模主要依赖于两张噪声贴图:

R通道是PW噪声,GBA通道则是频率逐次增加的Worley噪声(从效果上看,更像是WorleyFBM噪声),分辨率为128

上面这张贴图用于给出云层的基本形状。

分辨率为32的Worley噪声,频率比上一张贴图又进一步升高

上面这张贴图是高频噪声贴图,用于对云层边缘进行腐蚀处理,以得到更为逼真的细节效果。

2.2 Density-Height Functions

此前的大家(比如Quilez 13)通常是通过根据海拔对云层的密度进行offset&scale来绘制特定形状的云层,这个过程是通过一个density-height函数来实现的。

density-height函数得到的密度会被用于对噪声信号进行offset&scale处理最终得到最终的云层效果,但是这种做法有一个局限,那就是最终产生的云层类型比较少,效果看起来比较单调,这是因为云层的最大高度是固定的,因此最终得到的密度范围也就会是固定的了。

为了突破单个density-height函数的效果局限,《地平线零》中直接使用了三个density-height函数,分别对应三种不同种类的低空云:stratus(层云),startocumulus(层积云)以及cumulus(积云)

三种density-height函数 对应的三种云层形状

在运行时使用的时候,会根据天气以及TOD参数来对这三个函数的结果进行混合,其中天气参数是通过一张天气贴图(Weather Texture)给出的,具体细节后面会介绍。

2.3 Weather Texture

想要得到正确的云效表现,就需要知道云层覆盖区域任意一点的如下几个参数:

  1. 云层覆盖率(coverage)
  2. 云层降水概率(Precipitation)
  3. 云层种类(cloud type):0.0表示层云,0.5表示层积云,1.0表示积云。

这些参数取值范围均为[0, 1],其物理意义可以用概率来解释,可以直接用一张贴图将这三个参数pack起来,如下图所示:


天气贴图,后面的三张小图分别表示覆盖率,降水率以及云层种类

在现实生活中,只有在下雨的时候天上才能出现雨云(即只要出现雨云,一定是下雨天气),因为为了模拟这种表现,这里对云层种类以及覆盖率做了处理,当云层类型接近到积雨云(cumulonimbus,貌似前面的三种云层种类中没有这一种?)且覆盖率至少达到70%以上时(此时看到的就是积雨云),就必定触发下雨效果。

2.4 Cloud Sampler

《地平线零》的体积云是通过raymarching完成的,raymarching的基本流程就是为屏幕上的每个像素分配一条射线,在射线上按照一定的step长度进行步进采样,将采得的结果用于计算云层密度,并用在后续的光照计算上面,最终通过将各个采样点的光照结果经过衰减后累积起来就得到最终的云层结果。

从上面的过程可以得知,最关键核心的就是采样点的处理,而《地平线零》团队将这块内容基本都扔到了云层密度函数中,这个函数会负责根据天气等参数来计算采样点的密度值等参数。在介绍这个函数之前,《地平线零》还设计了一个用于表示当前采样点在云层中的高度的归一化标量值,这个值后面会用于建模过程。

// Fractional value for sample position in the cloud layer .
float GetHeightFractionForPoint ( float3 inPosition, float2 inCloudMinMax )
{
  // Get global fractional position in cloud zone .
  float height_fraction = ( inPosition.z − inCloudMinMax.x ) / (inCloudMinMax.y − inCloudMinMax.x ) ;
  return saturate ( height_fraction ) ;
}

此外,为了后面跟噪声组合出所需要的云层形状,这里还提供了一个remapping函数,用于实现将某个density range转换到另一个density range:

// Utility function that maps a value from one range to another .
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 ) )
}

云层建模算法的第一步是从低频的PW噪声中构建出云层的基本形状,整个过程包括如下几个步骤:

  1. 对基本形状3D贴图(前述第一张噪声贴图)进行采样(采样坐标为当前采样点的世界坐标转换后的uvw),得到第一个通道为PW噪声,后面三个通道则为低频的Worley噪声数据
  2. 使用PW噪声来生成基本的云层形状
  3. PW噪声虽然能够提供一个比较合理的云层密度函数,但是在细节上表现就有所不足,因此这里会通过前面介绍过的remapping函数将后面三个通道的worley噪声添加到PW噪声的边缘上。这种做法既保证了PW噪声内部的同质性,同时还保证了边缘上细节的丰富程度。
  4. 基于天气贴图计算得到的云层类型属性,给出density-height函数。
  5. 将云层基本形状数据与Density-height函数相乘来得到最终的云层模型表现。

代码实现如下所示:

float SampleCloudDensity ( float3 p , float3 weather_data )
{
  // Read the low−frequency Perlin−Worley and Worley noises .
  float4 low_frequency_noises = tex3Dlod ( Cloud3DNoiseTextureA, Cloud3DNoiseSamplerA , float4 ( p, mip_level ) ).rgba ;

  // Build an FBM out of the low frequency Worley noises  that can be used to add detail to the 
  // low−frequency Perlin−Worley noise .
  float low_freq_FBM = ( low_frequency_noises.g ∗ 0.625 ) + ( low_frequency_noises.b ∗ 0.25 ) + ( low_frequency_noises.a ∗ 0.125 ) ;

  // define the base cloud shape by dilating it with the
  // low−frequency FBM made of Worley noise .
  float base_cloud = Remap (low_frequency_noises.r, − ( 1.0 − low_freq_FBM ) , 1.0 , 0.0 , 1.0 ) ;

  // Get the density−height gradient using the density height
  // function explained in Section 2.2 .
  float density_height_gradient = GetDensityHeightGradientForPoint ( p , weather_data ) ;

  // Apply the height function to the base cloud shape .
  base_cloud ∗= density_height_gradient ;
基本云层效果

得到云层基本形状之后,我们再从天气贴图中取出云层覆盖率(coverage)参数来为不同地点的云层进行随机化处理,整个过程分为两步完成:

  1. 为了保证在覆盖率参数动态变化的时候云层的生长动画更为真实,这里会用云层覆盖率对基本形状进行remapping处理来实现膨胀效果。
  2. 为了使得云层密度跟随覆盖率增长的过程更为美观,这里直接将覆盖率乘到云层密度上。
// Cloud coverage is stored in weather data’s red channel .
float cloud_coverage = weather_data.r ;

// Use remap to apply the cloud coverage a tt r ibu t e .
float base_cloud_with_coverage = Remap ( base_cloud, cloud_coverage , 1.0 , 0.0 , 1.0) ;

// Multiply the result by the cloud coverage attribute so
// that smaller clouds are lighter and more aesthetically pleasing .
base_cloud_with_coverage ∗= cloud_coverage ;
覆盖率调制后效果

通过覆盖率调制后,效果变得随机起来,不再跟之前一样重复样式太高。之后来为云层添加细节以实现水蒸气上升不稳定而产生的的小尺寸菜花云以及大气扰动导致的轻薄扭曲云层,这个过程分为三步完成:

  1. 使用uv动画控制的curl noise来对云层底部的采样坐标进行扰动,之后用扰动后的坐标来对高频3D噪声贴图进行采样用于实现大气流动导致的扰动效果。
  2. 用高频Worley噪声构造一个FBM来为云层边缘添加细节。
  3. 使用高频FBM噪声对上一步得到的基本云层形状数据进行收缩:在云层底部,用翻转后的Worley噪声来生成轻薄云效;在云层顶部,用Worley噪声来进行收缩会得到菜花状云效。
  // Add some turbulence to bottoms of clouds .
  p.xy += curl_noise.xy ∗ (1.0 − height_fraction ) ;

  // Sample high−frequency noises .
  float3 high_frequency_noises = tex3Dlod ( Cloud3DNoiseTextureB, Cloud3DNoiseSamplerB, float4 ( p ∗ 0.1 , mip_level ) ).rgb ;

  // Build−high frequency Worley noise FBM.
  float high_freq_FBM = ( high_frequency_noises.r ∗ 0.625 ) + ( high_frequency_noises.g ∗ 0.25 ) + ( high_frequency_noises.b ∗ 0.125 );

  // Get the height fraction for use with blending noise types over height .
  float height_fraction = GetHeightFractionForPoint ( p, inCloudMinMax ) ;

// Transition from wispy shapes to billowy shapes over height .
float high_freq_noise_modifier = mix ( high_freq_FBM, 1.0 − high_freq_FBM , saturate ( height_fraction ∗ 10.0));

// Erode the base cloud shape with the distorted high−frequency Worley noises .
float final_cloud = Remap ( base_cloud_with_coverage, high_freq_noise_modifier ∗ 0.2 , 1.0 , 0.0 , 1.0);

return final_cloud ;

最终的云层形状

经过上述步骤,云层的基本建模就完成了,不过为了能够让云动起来,还需要添加一些其他的处理。为了模拟云层从某个大气层海拔向另一个海拔转移过程的shearing效果(如下图所示),这里会使用采样点的海拔来对风力向量进行偏移处理。另外,前面用到的两张3D贴图也同样需要使用风力向量进行偏移并随着时间往上移动,不过两者的偏移速率要有所不同,这样得到的效果会更为真实

Shearing效果
// Wind settings .
float3 wind_direction = float3 (1.0 , 0.0 , 0.0) ;
float cloud_speed = 10.0;

// cloud top offset pushes the tops of the clouds along
// this wind direction by this many units .
float cloud_top_offset = 500.0;

// Skew in wind direction .
p += height_fraction ∗ wind_direction ∗ cloud_top_offset ;

// Animate clouds in wind direction and add a small upward bias to the wind direction .
p+= ( wind_direction + float3 (0.0 , 0.1 , 0.0) ) ∗ time ∗ cloud_speed ;

这段代码必须要在3D贴图采样之前完成(可以都放到CloudDensitySample函数中)。

2.5 Results

结果

通过上面的建模方法,可以得到如上图所示的建模结果,可以实现种类众多的云层效果。

3. Cloud Lighting

体积云的光照有众多的研究报告,不过到目前为止,大多需要使用大量的采样来输出较好的效果,而这会导致性能的严重下降,而《地平线零》则试图找到一个能同时兼顾效率与质量的光照方案。

《地平线零》给出的光照方案能够实现如下三种效果的模拟:

  1. 云层中的多次散射效果与方向光照明效果
  2. 正视太阳方向时云层上的银边效果
  3. 背向太阳观察时云层上的暗边效果
    分别如下面的三张小图所示:


    《地平线零》尝试复现的三种效果

3.1 Volumetric Scattering

当光线进入云层之后,大部分的光线会先与云层中的水滴以及冰粒交互发生多次反射折射之后才进入人眼[Van De Hulst 57],光子进入云层后可能会发生如下的三种作用:

光子与云层的三种作用
  1. 被水滴或者其他粒子比如灰尘等所吸收,这个过程称为extinction(消散)或者吸收(absorption)
  2. 穿出云层抵达人眼,这个称之为内散射in-scattering
  3. 穿出云层但是并未进入人眼,称为外散射out-scattering
    不考虑其中细节,可以直接使用比尔定律来模拟三种作用的累计输出效果。

3.2 比尔定律(Beer's Law)

比尔定律最开始是一个用于化学分析的工具,这个定律可以用于描述光线穿过一个材质后的衰减作用,如下图所示:

比尔定律:Transmittance / Optical-Depth

将比尔定律用于体积渲染上,可以用之来计算光照transmittance随着光学厚度的变化而变化的规律 [Wrenninge 13]。

如果我们要模拟的大气介质是非同质的,比如云层,那么我们就必须要沿着射线方向对光学厚度进行积分计算,这是电影行业模拟云层的通用模型,也是《地平线零》的云层光照的基本模型,用代码来描述的话,如下所述:

light_energy = exp ( − density_samples_along_light_ray ) ;

3.3 Henyey-Greenstein Phase Function

穿过云层的光线继续向前的概率会更高一些 [Pharr and Humphreys 10],而这时银边效果背后的物理原理

前向散射

Henyey-Greenstein相函数从1941年就被提出用于模拟光线与星际尘云交互后各个方向上散射强度与入射光方向夹角的依赖规律。而在体积渲染中,这个函数也可以用来模拟光线在特定的介质中沿着各个方向的散射概率函数。

《地平线零》使用了单个带有偏心率eccentricity(方向组件)的H-G相函数(g = 0.2)来模拟更多光线向前散射的效果:
p_{HG}(\theta, g) = \frac{1}{4\pi}\frac{1-g^2}{[1+g^2-2gcos(\theta)]^{3/2}}

代码实现:

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;
}

添加相函数前后效果对比如下图所示:

without & with phase function

3.4 In-Scattering Probability Function (Powdered Sugar Effect)

比尔定律是一个用于描述光照强度随着传播距离而衰减的定律,但是却无法模拟当观察方向与太阳光方向一致时的in-scattering效果,这个效果的表现就是云层表面的暗边效果,而这个效果跟糖粉或者面粉堆在一起时的光照效果一致,如下图所示:

暗边效果

这个效果在圆形云层的高密度区域会比较明显,由于这个效果的存在,使得云层的多个凸起之间的缝隙褶皱的亮度比凸起位置的亮度更高,而这是跟比尔定律相反的。

前面说过,in-scattering指的是在云层中经过多次散射折射后穿出云层进入人眼的光线在云层中所发生的作用,而这种作用不仅仅只发生在背阳面,同时也发生在如下图所示的向阳面:

向阳in-scattering

又由于大部分的光线在HG相函数的作用下会向前散射,因此这里需要一个足够长的光学厚度来促使光子传输方向发生180度的翻转成为可能。而云层边缘由于厚度不足,因此不太可能发生这种in-scattering,而云层中心由于厚度过高在比尔定律的作用下被全部消耗掉了,产生这种in-scattering的可能性也比较低。只有处于云层凸起中间位置的缝隙褶皱,发生这种in-scattering的可能性才大大增加,从而导致前面所说的暗边效应。

为了生成这种效果,《地平线零》尝试用概率来进行模拟。首先假设视线方向看向一个凹凸面,向云层的这个凹凸面发射的光线在凹凸面上以及周边的褶皱缝隙上是平行的,如下图所示:

暗边模拟示意图

如果对于处于凸起上某点后某个深度位置的点p1进行采样,以及对缝隙处某个同样深度的点p2进行采样,我们可以看到,按照同样的接收角度,p2点能够接收到的光线数目更多,因此就应该显得更为明亮。

在这种思想的指导下,《地平线零》给出了一个新的光强随距离变化的函数,由于这个作用跟比尔定律是完全相反的,因此这里直接使用比尔定律沿着中间数值翻转(称为反比尔定律)来给出模拟:

比尔定律以及反比尔定律

为了减小计算量,《地平线零》将两个函数合二为一,得到比尔-粉末函数(注意,在实现的时候,为了能够更加的逼近原始的归一化函数,这里会将两者乘积再乘上2):

powder_sugar_effect = 1.0 − exp ( − light_samples ∗ 2.0 ) ;
beers_law = exp ( − light_samples ) ;
light_energy = 2.0 ∗ beers_law ∗ powder_sugar_effect ;
Beer's-Powder function 结果对比

3.5 Rain Clouds

《地平线零》还添加了对雨云的支持。因为水滴密度更高,对光线的吸收作用更强,因此雨云比其他的低空云效要更暗一点。

前面说过,天气贴图中有一个叫做降水率的参数,这里可以使用这个参数来对云层材质的厚度进行调试来得到暗度更高的雨云效果。调制后的厚度会导致云层的密度增加,用这个密度来对Beer's-Powerder函数进行计算,就能得到期望的雨云效果了,结果如下图所示,其中变量p表示的就是降水率:

调制前后效果对比

3.6 Lighting Model Summary

总的来说,《地平线零》的云层光照系统结合了如下四点:

  1. 比尔定律
  2. HG相函数作用
  3. 糖粉面粉效果
  4. 雨云吸收增益作用

如果E表示的是光线的能量,d表示的是光线采样点位置的密度,p表示的是对雨云的吸收率增益的降水率,而g表示的是光照方向上的偏心率,\theta表示的是视线方向与光线方向的夹角的话,最终的光照模型可以用如下的公式来给出:

E = 2.0 \times e^{-dp} \times(1-e^{-2d}) \times \frac{1}{4\pi} \frac{1-g^2}{[1+g^2-2gcos(\theta)]^{3/2}}

4. Cloud Rendering

由于云层并不是充满整个相机的,为了同时兼顾渲染效率与渲染质量,就需要很清楚的知道在哪些地方进行采样计算。

4.1 Spherical Atmosphere

渲染的第一点就是要计算出云层采样的起始位置。由于地球是圆形的,因此cloud layer也是球形的,那么当观察者站在一个平整的表面(比如海面上),地球的弯曲效果就会使得云层随着距离的增加而向着地平线靠近。

为了模拟这种效果,《地平线零》的方案的设计的raymarching起始高度为1.5 KM,大气层厚度为3.5 KM,并使用一个球星相交测试来计算出raymarching的起始位置与结束位置,不过这种做法在视线接近水平的时候,会导致raymarching的路径变得非常的长,如果步进长度不变的话,就需要增加采样点,具体而言,对于相机头顶正上方的光线,采样数为64,而水平方向的采样点数目为128。另外,整个渲染过程还有一些其他的优化策略,可以使得raymarching能够更早的结束以进一步优化性能。

4.2 Ray March Optimizations

为了避免在每个采样位置都对云层密度函数进行计算,这里《地平线零》在距离云层较远的时候,会跳过高频密度函数的计算。由于云层密度函数是通过低频的PW噪声来表示基本形状,高频Worley噪声来对基本形状的边缘进行腐蚀处理来得到细节,这里所说的跳过高频密度函数计算,指的就是不对高频的Worley噪声进行采样,如下图所示,在进入云层前区域以及穿出云层后区域,都会应用低频采样算法,这样可以极大的减少指令数,同时降低带宽消耗。

跳过高频噪声采样示意图

在代码实现上,会使用一个初始值为0的cloud_test变量来表示是否与云层发生碰撞,之后再循环过程中将这个变量用于判定当前采样点是否处于云层中的判定依据,只要此值一直是0,那么就继续按照低频采样的方式进行,直到碰到云层,这个数值变为非0,那么就转向高频采样模式,在高频采样模式中,如果连续六次采样密度为0,那么就继续转向低频采样模式,从而保证光线穿出云层后的实现性能足够低。

float density = 0.0;
float cloud_test = 0.0;
int zero_density_sample_count = 0;
// Start the main ray−march loop .
for ( int i = 0; i <sample_count ; i++)
{
  // cloud test starts as zero so we always evaluate the
  // second case from the beginning .
  if ( cloud_test > 0.0)
  {
    // Sample density the expensive way by setting the last parameter to false , indicating a full sample .
    float sampled_density = SampleCloudDensity ( p,  weather_data , mip_level , false ) ;
    // If we just samples a zero , increment the counter .
    if ( sampled_density = 0.0)
    {
      zero_density_sample_count ++;
    }
    // If we a redoing an expensive sample that is still potentially in the cloud :
    if ( zero_density_sample_count != 6)
    {
        density += sampled_density ;
        p += step ;
    } // If not , then set cloud test to zero so that we go back to the cheap sample case .
    else
    {
      cloud_test = 0.0;
      zero_density_sample_count = 0;
    }
  }
  else
  {
    // Sample density the cheap way , only using the low−frequency noise .
    cloud_test = SampleCloudDensity( p, weather_data, mip_level , true ) ;
    if ( cloud_test == 0.0 )
    {
      p += step ;
    }
  }
}

为了计算光照,在每个视角射线上的采样点上,还需要朝着光源方向进行一次额外的raymarching采样,因而需要更多的采样点。这些采样点的光照输入之和会根据当前视线上采样点的累计密度值进行衰减处理,示意图如下所示:

光照采样示意图

考虑到游戏硬件的情况,视线上的每个采样点朝向光源的raymarching steps不超过6,跟前面一样,我们也可以放弃视线上不在云层内的采样点沿着光源方向上的raymarching处理,实际证明这个优化对于质量的影响非常有限:

density += sampled_density ;
if ( sampled_density != 0.0 )
{
  // SampleCloudDensityAlongRay just walks in the given direction from the start point and takes X number o f lighting samples .
  density_along_light_ray = SampleCloudDensityAlongRay ( p )
}
p += step ;

4.3 Cone-Sampled Lighting

想要计算光源到视线上的某个采样点之间的光照强度,一个最明显的方式就是取得两点之间的Transmittance,不过实际上采样点的光照输入还会明显受到光源方向上的周围环境的影响,这个影响可以用一个漏斗来模拟,如下图所示:

锥形采样区域

某点的输入光强应该计算整个锥形区域方向上的输入光强,为了保证按照这种方式统计得到的光强依然遵循比尔定律,这里尝试将采样点分散到漏斗中,这样就可以利用周边射线的采样数据来进行加速。不过由于单条射线上的采样点数目减少了,使得采样质量直线下降,这种下降在锥形采样区域使用相邻射线的结果的作用下有所回升,但是依然无法回复最开始的质量,还是存在一定的带状瑕疵。

《地平线零》这里尝试对一个更低mip level进行采样来消除这种瑕疵。为了模拟漏斗中各个采样点的偏移,这里使用了一个六个噪声点的采样pattern,采样点的分布范围为[-1, -1, -1]到[1,1,1]。随着噪声点逐渐远离视线上的采样点,还会逐渐增加此噪声点的幅度。当沿着视线方向上的累计密度已经超过了一定的阈值,此时光源对于此点的贡献就可以使用一个常量来替代(这里使用的是0.3),在这种情况下,同样也会将光线方向的采样模式切换成低频采样模式来提升性能,从表现上来看, 质量损失非常有限。

static float3 noise_kernel [] =
{
  some noise vectors
}

// How wide to make the cone .
float cone_spread_multplier = length ( light_step ) ;

// A function to gather density in a cone for use with lighting clouds .
float SampleCloudDensityAlongCone ( p, ray_direction )
{
  float density_along_cone = 0.0;

  // Lighting ray−march loop .
  for ( int i=0; i<=6; i++)
  {
    // Add the current step offset to the sample position .
    p += light_step + ( cone_spread_multiplier ∗ noise_kernel [ i ] ∗ float ( i ) );

    if ( density_along_view_cone < 0.3)
    {
      // Sample cloud density the expensive way .
      density_along_cone += SampleCloudDensity( p, weather_data , mip_level + 1, false ) ;
    }
    else
    {
      // Sample cloud density the cheap way , using only one level of noise .
      density_along_cone += SampleCloudDensity( p, weather_data , mip_level + 1, true ) ;
    }
  }
}

此外,为了考虑距离当前采样点较远位置的其他云层的阴影效果,此处还会在在锥形半径的三倍距离处添加一个额外的采样点(共七个),如下图所示:

额外采样点机制

4.4 High Altitude Clouds

《地平线零》给出的体积云渲染方案只用在低空的层积云上面,对于高空的卷云等云种则是直接使用不断滚动的高清贴图来实现。 不过为了能够保证高空云效果与体积云能够实现完美的衔接,对这个贴图的采样过程需要放到raymarching的最后来进行。

这张高空云贴图的分辨率为512^2,共包含RGB三个通道,其滚动逻辑是根据风力方向来设计的,这里使用的风力方向跟之前体积云计算中的风力方向不是同一个,从而可以得到不同海拔高度上不同风力效果的表现:

高空云贴图的三个通道

4.5 Results

随着时间变化的最终云效

5. Conclusion and Future Work

《地平线零》给出的体积云效果可以实现非常逼真的实时云效,从而替换了此前业界通过模型来模拟云层的僵硬现状,这种做法可以有效降低包体尺寸,整套方案在GPU上的消耗大约为20 ms,但是通过TAA+分帧更新的策略,可以将这个消耗降低到2 ms左右 [Schneider 15, slide 91–93]。

体积云光照方案三点改进中关于暗边的in-scattering方案实际上是拍脑袋想出来的,不过后面可能会进一步加深这一块的研究。这里计划后面会采用 [Wrenninge 15]中给出的暴力方案(可以实现一个更为自然的暗边云效)来沿着视线方向上的射线进行数据搜集,之后对搜集到的数进行拟合,尝试给出一个拟合函数来更精确的模拟这个现象。

6. 参考文献

[Beer 52] A. Beer. “Bestimmung der Absorption des rothen Lichts in farbigen Fl¨ussigkeiten” (Determination of the Absorption of Red Light in Colored Liquids). Annalen der Physik und Chemie 86 (1852), 78–88.
[Clausse and Facy 61] R. Clausse and L. Facy. The Clouds. London: Evergreen Books, LTD., 1961.
[Henyey and Greenstein 41] L. G. Henyey and J. L. Greenstein. “Diffuse Radiation in the Galaxy.” Astrophysical Journal 93 (1941), pp. 78–83.
[Mandelbrot and van Ness 68] B. Mandelbrot and J. W. van Ness. “Fractional Brownian Motions, Fractional Noises and Applications.” SIAM Review 10:4 (1968), 422–437.
[Perlin 85] K. Perlin. “An Image Synthesizer.” In Proceedings of the 12th Annual Conference on Computer Graphics and Interactive Techniques, pp. 287–296. New York: ACM Press, 1985.
[Pharr and Humphreys 10] M. Pharr and G. Humphreys. Physically Based Rendering: From Theory to Implementation. Boston: Morgan Kaufmann, 2010.
[Quilez 13] I. Quilez. “Clouds.” Shadertoy.com, https://www.shadertoy.com/view/xslgrr, 2013.
[Schneider 15] A. Schneider. “The Real-Time Volumetric Cloudscapes Of Horizon: Zero Dawn.” Paper presented at ACM SIGGRAPH, Los Angeles, CA, August 26, 2015.
[Simul 13] Simul. “TrueSKY.” http://simul.co/truesky/, 2013.
[Van De Hulst 57] H. Van De Hulst. Light Scattering by Small Particles. New York: Dover Publications, 1957.
[Worley 96] Steven S. Worley. “A Cellular Texture Basis Function.” In Proceedings of the 23rd Annual Conference on Computer Graphics and Interactive Techniques, pp. 291–294. New York: ACM Press, 1996.
[Wrenninge 13] M. Wrenninge. Production Volume Rendering: Design and Implementation. Boca Raton, FL: CRC Press, 2013.
[Wrenninge 15] M. Wrenninge. “Art-Directable Multiple Volumetric Scattering.” In ACM SIGGRAPH 2015 Talks, article no. 24. New York: ACM Press, 2015.

上一篇下一篇

猜你喜欢

热点阅读