虚幻4渲染编程(环境模拟篇)【第四卷:体积云天空模拟(4) - 基于Texture的体积云】...

本文介绍了使用预烘焙技术在虚幻4中实现体积云渲染,通过Weather-map贴图存储云朵信息,结合3D噪声纹理增加细节。采用非等距步长Marching方法进行空间追踪,并利用Henyey-Greenstein光照模型提升效果。通过调整参数和添加 jitter 技术解决摩尔纹问题,实现了更自然的云层交互效果。

MY BLOG DIRECTORY:

小IVan:专题概述及目录

INTRODUCTION:

前面三节分别介绍了高度场体积云,3D纹理体积云,大面积程序化生成的体积云天空,某种程度上说前面的三节是非常初级的实现方案,因为它存在太多问题。最突出的一个问题就是可控性极差,因为可控性差也就导致了美术表现无法控制等一系列问题。如果想要进一步提高效果,就需要加入更多计算,性能下降显著,几乎是指数下降。所以我考虑使用预烘焙的方式,将大量的计算烘焙到贴图里。


MAIN CONTENT:

[1]Modeling

渲染体积云的一个主要思路是先建立体积场把云朵“建模”出来,然后考虑光照,然后考虑变化。不同于前面几篇文章的实现方法,这篇使用预计算的方式。所以我们首先需要准备一张名为Weather-map的贴图来存放我们的数据。

(1)Weather-map and Cloud Noise Texture 3D

在创建细节丰富的体积云朵之前需要先为其创建一个低频的空间范围,然后再往这个空间上叠各种细节信息。weather map将负责完成这个信息的储存。

v2-4963925c73512347604e5086949b1dd8_b.jpg

R通道储存云朵会出现的位置,G通道用来存储高频一点的云朵位置信息,B通道储存云朵的云峰的高度,A通道储存云朵在世界空间种的高度。

v2-9e92f8a53ef998bec1b04146c554a6be_b.jpg

在开始Marching前我们需要确定的一点是,我们是在贴图空间种做Marching的,所以我们需要把世界空间转换到UV空间。

RayPos = (WorldPos - ActorPos) / (MarchingBoxScale * 100) + 0.5

Coverage = WeatherMap.r

HeghtScale = WeatherMap.a

(1)基于求交检测后重定位Marching原点的方法:

v2-f7999860d3ac0f52ac5d3b9a38ec58d1_b.jpg

这种方法的优势是能Marching整个天空,缺点是没法穿到云里。必须要使用两个SphereHit或者两个PannerHit求到Marching的高度范围,如果只有一个平面的话云会有畸变,远处的云会被压扁。

v2-90fce56ae8700aecf278a30cd2a0c910_b.jpg

Weather-map的R通道用于控制云朵的位置,下面可以做个简单试验,绘制几个简单的方块:

v2-cb24e77877aacc86b22052a03fcd0196_b.jpg

代码如下:

v2-acb1846b8234369cfd8e58bd7133b2ab_b.jpg
v2-c156909ab54e3cc0ba4baaa78e2c088f_b.jpg

你将会看到如下效果:

v2-4915e626c709ae088dc601eb35a9dac9_b.gif

如果我们正常绘制简单的云朵的形状:

v2-f91e18243be99d441d0d59d3bc4b4f65_b.jpg

那么将会看到我们已经为体积云初步勾勒出了一个立体的空间范围:

v2-715739eb1a0dc4b154ec633eeaba2b60_b.jpg
v2-63b126d8cccc06540d9c9c9d6367fc46_b.jpg
v2-61fa296fd26e15197e405da26a0b5be8_b.jpg

(2)基于非等距步长Marching方法从摄像机原点开始Marching的方法

但是使用HeitSphere或者HitPane这种定位Marching起始点的方式是无法穿梭导云层里的,所以我们需要准备一个inversebox然后使用第三节里面的非等距距离追踪方法,从视口开始marching这样就可以穿梭到云层里了。然后使用WeatherMap的A通道来限制空间中云层的Z方向高度即可。

v2-6ad854980a69f12b5e67481e073f3eeb_b.jpg
v2-4f5f49eb61611005a8ab67b8e219f971_b.jpg
        float HeightScale = WeatherTex.SampleLevel(WeatherTexsampler2D, raypos.zz, 0).a;
      


(2)Add detail for cloud

分别准备一张WeatherNoiseTextureLow和WeatherNoiseTextureHeigh的3DNoiseTexture,分别在RGBA四个通道里叠加各种频率的噪声。

v2-413b55557322a4d9e7fe383bd8c41f00_b.jpg

R通道存放低频的Perlin-Worley,G通道存放中等频率的Worley-noise,B通道存放高频率的Worley-noise,A通道存放更高频率的Worley-noise

用这种方式可以方便地将一个FBM4存储在一张贴图里,并且一次Sample就把它采出来。

将WeatherNoise和WeatherNoiseTextureLow和WeatherNoiseTextureHeigh分别叠加后就可以得到我们的最基础的云朵形状了。

v2-218480ce528d4e552a2c8c410620c17e_b.jpg

代码如下:

v2-220b11cc87e5f15a69646f7d0ec80ec6_b.jpg

[2]Lighting

还是使用上一节说过的HenyeyGreenstein光照模型

v2-7d0bdf68cbbefd80512b743f46a0b8c8_b.jpg

在浓度积分的同时在做一次沿着太阳方向的积分即可。

                if(density > 0.001f)
        {
            float3 sundenspos = raypos;
            for (int j = 0; j < 6; j++)
            {
                sundensity += GetDensity(sundenspos, actorpos, weathertex, weathertexSampler, weathernoisetex, weathernoisetexSampler, weathercloudtex, weathercloudtexSampler);

                sundenspos += SunDir * Marching_Size;
            }
        }
      


至此完了了天空渲染大方面的探索,剩下的是对上述四个方案的完善提高,比如Noise的叠加方式及优化。我还是觉得把它集成到引擎渲染管线里比较省,现在我把所有计算全部堆在PixleShader里的确不是一个好的选择。

下面是我的代码:

        #define Marching_Steps 136
#define Marching_Size 20000.0f
#define Marching_Box_Scale 200000.0f
#define MarchingSizeReScale 1.2f
#define GlobalCoverage 0.9f
#define M_PI 3.141592f;

#define SunDir float3(1, 1, 1)
#define BeerTerm 0.5f
#define InScatter 0.6f
#define OutScatter 0.7f
#define ExtraEdgeInt 0.5f
#define ExtraEdgeExp 1.2f
#define MixScatter 0.5f

float remap(float V, float L0, float H0, float Ln, float Hn)
{
    return Ln + (V - L0) * (Hn - Ln) / (H0 - L0);
}

float3 ScaleWorldSpaceToTextureSpace(float3 WorldSapcePosition, float3 actorpos)
{
    return ((WorldSapcePosition - actorpos) / (Marching_Box_Scale * 100) * MarchingSizeReScale) + 0.5f;
}

float HenyeyGreenstein(float cos_angle, float g)
{
    float g2 = g * g;
    return ((1.0f - g2) / pow((1.0f + g2 - 2.0f * g * cos_angle), 1.5f)) / 4 * M_PI;
}

float GetDensity(float3 raypos, float3 actorpos, Texture2D WeatherTex, SamplerState WeatherTexsampler2D,
Texture3D weathernoisetex, SamplerState weathernoisetexSampler, Texture3D weathercloudtex, SamplerState weathercloudtexSampler)
{
	//Marching in tangent space
    raypos = ScaleWorldSpaceToTextureSpace(raypos, actorpos);
	//The density of cloud
    float density = 0;

    float3 WeatherData = WeatherTex.SampleLevel(WeatherTexsampler2D, raypos.xy, 0).rgb;
    float HeightScale = WeatherTex.SampleLevel(WeatherTexsampler2D, raypos.zz, 0).a;
    HeightScale = saturate(remap(HeightScale, 0, 0.07, 0, 1));
    float cloudcover = WeatherData.r;

    float4 LowFrecNoise = weathernoisetex.SampleLevel(weathercloudtexSampler, frac(raypos * 10), 0);
    float lowfreqFBM = LowFrecNoise.g * 0.625f + LowFrecNoise.b * 0.25f + LowFrecNoise.a * 0.125f;
    float cloudbase = saturate(remap(LowFrecNoise.r, -(1 - lowfreqFBM), 1, 0, 1));

    cloudcover = pow(cloudcover, saturate(remap(0.5, 0.65, 0.95, 1, GlobalCoverage)));
    float basecloudcolver = saturate(remap(cloudbase, 1 - cloudcover, 1, 0, 1));

    float3 highFreqNoise = weathercloudtex.SampleLevel(weathernoisetexSampler, frac(raypos * 20), 0);
    float highFreqFBM = (highFreqNoise.r * 0.625f) + (highFreqNoise.g * 0.25f) + (highFreqNoise.b * 0.125f);
    float highFreqNoiseModifier = saturate(remap(highFreqNoise.r, highFreqFBM, 1 - highFreqFBM, 0, 1));
    highFreqNoiseModifier *= 0.35 * exp(-GlobalCoverage * 0.75);

    density = saturate(remap(basecloudcolver, highFreqFBM, 1, 0, 1)) * HeightScale;
    //density = cloudcover * HeightScale;

    return density;
}

float4 mainImage(float2 viewsize, float time, float3 ro, float3 actorpos, float3 wpos, Texture2D weathertex, SamplerState weathertexSampler, Texture3D weathernoisetex, SamplerState weathernoisetexSampler, Texture3D weathercloudtex, SamplerState weathercloudtexSampler, float4 DebugData)
{
    float4 finalcol = float4(0, 0, 0, 0);
    float3 rd = normalize(wpos - ro);
    float3 raypos = ro;

    float cos_angle = dot(SunDir, rd);

    float density = 0.0f;
    float sundensity = 0.0f;
    for (int i = 0; i < Marching_Steps; i++)
    {
        density += GetDensity(raypos, actorpos, weathertex, weathertexSampler, weathernoisetex, weathernoisetexSampler, weathercloudtex, weathercloudtexSampler);

		if(density > 0.001f)
        {
            float3 sundenspos = raypos;
            for (int j = 0; j < 6; j++)
            {
                sundensity += GetDensity(sundenspos, actorpos, weathertex, weathertexSampler, weathernoisetex, weathernoisetexSampler, weathercloudtex, weathercloudtexSampler);

                sundenspos += SunDir * Marching_Size;
            }
        }

        raypos += rd * Marching_Size * exp(0.015 * i);
    }

    float edgeLEx = ExtraEdgeInt * pow(saturate(cos_angle), ExtraEdgeExp);
    float scatter = lerp(max(HenyeyGreenstein(cos_angle, InScatter), edgeLEx), HenyeyGreenstein(cos_angle, -OutScatter), MixScatter);

    float AttenPrim = exp(-BeerTerm * sundensity);

    finalcol.r = density;
    finalcol.g = sundensity * AttenPrim;

    return finalcol;
}

//return  mainImage(viewsize, time, ro, actorpos, wpos, weathertex, weathertexSampler, weathernoisetex, weathernoisetexSampler, weathercloudtex, weathercloudtexSampler,  DebugData);
      


有了上述的研究之后,我对效果进行了进一步改进

v2-13a3cd981938a4fa91a12124a94d9109.jpg https://www.zhihu.com/video/1150731234710814720

因为云层采样始终都有摩尔纹,所以我对每步做了jitter就是给步长加了个噪波缩放

        float hash(float3 p)
{
    p = frac(p * 0.3183099 + 0.1);
    p *= 17.0;
    return frac(p.x * p.y * p.z * (p.x + p.y + p.z));
}

float Noise(in float3 x)
{
    float3 p = floor(x);
    float3 f = frac(x);
    f = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(lerp( hash(p + float3(0,0,0)), 
                        hash(p + float3(1,0,0)),f.x),
                   lerp( hash(p + float3(0,1,0)), 
                        hash(p + float3(1,1,0)),f.x),f.y),
               lerp(lerp( hash(p + float3(0,0,1)), 
                        hash(p + float3(1,0,1)),f.x),
                   lerp( hash(p + float3(0,1,1)), 
                        hash(p + float3(1,1,1)),f.x),f.y),f.z);
}

float map5(in float3 p)
{
    float3 q = p;
    float f = 0.0f;
    f  = 0.50000 * Noise( q );
    q = q * 2.02;
    f += 0.25000 * Noise( q );
    q = q * 2.03;
    f += 0.12500 * Noise( q );
    q = q * 2.01;
    f += 0.06250 * Noise( q );
    q = q * 2.02;
    f += 0.03125 * Noise( q );
	return clamp(f, 0.0, 1.0);
}

float map3( in float3 p )
{
    float3 q = p;
    float f = 0.0f;
    f  = 0.50000 * Noise( q );
    q = q * 2.02;
    f += 0.25000 * Noise( q );
    q = q * 2.03;
    f += 0.12500 * Noise( q );
	return clamp(f, 0.0, 1.0);
}

float remap(float V, float L0, float H0, float Ln, float Hn)
{
    return Ln + (V - L0) * (Hn - Ln) / (H0 - L0);
}

float3 ScaleWorldSpaceToTextureSpace(float3 WorldSapcePosition, float3 actorpos)
{
    float Marching_Box_Scale = 1000.0f;
    float MarchingSizeReScale = 1.0f;
    return ((WorldSapcePosition - actorpos) / (Marching_Box_Scale * 100) * MarchingSizeReScale) + 0.5f;
}

float GetDensity(
    float3 raypos, 
    float3 actorpos, 
    Texture2D WeatherTex, 
    SamplerState WeatherTexsampler2D,
    Texture3D NoiseTex, 
    SamplerState NoiseTexSampler,
    Texture3D DetailNoiseTex, 
    SamplerState DetailNoiseTexSampler,
    float time
)
{
    float Density = 0.01;
    float2 speed = time * 0.001;

    float ActorMax = 3000.0f;
    float ActorMin = 1000.0f;
    float HeightGradient = clamp(raypos.z, 0, ActorMax) / (ActorMax);
    
    raypos = ScaleWorldSpaceToTextureSpace(raypos, actorpos);

	//Texture Tiles
    float LowFreqNoiseScale = 10;
    float HighFreqNoiseScale = 50;
    float WeatherTextureScale = 0.8;

    float3 WeatherData = WeatherTex.SampleLevel(WeatherTexsampler2D, raypos.xy * WeatherTextureScale, 0).rgb;
    float LowCoverage = WeatherData.r;
    float HighCoverage = WeatherData.g;
    float CloudPeakData = WeatherData.b;
    float CoudWeatherNoiseHigh = WeatherData.g;

    float ShapeAltering = saturate(remap(HeightGradient, 0, 0.07, 0, 1)) * saturate(remap(HeightGradient, WeatherData.b * 0.2, WeatherData.b, 1, 0));

    float4 LowFrecNoise = NoiseTex.SampleLevel(NoiseTexSampler, frac(raypos * LowFreqNoiseScale), 0);
    float lowfreqFBM = LowFrecNoise.r * 0.5f + LowFrecNoise.g * 0.25f + LowFrecNoise.b * 0.125f + LowFrecNoise.a * 0.064f;
    lowfreqFBM = smoothstep(0.5, 0.55, lowfreqFBM);
    lowfreqFBM = lerp(lowfreqFBM, 1 - lowfreqFBM, HeightGradient);

    float4 HighFrecNoise = NoiseTex.SampleLevel(NoiseTexSampler, frac(raypos * HighFreqNoiseScale), 0);
    float HighfreqFBM = HighFrecNoise.r * 0.5f + HighFrecNoise.g * 0.25f + HighFrecNoise.b * 0.125f + HighFrecNoise.a * 0.064f;
    HighfreqFBM = smoothstep(0.5, 0.55, HighfreqFBM);
    HighfreqFBM = lerp(lowfreqFBM, 1 - HighfreqFBM, HeightGradient);

    LowCoverage = smoothstep(0.45, 0.75, LowCoverage);
    HighCoverage = smoothstep(0.15, 0.75, HighCoverage);
    ShapeAltering = smoothstep(0.01, 0.75, ShapeAltering);

    float Coverage = max(LowCoverage, saturate(LowCoverage.r - 0.5) * 2 * HighCoverage);
    float shape = HighfreqFBM * Coverage;
    
    Density = ShapeAltering * shape;// * HeightScale;

    return Density;
}

float4 MainImage(
    FMaterialPixelParameters Parameters,
    Texture2D WeatherMap, 
    SamplerState WeatherMapSampler, 
    Texture3D NoseTex, 
    SamplerState NoseTexSampler, 
    Texture3D DetailNoiseTex, 
    SamplerState DetailNoiseTexSampler
)
{
    float time = View.GameTime;
    float2 ViewSize = View.ViewSizeAndInvSize.xy;
    float3 ro = ResolvedView.WorldCameraOrigin;
    float3 actorpos = GetActorWorldPosition(Parameters.PrimitiveId);
    float3 wpos = GetWorldPosition(Parameters);

    float4 Output = float4(0, 0, 0, 0);
    float3 rd = normalize(wpos - ro);
    float3 raypos = ro;

    float MaxDistance = 100000.0f;
    float PerStepSize = 10.0f;
    int Marching_Steps = 256;
    int Sun_Marching_Steps = 8;
    float Marching_Size = MaxDistance / Marching_Steps;
    float3 SunDir = normalize(float3(0.2, 0.2, 1));
    float BeerTerm = 0.6f;

    float density = 0;
    float SunDensity = 0;
    for(int step = 0; step < Marching_Steps; step++)
    {
        density += GetDensity(raypos, 
                              actorpos, 
                              WeatherMap, 
                              WeatherMapSampler, 
                              NoseTex, 
                              NoseTexSampler,
                              DetailNoiseTex,
                              DetailNoiseTexSampler,
                              time
                            );
        if(density > 0.001f)
        {
            float3 SunDensPos = raypos;
            
            for(int SunStep = 0; SunStep < Sun_Marching_Steps; SunStep++)
            {
                 SunDensity += GetDensity(
                                     SunDensPos, 
                                     actorpos, 
                                     WeatherMap, 
                                     WeatherMapSampler, 
                                     NoseTex, 
                                     NoseTexSampler,
				     DetailNoiseTex,
				     DetailNoiseTexSampler,
                                     time
                                    );
                //jitter the sample step length
                SunDensPos += SunDir * PerStepSize * exp(0.02 * step) * map3(SunDensPos);
            }
        }

        if(density > 1)
            break;

        //jitter the sample step length
        raypos += rd * PerStepSize * exp(0.02 * step) * map3(raypos);
    }

    float AttenPrim = exp(-BeerTerm * SunDensity);
    density = saturate(density);
    Output.rgb = AttenPrim; 

    Output.a = density;

    return Output;
}
/*
return MainImage(
    Parameters, 
    WeatherMap, 
    WeatherMapSampler, 
    NoseTex, 
    NoseTexSampler, 
    DetailNoiseTex, 
    DetailNoiseTexSampler
);
*/
      

这样就能避免采样摩尔纹了。

要做云于物体的交互只需要采一下深度就可以了

v2-7a2aa9e06181c817fa651ae7f07e8f7a.jpg https://www.zhihu.com/video/1150778003511390208
v2-aeb26122411ee9b5cf92b09f63abbd0d_b.jpg
v2-5ff9bb3f7a54c9e26e5e790bf9c65a49_b.jpg

最后用Henyey-Greenstein's phase function效果如下:

v2-059f035bc39952e9c89aaba9f055e9b3.jpg https://www.zhihu.com/video/1153647080408084480

代码如下:

        float hash(float3 p)
{
    p = frac(p * 0.3183099 + 0.1);
    p *= 17.0;
    return frac(p.x * p.y * p.z * (p.x + p.y + p.z));
}

float Noise(in float3 x)
{
    float3 p = floor(x);
    float3 f = frac(x);
    f = f * f * (3.0 - 2.0 * f);

    return lerp(lerp(lerp( hash(p + float3(0,0,0)), 
                        hash(p + float3(1,0,0)),f.x),
                   lerp( hash(p + float3(0,1,0)), 
                        hash(p + float3(1,1,0)),f.x),f.y),
               lerp(lerp( hash(p + float3(0,0,1)), 
                        hash(p + float3(1,0,1)),f.x),
                   lerp( hash(p + float3(0,1,1)), 
                        hash(p + float3(1,1,1)),f.x),f.y),f.z);
}

float map5(in float3 p)
{
    float3 q = p;
    float f = 0.0f;
    f  = 0.50000 * Noise( q );
    q = q * 2.02;
    f += 0.25000 * Noise( q );
    q = q * 2.03;
    f += 0.12500 * Noise( q );
    q = q * 2.01;
    f += 0.06250 * Noise( q );
    q = q * 2.02;
    f += 0.03125 * Noise( q );
	return clamp(f, 0.0, 1.0);
}

float map3( in float3 p )
{
    float3 q = p;
    float f = 0.0f;
    f  = 0.50000 * Noise( q );
    q = q * 2.02;
    f += 0.25000 * Noise( q );
    q = q * 2.03;
    f += 0.12500 * Noise( q );
	return clamp(f, 0.0, 1.0);
}

float remap(float V, float L0, float H0, float Ln, float Hn)
{
    return Ln + (V - L0) * (Hn - Ln) / (H0 - L0);
}

float3 ScaleWorldSpaceToTextureSpace(float3 WorldSapcePosition, float3 actorpos)
{
    float Marching_Box_Scale = 5000.0f;
    float MarchingSizeReScale = 1.0f;
    return ((WorldSapcePosition - actorpos) / (Marching_Box_Scale * 100) * MarchingSizeReScale) + 0.5f;
}

float HG(float cos_angle, float g)
{
    float g2 = g * g;
    float val = (1 - g2) / (pow((1 + g2 - 2 * g * cos_angle), 1.5) * (4 * 3.1415927));
    return val;
}

float InOutScater(float cos_angle, float SunDensity)
{
    float cloud_inscatter = 0.8f;
    float cloud_silver_intensity = 10.0f;
    float cloud_silver_exponent = 0.5f;
    float cloud_outscatter = 0.6f;
    float cloud_in_vs_outscatter = 0.5f;

    float first_hg = HG(cos_angle, cloud_inscatter);
    float second_hg = cloud_silver_intensity * HG(cos_angle, cloud_inscatter) * exp(-0.8 * SunDensity);
    float in_scatter_hg = max(first_hg, second_hg);
    float out_scatter_hg = HG(cos_angle, -cloud_outscatter);
    return lerp(in_scatter_hg, out_scatter_hg, cloud_in_vs_outscatter);
}

float Attenuation(float DensityToSun, float cos_angle, float BeerTerm)
{
    float CouldAttuentionClampVal = 0.8;
    float prim = exp(-BeerTerm * DensityToSun);
    float scnd = exp(-BeerTerm * CouldAttuentionClampVal) * 0.7;

    float checkVal = remap(cos_angle, 0, 1, 0, scnd * 0.5);

    return max(checkVal, prim);

}

float GetDensity(
    float3 raypos, 
    float3 actorpos, 
    Texture2D WeatherTex, 
    SamplerState WeatherTexsampler2D,
    Texture3D NoiseTex, 
    SamplerState NoiseTexSampler,
    Texture3D DetailNoiseTex, 
    SamplerState DetailNoiseTexSampler,
    float time
)
{
    float Density = 0.01;
    float2 speed = time * 0.001;

    float ActorMax = 5000.0f;
    float ActorMin = 2000.0f;
    float HeightGradient = clamp(raypos.z - ActorMin, 0, ActorMax) / (ActorMax);
    
    raypos = ScaleWorldSpaceToTextureSpace(raypos, actorpos);

	//Texture Tiles
    float LowFreqNoiseScale = 70;
    float HighFreqNoiseScale = 100;
    float WeatherTextureScale = 1.5;
    float CloudPeakDataOffset = 0.01;

    float3 WeatherData = WeatherTex.SampleLevel(WeatherTexsampler2D, raypos.xy * WeatherTextureScale, 0).rgb;
    float LowCoverage = WeatherData.r;
    float HighCoverage = WeatherData.g;
    float CloudPeakData = WeatherData.b;
    float CoudWeatherNoiseHigh = WeatherData.g;

    float ShapeAltering = saturate(remap(HeightGradient, 0, 0.07, 0, 1)) * saturate(remap(HeightGradient, WeatherData.b * 0.2, WeatherData.b, 1, 0));
    ShapeAltering *= CloudPeakData * (1 + CloudPeakDataOffset);

    float4 LowFrecNoise = NoiseTex.SampleLevel(NoiseTexSampler, frac(raypos * LowFreqNoiseScale), 0);
    float lowfreqFBM = LowFrecNoise.r * 0.7f + LowFrecNoise.g * 0.3f;
    float lowfreqFBMSec = LowFrecNoise.b * 0.6f + LowFrecNoise.a * 0.4f;
    lowfreqFBM = smoothstep(0.5, 0.55, lowfreqFBM);
    lowfreqFBM = lerp(lowfreqFBM, 1 - lowfreqFBM, HeightGradient);
    lowfreqFBM = remap(lowfreqFBM, (1 - lowfreqFBMSec) * 0.5, 1, 0, 1);

    float4 HighFrecNoise = NoiseTex.SampleLevel(NoiseTexSampler, frac(raypos * HighFreqNoiseScale), 0);
    float HighfreqFBM = HighFrecNoise.r * 0.5f + HighFrecNoise.g * 0.25f + HighFrecNoise.b * 0.125f + HighFrecNoise.a * 0.064f;
    HighfreqFBM = smoothstep(0.5, 0.55, HighfreqFBM);

    LowCoverage = smoothstep(0.15, 0.85, LowCoverage);
    HighCoverage = smoothstep(0.15, 0.85, HighCoverage);
    ShapeAltering = smoothstep(0.01, 0.75, ShapeAltering);

    float Coverage = max(LowCoverage, saturate(LowCoverage.r - 0.5) * 2 * HighCoverage);
    float shape = remap(lowfreqFBM, HighfreqFBM - 1, 1, 0, 1) * Coverage;
    shape = smoothstep(0.3, 0.65, shape);

    Density = ShapeAltering * shape;// * HeightScale;

    return Density;
}

float4 MainImage(
    FMaterialPixelParameters Parameters,
    Texture2D WeatherMap, 
    SamplerState WeatherMapSampler, 
    Texture3D NoseTex, 
    SamplerState NoseTexSampler, 
    Texture3D DetailNoiseTex, 
    SamplerState DetailNoiseTexSampler
)
{
    float time = View.GameTime;
    float2 ViewSize = View.ViewSizeAndInvSize.xy;
    float3 ro = ResolvedView.WorldCameraOrigin;
    float3 actorpos = GetActorWorldPosition(Parameters.PrimitiveId);
    float3 wpos = GetWorldPosition(Parameters);
    float SceneDepth = CalcSceneDepth(ScreenAlignedPosition(GetScreenPosition(Parameters))).r;

    float4 Output = float4(0, 0, 0, 0);
    float3 rd = normalize(wpos - ro);
    float3 raypos = ro;

    float MaxDistance = 100000.0f;
    float PerStepSize = 10.0f;
    int Marching_Steps = 256;
    int Sun_Marching_Steps = 8;
    float Marching_Size = MaxDistance / Marching_Steps;
    float3 SunDir = normalize(float3(0.2, 0.2, 1));
    float BeerTerm = 0.6f;

    float density = 0;
    float SunDensity = 0;
    float MarchingDepth = 0;
    for(int step = 0; step < Marching_Steps; step++)
    {
        density += GetDensity(raypos, 
                              actorpos, 
                              WeatherMap, 
                              WeatherMapSampler, 
                              NoseTex, 
                              NoseTexSampler,
                              DetailNoiseTex,
                              DetailNoiseTexSampler,
                              time
                            );
        if(density > 0.001f)
        {
            float3 SunDensPos = raypos;
            
            for(int SunStep = 0; SunStep < Sun_Marching_Steps; SunStep++)
            {
                 SunDensity += GetDensity(
                                     SunDensPos, 
                                     actorpos, 
                                     WeatherMap, 
                                     WeatherMapSampler, 
                                     NoseTex, 
                                     NoseTexSampler,
				     DetailNoiseTex,
				     DetailNoiseTexSampler,
                                     time
                                    );

                SunDensPos += SunDir * PerStepSize * exp(0.02 * step) * map3(SunDensPos);
            }
        }

        if(density > 1)
            break;

		
        float CurStepSize = PerStepSize * exp(0.02 * step) * map3(raypos);
        raypos += rd * CurStepSize;
        MarchingDepth += CurStepSize;

        if (MarchingDepth >= SceneDepth)
            break;

    }

    float cos_angle = dot(rd, SunDir);

    float AttenPrim = Attenuation(SunDensity, cos_angle, BeerTerm);
    density = saturate(density);
    Output.rgb = AttenPrim * InOutScater(cos_angle, SunDensity);

    Output.a = density;

    return Output;
}
/*
return MainImage(
    Parameters, 
    WeatherMap, 
    WeatherMapSampler, 
    NoseTex, 
    NoseTexSampler, 
    DetailNoiseTex, 
    DetailNoiseTexSampler
);
*/
      

材质如下:

v2-ab8a7a2b0e11c2dc62451fb9587876d2_b.jpg

我把我的3DNoise和WeatherTexture上传到这里:链接:文件分享

如果觉得云的底部太平了,可以修改如下参数:

v2-ab503b6c7a5e8b6e0154ccceb6ff9196_b.jpg

修改后云的底部会自然很多,效果如下:

v2-e52aabfc383a76ed02a4bae6087eb54f.jpg https://www.zhihu.com/video/1154492709203951616

最后把大气和高度雾加上:

v2-0e46cb60ff9d043d3be90e86d14098aa.jpg https://www.zhihu.com/video/1154495700200013824

SUMMARY AND OUTLOOK:

目前使用3D纹理来制作体积云海算是主流方法了,还有很多地方需要改进,比如形状控制和shading,难怪truesky能卖那么贵,其实是有理由的。

Enjoy it.


Next:

小IVan:虚幻4渲染编程(环境模拟篇)【第五卷:可交互物理植被模拟 - 上】

reference:

(1)diva-portal.org/smash/g

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值