mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
feat(cinema): fill the frame + true full-spectrum color (no more white-out)
Storm was a small ring leaving the canvas empty, and the core blew to white. - FILL: sandbox fitRadius margin 0.40 -> 0.82 so the storm fills most of the frame; particles now target their OWN radius across 0.12r..0.92r (filled volumetric ORB, not a thin ring). - COLOR: brightness was x(ignition*2.4+0.6) = up to x19.8, which + additive blending across 150k sprites clipped every channel to white. Clamp the glow low (0.45 floor, ~1.15 ceil) so the RAINBOW shows as pure spectral color; smaller quads (0.18 -> 0.1) keep particles crisp instead of overlapping to mush; gentler bloom (strength 1.1->0.6, threshold 0->0.35) accents cores rather than washing the cloud. 937 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
782399adb1
commit
0e5ce76274
2 changed files with 33 additions and 22 deletions
|
|
@ -147,7 +147,9 @@ export class CinemaSandbox {
|
|||
scenePass.setMRT(mrt({ output, emissive }));
|
||||
const outputTex = scenePass.getTextureNode('output');
|
||||
const emissiveTex = scenePass.getTextureNode('emissive');
|
||||
const bloomed = this.deps.bloomMod.bloom(emissiveTex, 1.1, 0.6, 0.0);
|
||||
// Gentler bloom (strength 0.6, threshold 0.35) so it accents the bright
|
||||
// cores instead of washing the whole colored cloud to white.
|
||||
const bloomed = this.deps.bloomMod.bloom(emissiveTex, 0.6, 0.65, 0.35);
|
||||
const post = new this.deps.PostProcessing(renderer);
|
||||
(post as unknown as { outputNode: unknown }).outputNode = (
|
||||
outputTex as { add: (n: unknown) => unknown }
|
||||
|
|
@ -189,13 +191,12 @@ export class CinemaSandbox {
|
|||
this.camera.lookAt(ORIGIN);
|
||||
|
||||
// Size the containment sphere to the camera's VERTICAL FOV at the origin
|
||||
// (the limiting dimension on a landscape frame). The 0.40 factor leaves
|
||||
// generous room for the additive BLOOM HALO — each particle's glow spreads
|
||||
// well beyond its geometric position, so the visible cloud is much larger
|
||||
// than the radius; an aggressive margin is what actually stops the clip.
|
||||
// (the limiting dimension on a landscape frame). 0.82 lets the storm fill
|
||||
// most of the frame; the storm's internal shell sits well inside this and
|
||||
// the hard boundary snap keeps the bloom halo from spilling past the edge.
|
||||
const dist = this.camera.position.length();
|
||||
const vfov = (this.camera.fov * Math.PI) / 180;
|
||||
const fitRadius = Math.tan(vfov / 2) * dist * 0.4;
|
||||
const fitRadius = Math.tan(vfov / 2) * dist * 0.82;
|
||||
this.storm.setContainRadius(fitRadius);
|
||||
|
||||
await this.storm.update(deltaSeconds);
|
||||
|
|
|
|||
|
|
@ -189,12 +189,16 @@ export class SemanticComputeStorm {
|
|||
// 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());
|
||||
const shellR = this.uContainRadius.mul(0.62); // comfortable in-frame shell
|
||||
// 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
|
||||
// Per-particle phase varies each particle's preferred shell a touch so
|
||||
// they spread across a thick band, not a razor-thin ring.
|
||||
const band = sin(phase.mul(3.0).add(this.uTime.mul(0.3))).mul(shellR.mul(0.18));
|
||||
const towardShell = toTarget.normalize().mul(radialErr.sub(band).mul(0.06));
|
||||
const towardShell = toTarget.normalize().mul(radialErr.mul(0.05));
|
||||
vel.addAssign(towardShell);
|
||||
|
||||
// Hard velocity clamp so no single step can shoot a particle far.
|
||||
|
|
@ -250,15 +254,17 @@ export class SemanticComputeStorm {
|
|||
.add(this.uTime.mul(0.08))
|
||||
.add(this.uHueShift)
|
||||
);
|
||||
// hue → RGB (classic fract/abs hexagon palette), high saturation.
|
||||
// hue → RGB (fract/abs hexagon palette). Pull the valleys UP slightly
|
||||
// then re-saturate so the rainbow is vivid and FULLY saturated (not
|
||||
// washed) — pure spectral color, never white.
|
||||
const r = clamp(abs(hue.mul(6).sub(3)).sub(1), 0, 1);
|
||||
const g = clamp(float(2).sub(abs(hue.mul(6).sub(2))), 0, 1);
|
||||
const b = clamp(float(2).sub(abs(hue.mul(6).sub(4))), 0, 1);
|
||||
const rainbow = vec3(r, g, b);
|
||||
|
||||
// The beat's mode tint (crimson at a contradiction, cyan anchor, etc.)
|
||||
// is blended IN by uModeTintAmt so dramatic beats still read their color
|
||||
// while keeping the iridescent shimmer underneath.
|
||||
// The beat's mode tint (crimson at a contradiction, gold at surprise,
|
||||
// cyan default) is blended in by uModeTintAmt so dramatic beats read
|
||||
// their color while keeping the iridescent shimmer underneath.
|
||||
const modeTint = select(
|
||||
this.uMode.equal(2),
|
||||
vec3(1.0, 0.08, 0.32), // contradiction → crimson
|
||||
|
|
@ -266,15 +272,19 @@ export class SemanticComputeStorm {
|
|||
);
|
||||
const tinted = mix(rainbow, modeTint, this.uModeTintAmt);
|
||||
|
||||
// Brighten on ignition so beats blaze through the bloom pass; the +0.6
|
||||
// floor keeps the rainbow glowing between beats.
|
||||
return tinted.mul(this.uIgnition.mul(2.4).add(0.6));
|
||||
// Brightness is CLAMPED low so the rainbow shows as COLOR, not white.
|
||||
// Additive blending across 150k overlapping sprites compounds fast — a
|
||||
// high multiplier blows the core to pure white (the bug you saw). Keep
|
||||
// the glow gentle (0.45 floor, +ignition up to ~1.1) and let the
|
||||
// selective bloom pass do the blooming, not raw over-bright color.
|
||||
const glow = clamp(this.uIgnition.mul(0.18).add(0.45), 0, 1.15);
|
||||
return tinted.mul(glow);
|
||||
})();
|
||||
|
||||
// One instanced sprite per particle; positions come from the GPU storage
|
||||
// buffer via positionNode, so the geometry is a single unit quad and the
|
||||
// instance count is the particle count.
|
||||
const geometry = new THREE.PlaneGeometry(0.18, 0.18);
|
||||
// One instanced sprite per particle. Small quads (0.1) keep individual
|
||||
// particles as crisp colored points of light rather than overlapping into
|
||||
// white mush across the now-larger volume.
|
||||
const geometry = new THREE.PlaneGeometry(0.1, 0.1);
|
||||
const mesh = new THREE.InstancedMesh(geometry, mat as unknown as THREE.Material, this.count);
|
||||
mesh.frustumCulled = false;
|
||||
this.material = mat;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue