Back to shaders
Shader test bench
20260309
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; // TD resolution — e.g. 1920 1080
uniform float uSpeed; // animation speed [0.3–2.0] default 1.0
uniform float uGlow; // glow / emission [0.0–1.5] default 0.8
uniform float uPulse; // music pulse energy [0.0–1.0] default 0.5
uniform float uFog; // depth fog density [0.0–1.0] default 0.35
#define fragColor gl_FragColor
// ── Hatsune Miku palette ─────────────────────────────────────────────────
#define C_MIKU vec3(0.224, 0.773, 0.733) // #39C5BB iconic teal
#define C_MIKU_D vec3(0.090, 0.320, 0.320) // deep teal shadow
#define C_MIKU_L vec3(0.400, 0.920, 0.880) // light teal highlight
#define C_PINK vec3(0.953, 0.361, 0.529) // #F35C87 accent pink
#define C_CYAN vec3(0.500, 0.960, 1.000) // electric cyan
#define C_VOID vec3(0.012, 0.020, 0.045) // deep void background
#define C_WHITE vec3(0.930, 0.965, 0.975) // near-white highlight
#define C_PURPLE vec3(0.420, 0.260, 0.650) // purple accent
#define C_GRID vec3(0.045, 0.160, 0.165) // cyber grid line color
#define C_SAKURA vec3(1.000, 0.760, 0.810) // soft sakura pink
#define C_HAIR vec3(0.160, 0.560, 0.540) // darker hair teal
// ── utility ──────────────────────────────────────────────────────────────
mat2 rot2(float a) { float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }
float hash21(vec2 p) {
p = fract(p * vec2(127.1, 311.7));
p += dot(p, p + 19.19);
return fract(p.x * p.y);
}
float hash31(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
}
float noise3(vec3 p) {
vec3 i=floor(p), f=fract(p), u=f*f*(3.-2.*f);
return mix(
mix(mix(hash31(i), hash31(i+vec3(1,0,0)), u.x),
mix(hash31(i+vec3(0,1,0)), hash31(i+vec3(1,1,0)), u.x), u.y),
mix(mix(hash31(i+vec3(0,0,1)), hash31(i+vec3(1,0,1)), u.x),
mix(hash31(i+vec3(0,1,1)), hash31(i+vec3(1,1,1)), u.x), u.y),
u.z);
}
const mat3 M3 = mat3( 0.00, 0.80, 0.60,
-0.80, 0.36,-0.48,
-0.60,-0.48, 0.64);
float fbm3(vec3 p) {
float v=0., a=.5;
for(int i=0;i<4;i++){ v+=a*noise3(p); p=M3*p*2.+.5; a*=.5; }
return v;
}
// ── smooth min — organic SDF blending ────────────────────────────────────
float smin(float a, float b, float k) {
float h = max(k - abs(a - b), 0.0) / k;
return min(a, b) - h * h * k * 0.25;
}
// ── 3D signed distance functions ─────────────────────────────────────────
float sdSphere(vec3 p, float r) { return length(p) - r; }
float sdBox(vec3 p, vec3 b) {
vec3 q = abs(p) - b;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdOctahedron(vec3 p, float s) {
p = abs(p);
return (p.x + p.y + p.z - s) * 0.57735027;
}
float sdTorus(vec3 p, vec2 t) {
vec2 q = vec2(length(p.xz) - t.x, p.y);
return length(q) - t.y;
}
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
vec3 pa = p - a, ba = b - a;
return length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)) - r;
}
// ── centerpiece — the heart of the Miku tribute ─────────────────────────
// TODO(human): Design the central 3D sculpture using SDF primitives.
//
// Available SDFs:
// sdSphere(p, radius)
// sdBox(p, vec3(halfX, halfY, halfZ))
// sdOctahedron(p, size)
// sdTorus(p, vec2(majorR, minorR))
// sdCapsule(p, pointA, pointB, radius)
//
// Combining SDFs:
// min(a, b) — union (join shapes)
// max(a, b) — intersection (keep overlap)
// max(a, -b) — subtraction (carve b from a)
// smin(a, b, k) — smooth blend (k=smoothness, try 0.1–0.5)
//
// Use rot2() on p.xz or p.xy to rotate, and (p - offset) to translate.
// T is the current time — use it for animation.
//
// Example ideas:
// - A torus with an octahedron core (digital crown)
// - A sphere with box-shaped cuts (cyber crystal)
// - Multiple blended spheres orbiting (musical energy)
//
float centerpiece(vec3 p, float T) {
// ── voice ring — tilts gently like a spinning record ─────────
vec3 rp = p;
rp.xy *= rot2(sin(T * 0.3) * 0.25); // gentle tilt
rp.xz *= rot2(T * 0.4); // slow spin
float ring = sdTorus(rp, vec2(0.5, 0.12));
// ── crystal core — spins faster, pulses with energy ──────────
vec3 cp = p;
cp.xz *= rot2(T * 1.2); // fast spin on Y axis
cp.xy *= rot2(T * 0.7); // tumble on Z axis
float pulse = 0.35 + sin(T * 2.5) * 0.05; // breathing size
float core = sdOctahedron(cp, pulse);
// ── hollow out the crystal — carve a sphere from inside ──────
float hollow = sdSphere(p, 0.22); // inner void
core = max(core, -hollow); // subtraction!
// ── inner spark — tiny spinning sphere visible through gaps ──
vec3 sp = p;
sp.xz *= rot2(T * 3.0); // fast spin
float spark = sdOctahedron(sp, 0.1 + sin(T * 4.0) * 0.03);
core = min(core, spark); // add inside the hollow
// ── second ring — perpendicular, like an atom orbit ──────────
vec3 rp2 = p;
rp2.yz *= rot2(1.57); // flip 90° (vertical)
rp2.xz *= rot2(T * -0.3); // counter-rotate
float ring2 = sdTorus(rp2, vec2(0.55, 0.06)); // thinner outer ring
// ── third ring — diagonal, completing the gyroscope ──────────
vec3 rp3 = p;
rp3.xy *= rot2(0.78); // 45° tilt
rp3.yz *= rot2(0.78); // diagonal plane
rp3.xz *= rot2(T * 0.5); // slow spin
float ring3 = sdTorus(rp3, vec2(0.48, 0.04)); // thinnest ring
// ── combine everything ───────────────────────────────────────
float d = smin(ring, core, 0.18); // melt ring + crystal
d = smin(d, ring2, 0.08); // atom orbit ring
d = smin(d, ring3, 0.06); // diagonal ring
return d;
}
// ── material ID helper ───────────────────────────────────────────────────
#define MAT_FLOOR 0
#define MAT_CENTER 1
#define MAT_ORB_L 2 // large orbiter (×3)
#define MAT_ORB_S 3 // small orbiter (×9)
#define MAT_RIBBON 4
// ── scene SDF + material ─────────────────────────────────────────────────
vec2 sceneSDF(vec3 p, float T) {
// returns vec2(distance, materialID)
float d = 1e9;
float mat = -1.0;
// ── centerpiece ──────────────────────────────────────────────────────
float cp = centerpiece(p, T);
if (cp < d) { d = cp; mat = float(MAT_CENTER); }
// ── 3 large orbiting octahedron crystals — the "3" in 39 ─────────────
for (int i = 0; i < 3; i++) {
float fi = float(i);
float ang = fi / 3.0 * 6.2831 + T * 0.35;
float orbitR = 2.2 + sin(T * 0.25 + fi * 2.09) * 0.25;
float bobY = sin(T * 0.55 + fi * 2.09) * 0.5;
vec3 op = p - vec3(cos(ang) * orbitR, bobY, sin(ang) * orbitR);
op.xz *= rot2(T * 0.6 + fi * 2.09);
op.xy *= rot2(T * 0.4 + fi);
float pulse = 1.0 + sin(T * 2.5 + fi * 2.09) * uPulse * 0.15;
float shape = sdOctahedron(op, 0.32 * pulse);
if (shape < d) { d = shape; mat = float(MAT_ORB_L); }
}
// ── 9 small orbiting cubes — the "9" in 39 ──────────────────────────
for (int i = 0; i < 9; i++) {
float fi = float(i);
float ang = fi / 9.0 * 6.2831 + T * 0.55 + 0.35;
float orbitR = 3.6 + sin(T * 0.35 + fi * 0.7) * 0.4;
float bobY = sin(T * 0.7 + fi * 0.7) * 0.7 + cos(T * 0.3 + fi) * 0.2;
vec3 op = p - vec3(cos(ang) * orbitR, bobY, sin(ang) * orbitR);
op.xz *= rot2(T * 1.2 + fi);
op.xy *= rot2(T * 0.9 + fi * 0.7);
float shape = sdBox(op, vec3(0.11 + 0.03 * sin(T + fi)));
if (shape < d) { d = shape; mat = float(MAT_ORB_S); }
}
// ── twin-tail ribbons — torus arcs sweeping behind center ────────────
for (int s = 0; s < 2; s++) {
float side = float(s) * 2.0 - 1.0;
vec3 rp = p - vec3(side * 0.4, 0.6, -0.2);
rp.xz *= rot2(side * 0.5 + T * 0.2);
rp.xy *= rot2(side * 0.3);
float ribbon = sdTorus(rp, vec2(1.2 + sin(T * 0.4) * 0.15, 0.045));
// cut to arc (only keep back half + flowing tail)
float cut = -rp.z - 0.2 * sin(rp.x * 2.0 + T);
ribbon = max(ribbon, cut);
if (ribbon < d) { d = ribbon; mat = float(MAT_RIBBON); }
}
// ── ground plane ─────────────────────────────────────────────────────
float floor_ = p.y + 2.0;
if (floor_ < d) { d = floor_; mat = float(MAT_FLOOR); }
return vec2(d, mat);
}
// ── normal via central differences ───────────────────────────────────────
vec3 calcNormal(vec3 p, float T) {
vec2 e = vec2(0.0008, 0.0);
return normalize(vec3(
sceneSDF(p + e.xyy, T).x - sceneSDF(p - e.xyy, T).x,
sceneSDF(p + e.yxy, T).x - sceneSDF(p - e.yxy, T).x,
sceneSDF(p + e.yyx, T).x - sceneSDF(p - e.yyx, T).x
));
}
// ── soft shadow ray ──────────────────────────────────────────────────────
float softShadow(vec3 ro, vec3 rd, float tMin, float tMax, float k, float T) {
float res = 1.0, t = tMin;
for (int i = 0; i < 32; i++) {
float d = sceneSDF(ro + rd * t, T).x;
if (d < 0.001) return 0.0;
res = min(res, k * d / t);
t += clamp(d, 0.02, 0.2);
if (t > tMax) break;
}
return clamp(res, 0.0, 1.0);
}
// ── ambient occlusion ────────────────────────────────────────────────────
float calcAO(vec3 p, vec3 n, float T) {
float ao = 0.0, s = 1.0;
for (int i = 1; i <= 5; i++) {
float d = 0.04 * float(i);
ao += s * (d - sceneSDF(p + n * d, T).x);
s *= 0.5;
}
return clamp(1.0 - ao * 3.0, 0.0, 1.0);
}
// ── digital particles — 2D overlay ───────────────────────────────────────
float digiParticles(vec2 uv, float T) {
float p = 0.0;
for (int i = 0; i < 25; i++) {
float fi = float(i);
float spd = 0.025 + hash21(vec2(fi, 30.0)) * 0.045;
vec2 pos = vec2(
hash21(vec2(fi, 31.0)) + sin(T * 0.25 + fi * 1.7) * 0.015,
fract(T * spd + hash21(vec2(fi, 32.0)))
);
float r = 0.0015 + hash21(vec2(fi, 33.0)) * 0.003;
float blink = 0.5 + 0.5 * sin(T * 2.8 + fi * 2.1);
p += smoothstep(r * 4.0, 0.0, length(uv - pos)) * blink;
}
return p;
}
// ── 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.38;
// ── camera — slow orbit ──────────────────────────────────────────────
float camAng = T * 0.12;
float camR = 6.5 + sin(T * 0.15) * 0.8;
float camH = 2.2 + sin(T * 0.18) * 0.6;
vec3 ro = vec3(cos(camAng) * camR, camH, sin(camAng) * camR);
vec3 ta = vec3(0.0, 0.15 + sin(T * 0.1) * 0.15, 0.0);
vec3 fwd = normalize(ta - ro);
vec3 right = normalize(cross(fwd, vec3(0, 1, 0)));
vec3 up = cross(right, fwd);
vec3 rd = normalize(screen.x * right + screen.y * up + 1.6 * fwd);
// ── raymarch ─────────────────────────────────────────────────────────
float t = 0.0;
vec3 col = C_VOID;
int mat = -1;
vec3 hitP;
bool hit = false;
for (int i = 0; i < 120; i++) {
hitP = ro + rd * t;
vec2 res = sceneSDF(hitP, T);
if (res.x < 0.0008) { hit = true; mat = int(res.y); break; }
if (t > 60.0) break;
t += res.x * 0.85; // slight under-step for stability
}
// ── light setup ──────────────────────────────────────────────────────
vec3 lDir = normalize(vec3(0.5, 0.95, 0.35));
vec3 lDir2 = normalize(vec3(-0.6, 0.3, -0.5));
if (hit) {
vec3 n = calcNormal(hitP, T);
float ao = calcAO(hitP, n, T);
float sha = softShadow(hitP + n * 0.005, lDir, 0.02, 15.0, 12.0, T);
// common lighting
float diff = max(dot(n, lDir), 0.0);
float diff2 = max(dot(n, lDir2), 0.0) * 0.25; // fill light
float spec = pow(max(dot(reflect(-lDir, n), -rd), 0.0), 48.0);
float fres = pow(1.0 - max(dot(n, -rd), 0.0), 3.5);
if (mat == MAT_FLOOR) {
// ── cyber grid floor ─────────────────────────────────────────
vec2 gp = hitP.xz;
// major grid (every 1 unit)
vec2 gMaj = abs(fract(gp) - 0.5);
float lineMaj = smoothstep(0.015, 0.0, min(gMaj.x, gMaj.y));
// minor grid (every 0.25 units)
vec2 gMin = abs(fract(gp * 4.0) - 0.5);
float lineMin = smoothstep(0.04, 0.0, min(gMin.x, gMin.y)) * 0.25;
float line = max(lineMaj, lineMin);
col = mix(C_VOID * 1.2, C_GRID, line);
col += C_MIKU * lineMaj * 0.35;
// pulse wave on grid
float dist = length(hitP.xz);
float wave = smoothstep(0.15, 0.0, abs(fract(dist * 0.3 - T * 0.25) - 0.5)) * 0.4;
col += C_MIKU * wave * lineMaj;
col += C_PINK * wave * lineMin * 0.5;
// floor reflective hint
col += C_MIKU_D * fres * 0.15;
// distance fade
col *= exp(-dist * 0.04);
col *= ao;
} else if (mat == MAT_CENTER) {
// ── centerpiece material — holographic teal ──────────────────
vec3 matC = C_MIKU;
// iridescent shift based on view angle
float iri = dot(n, rd) * 0.5 + 0.5;
matC = mix(matC, C_PINK, pow(iri, 3.0) * 0.35);
matC = mix(matC, C_CYAN, pow(1.0 - iri, 4.0) * 0.3);
col = matC * (0.12 + diff * 0.65 * sha) + matC * diff2;
col += C_WHITE * spec * sha * 1.2;
col += C_MIKU_L * fres * 0.6;
// inner glow / emission
col += C_MIKU * uGlow * 0.35;
col += C_CYAN * uGlow * 0.08;
// pulsing energy
float ep = sin(T * 3.0) * 0.5 + 0.5;
col += C_MIKU_L * ep * uPulse * 0.2;
col *= ao;
} else if (mat == MAT_ORB_L) {
// ── large orbiting crystals — shifting teal/purple ───────────
float hueShift = hash31(floor(hitP * 2.0 + 0.5));
vec3 matC = mix(C_MIKU, C_PURPLE, hueShift * 0.55);
col = matC * (0.10 + diff * 0.60 * sha) + matC * diff2;
col += C_WHITE * spec * sha * 0.9;
col += matC * fres * 0.45;
col += matC * uGlow * 0.2;
// crystal facet sparkle
float sparkle = pow(abs(sin(dot(n, vec3(17.3, 31.7, 7.1)) * 40.0)), 16.0);
col += C_WHITE * sparkle * 0.5;
col *= ao;
} else if (mat == MAT_ORB_S) {
// ── small orbiting cubes — electric cyan ─────────────────────
vec3 matC = mix(C_CYAN, C_MIKU_L, 0.4);
col = matC * (0.08 + diff * 0.55 * sha) + matC * diff2;
col += C_WHITE * spec * sha * 0.7;
col += matC * fres * 0.35;
col += matC * uGlow * 0.15;
col *= ao;
} else if (mat == MAT_RIBBON) {
// ── twin-tail ribbons — glowing teal with pink edge ──────────
vec3 matC = C_HAIR;
col = matC * (0.15 + diff * 0.5 * sha);
col += C_WHITE * spec * sha * 0.6;
col += C_MIKU * fres * 0.7;
// strong edge glow (like hair catching backlight)
col += C_MIKU_L * pow(fres, 2.0) * 1.2;
col += C_PINK * pow(fres, 4.0) * 0.4;
col += matC * uGlow * 0.3;
col *= ao;
}
// ── depth fog — teal-tinted ──────────────────────────────────────
vec3 fogCol = C_VOID + C_MIKU_D * 0.08;
col = mix(col, fogCol, 1.0 - exp(-t * uFog * 0.055));
} else {
// ── background void — subtle radial gradient ─────────────────────
col = C_VOID;
float bg = smoothstep(0.9, 0.0, length(screen));
col += C_MIKU_D * bg * 0.12;
col += C_PURPLE * 0.02 * smoothstep(0.0, -0.3, rd.y);
// distant star field
float stars = pow(hash21(floor(screen * 180.0)), 22.0);
col += C_WHITE * stars * 0.6;
col += C_MIKU_L * stars * 0.2;
}
// ── 2D overlays ──────────────────────────────────────────────────────
// digital particles
float parts = digiParticles(uv, T);
col += mix(C_MIKU, C_CYAN, hash21(uv * 99.0)) * parts * 0.45;
// sound wave rings (screen-space)
float ringD = length(screen);
for (int i = 0; i < 3; i++) {
float fi = float(i);
float r = fract(T * 0.18 + fi * 0.33) * 0.8;
float fade = pow(1.0 - fract(T * 0.18 + fi * 0.33), 2.0);
float ring = smoothstep(0.006, 0.0, abs(ringD - r));
col += C_MIKU * ring * fade * 0.25 * uPulse;
}
// ── scanline / holographic noise ─────────────────────────────────────
float scan = sin(uv.y * uRes.y * 0.5 + T * 8.0) * 0.015;
col += scan * C_MIKU * 0.15;
// subtle color aberration (digital artifact)
col.r += (hash21(uv + T) - 0.5) * 0.008;
col.b += (hash21(uv + T + 1.0) - 0.5) * 0.008;
// ── vignette — teal-purple corners ───────────────────────────────────
float vig = dot(uv - 0.5, uv - 0.5);
col *= 1.0 - vig * 1.6;
col += C_MIKU_D * vig * vig * 0.6;
// ── bloom ────────────────────────────────────────────────────────────
float lum = dot(col, vec3(0.2126, 0.7152, 0.0722));
col += C_MIKU * max(0.0, lum - 0.45) * 0.55;
col += C_WHITE * max(0.0, lum - 0.78) * 0.40;
col += C_PINK * max(0.0, lum - 0.65) * 0.12;
// ── Reinhard tone map + gamma ────────────────────────────────────────
col = col / (col + 0.32) * 1.22;
col = pow(max(col, 0.0), vec3(0.86));
fragColor = vec4(col, 1.0);
}