Back to shaders

Shader test bench

20260312

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;
uniform float u_time;
uniform vec2  uRes;
uniform float uSpeed;    // [0.3–2.0]  default 1.0
uniform float uGlow;     // [0.0–2.0]  default 1.0
uniform float uPulse;    // [0.0–1.0]  default 0.5
uniform float uFog;      // [0.0–1.0]  default 0.35
#define fragColor gl_FragColor

#define PI      3.14159265
#define TAU     6.28318530

// ── rotation ─────────────────────────────────────────────────────────────
mat3 rotX(float a){float c=cos(a),s=sin(a);return mat3(1,0,0,0,c,-s,0,s,c);}
mat3 rotY(float a){float c=cos(a),s=sin(a);return mat3(c,0,s,0,1,0,-s,0,c);}
mat3 rotZ(float a){float c=cos(a),s=sin(a);return mat3(c,-s,0,s,c,0,0,0,1);}

// ── noise ────────────────────────────────────────────────────────────────
float hash(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 hash3(vec3 p) {
    p = fract(p * vec3(0.1031, 0.1030, 0.0973));
    p += dot(p, p.yxz + 33.33);
    return fract((p.x + p.y) * p.z);
}

float vnoise(vec3 p) {
    vec3 i = floor(p);
    vec3 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    float a = hash3(i);
    float b = hash3(i + vec3(1,0,0));
    float c = hash3(i + vec3(0,1,0));
    float d = hash3(i + vec3(1,1,0));
    float e = hash3(i + vec3(0,0,1));
    float ff= hash3(i + vec3(1,0,1));
    float g = hash3(i + vec3(0,1,1));
    float h = hash3(i + vec3(1,1,1));
    return mix(
        mix(mix(a,b,f.x), mix(c,d,f.x), f.y),
        mix(mix(e,ff,f.x), mix(g,h,f.x), f.y),
        f.z
    );
}

// 2-octave fbm (was 4 — halved)
float fbm(vec3 p) {
    float v = 0.5 * vnoise(p) + 0.25 * vnoise(p * 2.1);
    return v;
}

// ── smooth booleans ──────────────────────────────────────────────────────
float smin(float a, float b, float k) {
    float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0);
    return mix(b, a, h) - k*h*(1.0-h);
}
float smax(float a, float b, float k) { return -smin(-a, -b, k); }

// ── GGX specular ─────────────────────────────────────────────────────────
float ggx(float NdH, float rough) {
    float a2 = rough * rough;
    a2 *= a2;
    float d = NdH * NdH * (a2 - 1.0) + 1.0;
    return a2 / (PI * d * d + 0.0001);
}

float schlickFresnel(float VdH) {
    float f = 1.0 - VdH;
    return 0.04 + 0.96 * f * f * f * f * f;
}

// ═══════════════════════════════════════════════════════════════════════════
// Curves with secondary wobble
// ═══════════════════════════════════════════════════════════════════════════
vec3 trefoil(float t, float R, float r, float T) {
    float ct = cos(t), st = sin(t);
    float c2 = cos(2.0*t), s2 = sin(2.0*t);
    vec3 base = vec3(
        (R + r * c2) * ct,
        (R + r * c2) * st,
        r * s2
    ) * 0.6;
    float wobble = sin(t * 5.0 + T * 1.2) * 0.012;
    base.y += wobble;
    base.x += wobble * 0.6 * cos(t * 3.0);
    return base;
}

vec3 trefoil2(float t, float R, float r, float T) {
    return rotZ(PI/6.0) * trefoil(t + TAU/3.0, R, r, T);
}

vec3 cinquefoil(float t, float R, float r, float T) {
    float c2 = cos(2.0*t), s2 = sin(2.0*t);
    float c5 = cos(5.0*t), s5 = sin(5.0*t);
    vec3 base = vec3(
        (R + r * c2) * c5,
        (R + r * c2) * s5,
        r * s2
    ) * 0.35;
    base.z += sin(t * 3.0 + T * 0.9) * 0.008;
    return base;
}

// ── tube SDF — reduced samples: 64/64/48 (was 96/96/80) ─────────────────
float sdCurve(vec3 p, int curveID, float T, float rad, int samples) {
    float md = 1e10;
    float R = 1.0 + 0.05 * sin(T * 0.3);
    float r = 0.45 + 0.03 * sin(T * 0.5);
    for (int i = 0; i < samples; i++) {
        float t = float(i) / float(samples) * TAU;
        vec3 cp;
        if (curveID == 0)      cp = trefoil(t, R, r, T);
        else if (curveID == 1) cp = trefoil2(t, R, r, T);
        else                   cp = cinquefoil(t, R * 1.6, r * 0.8, T);
        float d = length(p - cp) - rad;
        md = min(md, d);
    }
    return md;
}

// ═══════════════════════════════════════════════════════════════════════════
// Celtic Weave
// ═══════════════════════════════════════════════════════════════════════════
vec2 celticWeave(vec3 p, float d1, float d2, float d3, float T) {
    float angle = atan(p.y, p.x);
    float wave12 = sin(angle * 3.0 - T * 1.5);
    float wave13 = sin(angle * 5.0 + T * 0.8);
    float cut = 0.035, blend = 0.02;

    float s1 = d1, s2 = d2;
    if (wave12 > 0.0) s2 = smax(d2, -(d1 - cut), blend);
    else              s1 = smax(d1, -(d2 - cut), blend);

    float s3 = d3;
    if (wave13 > 0.0) s3 = smax(d3, -min(d1, d2) + cut * 0.6, blend);

    float d = smin(s1, smin(s2, s3, blend), blend);
    float id = 0.0;
    if (abs(d - s2) < abs(d - s1) && abs(d - s2) < abs(d - s3)) id = 1.0;
    else if (abs(d - s3) < abs(d - s1)) id = 2.0;
    return vec2(d, id);
}

// ═══════════════════════════════════════════════════════════════════════════
// Scene SDF
// ═══════════════════════════════════════════════════════════════════════════
vec2 map(vec3 p, float T) {
    p = rotY(T * 0.18) * rotX(sin(T * 0.12) * 0.35) * p;
    float tubeR = 0.048 + 0.01 * sin(T * 0.7) * uPulse;
    float d1 = sdCurve(p, 0, T, tubeR, 64);
    float d2 = sdCurve(p, 1, T, tubeR, 64);
    float d3 = sdCurve(p, 2, T, tubeR * 0.7, 48);
    return celticWeave(p, d1, d2, d3, T);
}

vec3 calcNormal(vec3 p, float T) {
    vec2 e = vec2(0.001, 0.0);
    return normalize(vec3(
        map(p+e.xyy,T).x - map(p-e.xyy,T).x,
        map(p+e.yxy,T).x - map(p-e.yxy,T).x,
        map(p+e.yyx,T).x - map(p-e.yyx,T).x
    ));
}

// ── AO: 4 steps (was 6) ─────────────────────────────────────────────────
float calcAO(vec3 p, vec3 n, float T) {
    float ao = 0.0, scale = 1.0;
    for (int i = 0; i < 4; i++) {
        float dist = 0.02 + 0.06 * float(i);
        ao += (dist - map(p + n * dist, T).x) * scale;
        scale *= 0.55;
    }
    return clamp(1.0 - ao * 5.0, 0.0, 1.0);
}

// ── soft shadow: 16 steps (was 24) ──────────────────────────────────────
float softShadow(vec3 ro, vec3 rd, float mint, float maxt, float k, float T) {
    float res = 1.0, t = mint;
    for (int i = 0; i < 16; i++) {
        float h = map(ro + rd * t, T).x;
        res = min(res, k * h / t);
        t += clamp(h, 0.008, 0.15);
        if (t > maxt) break;
    }
    return clamp(res, 0.0, 1.0);
}

float sss(vec3 p, vec3 n, vec3 lightDir, float T) {
    float thickness = map(p - n * 0.08, T).x;
    float scatter = max(0.0, dot(normalize(-n + lightDir * 0.6), lightDir));
    return pow(scatter, 3.0) * exp(-thickness * 12.0) * 0.4;
}

// ═══════════════════════════════════════════════════════════════════════════
// Volumetric god rays — 20 steps (was 40), 6 shadow steps (was 12)
// ═══════════════════════════════════════════════════════════════════════════
vec3 godRayLightPos(float T) {
    return vec3(
        2.5 * sin(T * 0.13 + 0.5),
        1.8 + 0.6 * sin(T * 0.09),
        2.5 * cos(T * 0.13 + 0.5)
    );
}

float volumetricGodRays(vec3 ro, vec3 rd, float maxT, float T) {
    vec3 lightPos = godRayLightPos(T);
    float accum = 0.0;
    int steps = 20;
    float stepSize = min(maxT, 5.0) / float(steps);

    for (int i = 0; i < 20; i++) {
        float tS = (float(i) + 0.5) * stepSize;
        vec3 sp = ro + rd * tS;
        float sceneDist = map(sp, T).x;

        if (sceneDist > 0.01) {
            vec3 toLight = lightPos - sp;
            float lightDist = length(toLight);
            vec3 lightDir = toLight / lightDist;

            // 6-step shadow ray (was 12)
            float shadow = 1.0;
            float tSh = 0.03;
            for (int j = 0; j < 6; j++) {
                float hSh = map(sp + lightDir * tSh, T).x;
                if (hSh < 0.003) { shadow = 0.0; break; }
                shadow = min(shadow, 6.0 * hSh / tSh);
                tSh += clamp(hSh, 0.03, 0.2);
                if (tSh > lightDist) break;
            }
            shadow = clamp(shadow, 0.0, 1.0);

            // 2-octave fbm fog density
            vec3 noisePos = sp * 1.8 + vec3(T * 0.06, T * 0.04, T * -0.03);
            float density = fbm(noisePos);
            density *= exp(-length(sp) * 0.4) * 0.035;

            float atten = 1.0 / (1.0 + lightDist * lightDist * 0.15);

            // Henyey-Greenstein phase
            float cosTheta = dot(rd, lightDir);
            float g = 0.6;
            float phase = (1.0 - g*g) / (4.0 * PI * pow(1.0 + g*g - 2.0*g*cosTheta, 1.5));

            accum += density * shadow * atten * phase * stepSize;
        }
    }
    return accum * uGlow * 8.0;
}

// ── dust motes: 10 (was 20) ─────────────────────────────────────────────
float dustMotes(vec3 ro, vec3 rd, float T) {
    float motes = 0.0;
    for (int i = 0; i < 10; i++) {
        float fi = float(i);
        vec3 motePos = vec3(
            sin(fi * 1.7 + T * 0.12) * 1.5,
            cos(fi * 2.3 + T * 0.08) * 1.0 + sin(fi * 0.7 + T * 0.15) * 0.4,
            sin(fi * 3.1 + T * 0.1) * 1.5
        );
        vec3 toMote = motePos - ro;
        float tProj = dot(toMote, rd);
        if (tProj < 0.0) continue;
        float dist = length(ro + rd * tProj - motePos);
        float brightness = exp(-dist * dist * 800.0) * 0.1;
        brightness *= 0.6 + 0.4 * sin(fi * 4.7 + T * 2.5);
        motes += brightness;
    }
    return motes * uGlow;
}

// ═══════════════════════════════════════════════════════════════════════════
// Main
// ═══════════════════════════════════════════════════════════════════════════
void main() {
    vec2 uv = vUV.st;
    float asp = uRes.x / uRes.y;
    vec2 screen = (uv - 0.5) * vec2(asp, 1.0);
    float T = u_time * uSpeed * 0.65;

    // ── eased cinematic camera ───────────────────────────────────────────
    float camPhase = T * 0.2;
    float camEase = camPhase + 0.15 * sin(camPhase * 0.7);
    float camDist = 2.5 + 0.5 * sin(T * 0.25) * cos(T * 0.11);
    vec3 camPos = vec3(
        sin(camEase) * camDist,
        sin(T * 0.15) * 0.7 + 0.4 + 0.15 * cos(T * 0.07),
        cos(camEase) * camDist
    );
    vec3 camTarget = vec3(sin(T * 0.05) * 0.08, sin(T * 0.07) * 0.1, cos(T * 0.06) * 0.05);
    vec3 camFwd   = normalize(camTarget - camPos);
    vec3 camRight = normalize(cross(camFwd, vec3(0,1,0)));
    vec3 camUp    = cross(camRight, camFwd);

    float focalLen = 1.6 + 0.08 * sin(T * 0.2);
    vec2 distorted = screen * (1.0 + 0.035 * dot(screen, screen));
    vec3 rd = normalize(distorted.x * camRight + distorted.y * camUp + focalLen * camFwd);

    // ── raymarch: 120 steps (was 150) ────────────────────────────────────
    float t = 0.0;
    float strandID = 0.0;
    bool hit = false;

    for (int i = 0; i < 120; i++) {
        vec3 p = camPos + rd * t;
        vec2 res = map(p, T);
        if (res.x < 0.0004) {
            hit = true;
            strandID = res.y;
            break;
        }
        if (t > 7.0) break;
        t += res.x * 0.78;
    }

    // ── shading ──────────────────────────────────────────────────────────
    vec3 col = vec3(0.0);
    float hitDist = hit ? t : 7.0;

    if (hit) {
        vec3 p = camPos + rd * t;
        vec3 n = calcNormal(p, T);
        vec3 v = -rd;
        float angle = atan(p.z, p.x);

        vec3 L1 = normalize(godRayLightPos(T) - p);
        vec3 L2 = normalize(vec3(-0.5, -0.2, -0.7));

        float NdL1 = max(dot(n, L1), 0.0);
        float NdL2 = max(dot(n, L2), 0.0);

        vec3 H1 = normalize(L1 + v);
        vec3 H2 = normalize(L2 + v);
        float NdH1 = max(dot(n, H1), 0.0);
        float NdH2 = max(dot(n, H2), 0.0);
        float VdH1 = max(dot(v, H1), 0.0);

        float spec1 = ggx(NdH1, 0.18) * schlickFresnel(VdH1);
        float spec2 = ggx(NdH2, 0.27) * 0.4;

        float fresnel = pow(1.0 - max(dot(n, v), 0.0), 4.5);
        float rim = fresnel * (0.6 + 0.4 * sin(angle * 5.0 - T * 3.5));

        float scatter = sss(p, n, L1, T);
        float shadow  = softShadow(p + n * 0.005, L1, 0.01, 1.8, 14.0, T);
        float ao      = calcAO(p, n, T);

        vec3 refl = reflect(-v, n);
        float envRefl = pow(max(refl.y * 0.5 + 0.5, 0.0), 2.0) * 0.15;
        envRefl += pow(max(dot(refl, L1), 0.0), 16.0) * 0.3;

        float strandBright = strandID > 1.5 ? 0.5 : (strandID > 0.5 ? 0.8 : 1.0);
        float pulse = 0.8 + 0.2 * sin(angle * 3.0 - T * 2.5) * uPulse;
        float hotPulse = pow(max(sin(angle * 3.0 - T * 2.5), 0.0), 8.0) * uPulse * 0.3;

        float lum = 0.0;
        lum += NdL1 * 0.65 * shadow;
        lum += NdL2 * 0.2;
        lum += spec1 * 1.8 * uGlow;
        lum += spec2 * 0.5 * uGlow;
        lum += rim * 0.8 * uGlow;
        lum += scatter * uGlow;
        lum += envRefl * uGlow;
        lum += hotPulse * uGlow;
        lum += 0.05;
        lum *= ao * strandBright * pulse;

        col = vec3(lum);
        col *= exp(-t * t * uFog * 0.07);
    }

    // ── volumetric + motes + atmosphere ──────────────────────────────────
    col += vec3(volumetricGodRays(camPos, rd, hitDist, T));
    col += vec3(dustMotes(camPos, rd, T));
    col += vec3(
        exp(-length(screen) * 2.2) * 0.025 * uGlow +
        exp(-abs(length(screen) - 0.55) * 7.0) * 0.008 * uGlow
    );

    // ── vignette ─────────────────────────────────────────────────────────
    vec2 vigUV = uv - 0.5;
    col *= smoothstep(1.0, 0.25, dot(vigUV * vec2(1.1, 1.4), vigUV * vec2(1.1, 1.4)));

    // ── ACES + S-curve + monochrome ──────────────────────────────────────
    col = (col * (2.51 * col + 0.03)) / (col * (2.43 * col + 0.59) + 0.14);
    col = smoothstep(0.0, 1.0, col);
    col = pow(max(col, 0.0), vec3(0.93));
    float mono = dot(col, vec3(0.299, 0.587, 0.114));
    col = vec3(mono);

    // ── grain + bloom ────────────────────────────────────────────────────
    col += (hash(uv * uRes + fract(u_time * 7.3)) - 0.5) * 0.025;
    col += max(mono - 0.65, 0.0) * 0.18 * exp(-length(screen) * 1.2);

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