makepad/apps/vj/resources/effects/86_fractal_descent.splash
Admin f44616b2a7 makepad-vj effects: the effect renderstack, with the shaders inside the documents
The VJ's EFFECT surface is a small renderer of its own. An effect is a
`.splash` DOCUMENT — engine choice, stages, fields, parameters and now the
SHADERS THEMSELVES live in the document rather than in Rust. That is the
shape this commit introduces (dev has never seen an intermediate one): a
document is the whole effect, and the Rust side is the engine families that
documents draw with.

The engine families: particles and emitters, GPU sim swarms and fluid, static
meshes (firefly synchrony, harmonograph loom, domino liturgy), tiles, flock,
clouds, city, pipes, stock charts, an SDF raymarcher with a subclassable
`scene_sdf`, and a mountain-jet endless range with a beat-pulsed fighter.

Three things make them a system rather than a demo reel:

  - SIM FIELDS — a float simulation-texture primitive, so an engine can carry
    GPU state across frames (wind, particles, fluid) instead of being a pure
    function of the clock.
  - CONTENT COUPLING — `content:` in a document and `input0` on every engine's
    tex0, so an effect can consume the program video: drapes, backdrops,
    mirrors, billboards, chrome, frescoes, canopy, glass, silk, wall, swarm,
    pen and mosaic families all take the picture playing behind them.
  - MUSIC BINDING — shaders read the beat, so the whole library moves with the
    track rather than on its own clock.

Roughly a hundred seeded preset documents ship with it, each with a lazily
rendered animated thumbnail (rendered 4x and box-downscaled, 16-tap SSAA);
the thumbnail pass went from four minutes to eleven seconds. CONTRACT.md is
the document contract and its verify recipe; IDEAS.md is the campaign tracker.
2026-08-23 01:35:44 +02:00

99 lines
4 KiB
Text

// FRACTAL DESCENT — a kaleidoscopic-IFS fractal orbited in the dark, its
// fold angle WANDERING with the beat count so the shape never repeats.
// Pattern taught: an iterated fold (abs + rotate + scale-out) inside a
// scene_sdf subclass — five iterations, own constants; the marcher, AO and
// palette all come free from the base shader. `p0` binds extra fold angle.
//
// THE LAW THIS DOCUMENT LEARNED THE HARD WAY: a shader that is a function
// of `beat` must be a BOUNDED one. `beat` is the host's SONG clock, not a
// stopwatch that starts when the effect loads — cue this into a deck an
// hour into a set and the first frame is at beat 6000. The original fold
// angle was `0.52 + beat * 0.010`, an unbounded creep: lovely for the ten
// seconds a gallery grab ever sees, and by beat ~120 the IFS attractor has
// escaped the marcher and the deck shows BLACK. So the wander here is two
// slow sines with an irrational-ish frequency ratio (quasi-periodic — it
// still never repeats) clamped into the band where the attractor is a
// picture, and the FOLD dial is centred on 0.5 so no knob can leave it.
{
name: "Fractal Descent"
engine: "raymarch"
steps: 84
max_dist: 30.0
cam: "orbit"
cam_speed: 0.14
cam_dist: 3.6
cam_height: 1.0
cam_fov: 62.0
shadow: 1
beat_pulse: "0.6 * (0.2 + 1.6*p2)"
beat_rate: 1.0
fog: 0.05
glow: "1.45 * (0.25 + 1.5*p1)"
// FOLD is knob-shaped: 0.5 is neutral, the binding breathes around it
// over the bar, and the scene reads it as an OFFSET from centre.
p0: "0.5 + 0.16 * sin(bar * tau)"
// PERFORMANCE DIALS — FOLD is this doc's own p0 lever (the scene hook
// reads self.user.x; touching it holds the fold open); GLOW and PUMP
// route p1/p2. Mid-knob = the stock look, exact.
p1: 0.5 p2: 0.5
dials: [
{name: "FOLD", bind: "p0", default: 0.5},
{name: "GLOW", bind: "p1", default: 0.5},
{name: "PUMP", bind: "p2", default: 0.5}
]
color_bg: #x05040f
color_a: #x8f4dff
color_b: #x0fd0c8
color_c: #xf0eaff
stages: [
{kind: "bloom", threshold: 0.5, strength: 1.35, levels: 3}
]
shader: draw.DrawVjFxRaymarch {
scene_sdf: fn(p: vec3) -> vec2 {
// Kaleidoscopic IFS: fold into the positive octant, then TWO
// rotations (xz and yz — the second is what lifts the fractal
// out of a flat extrusion), scale outward. Own constants.
let mut z = p * 0.72
let k = 1.84
let off = vec3(1.18, 0.62, 1.34)
// The fold angle WANDERS but never escapes: two slow sines of
// the beat count (periods ~153 and ~370 beats, ratio not a
// simple fraction, so the pair never quite comes back around)
// plus the FOLD dial as a signed offset from mid-knob. The
// clamp is the guarantee — outside roughly 0.2..0.9 the
// attractor either collapses or runs off past `max_dist`, and
// the picture goes black.
let b = self.time_beat.y
let wander = 0.14 * sin(b * 0.041) + 0.08 * sin(b * 0.017 + 2.3)
let fold = (self.user.x - 0.5) * 0.24
let ang = clamp(0.54 + wander + fold, 0.20, 0.90)
let mut s = 0.72
let mut trap = 10.0
let mut i = 0.0
loop {
if i > 5.5 {
break
}
z = abs(z)
let r = self.rot2(vec2(z.x, z.z), ang)
z = vec3(r.x, z.y, r.y)
let r2 = self.rot2(vec2(z.y, z.z), 0.61 - ang * 0.35)
z = vec3(z.x, r2.x, r2.y)
z = z * k - off * (k - 1.0)
s = s * k
// Orbit trap: nearest approach to the origin, in world
// scale — the palette rides the fractal's onion layers.
trap = min(trap, length(z) / s)
i = i + 1.0
}
let d = self.sd_box(z, vec3(0.90, 0.90, 0.90)) / s
return vec2(d * 0.82, fract(trap * 2.6))
}
}
}