added monolith

This commit is contained in:
2026-05-20 16:08:00 +02:00
parent b818f4bc43
commit 2c3418f608
15 changed files with 1872 additions and 1352 deletions
+312
View File
@@ -0,0 +1,312 @@
// monolith — "Glitching Monolith" mandelbulb raymarcher.
//
// A power-N mandelbulb DE raymarched as particle-dithered surface + soft halo
// on a deep void; sub-bass breathes its scale (CPU side), mid-band swirls the
// feedback trail, hi-band drives a coarse-cell UV-displacement glitch grid +
// a simulated frame-rate stutter (the feedback decay floor pins near 1.0 so
// the previous frame survives unchanged — looks like the renderer dropped to
// ~10 fps for a few frames). Particle aesthetic comes from a per-pixel hash
// threshold against the surface intensity: each pixel "is" a particle that
// either shows or doesn't, sold by dense fine grain on top.
//
// Cost bounded the same way breakcore is: bounding-sphere ray test discards
// background pixels in one op, march is sphere-traced with the shader's hard
// 96-step ceiling, normal (6× map) is gated to pixels actually on-surface.
// Bulb iteration count is fixed at 8 (taste — higher melts detail, lower
// reads blocky). Pure function of UBO + hash(fragCoord, frame) so `--render`
// is bit-reproducible.
//
// UBO header is **10** std140 rows (40 f32). Rust↔WGSL coupled — change one
// ⇒ change the other. Naga only validates this WGSL at pipeline-create on a
// real GPU. No nodes array: the form is the DE itself. `col2` carries a
// secondary neon accent so the surface can paint two contrasting hues at
// once — the bulb never reads as one wash.
const PI: f32 = 3.14159265;
const BULB_ITERS: i32 = 8;
struct U {
cam: vec4<f32>, // yaw, pitch, roll, dist
p0: vec4<f32>, // scale, glow_gain, ca_px, edge_softness
col0: vec4<f32>, // base.rgb, fade
col1: vec4<f32>, // accent.rgb (primary neon), flash
p1: vec4<f32>, // res_w, res_h, frame, time
p2: vec4<f32>, // march_steps, power_n, feedback_on, world_r
p3: vec4<f32>, // grain, glitch_a, fog, beat
p4: vec4<f32>, // loud, low, mid, high
p5: vec4<f32>, // stutter_w, swirl, aspect, tonality
col2: vec4<f32>, // accent2.rgb (secondary neon), _
};
@group(0) @binding(0) var<uniform> u: U;
@group(0) @binding(1) var prev_tex: texture_2d<f32>;
@group(0) @binding(2) var prev_smp: sampler;
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
};
@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
var o: VsOut;
let x = f32((vi << 1u) & 2u);
let y = f32(vi & 2u);
o.uv = vec2<f32>(x, y);
o.pos = vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
return o;
}
fn hash21(p: vec2<f32>) -> f32 {
var q = fract(p * vec2<f32>(123.34, 345.45));
q = q + dot(q, q + 34.345);
return fract(q.x * q.y);
}
// yaw(Y) -> pitch(X) -> roll(Z), shared convention with breakcore.
fn rot(v: vec3<f32>) -> vec3<f32> {
let sy = sin(u.cam.x); let cy = cos(u.cam.x);
let sp = sin(u.cam.y); let cp = cos(u.cam.y);
let sr = sin(u.cam.z); let cr = cos(u.cam.z);
let x1 = v.x * cy - v.z * sy;
let z1 = v.x * sy + v.z * cy;
let y2 = v.y * cp - z1 * sp;
let z2 = v.y * sp + z1 * cp;
let x3 = x1 * cr - y2 * sr;
let y3 = x1 * sr + y2 * cr;
return vec3<f32>(x3, y3, z2);
}
// Mandelbulb distance estimator (Quilez / Hart). Iterates z_{n+1} = z_n^P + c
// in spherical coords; the running `dr` tracks |dz/dz0| so we can return a
// proper distance bound 0.5·log(r)·r/dr.
fn de_bulb(p0: vec3<f32>, power: f32) -> f32 {
var z = p0;
var dr = 1.0;
var r = 0.0;
for (var i = 0; i < BULB_ITERS; i = i + 1) {
r = length(z);
if (r > 2.0) { break; }
let theta = acos(clamp(z.z / max(r, 1e-6), -1.0, 1.0));
let phi = atan2(z.y, z.x);
dr = pow(r, power - 1.0) * power * dr + 1.0;
let zr = pow(r, power);
let t2 = theta * power;
let p2 = phi * power;
z = zr * vec3<f32>(sin(t2) * cos(p2), sin(t2) * sin(p2), cos(t2));
z = z + p0;
}
return 0.5 * log(max(r, 1e-6)) * r / max(dr, 1e-6);
}
// Scene = scaled bulb. `scl` is the sub-bass-breath spring multiplier.
fn map(p: vec3<f32>) -> f32 {
let scl = u.p0.x;
return de_bulb(p * (1.0 / max(scl, 1e-3)), u.p2.y) * scl;
}
fn calc_normal(p: vec3<f32>) -> vec3<f32> {
let e = vec2<f32>(0.0022, 0.0);
return normalize(vec3<f32>(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx),
));
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let res_w = u.p1.x;
let res_h = u.p1.y;
let frame = u.p1.z;
let glow = u.p0.y;
let ca_px = u.p0.z;
let edge = u.p0.w;
let base = u.col0.xyz;
let accent = u.col1.xyz;
let accent2 = u.col2.xyz;
let fade = u.col0.w;
let flash = u.col1.w;
let time = u.p1.w;
let rb = u.p2.w; // bounding-sphere radius
let grain_a = u.p3.x;
let glitch = u.p3.y; // 0..~1.2 coarse-cell UV shove (hi-band)
let fog = u.p3.z;
let beat = u.p3.w;
let loud = u.p4.x;
let lowf = u.p4.y; // [0..1] sub-bass level — gravity well weight
let midf = u.p4.z; // [0..1] mid level — swirl gain
let highf = u.p4.w; // [0..1] hi/snare level — rim flicker
let stut = u.p5.x; // 0..1 simulated-stutter weight
let swirl = u.p5.y; // accumulated swirl angle (rad)
let aspect = u.p5.z;
let tonal = u.p5.w;
// --- hi-band glitch grid: coarse-cell UV displacement of the FEEDBACK
// sample only — the bulb itself stays geometrically stable so a busy hat
// pattern can't tear the whole picture. Deadband below 0.35 so quiet
// music produces no glitch at all; only a small fraction of cells shove
// (threshold 0.85) so the effect is sparse, not screen-filling.
let glitch_eff = max(clamp(glitch, 0.0, 1.0) - 0.35, 0.0) / 0.65;
var glitch_off = vec2<f32>(0.0);
if (glitch_eff > 0.001) {
let cells = 18.0 + 14.0 * glitch_eff;
let cy = floor(in.uv.y * cells);
let cx = floor(in.uv.x * cells);
let hold = max(3.0 + 5.0 * (1.0 - glitch_eff), 1.0);
let stuck_frame = floor(frame / hold);
let pick = step(0.85, hash21(vec2<f32>(cx + cy * 7.0, stuck_frame)));
let sx = (hash21(vec2<f32>(cx * 3.7, cy + stuck_frame)) - 0.5)
* 0.045 * glitch_eff * pick;
let sy = (hash21(vec2<f32>(cx + cy * 11.0, stuck_frame * 2.0)) - 0.5)
* 0.020 * glitch_eff * pick;
glitch_off = vec2<f32>(sx, sy);
}
let uv = in.uv;
let ndc = vec2<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);
let dist = u.cam.w;
let focal = 1.6;
let ro = vec3<f32>(0.0, 0.0, -dist);
let rd = normalize(vec3<f32>(ndc.x * aspect, ndc.y, focal));
// Ray vs bounding sphere — the background-pixel early-out that keeps the
// raymarch from melting the GPU. `rb` is set to (scale + breath + pad).
let b = dot(ro, rd);
let c = dot(ro, ro) - rb * rb;
let disc = b * b - c;
var col = vec3<f32>(0.0);
var inten = 0.0;
var hit_t = -1.0;
var dmin = 1e9;
if (disc > 0.0) {
let sq = sqrt(disc);
var t = max(-b - sq, 0.0);
let t_end = -b + sq;
let min_step = max(t_end - t, 1e-3) / 96.0;
let steps = min(i32(u.p2.x), 96);
for (var s = 0; s < steps; s = s + 1) {
let d = map(rot(ro + rd * t));
if (d < dmin) { dmin = d; }
if (d < 0.0018) { hit_t = t; break; }
t = t + max(d * 0.80, min_step);
if (t > t_end) { break; }
}
// Tight dust halo from closest approach (not unbounded accumulation,
// so dense regions can't white-out). The wider tail is intentionally
// dim — a noisy/atonal song widens it via `edge`, tonal stays crisp.
let dl = max(dmin, 0.0);
let halo = exp(-dl * dl * 7000.0 * edge) + 0.02 * exp(-dl * 30.0);
let tt = select(t, hit_t, hit_t > 0.0);
let depth = clamp((tt + b) / max(2.0 * sq, 1e-3), 0.0, 1.0);
inten = clamp(halo * glow * (1.0 - fog * depth), 0.0, 1.0);
if (inten > 0.015) {
let rp = rot(ro + rd * tt);
// Position-dependent **tint mix** across the bulb surface — a
// slow standing wave through (x,y,z) so neighbouring regions
// read as different neons. Drifts on time so the colour
// distribution evolves without snapping. This is what keeps the
// frame from being one hue: half the bulb sits closer to
// `accent`, the other half closer to `accent2`.
let tint = 0.5 + 0.5 * sin(rp.x * 3.2 + rp.y * 2.4 + rp.z * 1.8
+ time * 0.30);
let acc_mix = mix(accent, accent2, tint);
// Surface shade — gated to genuinely on-surface pixels so the
// 6×map() normal cost can't run over the entire halo screen.
// Accent contributions are deliberately small: the body stays
// brutalist-grey, the neon shows only where it earns its keep
// (fresnel edge + hi-band onset rim).
var body = base * 0.55;
if (dmin < 0.010) {
let n = calc_normal(rp);
let vdir = normalize(rot(-rd));
let lambert = clamp(dot(n, normalize(vec3<f32>(0.4, 0.7, -0.55))), 0.0, 1.0);
let fres = pow(1.0 - clamp(dot(n, vdir), 0.0, 1.0), 3.0);
// Body diffuse → silver; fresnel edge → position-mixed neon
// (the two-colour bulb effect lives here).
body = base * (0.20 + 0.80 * lambert) + acc_mix * fres * 0.28;
// Hi-band rim → **secondary** accent² (not the body's main)
// so a snare flashes a contrasting hue against the body's
// base accent. Quadratic in highf so light hats stay near
// zero, only strong snares light the edge.
body = body + accent2 * highf * highf * fres * fres * 0.40;
}
// Particle-dither: per-pixel hash threshold against intensity.
// Each pixel is a "particle" that either shows or doesn't —
// identifies the form as point-cloud rather than solid surface.
let pcell = hash21(uv * vec2<f32>(res_w, res_h) * 0.85
+ vec2<f32>(frame * 0.013, frame * 0.029));
let pkeep = step(pcell, clamp(inten * 1.65 + 0.12, 0.0, 1.0));
col = body * inten * (0.50 + 0.55 * pkeep);
// Core punch — uses the per-pixel tint so the bulb's deep core
// glows different shades in different regions, not one hot dot.
col = col + acc_mix * pow(inten, 8.0) * 0.20;
// Onset spark — gated by flash² and uses accent2 (the snare
// colour) so onsets actively introduce the contrast hue.
col = col + accent2 * flash * flash * pow(inten, 3.0) * 0.25;
}
}
// Sub-bass gravity-well: radial darkening of the outer void, gated by
// sub-bass loudness². Quadratic so a sustained low hum reads heavy but
// doesn't reach the threshold without a real 808. A faint warm tint
// (wine, mixed from accent + base) bleeds into the dark ring so a kick
// colours the periphery instead of just dimming it.
let r2 = dot(ndc, ndc);
let gw = lowf * lowf * smoothstep(0.0, 1.6, r2);
col = col * (1.0 - 0.28 * gw);
let warm = mix(base, accent * 0.6, 0.45);
col = col + warm * gw * 0.05;
// --- mid-band ink-in-water swirl: feedback sample comes from a small
// rotation around centre + the glitch grid's per-cell shove. Pad swells
// make ribbons drift; a snare burst makes a sparse subset of cells jump.
// Both effects are sub-pixel-ish in magnitude so the trail doesn't tear.
var fb_uv = uv + glitch_off;
if (u.p2.z > 0.5) {
var c2 = fb_uv - vec2<f32>(0.5);
let cs = cos(swirl * 0.5); let sn = sin(swirl * 0.5);
c2 = vec2<f32>(c2.x * cs - c2.y * sn, c2.x * sn + c2.y * cs);
let zr = 1.0 + 0.004 * (midf - 0.3);
c2 = c2 * zr;
fb_uv = clamp(c2 + vec2<f32>(0.5), vec2<f32>(0.0), vec2<f32>(1.0));
}
// Dense fine grain — sells the "millions of particles" texture.
col = max(col + (hash21(uv * vec2<f32>(res_w, res_h)
+ vec2<f32>(frame * 1.1, frame * 1.7)) - 0.5) * grain_a,
vec3<f32>(0.0));
// Phosphor feedback + datamosh. Stutter raises the decay floor briefly
// so the previous frame survives a beat (reads as a dropped frame), but
// capped well below 1.0 so the trail still drains — no runaway wash.
// CA across the tap is small by default; only the active stutter window
// lifts it.
if (u.p2.z > 0.5) {
let dec_base = clamp(1.0 - 3.5 * fade, 0.30, 0.85);
let stutter_floor = mix(dec_base, 0.90, clamp(stut, 0.0, 1.0));
let decay = clamp(stutter_floor + 0.10 * beat, 0.30, 0.94);
let ca = ca_px / max(res_w, 1.0);
let off = (fb_uv - vec2<f32>(0.5)) * ca;
let pr = textureSampleLevel(prev_tex, prev_smp, fb_uv + off, 0.0).r;
let pg = textureSampleLevel(prev_tex, prev_smp, fb_uv, 0.0).g;
let pb = textureSampleLevel(prev_tex, prev_smp, fb_uv - off, 0.0).b;
col = max(col, vec3<f32>(pr, pg, pb) * decay);
}
// Cold void floor — silver/blue, very small so it cannot accumulate
// through the feedback trail. A tonality-gated mix toward `accent2`
// gives the void itself a faint partner-neon cast so the corners of the
// frame aren't pure black. Quadratic loudness gating so the floor only
// lifts on real energy, not analyser noise.
let vig = max(1.0 - 0.85 * length(ndc), 0.0);
let bgt_cold = mix(vec3<f32>(0.015, 0.020, 0.035),
vec3<f32>(0.04, 0.05, 0.10), tonal);
let bgt = mix(bgt_cold, accent2 * 0.08, 0.25 * tonal);
col = col + bgt * vig * (0.005 + 0.010 * loud * loud);
return vec4<f32>(min(col, vec3<f32>(1.0)), 1.0);
}