Shader Example: Raymarching Cloud

Live Demo

Fragment Shader

Edit the shader and press Run (or Ctrl+Enter) to recompile. The source below the demo is the same text the playground is running.

Snippets

Shadertoy

// Shadertoy: 3D Raymarched Cloud Example
// iResolution, iTime, iMouse available
//
// Hold the left mouse button and drag to orbit: left and right turns, up and
// down moves the camera above or below the deck.

#define MAX_STEPS   112
#define LIGHT_STEPS 6
#define FAR         90.0
#define PI          3.14159265359

// The slab the cloud layer occupies. The march is clipped to it, so steps are
// spent inside the cloud rather than in the empty air in front of it.
#define CLOUD_BOTTOM -0.6
#define CLOUD_TOP     3.0

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

float noise(vec3 x)
{
    vec3 i = floor(x);
    vec3 f = fract(x);
    f = f * f * (3.0 - 2.0 * f);

    float n000 = hash(i + vec3(0,0,0));
    float n100 = hash(i + vec3(1,0,0));
    float n010 = hash(i + vec3(0,1,0));
    float n110 = hash(i + vec3(1,1,0));
    float n001 = hash(i + vec3(0,0,1));
    float n101 = hash(i + vec3(1,0,1));
    float n011 = hash(i + vec3(0,1,1));
    float n111 = hash(i + vec3(1,1,1));

    return mix(
        mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
        mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
        f.z
    );
}

float fbm(vec3 p)
{
    float v = 0.0;
    float a = 0.5;
    for (int i = 0; i < 5; i++)
    {
        v += a * noise(p);
        p *= 2.02;
        a *= 0.5;
    }
    return v;
}

float cloudDensity(vec3 p)
{
    // Move clouds over time
    p.xz += vec2(iTime * 0.6, iTime * 0.2);

    // The large shape decides both where there is cloud and how tall it stands.
    // Using it only as a density threshold inside a fixed slab gives an even
    // blanket with a flat top - which is what this did, and why side light
    // produced no shadows: there was nothing standing up to cast one.
    float shape = fbm(p * 0.32);
    float coverage = smoothstep(0.40, 0.72, shape);
    if (coverage <= 0.001) return 0.0;

    // Dense columns tower; thin ones stay low.
    float topY = mix(0.5, CLOUD_TOP, coverage);
    float heightMask = smoothstep(CLOUD_BOTTOM, 0.3, p.y)
                     * (1.0 - smoothstep(topY - 1.0, topY, p.y));
    if (heightMask <= 0.001) return 0.0;

    // Detail erodes the edges, where coverage is still climbing, and leaves the
    // dense core alone so it does not dissolve into noise.
    float detail = fbm(p * 1.45 + 11.0);
    float d = coverage - 0.42 * detail * (1.0 - coverage);

    return clamp(d * heightMask * 2.0, 0.0, 1.0);
}

// Henyey-Greenstein. Cloud droplets scatter strongly forwards, which is what
// puts the bright rim on a cloud seen against the sun. Without a phase function
// every part of the cloud is lit the same from every direction and it reads
// flat however good the shadowing is.
float hg(float cosTheta, float g)
{
    float g2 = g * g;
    return (1.0 - g2) / (4.0 * PI * pow(max(1.0 + g2 - 2.0 * g * cosTheta, 1e-4), 1.5));
}

// Optical depth from a point towards the sun. Steps grow, so the near field -
// where self-shadowing actually shows - is sampled finely and the far field
// cheaply. Weighting each sample by its own step length is what makes this a
// depth rather than an average.
float lightDepth(vec3 pos, vec3 lightDir)
{
    float depth = 0.0;
    float t = 0.15;

    for (int i = 0; i < LIGHT_STEPS; i++)
    {
        depth += cloudDensity(pos + lightDir * t) * t;
        t *= 1.9;
    }

    return depth;
}

mat3 camera(vec3 ro, vec3 ta)
{
    vec3 f = normalize(ta - ro);
    vec3 r = normalize(cross(vec3(0,1,0), f));
    vec3 u = cross(f, r);
    return mat3(r, u, f);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord)
{
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

    vec3 lightDir = normalize(vec3(0.82, 0.26, 0.28));

    // Above the deck, looking down across it. The sun rakes the tops and the
    // gaps between billows fall into shadow, which is where the shape comes
    // from. The camera used to sit at y = 1.5, inside a layer spanning -1 to
    // 2.8, so every ray ran the same length through the same soup and the whole
    // frame came out an even grey.
    vec3 ro = vec3(0.0, 3.7, -14.0);
    vec3 ta = vec3(0.0, 2.9, 3.0);

    if (iMouse.z > 0.0)
    {
        float mx = (iMouse.x / iResolution.x - 0.5) * 6.2831;
        float my = iMouse.y / iResolution.y;
        ro = vec3(13.0 * sin(mx), mix(-1.5, 9.0, my), -13.0 * cos(mx));
    }

    mat3 cam = camera(ro, ta);
    vec3 rd = normalize(cam * vec3(uv, 1.5));

    // Sky
    float cosL = dot(rd, lightDir);
    float sun = max(cosL, 0.0);

    vec3 skyCol = mix(vec3(0.86, 0.87, 0.92), vec3(0.16, 0.36, 0.76),
                      clamp(rd.y * 2.4 + 0.10, 0.0, 1.0));
    skyCol += vec3(1.0, 0.80, 0.55) * pow(sun, 200.0) * 1.6;
    skyCol += vec3(1.0, 0.74, 0.48) * pow(sun, 6.0) * 0.16;

    // A strong forward lobe with a weak backward one: the forward lobe is the
    // silver lining, the backward lobe keeps the shadowed side from going dead.
    float phase = mix(hg(cosL, 0.76), hg(cosL, -0.28), 0.3);

    // Clip the march to the cloud slab.
    float tEnter = 0.0;
    float tExit  = -1.0;

    if (abs(rd.y) > 1e-4)
    {
        float tA = (CLOUD_TOP    - ro.y) / rd.y;
        float tB = (CLOUD_BOTTOM - ro.y) / rd.y;
        tEnter = max(0.0, min(tA, tB));
        tExit  = min(FAR, max(tA, tB));
    }
    else if (ro.y > CLOUD_BOTTOM && ro.y < CLOUD_TOP)
    {
        tEnter = 0.0;
        tExit  = FAR;
    }

    float transmittance = 1.0;
    vec3 accum = vec3(0.0);

    if (tExit > tEnter)
    {
        float stepLen = max((tExit - tEnter) / float(MAX_STEPS), 0.05);

        // Jitter the first sample, or the fixed step draws visible shells
        // through the cloud where every ray crosses a boundary together.
        float t = tEnter + hash(vec3(fragCoord, iTime)) * stepLen;

        for (int i = 0; i < MAX_STEPS; i++)
        {
            if (t > tExit || transmittance < 0.01) break;

            vec3 p = ro + rd * t;
            float d = cloudDensity(p);

            if (d > 0.005)
            {
                float sunT = exp(-lightDepth(p, lightDir) * 2.9);

                // Powder. Light arriving at the outside of a clump has not been
                // scattered yet, so an edge reads darker than the lit core
                // behind it. Without it a cloud looks like a cutout.
                float powder = 1.0 - exp(-d * 9.0);

                vec3 sunLight = vec3(1.0, 0.88, 0.70) * sunT * (phase * 2.2 + 0.50) * powder;
                vec3 ambient  = mix(vec3(0.20, 0.26, 0.40), vec3(0.74, 0.82, 0.98),
                                    smoothstep(CLOUD_BOTTOM, CLOUD_TOP, p.y)) * 0.30;

                // Beer-Lambert over the distance actually travelled. This used a
                // hardcoded 0.18 while the march stepped anywhere from 0.08 to
                // 0.3, so the opacity never matched the geometry.
                float alpha = 1.0 - exp(-d * 3.2 * stepLen);

                accum += (sunLight + ambient) * alpha * transmittance;
                transmittance *= (1.0 - alpha);
            }

            t += stepLen;
        }
    }

    vec3 col = accum + skyCol * transmittance;

    // Simple tonemap + gamma
    col = col / (1.0 + col);
    col = pow(col, vec3(0.4545));

    fragColor = vec4(col, 1.0);
}