Trying to port cloud shader

I’m trying to port a cloud shader from the excellent Game Programming Gems series. Specifically, Game Programming Gems 5 “Realistic Cloud Rendering on Modern GPUs”. The idea is basically to raymarch a heightmap field representing your clouds, accumulating density and then approximating light scattering with that density value.
I’ve pored over my code and it looks exactly like the code presented in the book, but I can’t for the life of me seem to get it working quite right.

Shader "Custom/CloudShader" {
    Properties {
        _Color ("Light Color", Color) = (1,1,1,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _SunDir ("Sun Direction", Vector) = (0,1,0,0)
    }
    SubShader {
    Pass {
    
    Blend SrcAlpha OneMinusSrcAlpha


    CGPROGRAM
    #pragma target 3.0
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"


    sampler2D _MainTex;
    float4 _Color;
    float4 _SunDir;


    struct v2f {
        float4 pos : SV_POSITION;
        float4 tex : TEXCOORD0;
    };


    v2f vert (appdata_base v)
    {
        v2f o;
        o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
        o.tex = v.texcoord;
        return o;
    }


    half4 frag (v2f i) : COLOR
    {
        float4 tex = tex2D( _MainTex, i.tex.xy + float2( _Time.y * 0.01, 0 ) );


        float cloudCover = 0.55;
        float cloudSharp = 0.95;


        float Density = 0;


        tex = max( tex - cloudCover, 0 );


        //_SunDir = normalize( _SunDir );
        _SunDir.z *= 128;


        float3 EndTracePos = float3( i.tex.xy, -tex.r * 0.25 );
        float3 TraceDir = EndTracePos - _SunDir.xyz;
        TraceDir = normalize( TraceDir );
        float3 CurTracePos = _SunDir + TraceDir * 1.25;
        TraceDir *= 2.0;


        tex = pow(tex * 20, cloudSharp );


        for( int i = 0; i < 64; i++ )
        {
            CurTracePos += TraceDir;
            float4 tex2 = tex2D( _MainTex, CurTracePos.xy + float2( _Time.y * 0.01, 0 ) ) * 128;
            Density += 0.1 * step( CurTracePos.z*2, tex2.r*2 );
        }


        //Density = clamp( Density, 0, 1.5 );
        float Light = 1 / exp( Density * 0.2 ) * 1.75;
        //float Light = Density;
        return half4 (Light * _Color.r, Light * _Color.g, Light * _Color.b, tex.r);
    }
    ENDCG


    }
    }
}

It’s very frustrating. I feel like it’s very close, but not quite there. Maybe would help to know exactly what range of values “SunDir” is supposed to have. I unfortunately got my GPG used, and it doesn’t come with CD. The GPG website additionally appears to be what is scientifically referred to as “completely screwed”, so that’s no help for either getting the CD, or checking the errata.Anybody else done a similar shader and could help?

EDIT: For reference, here’s the original code snippet they provided:

[/FONT]
[FONT=Georgia]float Density = 0.0f;[/FONT]
[FONT=Georgia]float3 EndTracePos = float3(uv, -tex.r);[/FONT]
[FONT=Georgia]float3 TraceDir = EndTracePos - SunPos;[/FONT]
[FONT=Georgia]TraceDir = normalize(TraceDir);[/FONT]
[FONT=Georgia]float3 CurTracePos = SunPos + TraceDir * 1.25f;[/FONT]
[FONT=Georgia]TraceDir *= 2.0f;[/FONT]
[FONT=Georgia]for( int i = 0; i < 64; i++)[/FONT]
[FONT=Georgia]{[/FONT]
[FONT=Georgia]    CurTracePos += TraceDir;[/FONT]
[FONT=Georgia]    float4 tex2 = tex2D(DensityFieldTexture, CurTracePos.xy) * 255.0f;[/FONT]
[FONT=Georgia]    Density += 0.1f * step(CurTracePos.z *2, tex2.r*2);[/FONT]
[FONT=Georgia]}[/FONT]
[FONT=Georgia]float Light = 1.0f / exp(Scattering * 0.4f); // I replaced "Scattering" with "Density" in mine, I assume they're the same thing and this is a typo...???[/FONT]
[FONT=Georgia]

EDIT 2:
I now multiply SunDir z position by 256, which seems to work better.
Now feeding the value 0.5, 0.5, 1 seems to look right for some views - but in motion it looks pretty bad, the lighting acts like there’s some kind of parallax effect and often does not match the actual outline of the clouds.

Never mind, got it working :slight_smile:

Could you share it?

Keep in mind this shader’s gone through some major changes. Actually it’s currently designed to be plugged into the Time of Day asset (so for instance it assumes it is being mapped to a half-sphere and will implicitly calculate UVs based on that).

Shader "Custom/CloudShader" {
    Properties {
        _Color ("Light Color", Color) = (1,1,1,1)
        _Ambient ("Ambient Color", Color) = (0,0,0,0)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Jitter ("Jitter", 2D) = "white" {}
        _SunDir ("Sun Direction", Vector) = (0,1,0,0)
        _CloudCover ("Cloud Cover", Range(0,1)) = 0.5
        _CloudSharpness ("Cloud Sharpness", Range(0.2,0.99)) = 0.8
        _CloudDensity ("Density", Range(0,1)) = 1
        _CloudSpeed ("Cloud Speed", Vector) = (0.001, 0, 0, 0)
    }
    SubShader {
    Tags
        {
            "Queue"="Transparent-450"
            "RenderType"="Transparent"
            "IgnoreProjector"="True"
        }
    Pass {
    
    Blend SrcAlpha OneMinusSrcAlpha
    Cull Front
    ZWrite Off


    CGPROGRAM
    #pragma target 3.0
    #pragma vertex vert
    #pragma fragment frag
    #include "UnityCG.cginc"


    sampler2D _MainTex;
    sampler2D _Jitter;


    float4 _Color;
    float4 _Ambient;
    float4 _SunDir;
    float4 _CloudSpeed;


    float _CloudCover;
    float _CloudDensity;
    float _CloudSharpness;


    struct v2f {
        float4 pos : SV_POSITION;
        float4 tex : TEXCOORD0;
    };


    v2f vert (appdata_base v)
    {
        v2f o;
        o.pos = mul (UNITY_MATRIX_MVP, v.vertex);


        float3 vertnorm = normalize(v.vertex.xyz);
        float2 vertuv   = vertnorm.xz / (vertnorm.y + 0.1);
        o.tex = float4( vertuv.xy * 0.1, 0, 0 );
        return o;
    }


    float4 frag (v2f i) : COLOR
    {
        float2 offset = _Time.y * _CloudSpeed.xy;
        float4 tex = tex2D( _MainTex, ( i.tex.xy ) + offset );


        float Density = 0;


        //_SunDir = normalize( _SunDir );
        //_SunDir.z *= 256;


        
        tex = max( tex - ( 1 - _CloudCover ), 0 );


        float3 EndTracePos = float3( i.tex.xy, -tex.a * _CloudDensity );
        float3 TraceDir = EndTracePos - _SunDir.xyz;
        TraceDir = normalize( TraceDir );
        float3 CurTracePos = _SunDir + TraceDir * 384;
        TraceDir *= 4.0;


        float3 jitter = ( tex2D( _Jitter, ( i.tex.xy * 10 ) + offset ) * 2 - 1 ) * 0.002;


        tex.a = 1.0 - pow( _CloudSharpness, tex.a * 255 );


        for( int i = 0; i < 32; i++ )
        {
            CurTracePos += TraceDir;
            float4 tex2 = tex2D( _MainTex, ( CurTracePos.xy ) + offset + jitter ) * 256 * _CloudDensity;
            Density += 0.1 * step( CurTracePos.z*2, tex2.a*2 );
        }


        //Density = clamp( Density, 0, 1.5 );
        float Light = 1 / exp( Density * 0.4 );
        //float Light = Density;
        float4 res = float4 (Light * _Color.r, Light * _Color.g, Light * _Color.b, tex.a);
        res.xyz += _Ambient.xyz;
        return res;
    }
    ENDCG


    }
    }
}

_SunDir is a little wierd, as X and Y seem to work best in the -0.5 to 0.5 range. Z should pretty much stay locked at 550 I’ve found.
The shader takes as input a cloud texture (pretty much just a perlin noise texture) with alpha. I recommend Alpha 8 to save on space (don’t use compressed, it will look like crap). It also takes a Jitter texture, just a random noise texture (I think I took a 1024x1024 image and hit the Add Noise button in Photoshop for this). Jitter will make it look noisy, but also reduces banding due to the low-precision raymarch.

EDIT: Actually, one note: _SunDir X and Y are in UV space. So the range they should have depends on the range of UVs your cloud mesh has (or in this case, the range of vertex X and Z position).