Back to shaders
Shader test bench
20250819
runnable fragment
Complete GLSL fragment shader. Stronghold runs it directly when the browser can compile it.
Code
precision mediump float;
// ✴︎ MANDALA QUASICRYSTAL — SMOOTH SEGMENT MORPH (no "watch tick")
// Uniforms: u_time (float), u_resolution (vec2), uAudio (optional 0..1)
uniform float u_time;
uniform vec2 u_resolution;
uniform float uAudio; // optional; if not wired, TD gives 0
#define fragColor gl_FragColor
const float PI = 3.14159265359;
const float TAU = 6.28318530718;
// ---------- Tunables
#define OCTAVES 5
#define SPEC_POWER 100.0
#define PARALLAX 0.10
#define NORMAL_EPS 1.6
#define EDGE_GLOW 0.22
#define EXPOSURE 2.0
#define GAMMA (1.0/1.65)
#define SATURATE 1.08
#define PALETTE_FLOW_SPEED 0.07
#define PALETTE_SWAY 0.16
#define CAMERA_ORBIT_SPEED 0.11
#define CAMERA_DOLLY_AMT 0.10
#define SWIRL_STRENGTH 0.20
#define SWIRL_BREATH 0.10
#define RING_FREQ 46.0
// ---------- Utils
mat2 rot(float a){ float s=sin(a), c=cos(a); return mat2(c,-s,s,c); }
float clamp01(float x){ return clamp(x,0.0,1.0); }
float easeInOutCubic(float x){ x=clamp01(x); return x<0.5 ? 4.0*x*x*x : 1.0-pow(-2.0*x+2.0,3.0)/2.0; }
float hash21(vec2 p){
p = fract(p*vec2(123.34, 345.45));
p += dot(p, p+34.345);
return fract(p.x*p.y);
}
float noise(vec2 x){
vec2 i = floor(x), f = fract(x);
f = f*f*(3.0-2.0*f);
float a = hash21(i);
float b = hash21(i+vec2(1,0));
float c = hash21(i+vec2(0,1));
float d = hash21(i+vec2(1,1));
return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
}
float fbm(vec2 x){
float v=0.0, a=0.5;
for(int i=0;i<OCTAVES;i++){ v+=a*noise(x); x*=2.0; a*=0.5; }
return v;
}
float lineAA(float d, float w){ return smoothstep(w, 0.0, abs(d)); }
// ---------- Building blocks
vec2 kaleid(vec2 p, float seg){
// supports non-integer seg, but we will still blend two integer folds for continuity
float a = atan(p.y,p.x), r=length(p), w = TAU/seg;
a = mod(a, w); a = abs(a - w*0.5);
return vec2(cos(a), sin(a))*r;
}
float quasicrystal(vec2 p, float k){
float s=0.0;
for(int i=0;i<5;i++){
float a = float(i)*PI/5.0;
s += cos(dot(p, vec2(cos(a),sin(a)))*k);
}
return s/5.0;
}
float superR(vec2 p, float m, float n1, float n2, float n3){
float th = atan(p.y, p.x);
float r = length(p)+1e-6;
float t = (abs(cos(m*th))+1e-6);
float u = (abs(sin(m*th))+1e-6);
float rr = pow(pow(t, n2)+pow(u, n3), -1.0/n1);
return 1.0 - smoothstep(rr*0.9, rr, r);
}
float hexWire(vec2 p, float s){
vec2 q = vec2(p.x*2.0, p.y*1.73205);
vec2 r = vec2(q.x+q.y, q.y-q.x)*0.5;
return lineAA(length(fract(r*s)-0.5), 0.14);
}
float spokes(vec2 p, float seg){
float th = atan(p.y,p.x);
float r = length(p);
return lineAA(sin(th*seg), 0.07)*smoothstep(1.2,0.0,r);
}
vec2 swirl(vec2 p, float strength, float time){
float r = length(p) + 1e-4;
float a = atan(p.y, p.x);
float off = strength * (1.0 - exp(-r*2.2)) * (0.8 + 0.2*sin(time*0.6));
a += off * r;
return vec2(cos(a), sin(a))*r;
}
// ---------- One “style” pass for a given segment count
float fieldHeightForSeg(vec2 p, float t, float seg){
// kaleidoscope + lens + micro-warp
vec2 pk = kaleid(p, seg);
float r0 = length(pk);
pk *= 1.0 + 0.13*sin(r0*4.0 - t*0.7 + 0.6*sin(t*0.21));
pk += 0.065 * (vec2(fbm(pk*3.0 + t*0.12), fbm(pk*2.6 - t*0.10)) - 0.5);
// features
float qc = quasicrystal(pk*1.55, mix(9.0, 18.0, easeInOutCubic(0.5+0.5*sin(t*0.11))));
float sf = superR(pk*1.08, floor(seg*0.5), 0.6, 0.8, 0.3);
float sp = spokes(pk, seg);
float rings = lineAA(sin(length(pk)*RING_FREQ - t*1.55), 0.042);
float hex = hexWire(pk*3.0, 1.0);
// weights (slow phases so they don’t fight the seg morph)
float s1 = 0.5 + 0.5*sin(t*0.16 + 0.0);
float s2 = 0.5 + 0.5*sin(t*0.19 + 1.3);
float s3 = 0.5 + 0.5*sin(t*0.14 + 2.6);
float sum = s1+s2+s3+1e-4;
float w1 = s1/sum, w2 = s2/sum, w3 = s3/sum;
float h = 0.0;
h += w1 * (0.55*rings + 0.45*hex);
h += w2 * (0.52*sp + 0.48*sf);
h += w3 * (0.58*lineAA(qc,0.10) + 0.30*sf);
// center bloom + breath
float r = length(pk);
float bloom = exp(-r*1.28)*(1.9) + smoothstep(0.30, 0.0, r)*0.55;
h *= bloom * (0.9 + 0.35*sin(t*0.72));
// tiny grain
h += (hash21(p*430.0 + t*36.0)-0.5)*0.045;
// tone to [0,1]
h = 1.0 - exp(-max(h,0.0)*2.0);
return clamp(pow(h, 0.86), 0.0, 1.0);
}
// ---------- Smooth segment morph (key fix for “watch tick”)
float fieldHeightSmooth(vec2 p, float t){
// continuous target segment (non-integer)
float target = mix(8.0, 20.0, 0.5 + 0.5*sin(t*0.12));
float s0 = floor(target);
float s1 = s0 + 1.0;
// eased crossfade based on fractional part
float k = easeInOutCubic(fract(target));
// compute both and blend
float h0 = fieldHeightForSeg(p, t, s0);
float h1 = fieldHeightForSeg(p, t, s1);
return mix(h0, h1, k);
}
// ---------- Normals & occlusion using the smooth field
vec3 calcNormal(vec2 p, float t, float e){
float hC = fieldHeightSmooth(p, t);
float hX = fieldHeightSmooth(p + vec2(e,0.0), t);
float hY = fieldHeightSmooth(p + vec2(0.0,e), t);
return normalize(vec3(-(hX-hC)/e, -(hY-hC)/e, 1.0));
}
float horizonOcc(vec2 p, float t, float e){
float hC = fieldHeightSmooth(p, t);
float accum = 0.0;
const int STEPS = 5;
for(int i=1;i<=STEPS;i++){
float d = float(i)*e*2.6;
float hS = fieldHeightSmooth(p + vec2(d,d), t);
accum += step(hS, hC - 0.03*float(i));
}
return accum/float(STEPS);
}
// ---------- Your 10-color palette
vec3 paletteIndex(int idx){
if(idx==0) return vec3(0x36,0x2d,0x78)/255.0; // #362d78
if(idx==1) return vec3(0x52,0x3f,0xa3)/255.0; // #523fa3
if(idx==2) return vec3(0x91,0x6c,0xcc)/255.0; // #916ccc
if(idx==3) return vec3(0xbd,0xa1,0xe5)/255.0; // #bda1e5
if(idx==4) return vec3(0xc8,0xc0,0xe9)/255.0; // #c8c0e9
if(idx==5) return vec3(0x84,0xba,0xe7)/255.0; // #84bae7
if(idx==6) return vec3(0x51,0x6a,0xd4)/255.0; // #516ad4
if(idx==7) return vec3(0x33,0x3f,0x87)/255.0; // #333f87
if(idx==8) return vec3(0x29,0x30,0x39)/255.0; // #293039
return vec3(0x28,0x36,0x31)/255.0; // #283631
}
float luma(vec3 c){ return dot(c, vec3(0.2126,0.7152,0.0722)); }
vec3 getPalette(float t, float timeShift){
float shift = timeShift + PALETTE_SWAY * sin(timeShift*2.5);
float idx = clamp(fract(t + shift)*9.0, 0.0, 9.0);
int i = int(floor(idx));
int j = min(i+1, 9);
float f = fract(idx);
return mix(paletteIndex(i), paletteIndex(j), f);
}
float bayer4(vec2 p){
ivec2 ip = ivec2(mod(floor(p), 4.0));
int m[16] = int[16](
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5
);
int v = m[ip.y*4 + ip.x];
return (float(v)+0.5)/16.0;
}
// ---------- Main
void main(){
vec2 fc = gl_FragCoord.xy;
vec2 uv = fc / u_resolution;
vec2 asp = vec2(u_resolution.x/u_resolution.y, 1.0);
float t = u_time;
float beat = clamp(uAudio, 0.0, 1.0);
// Camera: orbit + gentle dolly, both slightly low-passed by nested sines
float orbit = t*CAMERA_ORBIT_SPEED;
float dolly = 1.0 + CAMERA_DOLLY_AMT * sin(t*0.30 + 0.4*sin(t*0.12));
vec2 p = (uv - 0.5)*asp*2.0;
p = rot(orbit)*p;
p *= dolly;
// Tilt (squash) with mild breathing
float tilt = 0.32 + 0.14*sin(t*0.38 + 0.6*cos(t*0.19));
p.y *= mix(1.0, 0.56, tilt);
// Smooth swirl (also low-passed)
float swirlAmt = SWIRL_STRENGTH + SWIRL_BREATH*sin(t*0.33);
p = swirl(p, swirlAmt, t);
// Parallax
vec2 pPar = p + PARALLAX * p * length(p);
// Height (SMOOTH morph) & lighting
float h = fieldHeightSmooth(pPar, t);
float z = mix(-0.65, 0.65, h);
float e = NORMAL_EPS / u_resolution.y;
vec3 normal = calcNormal(pPar, t, e);
vec3 Ldir = normalize(vec3(0.55, 0.35, 0.76));
float diff = max(dot(normal, Ldir), 0.0);
vec3 V = vec3(0.0, 0.0, 1.0);
vec3 H = normalize(Ldir + V);
float spec = pow(max(dot(normal, H), 0.0), SPEC_POWER) * (0.55 + 0.20*beat);
float rim = pow(1.0 - max(dot(normal, V), 0.0), 2.0);
float hx = fieldHeightSmooth(pPar + vec2(e,0.0), t) - h;
float hy = fieldHeightSmooth(pPar + vec2(0.0,e), t) - h;
float edge = smoothstep(0.05, 0.25, sqrt(hx*hx + hy*hy)) * EDGE_GLOW;
float occ = horizonOcc(pPar, t, e) * 0.34;
float base = 0.18 + 0.82*diff + 0.28*spec + 0.32*rim + edge;
base *= (0.76 + 0.24*occ);
// Depth fog (slow modulation)
float r = length(p);
float fog = smoothstep(0.9, 0.2, 0.45*z + 0.55*(1.0 - r) + 0.06*sin(t*0.22));
float Lmono = mix(0.0, base, fog);
// Tone
float L = 1.0 - exp(-max(Lmono,0.0)*EXPOSURE);
L = pow(L, GAMMA);
L = clamp(L, 0.0, 1.0);
// Palette (depth-aware) with slow flow
float bias = clamp(0.5 + 0.35*(z) + 0.12*(diff - rim), 0.0, 1.0);
float grad = clamp(L*0.84 + 0.16*bias, 0.0, 1.0);
float timeShift = t * PALETTE_FLOW_SPEED;
vec3 col = getPalette(grad, timeShift);
// Subtle saturation, shimmer, dither, vignette
float g = luma(col);
col = mix(vec3(g), col, SATURATE);
col *= (1.0 - 0.030 * sin(fc.y*3.0 + t*10.0));
col += (bayer4(fc) - 0.5) * 0.004;
float vig = smoothstep(1.55, 0.35, r);
col *= vig;
fragColor = vec4(col, 1.0);
}