Back to shaders

Shader test bench

20260214

glsl-daily-practice generative glsl runnable fragment MIT
Source
runnable fragment

Complete GLSL fragment shader. Stronghold runs it directly when the browser can compile it.

Code

precision mediump float;
#define TDOutputSwizzle(c) (c)
uniform float u_time;
#define fragColor gl_FragColor

mat2 rot(float a)
{
    float c = cos(a);
    float s = sin(a);
    return mat2(c, -s, s, c);
}

float hash21(vec2 p)
{
    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

float ggxD(float NdotH, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
    return a2 / (3.14159265 * d * d);
}

float fresnelSchlick(float cosTheta, float f0)
{
    return f0 + (1.0 - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}

float fractalDE(vec3 p, float power)
{
    vec3 z = p;
    float dr = 1.0;
    float r = 0.0;

    for (int i = 0; i < 15; i++)
    {
        r = length(z);
        if (r > 2.0)
            break;

        float safeR = max(r, 1e-6);
        float theta = acos(clamp(z.z / safeR, -1.0, 1.0));
        float phi = atan(z.y, z.x);
        dr = power * pow(safeR, power - 1.0) * dr + 1.0;

        float zr = pow(safeR, power);
        theta *= power;
        phi *= power;
        z = zr * vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
        z += p;
    }

    return 0.5 * log(max(r, 1e-6)) * r / dr;
}

float fractalDETrap(vec3 p, float power, out vec3 trapOut)
{
    vec3 z = p;
    float dr = 1.0;
    float r = 0.0;
    float trapDot = 1e10;
    float trapXZ = 1e10;
    float trapY = 1e10;

    for (int i = 0; i < 15; i++)
    {
        r = length(z);
        if (r > 2.0)
            break;

        trapDot = min(trapDot, dot(z, z));
        trapXZ = min(trapXZ, length(z.xz));
        trapY = min(trapY, abs(z.y));

        float safeR = max(r, 1e-6);
        float theta = acos(clamp(z.z / safeR, -1.0, 1.0));
        float phi = atan(z.y, z.x);
        dr = power * pow(safeR, power - 1.0) * dr + 1.0;

        float zr = pow(safeR, power);
        theta *= power;
        phi *= power;
        z = zr * vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta));
        z += p;
    }

    trapOut = vec3(trapDot, trapXZ, trapY);
    return 0.5 * log(max(r, 1e-6)) * r / dr;
}

vec3 animateFractalSpace(vec3 p, float t)
{
    vec3 q1 = p;
    vec3 q2 = p;

    // Blend two gentle transform states with an eased phase.
    float phase = 0.5 + 0.5 * sin(t * 0.18);
    float ease = phase * phase * (3.0 - 2.0 * phase);

    q1.xy *= rot(0.08 + 0.14 * sin(t * 0.23));
    q1.xz *= rot(-0.28 + 0.10 * sin(t * 0.19 + 1.0));
    q1.yz *= rot(0.07 * sin(t * 0.21 + 2.2));

    q2.xy *= rot(-0.06 + 0.11 * sin(t * 0.17 + 2.4));
    q2.xz *= rot(-0.24 + 0.12 * sin(t * 0.15 + 4.2));
    q2.yz *= rot(0.09 * sin(t * 0.20 + 0.7));

    vec3 q = mix(q1, q2, ease);

    // Ribbon-like internal motion for a graceful transform feeling.
    float twist = 0.10 * sin(t * 0.36 + q.y * 1.1);
    float curl = 0.04 * sin(t * 0.54 + q.x * 1.3);
    q.xz *= rot(twist);
    q.yz *= rot(curl);

    vec3 flow = vec3(
        sin(q.y * 1.2 + t * 0.42),
        sin(q.z * 1.1 - t * 0.38),
        sin(q.x * 1.3 + t * 0.46)
    );
    q += 0.025 * flow;

    float breathe = 1.0 + 0.022 * sin(t * 0.42) + 0.012 * sin(t * 0.9);
    q *= breathe;
    q.y *= 1.0 + 0.032 * sin(t * 0.31);
    q.xz *= 1.0 - 0.016 * sin(t * 0.31);

    // Extra transformation animation: rotational morph + squash/stretch cycle.
    float transPhase = 0.5 + 0.5 * sin(t * 0.28);
    float transEase = transPhase * transPhase * (3.0 - 2.0 * transPhase);
    vec3 qMorph = q;
    qMorph.xz *= rot(-0.18 + 0.36 * transEase);
    qMorph.y *= mix(0.88, 1.14, transEase);
    qMorph.xz *= mix(1.1, 0.9, transEase);
    qMorph.x += 0.12 * sin(qMorph.y * 1.4 + t * 0.70);
    qMorph.z += 0.10 * cos(qMorph.y * 1.2 - t * 0.62);
    q = mix(q, qMorph, 0.34);

    return q;
}

float mapScene(vec3 p)
{
    float t = u_time * 1.4;
    float powerPhase = 0.5 + 0.5 * sin(t * 0.10);
    float powerEase = powerPhase * powerPhase * (3.0 - 2.0 * powerPhase);
    float powerA = 8.8 + 0.35 * sin(t * 0.13 + 0.25 * sin(t * 0.09));
    float powerB = 8.2 + 0.25 * sin(t * 0.11 + 2.4);
    float power = mix(powerA, powerB, powerEase * 0.6);

    vec3 qA = animateFractalSpace(p, t);
    float dA = fractalDE(qA, power);

    // Secondary transform state — more distinct for visible morphing
    vec3 qB = qA;
    qB.xy *= rot(0.2 * sin(t * 0.14));
    qB.yz *= rot(0.08 * cos(t * 0.18 + 1.3));
    qB.y += 0.12 * sin(qB.x * 1.1 + t * 0.22);
    qB.x += 0.06 * cos(qB.z * 0.9 - t * 0.26);
    float dB = fractalDE(qB, power - 0.08);
    float morph = 0.3 * (0.5 + 0.5 * sin(t * 0.12 + 1.0));

    return mix(dA, dB, morph);
}

vec4 mapSceneDetail(vec3 p)
{
    float t = u_time * 1.4;
    float powerPhase = 0.5 + 0.5 * sin(t * 0.10);
    float powerEase = powerPhase * powerPhase * (3.0 - 2.0 * powerPhase);
    float powerA = 8.8 + 0.35 * sin(t * 0.13 + 0.25 * sin(t * 0.09));
    float powerB = 8.2 + 0.25 * sin(t * 0.11 + 2.4);
    float power = mix(powerA, powerB, powerEase * 0.6);

    vec3 qA = animateFractalSpace(p, t);
    vec3 trapA;
    float dA = fractalDETrap(qA, power, trapA);

    vec3 qB = qA;
    qB.xy *= rot(0.2 * sin(t * 0.14));
    qB.yz *= rot(0.08 * cos(t * 0.18 + 1.3));
    qB.y += 0.12 * sin(qB.x * 1.1 + t * 0.22);
    qB.x += 0.06 * cos(qB.z * 0.9 - t * 0.26);
    vec3 trapB;
    float dB = fractalDETrap(qB, power - 0.08, trapB);
    float morph = 0.3 * (0.5 + 0.5 * sin(t * 0.12 + 1.0));

    float d = mix(dA, dB, morph);
    vec3 trap = mix(trapA, trapB, morph);
    return vec4(d, trap);
}

float rayMarch(vec3 ro, vec3 rd, out int steps)
{
    float t = 0.0;
    const float maxDist = 30.0;
    const float surfEps = 0.0004;

    for (int i = 0; i < 160; i++)
    {
        vec3 p = ro + rd * t;
        float d = mapScene(p);
        if (d < surfEps * (1.0 + t * 0.12))
        {
            steps = i;
            return t;
        }
        t += d * 0.76;
        if (t > maxDist)
            break;
    }

    steps = 160;
    return -1.0;
}

vec3 getNormal(vec3 p, float t)
{
    float eps = 0.001 + t * 0.00008;
    vec2 e = vec2(1.0, -1.0) * eps;
    return normalize(
        e.xyy * mapScene(p + e.xyy) +
        e.yyx * mapScene(p + e.yyx) +
        e.yxy * mapScene(p + e.yxy) +
        e.xxx * mapScene(p + e.xxx)
    );
}

float softShadow(vec3 ro, vec3 rd)
{
    float shade = 1.0;
    float t = 0.012;
    float ph = 1e10;
    for (int i = 0; i < 48; i++)
    {
        float h = mapScene(ro + rd * t);
        float y = h * h / (2.0 * ph);
        float d = sqrt(max(h * h - y * y, 0.0));
        shade = min(shade, 20.0 * d / max(0.0001, t - y));
        ph = h;
        t += clamp(h, 0.012, 0.2);
        if (h < 0.0002 || t > 14.0)
            break;
    }
    return clamp(shade, 0.0, 1.0);
}

float ambientOcclusion(vec3 p, vec3 n)
{
    float occ = 0.0;
    float scale = 1.0;
    for (int i = 1; i <= 7; i++)
    {
        float h = 0.008 + 0.05 * float(i);
        float d = mapScene(p + n * h);
        occ += (h - d) * scale;
        scale *= 0.6;
    }
    return clamp(1.0 - 2.0 * occ, 0.0, 1.0);
}

void main()
{
    vec2 uv = vUV.xy * 2.0 - 1.0;
    float time = u_time * 0.63;
    uv += 0.003 * vec2(sin(time * 0.43), cos(time * 0.39)) * smoothstep(1.35, 0.0, dot(uv, uv));

    // Slow cinematic orbit with eased altitude
    float orbitAngle = time * 0.09;
    float camDist = 4.6 + 0.5 * sin(time * 0.12);
    float altPhase = 0.5 + 0.5 * sin(time * 0.14);
    float altEase = altPhase * altPhase * (3.0 - 2.0 * altPhase);
    vec3 ro = vec3(
        sin(orbitAngle) * camDist,
        mix(-0.25, 0.4, altEase) + 0.08 * sin(time * 0.31),
        -cos(orbitAngle) * camDist
    );
    ro.x += 0.12 * sin(time * 0.27 + 1.4);
    ro.z += 0.10 * cos(time * 0.22 + 2.8);

    // Gentle target drift
    vec3 ta = vec3(
        sin(time * 0.11 + 0.8) * 0.12,
        sin(time * 0.15 + 1.2) * 0.08,
        cos(time * 0.09) * 0.05
    );
    vec3 ww = normalize(ta - ro);
    // Subtle camera roll
    float roll = 0.06 * sin(time * 0.1 + 1.5);
    vec3 up = vec3(sin(roll), cos(roll), 0.0);
    vec3 uu = normalize(cross(up, ww));
    vec3 vv = cross(ww, uu);
    // Breathing focal length — slow zoom in/out
    float focalPhase = 0.5 + 0.5 * sin(time * 0.08);
    float focalEase = focalPhase * focalPhase * (3.0 - 2.0 * focalPhase);
    float focal = mix(1.7, 2.1, focalEase) + 0.06 * sin(time * 0.31);
    vec3 rd = normalize(uv.x * uu + uv.y * vv + focal * ww);

    int steps = 0;
    float t = rayMarch(ro, rd, steps);

    vec3 col;
    if (t > 0.0)
    {
        vec3 p = ro + rd * t;
        vec3 n = getNormal(p, t);

        // Orbit trap detail at hit point
        vec4 detailed = mapSceneDetail(p);
        vec3 trap = detailed.yzw;
        float trapDetail = clamp(trap.x * 0.5, 0.0, 1.0);
        float trapEdge = clamp(trap.y, 0.0, 1.0);
        float trapCrease = clamp(trap.z * 2.0, 0.0, 1.0);

        // Trap-driven roughness: crevices rougher, smooth areas shinier
        float roughness = mix(0.12, 0.45, trapDetail);

        // Orbiting key light
        float lightAngle = time * 0.14;
        vec3 l = normalize(vec3(
            sin(lightAngle) * 0.7,
            0.65 + 0.12 * sin(time * 0.19),
            -cos(lightAngle) * 0.6
        ));
        // Sweeping back light — counter-orbit
        float backAngle = -time * 0.11 + 1.5;
        vec3 backL = normalize(vec3(
            sin(backAngle) * 0.55,
            0.3 + 0.1 * sin(time * 0.23),
            cos(backAngle) * 0.65
        ));
        // Drifting fill
        vec3 fillDir = normalize(vec3(
            0.35 + 0.2 * sin(time * 0.17),
            0.2,
            0.8 + 0.15 * cos(time * 0.21)
        ));
        vec3 h = normalize(l - rd);

        // Core dot products
        float NdotL = max(dot(n, l), 0.0);
        float NdotH = max(dot(n, h), 0.0);
        float NdotV = max(dot(n, -rd), 0.0);

        // Shadow
        float sh = softShadow(p + n * 0.003, l);

        // GGX specular — dual-lobe with trap-driven roughness
        float F = fresnelSchlick(NdotV, 0.04);
        float spec = ggxD(NdotH, roughness) * F;
        float specFine = ggxD(NdotH, roughness * 0.3) * fresnelSchlick(NdotV, 0.08);

        // Fresnel rim
        float rim = fresnelSchlick(NdotV, 0.02);
        float back = pow(max(dot(n, backL), 0.0), 1.6);
        float fill = max(dot(n, fillDir), 0.0);
        float edge = smoothstep(0.25, 1.0, rim);

        // Ambient occlusion — multi-sample + step-based
        float ao = ambientOcclusion(p, n);
        float stepAO = clamp(1.0 - float(steps) / 160.0, 0.0, 1.0);
        ao *= stepAO;

        // Subsurface scattering approximation
        float sss = pow(clamp(dot(rd, l), 0.0, 1.0), 2.0) * 0.14;
        sss *= clamp(1.0 - trapDetail * 2.0, 0.0, 1.0);
        sss *= (0.8 + 0.2 * trapCrease);

        // Environment reflection
        float envBright = 0.5 + 0.5 * dot(reflect(rd, n), l);
        float envRefl = fresnelSchlick(NdotV, 0.02) * envBright * ao;

        // Compose lighting
        float light = 0.06;
        light += 1.1 * NdotL * sh;
        light += 0.28 * fill * (0.5 + 0.5 * ao);
        light += 0.2 * back * (0.6 + 0.4 * ao);
        light += 0.7 * spec * sh;
        light += 0.3 * specFine * sh;
        light += 0.6 * rim * (0.4 + 0.6 * ao);
        light += sss;
        light += 0.15 * envRefl;
        light *= 0.9 + 0.55 * ao;

        // Surface micro-detail from orbit traps — 3-channel layered
        float detail = 0.92 + 0.08 * sin(trapDetail * 14.0 + time * 0.5);
        detail *= 0.95 + 0.05 * sin(trapEdge * 10.0 - time * 0.3);
        detail *= 0.97 + 0.03 * sin(trapCrease * 18.0 + time * 0.7);
        light *= detail;

        // Edge shimmer
        float beautyPulse = 0.5 + 0.5 * sin(time * 0.55 + t * 0.25);
        float shimmer = edge * (0.7 + 0.3 * beautyPulse);

        light += 0.06 * shimmer;
        light = smoothstep(0.0, 1.05, light);

        // Fog with forward scattering
        float fogDensity = 0.010 + 0.004 * sin(time * 0.15);
        float fog = 1.0 - exp(-fogDensity * t * t);
        float fogGray = 0.22 + 0.04 * (0.5 + 0.5 * sin(time * 0.18));
        float scatter = pow(max(dot(rd, l), 0.0), 6.0) * 0.12;
        fogGray += scatter * (1.0 - fog * 0.5);

        col = vec3(mix(light, fogGray, fog));
    }
    else
    {
        float h = clamp(0.5 + 0.5 * uv.y, 0.0, 1.0);
        col = mix(vec3(0.015), vec3(0.08), h);
        float bgGlow = exp(-3.0 * dot(uv, uv));
        col += vec3(0.04 * bgGlow);
    }

    // Breathing center glow
    float glowBreathe = 0.5 + 0.5 * sin(time * 0.2);
    float centerGlow = exp(-(2.2 + 0.6 * glowBreathe) * dot(uv, uv));
    col += vec3((0.02 + 0.015 * glowBreathe) * centerGlow);

    // Vignette
    float vignette = 0.75 + 0.25 * smoothstep(1.6, 0.0, dot(uv, uv));
    col *= vignette;

    // ACES filmic tone mapping
    col = col * (2.51 * col + 0.03) / (col * (2.43 * col + 0.59) + 0.14);

    // Convert to B&W — wider tonal range with S-curve
    float bw = dot(col, vec3(0.299, 0.587, 0.114));
    bw = smoothstep(0.005, 0.99, bw);
    float sCurve = bw * bw * (3.0 - 2.0 * bw);
    bw = mix(bw, sCurve, 0.35);
    bw = clamp(pow(bw, 0.82) * 1.06 + 0.008, 0.0, 1.0);

    // Film grain — luminance-adaptive
    float grain = hash21(uv * 500.0 + fract(time * 13.7)) * 0.035 - 0.0175;
    float grainMask = 1.0 - bw * 0.6;
    bw += grain * grainMask;

    col = vec3(clamp(bw, 0.0, 1.0));
    fragColor = TDOutputSwizzle(vec4(col, 1.0));
}