在Unity URP中实现Real-Time Volumetric Clouds(Part 2)
前一篇文章主要介绍实现体积云的理论基础。这一篇主要介绍一下代码实现。很遗憾的是《GPU Pro 7》中Real-Time Volumetric Cloudscapes文章只提供了部分代码片段,但并没有提供所有相关源码。我只能参考这些代码片段,以及另外一些开源实现,在Unity中实现体积云效果。
编辑器中效果如下:
鉴于自己实现的时候,并没有源码方便参考。我自己写的代码尽量参考原文中的命名和格式,方便感兴趣的同学对照学习。工程中也能找到两篇参考文章。
1. Cloud Sampler
1.1 GetHeightFractionForPoint
先介绍一下Cloud Sampler部分的辅助函数GetHeightFractionForPoint,获取采样点在云层中的高度百分比
原文中的代码如下
不过我觉得原文代码是错误的,第一个参数inPosition表示的是采样点位置坐标,但是为什么拿的是z轴的值去求高度啊?然道作者使用的Decima游戏引擎z轴是高度?文章里面其他部分并没有看到相关线索。
另外认为是错误的理由是这个函数并没有考虑到地球是圆形的情况,这里应该先算出采样点到地球圆心的距离,才可以算高度百分比,但是这里并没有。
下面是我的代码实现
// Fractional value for sample position in the cloud layer
float GetHeightFractionForPoint(float3 pos)
{
return saturate((distance(pos,_PlanetCenter) - (_SphereSize + _CloudHeightMinMax.x)) / _Thickness);
}1.2 GetDensityHeightGradientForPoint
原文中并未提供这个函数的代码,顾名思义这里是根据采样点位置获得云的浓度。从原文的其他代码中可以知道这个函数的第一个参数是sample position,我这里改成了height fraction,因为这个值已经求过了,没必要再算一遍。
float GetDensityHeightGradientForPoint(float height_fraction, float3 weather_data)
{
float cloudType = weather_data.g;
const float4 CloudGradient1 = float4(0.0, 0.065, 0.203, 0.371); //stratus
const float4 CloudGradient2 = float4(0.0, 0.156, 0.468, 0.674); //cumulus
const float4 CloudGradient3 = float4(0.0, 0.188, 0.818, 1); //cumulonimbus
float4 gradient = lerp(lerp(CloudGradient1, CloudGradient2, cloudType * 2.0), CloudGradient3, saturate(cloudType - 0.5) * 2.0);
return SampleGradient(gradient, height_fraction);
}
// samples the gradient
float SampleGradient(float4 gradient, float height)
{
return smoothstep(gradient.x, gradient.y, height) - smoothstep(gradient.z, gradient.w, height);
}这里跟原文实现不一样,我们weather_data第二个通道存储的是cloudType,做了简化处理,因为原文中cloudType存储在第三个通道,但是还是要依赖precipitation这个值来决定是cumulus还是cumulonimbus。
1.3 SmapleCloudDensity
float SampleCloudDensity(float3 pos, float height_fraction, float3 weatherData, float mip_level, bool is_cheap)
{
// cloud_top offset pushes the tops of the clouds along this wind direction by this many units
float cloud_top_offset = 500;
// Skew in wind direction
pos += height_fraction * _WindDirection * cloud_top_offset;
// Animate clouds in wind direction and add a small upward bias to the wind direction
pos += (_WindDirection + float3(0.0, 1.0, 0.0)) * _Time * _CloudSpeed;
// Read the low-frequency Perlin-Worley noise
float3 low_frequency_noises = tex3Dlod(_ShapeTexture, float4(pos * _Scale, mip_level)).rgb;
// define the base cloud shape
float base_cloud = Remap( low_frequency_noises.r * pow(1.2 - height_fraction, 0.1), _LowFreqMinMax.x, _LowFreqMinMax.y, 0.0, 1.0); // pick certain range from sample texture
// Get the density-height gradient using the density height function explained in Section 4.3.2
float density_height_gradient = GetDensityHeightGradientForPoint(height_fraction, weatherData);
// Apply the height function to the base cloud shape
base_cloud *= density_height_gradient;
// Cloud coverage is stored in weather data's red channel
float cloud_coverage = weatherData.r;
// Use remap to apply the cloud coverage attribute
float base_cloud_with_coverage = Remap(base_cloud, saturate(height_fraction / 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;
if (base_cloud_with_coverage > 0.0 && !is_cheap) // If cloud sample > 0 then erode it with detail noise
{
float3 curlNoise = mad(tex2Dlod(_CurlNoise, float4(pos.xz * _CurlDistortScale, 0, 0)).rgb, 2.0, -1.0); // sample Curl noise and transform it from to [-1, 1]
pos += float3(curlNoise.r, curlNoise.b, curlNoise.g) * height_fraction * _CurlDistortAmount; // distort position with curl noise
float detailNoise = tex3Dlod(_DetailTexture, float4(pos * _DetailScale, mip_level)).r; // Sample detail noise
float highFreqNoiseModifier = lerp(1.0 - detailNoise, detailNoise, saturate(height_fraction * 10.0)); // At lower cloud levels invert it to produce more wispy shapes and higher billowy
base_cloud_with_coverage = Remap(base_cloud_with_coverage, highFreqNoiseModifier * _HighFreqModifier, 1.0, 0.0, 1.0); // Erode cloud edges
}
return max(base_cloud_with_coverage * _SampleMultiplier, 0.0);
}这个函数跟原文不同地方在于,没有使用low-frequency噪声贴图的另外三个通道以及high-frequency噪声贴图的另外两个通道,因为我测试使用的数据贴图来自于另外一个开源项目,跟原文中格式有些不同。
1.4 SampleCloudDensityAlongCone
这个函数用于沿光线方向的一个漏斗形内采样云的密度,计算到达ray marching采样点时光的强度。
float SampleCloudDensityAlongCone(float3 pos, int mip_level, float3 lightDir)
{
const float3 RandomUnitSphere = // precalculated random vectors
{
{ -0.6, -0.8, -0.2 },
{ 1.0, -0.3, 0.0 },
{ -0.7, 0.0, 0.7 },
{ -0.2, 0.6, -0.8 },
{ 0.4, 0.3, 0.9 }
};
float heightFraction;
float densityAlongCone = 0.0;
const int steps = 5; // light cone step count
float3 weatherData;
for (int i = 0; i < steps; i++) {
pos += lightDir * _LightStepLength; // march forward
float3 randomOffset = RandomUnitSphere * _LightStepLength * _LightConeRadius * ((float)(i + 1));
float3 p = pos + randomOffset; // light sample point
// sample cloud
heightFraction = GetHeightFractionForPoint(p);
weatherData = sampleWeather(p);
densityAlongCone += SampleCloudDensity(p, heightFraction, weatherData, mip_level + 1, true);// * weatherDensity(weatherData);
}
pos += 32.0 * _LightStepLength * lightDir; // light sample from further away
weatherData = sampleWeather(pos);
heightFraction = GetHeightFractionForPoint(pos);
densityAlongCone += SampleCloudDensity(pos, heightFraction, weatherData, mip_level + 2, false);// * weatherDensity(weatherData) * 3.0;
return densityAlongCone;
} 1.5 Ray marching
Ray marching 函数基本按照原文的逻辑写的。暂未调整步进长度。
fixed4 Raymarch(float3 rayOrigin, float3 rayDirection, float stepSize, float steps, float cosAngle)
{
float3 pos = rayOrigin;
fixed4 res = 0.0; // cloud color
float lod = 0.0;
float3 stepVec = rayDirection * stepSize;
int sampleCount = steps;
float density = 0.0;
float cloud_test = 0.0;
int zero_density_sample_count = 0;
for (int i = 0; i < sampleCount; i++)
{
if (res.a >= 0.99) { // check if is behind some geometrical object or that cloud color aplha is almost 1
break;// if it is then raymarch ends
}
// sample weather
float3 weatherData = GetWeatherData(pos);
float heightFraction = GetHeightFractionForPoint(pos);
if (weatherData.r <= 0.1)
{
pos += stepVec;
zero_density_sample_count ++;
continue;
}
if (cloud_test > 0.0)
{
float sampled_density = SampleCloudDensity(pos, heightFraction, weatherData, lod, false);
if (sampled_density == 0.0)
{
zero_density_sample_count++;
}
if (zero_density_sample_count != 6)
{
density += sampled_density;
if (sampled_density != 0.0)
{
float4 particle = sampled_density; // construct cloud particle
float densityAlongCone = SampleCloudDensityAlongCone(pos, lod, _SunDir);
float totalEnergy = CalculateLightEnergy(densityAlongCone, cosAngle, sampled_density);
float3 directLight = _SunColor * totalEnergy;
float3 ambientLight = lerp(_CloudBaseColor, _CloudTopColor, heightFraction); // and ambient
directLight *= _SunLightFactor; // multiply them by their uniform factors
ambientLight *= _AmbientLightFactor;
particle.rgb = directLight + ambientLight; // add lights up and set cloud particle color
particle.rgb *= particle.a; // multiply color by clouds density
res = (1.0 - res.a) * particle + res; // use premultiplied alpha blending to acumulate samples
}
pos += stepVec;
}else
{
cloud_test = 0.0;
zero_density_sample_count = 0;
}
}else
{
cloud_test = SampleCloudDensity(pos, heightFraction, weatherData, lod, true);
if(cloud_test == 0.0){
pos += stepVec;
}
}
}
return res;
}以上部分实现以及测试贴图来自于jaagupku/volumetric-clouds,他的实现与我不同的是我是在skybox渲染时就渲染体积云,而他是放到相机后处理阶段。如果要学习weather贴图如何制作,也可以参考他的工程。
后记
我是先看的《GPU Pro 7》中Real-Time Volumetric Cloudscapes文章,然后尝试照着文章中提供的代码片段在Unity中实现。SIGGRAPH 2015中的分享PPT是最后才看到的,看完才觉得之前踩了很多的坑。
一,PPT中的示例图更加清楚明了,文章中的示例图做了一些节选,缺少了一些关键信息,导致不太容易理解。
二,文章中存在一些细节错误,导致多花了很多时间去理解。
[*]4.3 Cloud Modeling这一节中的Figure 4.2 示例图中,Cumulus和Cumulonimbus就拼写错误,写成了Comulus和Comulonimbus,让人大跌眼镜,这两个单词的拼写在PPT上是对的。
[*]4.3.1节中Perlin noise的翻转公式用的是abs(Perlin*2+1),这个应该是错的,这样算下来图不就全是白色的嘛。
页:
[1]