mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
feat(cinema): explode -> pixelate -> reform storm (kill the swirls)
Per direction: keep the mind-blowing explosion + pixelation moments, ditch the thin ribbon swirls. Complete physics rewrite: - removed orbital/stream/Rössler modes (the swirls + the off-center drift source) - each particle has a deterministic HOME on a volumetric shell around ORIGIN (centroid anchored — can never drift off-frame again) - uBurst detonation cycle: every beat blows particles radially out (explosion), then a home-spring crystallizes them back (reform); contradictions detonate hardest - PIXELATION: positions snap to a 3D grid that's fine when reformed, dissolved during the burst — the crystalline voxel look - hard velocity + radius clamps so it can never fly off or blow up 937 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0e5ce76274
commit
bc81da46eb
1 changed files with 59 additions and 58 deletions
|
|
@ -42,6 +42,7 @@ import {
|
|||
mix,
|
||||
fract,
|
||||
abs,
|
||||
floor,
|
||||
positionLocal,
|
||||
} from 'three/tsl';
|
||||
|
||||
|
|
@ -101,6 +102,9 @@ export class SemanticComputeStorm {
|
|||
// tint overrides the rainbow (0 = full rainbow, 1 = full mode color).
|
||||
private uHueShift = uniform(0);
|
||||
private uModeTintAmt = uniform(0.25);
|
||||
// Detonation cycle: spikes to 1 on each beat (explosion), decays to 0
|
||||
// (crystallize/reform). Drives the explode→pixelate→reform look.
|
||||
private uBurst = uniform(0);
|
||||
|
||||
constructor(
|
||||
renderer: { computeAsync: (node: ComputeDispatch) => Promise<void> },
|
||||
|
|
@ -148,74 +152,65 @@ export class SemanticComputeStorm {
|
|||
const vel = velStore.element(instanceIndex);
|
||||
const phase = phaseStore.element(instanceIndex);
|
||||
|
||||
const toTarget = vec3(this.uTarget).sub(pos);
|
||||
// ── EACH PARTICLE'S "HOME" — a deterministic point on a volumetric
|
||||
// spherical shell around the ORIGIN, derived purely from its phase.
|
||||
// The cloud reforms to these homes between beats, so the centroid is
|
||||
// ANCHORED to origin and CANNOT drift (the bug that pushed it off-frame).
|
||||
// No swirl/orbital/attractor terms — those drew the ugly ribbons.
|
||||
const a1 = phase.mul(12.9898).sin().mul(43758.5453);
|
||||
const a2 = phase.mul(78.233).sin().mul(12543.531);
|
||||
const u = fract(a1); // 0..1
|
||||
const v = fract(a2); // 0..1
|
||||
const theta = u.mul(6.28318); // azimuth
|
||||
const phi = v.mul(3.14159); // polar
|
||||
// Per-particle home radius fills the interior (0.30r..0.95r) for a
|
||||
// dense volumetric orb rather than a hollow shell.
|
||||
const homeFrac = float(0.3).add(fract(phase.mul(3.7)).mul(0.65));
|
||||
const homeR = this.uContainRadius.mul(homeFrac);
|
||||
const home = vec3(
|
||||
sin(phi).mul(cos(theta)),
|
||||
cos(phi),
|
||||
sin(phi).mul(sin(theta))
|
||||
).mul(homeR); // centered on origin (uTarget is always origin in sandbox)
|
||||
|
||||
// Mode 0 — Anchor: orbital swirl (tangential velocity around target).
|
||||
// Gentler than before so particles revolve rather than fling outward.
|
||||
const orbital = vec3(toTarget.z, float(0), toTarget.x.negate())
|
||||
.normalize()
|
||||
.mul(0.03);
|
||||
// ── DETONATION: on each beat uBurst≈1 → blow particles radially OUT
|
||||
// from origin (the explosion in photo 2). Strength scales with the
|
||||
// particle's own radius so the burst is a full-volume shockwave.
|
||||
const outDir = pos.normalize();
|
||||
vel.addAssign(outDir.mul(this.uBurst.mul(0.9)));
|
||||
|
||||
// Mode 1 — Connection: stream toward target + per-particle wave.
|
||||
const wave = vec3(
|
||||
sin(this.uTime.add(phase)).mul(0.02),
|
||||
cos(this.uTime.add(phase)).mul(0.02),
|
||||
sin(this.uTime.mul(1.5).add(phase)).mul(0.02)
|
||||
);
|
||||
const stream = toTarget.normalize().mul(0.08).add(wave);
|
||||
// ── REFORM: a spring pulling each particle back to its home. As uBurst
|
||||
// decays the spring wins, crystallizing the explosion back into the orb.
|
||||
const toHome = home.sub(pos);
|
||||
vel.addAssign(toHome.mul(0.045));
|
||||
|
||||
// Mode 2 — Contradiction: Rössler strange-attractor chaos.
|
||||
const dt = float(0.01);
|
||||
const dx = vel.y.negate().sub(vel.z).mul(dt);
|
||||
const dy = vel.x.add(vel.y.mul(0.2)).mul(dt);
|
||||
const dz = float(0.2).add(vel.z.mul(vel.x.sub(5.7))).mul(dt);
|
||||
const chaos = vec3(dx, dy, dz).mul(2.0);
|
||||
// Subtle living shimmer so the reformed orb breathes (mean-zero, no net
|
||||
// drift — uses the particle's own home direction, not a global bias).
|
||||
const shimmer = home.normalize().mul(sin(this.uTime.mul(1.3).add(phase.mul(6.1))).mul(0.015));
|
||||
vel.addAssign(shimmer);
|
||||
|
||||
// Runtime mode selection (select(), not cond()).
|
||||
const active = select(
|
||||
this.uMode.equal(0),
|
||||
orbital,
|
||||
select(this.uMode.equal(1), stream, chaos)
|
||||
);
|
||||
|
||||
vel.addAssign(active);
|
||||
// Ignition shockwave yanks particles toward the new node on each beat.
|
||||
vel.addAssign(toTarget.normalize().mul(this.uIgnition.mul(0.02)));
|
||||
|
||||
// RADIAL CONTAINMENT SPRING — the real fix for the runaway ring.
|
||||
// Every particle is pulled toward a TARGET SHELL radius (a fraction of
|
||||
// the contain radius), so the cloud forms a contained, breathing sphere
|
||||
// instead of spiraling outward into an ever-bigger ring that clips the
|
||||
// frame. The spring is two-sided: pulls IN when too far, pushes OUT when
|
||||
// collapsed to the center, giving the storm volume without escape.
|
||||
const distFromTarget = length(toTarget.negate());
|
||||
// Each particle targets its OWN radius spread across the whole interior
|
||||
// (0.12r .. 0.92r), so the cloud is a FILLED VOLUMETRIC ORB filling the
|
||||
// frame — not a thin ring. The per-particle preferred radius is a stable
|
||||
// function of its phase, gently breathing over time.
|
||||
const prefFrac = float(0.12).add(
|
||||
abs(sin(phase.mul(1.7).add(this.uTime.mul(0.12)))).mul(0.8)
|
||||
);
|
||||
const shellR = this.uContainRadius.mul(prefFrac);
|
||||
const radialErr = distFromTarget.sub(shellR); // + = outside, - = inside
|
||||
const towardShell = toTarget.normalize().mul(radialErr.mul(0.05));
|
||||
vel.addAssign(towardShell);
|
||||
|
||||
// Hard velocity clamp so no single step can shoot a particle far.
|
||||
// Hard velocity clamp — nothing can ever fly off or blow up.
|
||||
const speed = length(vel);
|
||||
const maxSpeed = float(0.9);
|
||||
const maxSpeed = float(1.3);
|
||||
vel.assign(vel.mul(min(maxSpeed, speed).div(speed.max(0.0001))));
|
||||
|
||||
pos.addAssign(vel);
|
||||
vel.mulAssign(0.94);
|
||||
vel.mulAssign(0.9); // strong damping → crisp crystallization, no overshoot
|
||||
|
||||
// Final hard safety net: clamp any particle that still ends up past the
|
||||
// contain radius back onto the boundary shell — guarantees nothing can
|
||||
// ever be off-screen, even mid chaos divergence.
|
||||
const finalToTarget = vec3(this.uTarget).sub(pos);
|
||||
const finalDist = length(finalToTarget.negate());
|
||||
// ── PIXELATION: as particles crystallize (low burst) snap positions to
|
||||
// a 3D GRID so the cloud resolves into discrete colored voxels — the
|
||||
// crystalline look of photo 3. The grid is finest when fully reformed
|
||||
// (burst≈0) and dissolves during the explosion (burst≈1).
|
||||
const cell = mix(float(0.55), float(6.0), clamp(this.uBurst, 0, 1)); // small cell = fine pixels
|
||||
const quantized = floor(pos.div(cell)).add(0.5).mul(cell);
|
||||
const pixelAmt = clamp(float(1).sub(this.uBurst.mul(1.4)), 0, 0.9);
|
||||
pos.assign(mix(pos, quantized, pixelAmt));
|
||||
|
||||
// Final hard safety net: clamp anything past the contain radius back
|
||||
// onto the boundary shell — guarantees nothing is ever off-screen.
|
||||
const finalDist = length(pos);
|
||||
const hardR = this.uContainRadius;
|
||||
const snapped = vec3(this.uTarget).sub(finalToTarget.normalize().mul(hardR));
|
||||
const snapped = pos.normalize().mul(hardR);
|
||||
pos.assign(mix(pos, snapped, finalDist.greaterThan(hardR).select(float(1), float(0))));
|
||||
})().compute(this.count);
|
||||
}
|
||||
|
|
@ -302,6 +297,9 @@ export class SemanticComputeStorm {
|
|||
// Ignition decays toward 0 between beats (the colorNode floor keeps the
|
||||
// storm glowing); spikes back up on transitionTo().
|
||||
this.uIgnition.value = Math.max(0, this.uIgnition.value - dt * 2.0);
|
||||
// Burst decays fast so the explosion crystallizes back within ~1.2s,
|
||||
// leaving the rest of the beat as a calm pixelated orb.
|
||||
this.uBurst.value = Math.max(0, this.uBurst.value - dt * 0.85);
|
||||
|
||||
// Wait for any in-flight compute to finish before queuing the next.
|
||||
if (this.computeInFlight) await this.computeInFlight;
|
||||
|
|
@ -317,6 +315,9 @@ export class SemanticComputeStorm {
|
|||
const mode = ROLE_MODE[role] ?? 1;
|
||||
this.uMode.value = mode;
|
||||
this.uIgnition.value = 8.0;
|
||||
// DETONATE: every beat explodes the orb, then it crystallizes/pixelates
|
||||
// back. Contradictions detonate hardest.
|
||||
this.uBurst.value = mode === 2 ? 1.0 : 0.8;
|
||||
// Dramatic beats (contradiction=2, surprise=3) push their mode color over
|
||||
// the rainbow so they read clearly; calm beats stay mostly iridescent.
|
||||
this.uModeTintAmt.value = mode >= 2 ? 0.7 : 0.22;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue