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.
84 lines
4.7 KiB
Text
84 lines
4.7 KiB
Text
// BAR SHATTER — PURE GEOMETRY, ZERO BLEND. Deck A breaks into glass
|
|
// tiles that slide apart and wink out; a beat of void; deck B's tiles fly
|
|
// back in and SNAP into the whole picture. Every shard carries its EXACT
|
|
// source rect (screen-anchored texels: a shard displaced by d shows
|
|
// src[uv - d], so the picture visibly breaks apart and reassembles like
|
|
// glass, never smearing). NO pixel ever shows a mix of A and B — the only
|
|
// softness in this transition is absence (the black void behind).
|
|
//
|
|
// Pattern taught here: the duo compositor's inverse shard map. For a
|
|
// screen pixel, the shard that covers it is found by testing the 3x3
|
|
// cell neighborhood (flight is capped under one tile-width while a shard
|
|
// is alive, so the search is exact); `shard_hit` returns the shard's
|
|
// texel with a hit flag in alpha.
|
|
{
|
|
name: "Bar Shatter"
|
|
engine: "transition"
|
|
dials: [
|
|
{name: "TILES", bind: "p0", default: 0.5},
|
|
{name: "BLAST", bind: "p1", default: 0.5},
|
|
{name: "STAG", bind: "p2", default: 0.5}
|
|
]
|
|
shader: draw.DrawVjFxDuo {
|
|
shard_hit: fn(uv: vec2, cell: vec2, n: float, phase: float, from_a: float) -> vec4 {
|
|
// Per-shard signature (stable hash of the cell id).
|
|
let r = fract(sin(dot(cell, vec2(127.1, 311.7))) * 43758.5453)
|
|
let r2 = fract(r * 7.13)
|
|
// Rim shards launch first; STAG spreads the takeoffs.
|
|
let centre = (cell + vec2(0.5, 0.5)) / n
|
|
let rim = length(centre - vec2(0.5, 0.5)) * 1.4142
|
|
let stg = self.user.z * 1.5
|
|
let local = clamp(phase * (1.0 + stg) - mix(r, rim, 0.6) * stg, 0.0, 1.0)
|
|
// Alive: a shard slides under ONE tile-width, then is GONE
|
|
// (the void). The cap keeps the 3x3 inverse search exact.
|
|
let alive = 1.0 - step(0.72, local)
|
|
let travel = pow(local / 0.72, 1.6) * min(0.35 + self.user.y * 0.75, 0.95)
|
|
let dir = normalize(
|
|
centre - vec2(0.5, 0.5)
|
|
+ vec2(r - 0.5, r2 - 0.5) * 0.7
|
|
+ vec2(0.0001, 0.0002)
|
|
)
|
|
let d = dir * travel / n
|
|
// Screen-anchored: the displaced shard covers uv iff the
|
|
// source point falls back inside its home cell.
|
|
let src = uv - d
|
|
let home = floor(src * n)
|
|
let hit = alive
|
|
* step(abs(home.x - cell.x), 0.01)
|
|
* step(abs(home.y - cell.y), 0.01)
|
|
* step(0.0, src.x) * step(src.x, 1.0)
|
|
* step(0.0, src.y) * step(src.y, 1.0)
|
|
let col = self.deck_b(src).mix(self.deck_a(src), from_a)
|
|
return vec4(col.x, col.y, col.z, hit)
|
|
}
|
|
trans: fn(uv: vec2, t: float) -> vec4 {
|
|
let n = floor(mix(5.0, 24.0, self.user.x))
|
|
// First half: A explodes OUT (phase 0→1 of A-shards). Second
|
|
// half: B implodes IN (phase runs back 1→0 as t rises).
|
|
let from_a = 1.0 - step(0.5, t)
|
|
let phase = mix(clamp(2.0 - t * 2.0, 0.0, 1.0), clamp(t * 2.0, 0.0, 1.0), from_a)
|
|
let base = floor(uv * n)
|
|
let mut col = vec3(0.0, 0.0, 0.0)
|
|
let mut covered = 0.0
|
|
let c1 = self.shard_hit(uv, base + vec2(-1.0, -1.0), n, phase, from_a)
|
|
col = mix(col, c1.xyz, c1.w * (1.0 - covered)); covered = max(covered, c1.w)
|
|
let c2 = self.shard_hit(uv, base + vec2(0.0, -1.0), n, phase, from_a)
|
|
col = mix(col, c2.xyz, c2.w * (1.0 - covered)); covered = max(covered, c2.w)
|
|
let c3 = self.shard_hit(uv, base + vec2(1.0, -1.0), n, phase, from_a)
|
|
col = mix(col, c3.xyz, c3.w * (1.0 - covered)); covered = max(covered, c3.w)
|
|
let c4 = self.shard_hit(uv, base + vec2(-1.0, 0.0), n, phase, from_a)
|
|
col = mix(col, c4.xyz, c4.w * (1.0 - covered)); covered = max(covered, c4.w)
|
|
let c5 = self.shard_hit(uv, base, n, phase, from_a)
|
|
col = mix(col, c5.xyz, c5.w * (1.0 - covered)); covered = max(covered, c5.w)
|
|
let c6 = self.shard_hit(uv, base + vec2(1.0, 0.0), n, phase, from_a)
|
|
col = mix(col, c6.xyz, c6.w * (1.0 - covered)); covered = max(covered, c6.w)
|
|
let c7 = self.shard_hit(uv, base + vec2(-1.0, 1.0), n, phase, from_a)
|
|
col = mix(col, c7.xyz, c7.w * (1.0 - covered)); covered = max(covered, c7.w)
|
|
let c8 = self.shard_hit(uv, base + vec2(0.0, 1.0), n, phase, from_a)
|
|
col = mix(col, c8.xyz, c8.w * (1.0 - covered)); covered = max(covered, c8.w)
|
|
let c9 = self.shard_hit(uv, base + vec2(1.0, 1.0), n, phase, from_a)
|
|
col = mix(col, c9.xyz, c9.w * (1.0 - covered)); covered = max(covered, c9.w)
|
|
return vec4(col.x, col.y, col.z, 1.0)
|
|
}
|
|
}
|
|
}
|