找回密码
 立即注册
查看: 397|回复: 0

[笔记] 在Unity URP中实现Real-Time Volumetric Clouds(Part 2)

[复制链接]
发表于 2021-12-14 21:22 | 显示全部楼层 |阅读模式
前一篇文章主要介绍实现体积云的理论基础。这一篇主要介绍一下代码实现。
很遗憾的是《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 [0, 1] 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[5] = // 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),这个应该是错的,这样算下来图不就全是白色的嘛。



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-11 00:22 , Processed in 0.093092 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表