Back to shaders

Shader test bench

20260209

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)
// TouchDesigner GLSL TOP - Raymarched Triply Periodic Minimal Surfaces
// Morphing Gyroid ↔ Schwarz P ↔ Schwarz Diamond
// Palette: Deep indigo → lavender → sky blue → dark teal

uniform float u_time;
#define fragColor gl_FragColor

// ─── Color Palette (10 colors) ───────────────────────────
const vec3 PAL[10] = vec3[10](
    vec3(0.212, 0.176, 0.471),  // #362d78 deep indigo
    vec3(0.322, 0.247, 0.639),  // #523fa3 purple
    vec3(0.569, 0.424, 0.800),  // #916ccc medium purple
    vec3(0.741, 0.631, 0.898),  // #bda1e5 light lavender
    vec3(0.784, 0.753, 0.914),  // #c8c0e9 pale lavender
    vec3(0.518, 0.729, 0.906),  // #84bae7 sky blue
    vec3(0.318, 0.416, 0.831),  // #516ad4 royal blue
    vec3(0.200, 0.247, 0.529),  // #333f87 dark blue
    vec3(0.161, 0.188, 0.224),  // #293039 charcoal
    vec3(0.157, 0.212, 0.192)   // #283631 dark teal
);

// Smooth palette interpolation with cubic easing
vec3 getColor(float t) {
    t = clamp(t, 0.0, 1.0) * 9.0;
    int i = int(floor(t));
    float f = smoothstep(0.0, 1.0, fract(t));
    return mix(PAL[min(i, 9)], PAL[min(i + 1, 9)], f);
}

// ─── Mathematical Constants ──────────────────────────────
const float PHI = 1.618033988749895;  // Golden ratio φ = (1+√5)/2
const float PI  = 3.141592653589793;
const float TAU = 6.283185307179586;  // 2π

// ─── Utilities ───────────────────────────────────────────
mat2 rot2(float a) {
    float c = cos(a), s = sin(a);
    return mat2(c, -s, s, c);
}

// Polynomial smooth minimum — blends two SDFs organically
// k controls the blending radius (Inigo Quilez's formulation)
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);
}

// ─── Signed Distance Functions ───────────────────────────
// Gyroid minimal surface (Alan Schoen, 1970)
// sin(x)cos(y) + sin(y)cos(z) + sin(z)cos(x) = 0
float gyroid(vec3 p, float s) {
    p *= s;
    return abs(sin(p.x) * cos(p.y) +
               sin(p.y) * cos(p.z) +
               sin(p.z) * cos(p.x)) / s;
}

// Schwarz P-surface (Hermann Schwarz, 1865)
// cos(x) + cos(y) + cos(z) = 0
float schwarzP(vec3 p, float s) {
    p *= s;
    return abs(cos(p.x) + cos(p.y) + cos(p.z)) / s;
}

// Schwarz Diamond surface
// Combines all sin/cos permutations of 3 axes
float schwarzD(vec3 p, float s) {
    p *= s;
    return abs(
        sin(p.x) * sin(p.y) * sin(p.z) +
        sin(p.x) * cos(p.y) * cos(p.z) +
        cos(p.x) * sin(p.y) * cos(p.z) +
        cos(p.x) * cos(p.y) * sin(p.z)
    ) / s;
}

// ─── Scene Definition ────────────────────────────────────
float map(vec3 p) {
    // Gentle rotation — Euler angles evolving at irrational rates
    p.xz *= rot2(u_time * 0.08);
    p.yz *= rot2(u_time * 0.05 * PHI);
    p.xy *= rot2(sin(u_time * 0.03) * 0.25);

    // Scale breathes with golden-ratio timing
    float scale = PI + sin(u_time * 0.1 / PHI) * 0.5;

    // Barycentric morph between 3 minimal surfaces
    // Weights cycle at 120° phase offsets → always sums to ~1
    float phase = u_time * 0.12;
    float w1 = sin(phase) * 0.5 + 0.5;
    float w2 = sin(phase + TAU / 3.0) * 0.5 + 0.5;
    float w3 = sin(phase + 2.0 * TAU / 3.0) * 0.5 + 0.5;
    float wt = w1 + w2 + w3;
    w1 /= wt; w2 /= wt; w3 /= wt;

    // Evaluate each surface
    float g = gyroid(p, scale);
    float s = schwarzP(p, scale);
    float d = schwarzD(p, scale);

    // Weighted blend + shell thickness
    float surface = g * w1 + s * w2 + d * w3;
    surface -= 0.03;

    // Bounding sphere (golden ratio × 1.6 ≈ 2.589)
    float bound = length(p) - PHI * 1.6;

    return max(surface, bound);
}

// ─── Rendering Helpers ───────────────────────────────────
vec3 calcNormal(vec3 p) {
    vec2 e = vec2(0.001, 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)
    ));
}

// Screen-space ambient occlusion via SDF
float calcAO(vec3 p, vec3 n) {
    float ao = 0.0;
    for (int i = 1; i <= 5; i++) {
        float h = 0.01 + 0.12 * float(i);
        ao += (h - map(p + n * h)) / pow(2.0, float(i));
    }
    return clamp(1.0 - ao * 2.5, 0.0, 1.0);
}

// ─── Color Mapping ───────────────────────────────────────
// Maps the 10-color palette onto the 3D geometry.
// Inputs: p = surface position, n = normal, dist = ray depth, ao = occlusion
vec3 mapColor(vec3 p, vec3 n, float dist, float ao) {
    // Layer 1: position-driven sweep through the full palette
    // Golden-ratio dot product ensures irrational, non-repeating color bands
    float t1 = dot(p, vec3(PHI, 1.0, PHI - 1.0)) * 0.18 + 0.5;

    // Layer 2: normal-based tint — surface curvature picks a second color
    float t2 = dot(n, normalize(vec3(1.0, PHI, 0.0))) * 0.5 + 0.5;

    // Layer 3: depth fade — near surfaces get indigos, far gets teals
    float t3 = clamp(dist / 12.0, 0.0, 1.0);

    // Layer 4: slow animation — colors flow across the whole surface
    float flow = u_time * 0.04;

    // Blend all layers: position is primary, normal adds iridescence,
    // depth adds atmospheric perspective
    vec3 cPos    = getColor(fract(t1 + flow));
    vec3 cNormal = getColor(fract(t2 + flow * 0.7));
    vec3 cDepth  = getColor(fract(t3 * 0.8 + flow * 0.3));

    // Mix: 50% position, 25% normal, 25% depth
    vec3 col = cPos * 0.50 + cNormal * 0.25 + cDepth * 0.25;

    // AO darkens into the deepest palette colors (charcoal/teal)
    col = mix(PAL[8], col, ao * 0.85 + 0.15);

    return col;
}

// ─── Main ────────────────────────────────────────────────
void main() {
    vec2 res = uTDOutputInfo.res.zw;
    vec2 uv = (gl_FragCoord.xy - 0.5 * res) / min(res.x, res.y);

    // Camera orbiting on golden-ratio-phased ellipse
    float camAngle = u_time * 0.1;
    float camR = 4.5 + sin(u_time * 0.07 / PHI) * 0.8;
    vec3 ro = vec3(
        camR * cos(camAngle),
        sin(u_time * 0.08) * 1.5,
        camR * sin(camAngle)
    );

    // Look-at camera matrix
    vec3 target = vec3(0.0);
    vec3 fwd   = normalize(target - ro);
    vec3 right = normalize(cross(fwd, vec3(0.0, 1.0, 0.0)));
    vec3 up    = cross(right, fwd);
    vec3 rd    = normalize(fwd * 1.8 + right * uv.x + up * uv.y);

    // ── Raymarch ──
    float t = 0.0;
    for (int i = 0; i < 128; i++) {
        vec3 p = ro + rd * t;
        float d = map(p);
        if (abs(d) < 0.0004 || t > 25.0) break;
        t += d * 0.7;  // relaxation factor for stability
    }

    vec3 col;

    if (t < 25.0) {
        vec3 p = ro + rd * t;
        vec3 n = calcNormal(p);
        float ao = calcAO(p, n);

        // Two-point lighting
        vec3 l1 = normalize(vec3(1.0, 1.2, 0.8));
        vec3 l2 = normalize(vec3(-0.6, 0.3, -1.0));
        float diff1 = max(dot(n, l1), 0.0);
        float diff2 = max(dot(n, l2), 0.0) * 0.25;

        // Blinn-Phong specular
        vec3 h1 = normalize(l1 - rd);
        float spec = pow(max(dot(n, h1), 0.0), 64.0);

        // Palette color
        col = mapColor(p, n, t, ao);

        // Fresnel rim glow
        float fresnel = pow(1.0 - abs(dot(-rd, n)), 4.0);

        // Compose final shading
        col = col * (0.12 + diff1 * 0.75 + diff2) * ao;
        col += spec * PAL[4] * 0.35;
        col += fresnel * getColor(0.45) * 0.3;
    } else {
        // Background — subtle gradient from charcoal to teal
        float bg = length(uv) * 0.3;
        col = mix(PAL[8], PAL[9], bg + uv.y * 0.2);
        col *= 1.0 - length(uv) * 0.25;
    }

    // Reinhard tone mapping + gamma correction
    col = col / (col + 0.8);
    col = pow(col, vec3(0.4545));

    fragColor = TDOutputSwizzle(vec4(col, 1.0));
}