Files
skills/shader-dev/techniques/shadow-techniques.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

25 KiB

SDF Soft Shadow Techniques

Core Principles

March from the surface point toward the light source, using the ratio of nearest distance to marching distance to estimate penumbra width.

Key Formulas

Classic formula: shadow = min(shadow, k * h / t)

  • h = SDF value at current position, t = distance traveled, k = penumbra hardness

Improved formula (geometric triangulation) — eliminates sharp edge banding artifacts:

y = h² / (2 * ph)       // ph = SDF value from previous step
d = sqrt(h² - y²)       // true closest distance perpendicular to the ray
shadow = min(shadow, d / (w * max(0, t - y)))

Negative extension — allows res to drop to -1, remapped with a C1 continuous function to eliminate hard creases:

res = max(res, -1.0)
shadow = 0.25 * (1 + res)² * (2 - res)

This is equivalent to smoothstep over [-1, 1] instead of [0, 1]. The step size is clamped with clamp(h, 0.005, 0.50) to ensure the ray penetrates slightly into geometry, capturing both outer and inner penumbra. This produces results close to ground truth for varying light sizes.

Implementation Steps

Step 1: Scene SDF

float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdPlane(vec3 p) { return p.y; }
float sdRoundBox(vec3 p, vec3 b, float r) {
    vec3 q = abs(p) - b;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}

float map(vec3 p) {
    float d = sdPlane(p);
    d = min(d, sdSphere(p - vec3(0.0, 0.5, 0.0), 0.5));
    d = min(d, sdRoundBox(p - vec3(-1.2, 0.3, 0.5), vec3(0.3), 0.05));
    return d;
}

Step 2: Classic Soft Shadow

// Classic SDF soft shadow
float calcSoftShadow(vec3 ro, vec3 rd, float mint, float tmax) {
    float res = 1.0;
    float t = mint;
    for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        float s = clamp(SHADOW_K * h / t, 0.0, 1.0);
        res = min(res, s);
        t += clamp(h, MIN_STEP, MAX_STEP);
        if (res < 0.004 || t > tmax) break;
    }
    res = clamp(res, 0.0, 1.0);
    return res * res * (3.0 - 2.0 * res);  // smoothstep smoothing
}

Step 3: Improved Soft Shadow (Geometric Triangulation)

// Improved version - geometric triangulation using adjacent SDF values
float calcSoftShadowImproved(vec3 ro, vec3 rd, float mint, float tmax, float w) {
    float res = 1.0;
    float t = mint;
    float ph = 1e10;
    for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        float y = h * h / (2.0 * ph);
        float d = sqrt(h * h - y * y);
        res = min(res, d / (w * max(0.0, t - y)));
        ph = h;
        t += h;
        if (res < 0.0001 || t > tmax) break;
    }
    res = clamp(res, 0.0, 1.0);
    return res * res * (3.0 - 2.0 * res);
}

Step 4: Negative Extension (Smoothest Penumbra)

// Negative extension - allows res to go negative for C1 continuous penumbra
float calcSoftShadowSmooth(vec3 ro, vec3 rd, float mint, float tmax, float w) {
    float res = 1.0;
    float t = mint;
    for (int i = 0; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        res = min(res, h / (w * t));
        t += clamp(h, MIN_STEP, MAX_STEP);
        if (res < -1.0 || t > tmax) break;
    }
    res = max(res, -1.0);
    return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
}

Step 5: Bounding Volume Optimization

// plane clipping -- clip the ray to the scene's upper bound
float tp = (SCENE_Y_MAX - ro.y) / rd.y;
if (tp > 0.0) tmax = min(tmax, tp);

// AABB bounding box clipping
vec2 iBox(vec3 ro, vec3 rd, vec3 rad) {
    vec3 m = 1.0 / rd;
    vec3 n = m * ro;
    vec3 k = abs(m) * rad;
    vec3 t1 = -n - k;
    vec3 t2 = -n + k;
    float tN = max(max(t1.x, t1.y), t1.z);
    float tF = min(min(t2.x, t2.y), t2.z);
    if (tN > tF || tF < 0.0) return vec2(-1.0);
    return vec2(tN, tF);
}

// usage: return 1.0 immediately if the ray misses the bounding box entirely
vec2 dis = iBox(ro, rd, BOUND_SIZE);
if (dis.y < 0.0) return 1.0;
tmin = max(tmin, dis.x);
tmax = min(tmax, dis.y);

Step 6: Shadow Color Rendering

// Classic colored shadow
vec3 shadowColor = vec3(sha, sha * sha * 0.5 + 0.5 * sha, sha * sha);

// per-channel power (penumbra region shifts warm)
vec3 shadowColor = pow(vec3(sha), vec3(1.0, 1.2, 1.5));

Step 7: Integration with Lighting Model

vec3 sunDir = normalize(vec3(-0.5, 0.4, -0.6));
vec3 hal = normalize(sunDir - rd);

float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
if (dif > 0.0001)
    dif *= calcSoftShadow(pos + nor * 0.01, sunDir, 0.02, 8.0);

float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
spe *= dif;

vec3 col = vec3(0.0);
col += albedo * 2.0 * dif * vec3(1.0, 0.9, 0.8);
col += 5.0 * spe * vec3(1.0, 0.9, 0.8);
col += albedo * 0.5 * clamp(0.5 + 0.5 * nor.y, 0.0, 1.0) * vec3(0.4, 0.6, 1.0);

Complete Code Template

Runs directly in ShaderToy, with A/B comparison of three soft shadow techniques.

#define ZERO (min(iFrame, 0))

// ---- Adjustable Parameters ----
#define MAX_MARCH_STEPS   128
#define MAX_SHADOW_STEPS   64   // 16~128
#define SHADOW_K          8.0   // 4~64, higher = harder
#define SHADOW_MINT      0.02   // 0.01~0.05
#define SHADOW_TMAX      8.0
#define SHADOW_MIN_STEP  0.01
#define SHADOW_MAX_STEP  0.20
#define SHADOW_W         0.10   // improved version penumbra width

// 0=classic, 1=improved(Aaltonen), 2=negative extension
#define SHADOW_TECHNIQUE   0

// ---- SDF Primitives ----
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdPlane(vec3 p) { return p.y; }
float sdRoundBox(vec3 p, vec3 b, float r) {
    vec3 q = abs(p) - b;
    return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
}
float sdTorus(vec3 p, vec2 t) {
    vec2 q = vec2(length(p.xz) - t.x, p.y);
    return length(q) - t.y;
}

// ---- Scene SDF ----
float map(vec3 p) {
    float d = sdPlane(p);
    d = min(d, sdSphere(p - vec3(0.0, 0.5, 0.0), 0.5));
    d = min(d, sdRoundBox(p - vec3(-1.2, 0.30, 0.5), vec3(0.25), 0.05));
    d = min(d, sdTorus(p - vec3(1.2, 0.25, -0.3), vec2(0.40, 0.08)));
    return d;
}

// ---- Normal ----
vec3 calcNormal(vec3 p) {
    vec2 e = vec2(0.0005, 0.0);
    return normalize(vec3(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)));
}

// ---- Raymarching ----
float castRay(vec3 ro, vec3 rd) {
    float t = 0.0;
    for (int i = ZERO; i < MAX_MARCH_STEPS; i++) {
        float h = map(ro + rd * t);
        if (h < 0.0002) return t;
        t += h;
        if (t > 20.0) break;
    }
    return -1.0;
}

// ---- Bounding Volume Clipping ----
float clipTmax(vec3 ro, vec3 rd, float tmax, float yMax) {
    float tp = (yMax - ro.y) / rd.y;
    if (tp > 0.0) tmax = min(tmax, tp);
    return tmax;
}

// ---- Shadow: Classic ----
float softShadowClassic(vec3 ro, vec3 rd, float mint, float tmax) {
    tmax = clipTmax(ro, rd, tmax, 1.5);
    float res = 1.0, t = mint;
    for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        float s = clamp(SHADOW_K * h / t, 0.0, 1.0);
        res = min(res, s);
        t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
        if (res < 0.004 || t > tmax) break;
    }
    res = clamp(res, 0.0, 1.0);
    return res * res * (3.0 - 2.0 * res);
}

// ---- Shadow: Improved ----
float softShadowImproved(vec3 ro, vec3 rd, float mint, float tmax, float w) {
    tmax = clipTmax(ro, rd, tmax, 1.5);
    float res = 1.0, t = mint, ph = 1e10;
    for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        float y = h * h / (2.0 * ph);
        float d = sqrt(h * h - y * y);
        res = min(res, d / (w * max(0.0, t - y)));
        ph = h;
        t += h;
        if (res < 0.0001 || t > tmax) break;
    }
    res = clamp(res, 0.0, 1.0);
    return res * res * (3.0 - 2.0 * res);
}

// ---- Shadow: Negative Extension ----
float softShadowSmooth(vec3 ro, vec3 rd, float mint, float tmax, float w) {
    tmax = clipTmax(ro, rd, tmax, 1.5);
    float res = 1.0, t = mint;
    for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
        float h = map(ro + rd * t);
        res = min(res, h / (w * t));
        t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
        if (res < -1.0 || t > tmax) break;
    }
    res = max(res, -1.0);
    return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
}

// ---- Unified Interface ----
float calcSoftShadow(vec3 ro, vec3 rd, float mint, float tmax) {
    #if SHADOW_TECHNIQUE == 0
        return softShadowClassic(ro, rd, mint, tmax);
    #elif SHADOW_TECHNIQUE == 1
        return softShadowImproved(ro, rd, mint, tmax, SHADOW_W);
    #else
        return softShadowSmooth(ro, rd, mint, tmax, SHADOW_W);
    #endif
}

// ---- AO ----
float calcAO(vec3 p, vec3 n) {
    float occ = 0.0, sca = 1.0;
    for (int i = ZERO; i < 5; i++) {
        float h = 0.01 + 0.12 * float(i) / 4.0;
        float d = map(p + h * n);
        occ += (h - d) * sca;
        sca *= 0.95;
    }
    return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
}

// ---- Checkerboard ----
float checkerboard(vec2 p) {
    vec2 q = floor(p);
    return mix(0.3, 1.0, mod(q.x + q.y, 2.0));
}

// ---- Render ----
vec3 render(vec3 ro, vec3 rd) {
    vec3 col = vec3(0.7, 0.75, 0.85) - 0.3 * rd.y;
    float t = castRay(ro, rd);
    if (t < 0.0) return col;

    vec3 pos = ro + rd * t;
    vec3 nor = calcNormal(pos);
    vec3 albedo = vec3(0.18);
    if (pos.y < 0.001)
        albedo = vec3(0.08 + 0.15 * checkerboard(pos.xz * 2.0));

    vec3 sunDir = normalize(vec3(-0.5, 0.4, -0.6));
    vec3 hal = normalize(sunDir - rd);

    float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
    if (dif > 0.0001)
        dif *= calcSoftShadow(pos + nor * 0.001, sunDir, SHADOW_MINT, SHADOW_TMAX);

    float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0);
    spe *= dif;
    float fre = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0);
    spe *= 0.04 + 0.96 * fre;

    float sky = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0);
    float occ = calcAO(pos, nor);

    vec3 lin = vec3(0.0);
    lin += 2.5 * dif * vec3(1.30, 1.00, 0.70);
    lin += 8.0 * spe * vec3(1.30, 1.00, 0.70);
    lin += 0.5 * sky * vec3(0.40, 0.60, 1.00) * occ;
    lin += 0.25 * occ * vec3(0.40, 0.50, 0.60);

    col = albedo * lin;
    col = pow(col, vec3(0.4545));
    return col;
}

// ---- Camera ----
mat3 setCamera(vec3 ro, vec3 ta) {
    vec3 cw = normalize(ta - ro);
    vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0)));
    vec3 cv = cross(cu, cw);
    return mat3(cu, cv, cw);
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
    float an = 0.3 * iTime;
    vec3 ro = vec3(3.5 * sin(an), 1.8, 3.5 * cos(an));
    vec3 ta = vec3(0.0, 0.3, 0.0);
    mat3 ca = setCamera(ro, ta);
    vec3 rd = ca * normalize(vec3(p, 1.8));
    vec3 col = render(ro, rd);
    fragColor = vec4(col, 1.0);
}

Standalone HTML + WebGL2 Template

When generating standalone HTML files, use the following complete template. Key points:

  • Must use canvas.getContext('webgl2')
  • Shaders use #version 300 es
  • Entry function is void main(), not void mainImage()
  • Use gl_FragCoord.xy to get pixel coordinates (available in WebGL2)
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Soft Shadows - SDF Raymarching</title>
    <style>
        body { margin: 0; overflow: hidden; background: #000; }
        canvas { display: block; width: 100vw; height: 100vh; }
    </style>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script>
        const canvas = document.getElementById('canvas');
        const gl = canvas.getContext('webgl2');

        if (!gl) {
            document.body.innerHTML = '<p style="color:#fff;">WebGL2 not supported</p>';
            throw new Error('WebGL2 not supported');
        }

        // Vertex shader: fullscreen quad
        const vsSource = `#version 300 es
            in vec4 aPosition;
            void main() {
                gl_Position = aPosition;
            }
        `;

        // Fragment shader: SDF soft shadows
        const fsSource = `#version 300 es
            precision highp float;

            uniform float iTime;
            uniform vec2 iResolution;
            uniform vec4 iMouse;

            out vec4 fragColor;

            #define ZERO (min(int(iTime), 0))
            #define MAX_MARCH_STEPS 128
            #define MAX_SHADOW_STEPS 64
            #define SHADOW_MINT 0.02
            #define SHADOW_TMAX 10.0
            #define SHADOW_MIN_STEP 0.01
            #define SHADOW_MAX_STEP 0.25
            #define SHADOW_W 0.08
            #define SHADOW_K 16.0

            // SDF primitives
            float sdSphere(vec3 p, float r) { return length(p) - r; }
            float sdPlane(vec3 p) { return p.y; }
            float sdRoundBox(vec3 p, vec3 b, float r) {
                vec3 q = abs(p) - b;
                return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - r;
            }
            float sdTorus(vec3 p, vec2 t) {
                vec2 q = vec2(length(p.xz) - t.x, p.y);
                return length(q) - t.y;
            }

            // Scene SDF
            float map(vec3 p) {
                float d = sdPlane(p);
                d = min(d, sdSphere(p - vec3(0.0, 0.6, 0.0), 0.6));
                d = min(d, sdRoundBox(p - vec3(-1.5, 0.4, 0.8), vec3(0.35), 0.08));
                d = min(d, sdTorus(p - vec3(1.6, 0.35, -0.5), vec2(0.45, 0.12)));
                return d;
            }

            // Normal
            vec3 calcNormal(vec3 p) {
                vec2 e = vec2(0.0005, 0.0);
                return normalize(vec3(
                    map(p + e.xyy) - map(p - e.xyy),
                    map(p + e.yxy) - map(p - e.yxy),
                    map(p + e.yyx) - map(p - e.yyx)));
            }

            // Raymarching
            float castRay(vec3 ro, vec3 rd) {
                float t = 0.0;
                for (int i = ZERO; i < MAX_MARCH_STEPS; i++) {
                    float h = map(ro + rd * t);
                    if (h < 0.0002) return t;
                    t += h;
                    if (t > 25.0) break;
                }
                return -1.0;
            }

            // Plane clipping
            float clipTmax(vec3 ro, vec3 rd, float tmax, float yMax) {
                float tp = (yMax - ro.y) / rd.y;
                if (tp > 0.0) tmax = min(tmax, tp);
                return tmax;
            }

            // Soft shadow (negative extension)
            float softShadow(vec3 ro, vec3 rd, float mint, float tmax, float w) {
                tmax = clipTmax(ro, rd, tmax, 2.0);
                float res = 1.0;
                float t = mint;
                for (int i = ZERO; i < MAX_SHADOW_STEPS; i++) {
                    float h = map(ro + rd * t);
                    res = min(res, h / (w * t));
                    t += clamp(h, SHADOW_MIN_STEP, SHADOW_MAX_STEP);
                    if (res < -1.0 || t > tmax) break;
                }
                res = max(res, -1.0);
                return 0.25 * (1.0 + res) * (1.0 + res) * (2.0 - res);
            }

            // Soft shadow call
            float calcSoftShadow(vec3 ro, vec3 rd) {
                return softShadow(ro, rd, SHADOW_MINT, SHADOW_TMAX, SHADOW_W);
            }

            // AO
            float calcAO(vec3 p, vec3 n) {
                float occ = 0.0, sca = 1.0;
                for (int i = ZERO; i < 5; i++) {
                    float h = 0.01 + 0.12 * float(i) / 4.0;
                    float d = map(p + h * n);
                    occ += (h - d) * sca;
                    sca *= 0.95;
                }
                return clamp(1.0 - 3.0 * occ, 0.0, 1.0);
            }

            // Checkerboard
            float checkerboard(vec2 p) {
                vec2 q = floor(p);
                return mix(0.25, 0.35, mod(q.x + q.y, 2.0));
            }

            // Render
            vec3 render(vec3 ro, vec3 rd) {
                // sky
                vec3 col = vec3(0.65, 0.72, 0.85) - 0.4 * rd.y;
                col = mix(col, vec3(0.3, 0.35, 0.45), exp(-0.8 * max(rd.y, 0.0)));

                float t = castRay(ro, rd);
                if (t < 0.0) return col;

                vec3 pos = ro + rd * t;
                vec3 nor = calcNormal(pos);

                // material color
                vec3 albedo = vec3(0.18);
                if (pos.y < 0.01) {
                    albedo = vec3(0.12 + 0.12 * checkerboard(pos.xz * 1.5));
                } else if (pos.y > 0.5 && length(pos.xz) < 0.7) {
                    albedo = vec3(0.85, 0.25, 0.2);
                } else if (pos.x < -1.0) {
                    albedo = vec3(0.2, 0.4, 0.85);
                } else if (pos.x > 1.0) {
                    albedo = vec3(0.25, 0.75, 0.35);
                } else {
                    albedo = vec3(0.9, 0.6, 0.2);
                }

                // lighting
                vec3 sunDir = normalize(vec3(-0.6, 0.45, -0.65));
                vec3 hal = normalize(sunDir - rd);

                float dif = clamp(dot(nor, sunDir), 0.0, 1.0);
                if (dif > 0.0001) {
                    dif *= calcSoftShadow(pos + nor * 0.01, sunDir);
                }

                float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 32.0);
                spe *= dif;

                float fre = pow(clamp(1.0 + dot(nor, rd), 0.0, 1.0), 5.0);
                spe *= 0.04 + 0.96 * fre;

                float sky = clamp(0.5 + 0.5 * nor.y, 0.0, 1.0);
                float occ = calcAO(pos, nor);

                vec3 lin = vec3(0.0);
                lin += 2.2 * dif * vec3(1.35, 1.05, 0.75);
                lin += 6.0 * spe * vec3(1.35, 1.05, 0.75);
                lin += 0.4 * sky * vec3(0.45, 0.6, 0.9) * occ;
                lin += 0.25 * occ * vec3(0.5, 0.55, 0.6);

                col = albedo * lin;
                col = pow(col, vec3(0.4545));

                // vignette
                vec2 uv = gl_FragCoord.xy / iResolution.xy;
                col *= 0.5 + 0.5 * pow(16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y), 0.2);

                return col;
            }

            // Camera
            mat3 setCamera(vec3 ro, vec3 ta) {
                vec3 cw = normalize(ta - ro);
                vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0)));
                vec3 cv = cross(cu, cw);
                return mat3(cu, cv, cw);
            }

            void main() {
                vec2 fragCoord = gl_FragCoord.xy;
                vec2 p = (2.0 * fragCoord - iResolution.xy) / iResolution.y;

                // slowly rotating camera
                float an = 0.15 * iTime;
                float dist = 5.5;
                vec3 ro = vec3(dist * sin(an), 2.2, dist * cos(an));
                vec3 ta = vec3(0.0, 0.3, 0.0);

                mat3 ca = setCamera(ro, ta);
                vec3 rd = ca * normalize(vec3(p, 2.0));

                vec3 col = render(ro, rd);
                fragColor = vec4(col, 1.0);
            }
        `;

        // Compile shader
        function createShader(gl, type, source) {
            const shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('Shader compile error:', gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        // Create program
        function createProgram(gl, vs, fs) {
            const program = gl.createProgram();
            gl.attachShader(program, vs);
            gl.attachShader(program, fs);
            gl.linkProgram(program);
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error('Program link error:', gl.getProgramInfoLog(program));
                return null;
            }
            return program;
        }

        const vs = createShader(gl, gl.VERTEX_SHADER, vsSource);
        const fs = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
        const program = createProgram(gl, vs, fs);

        // Fullscreen quad
        const positions = new Float32Array([
            -1, -1,  1, -1,  -1,  1,  1,  1
        ]);

        const posBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);

        const posLoc = gl.getAttribLocation(program, 'aPosition');
        gl.enableVertexAttribArray(posLoc);
        gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

        // Uniforms
        const uTime = gl.getUniformLocation(program, 'iTime');
        const uResolution = gl.getUniformLocation(program, 'iResolution');
        const uMouse = gl.getUniformLocation(program, 'iMouse');

        // Mouse tracking
        let mouseX = 0, mouseY = 0;
        canvas.addEventListener('mousemove', (e) => {
            mouseX = e.clientX;
            mouseY = canvas.height - e.clientY;
        });

        // Window resize
        function resize() {
            const dpr = Math.min(window.devicePixelRatio, 2);
            canvas.width = window.innerWidth * dpr;
            canvas.height = window.innerHeight * dpr;
            gl.viewport(0, 0, canvas.width, canvas.height);
        }
        window.addEventListener('resize', resize);
        resize();

        // Render loop
        function render(time) {
            time *= 0.001;
            gl.useProgram(program);
            gl.uniform1f(uTime, time);
            gl.uniform2f(uResolution, canvas.width, canvas.height);
            gl.uniform4f(uMouse, mouseX, mouseY, mouseX, mouseY);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
            requestAnimationFrame(render);
        }
        requestAnimationFrame(render);
    </script>
</body>
</html>

Common Variants

Analytic Sphere Shadow

vec2 sphDistances(vec3 ro, vec3 rd, vec4 sph) {
    vec3 oc = ro - sph.xyz;
    float b = dot(oc, rd);
    float c = dot(oc, oc) - sph.w * sph.w;
    float h = b * b - c;
    float d = sqrt(max(0.0, sph.w * sph.w - h)) - sph.w;
    return vec2(d, -b - sqrt(max(h, 0.0)));
}
float sphSoftShadow(vec3 ro, vec3 rd, vec4 sph, float k) {
    vec2 r = sphDistances(ro, rd, sph);
    if (r.y > 0.0)
        return clamp(k * max(r.x, 0.0) / r.y, 0.0, 1.0);
    return 1.0;
}

Terrain Heightfield Shadow

float terrainShadow(vec3 ro, vec3 rd, float dis) {
    float minStep = clamp(dis * 0.01, 0.5, 50.0);
    float res = 1.0, t = 0.01;
    for (int i = 0; i < 80; i++) {
        vec3 p = ro + t * rd;
        float h = p.y - terrainMap(p.xz);
        res = min(res, 16.0 * h / t);
        t += max(minStep, h);
        if (res < 0.001 || p.y > MAX_TERRAIN_HEIGHT) break;
    }
    return clamp(res, 0.0, 1.0);
}

Per-Material Soft/Hard Blending

float hsha = 1.0;  // global variable, set per material in map()
float mapWithShadowHardness(vec3 p) {
    float d = sdPlane(p); hsha = 1.0;
    float dChar = sdCharacter(p);
    if (dChar < d) { d = dChar; hsha = 0.0; }
    return d;
}
// in shadow loop: res = min(res, mix(1.0, SHADOW_K * h / t, hsha));

Multi-Layer Shadow Compositing

float sha_terrain = terrainShadow(pos, sunDir, 0.02);
float sha_trees   = treesShadow(pos, sunDir);
float sha_clouds  = cloudShadow(pos, sunDir);
float sha = sha_terrain * sha_trees;
sha *= smoothstep(-0.3, -0.1, sha_clouds);
dif *= sha;

Volumetric Light / God Rays

float godRays(vec3 ro, vec3 rd, float tmax, vec3 sunDir) {
    float v = 0.0, dt = 0.15;
    float t = dt * fract(texelFetch(iChannel0, ivec2(fragCoord) & 255, 0).x);
    for (int i = 0; i < 32; i++) {
        if (t > tmax) break;
        vec3 p = ro + rd * t;
        float sha = calcSoftShadow(p, sunDir, 0.02, 8.0);
        v += sha * exp(-0.2 * t);
        t += dt;
    }
    v /= 32.0;
    return v * v;
}
// col += intensity * godRays(...) * vec3(1.0, 0.75, 0.4);

Performance & Composition

Performance optimization:

  • Bounding volume clipping (plane/AABB) can reduce 30-70% of wasted iterations
  • Step clamping clamp(h, minStep, maxStep) prevents stalling / skipping thin objects
  • Early exit: res < 0.004 (classic) or res < -1.0 (negative extension)
  • Simplified map() omitting material calculations, returning distance only
  • Only compute shadow when dif > 0.0001; skip for backlit faces
  • Iteration count: simple scenes 1632, complex FBM 64128, terrain ~80
  • #define ZERO (min(iFrame,0)) prevents compiler loop unrolling

Composition tips:

  • AO: shadows control direct light, AO controls indirect light, col = diffuse * sha + ambient * ao
  • SSS: sss *= 0.25 + 0.75 * sha -- SSS weakens but does not vanish in shadow
  • Fog: complete lit+shadowed shading first, then mix(col, fogColor, 1.0 - exp(-0.001*t*t))
  • Normal mapping: perturbed normals for lighting, geometric normals for shadow determination
  • Reflection: refSha = calcSoftShadow(pos + nor*0.01, reflect(rd, nor), 0.02, 8.0)

Further Reading

For complete step-by-step tutorials, mathematical derivations, and advanced usage, see reference