mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-02 22:01:01 +02:00
fix(cinema): contain the particle storm on-screen (soft sphere + velocity clamp)
Particles (esp. the unbounded Rössler chaos mode) could fly off-screen. Add a camera-frame-sized spherical containment field: spring pull-back past the radius, hard velocity clamp, and a snap-to-shell safety net so no particle can escape. The sandbox sizes the radius from camera distance + vfov each frame so the storm reframes as the camera flies. Verified: check + build green.
This commit is contained in:
parent
66b10ded42
commit
4163f4fc80
2 changed files with 51 additions and 0 deletions
|
|
@ -165,6 +165,16 @@ export class CinemaSandbox {
|
||||||
async render(deltaSeconds: number): Promise<void> {
|
async render(deltaSeconds: number): Promise<void> {
|
||||||
if (!this.booted) return;
|
if (!this.booted) return;
|
||||||
this.camera.lookAt(this.target);
|
this.camera.lookAt(this.target);
|
||||||
|
|
||||||
|
// Keep the storm inside the frame: derive the largest world radius that
|
||||||
|
// fully fits the camera's vertical FOV at the current distance to target,
|
||||||
|
// minus a margin so the glow halo stays on-screen too. The storm clamps
|
||||||
|
// itself to this each frame, so it reframes as the camera flies.
|
||||||
|
const dist = this.camera.position.distanceTo(this.target);
|
||||||
|
const vfov = (this.camera.fov * Math.PI) / 180;
|
||||||
|
const fitRadius = Math.tan(vfov / 2) * dist * 0.62; // 0.62 = on-screen margin
|
||||||
|
this.storm.setContainRadius(fitRadius);
|
||||||
|
|
||||||
await this.storm.update(deltaSeconds);
|
await this.storm.update(deltaSeconds);
|
||||||
if (this.post) await this.post.renderAsync();
|
if (this.post) await this.post.renderAsync();
|
||||||
else await this.renderer.renderAsync(this.scene, this.camera);
|
else await this.renderer.renderAsync(this.scene, this.camera);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ import {
|
||||||
float,
|
float,
|
||||||
sin,
|
sin,
|
||||||
cos,
|
cos,
|
||||||
|
length,
|
||||||
|
clamp,
|
||||||
|
min,
|
||||||
|
mix,
|
||||||
positionLocal,
|
positionLocal,
|
||||||
} from 'three/tsl';
|
} from 'three/tsl';
|
||||||
|
|
||||||
|
|
@ -87,6 +91,10 @@ export class SemanticComputeStorm {
|
||||||
private uTime = uniform(0);
|
private uTime = uniform(0);
|
||||||
private uIgnition = uniform(0.6);
|
private uIgnition = uniform(0.6);
|
||||||
private uMode = uniform(0);
|
private uMode = uniform(0);
|
||||||
|
// World-space radius the storm is contained within. Particles past this get
|
||||||
|
// a spring force back so the storm NEVER flies off-screen. Sized to the
|
||||||
|
// camera framing by the sandbox via setContainRadius().
|
||||||
|
private uContainRadius = uniform(48);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
renderer: { computeAsync: (node: ComputeDispatch) => Promise<void> },
|
renderer: { computeAsync: (node: ComputeDispatch) => Promise<void> },
|
||||||
|
|
@ -164,8 +172,35 @@ export class SemanticComputeStorm {
|
||||||
vel.addAssign(active);
|
vel.addAssign(active);
|
||||||
// Ignition shockwave yanks particles toward the new node on each beat.
|
// Ignition shockwave yanks particles toward the new node on each beat.
|
||||||
vel.addAssign(toTarget.normalize().mul(this.uIgnition.mul(0.02)));
|
vel.addAssign(toTarget.normalize().mul(this.uIgnition.mul(0.02)));
|
||||||
|
|
||||||
|
// CONTAINMENT: soft spherical boundary around the focused target so the
|
||||||
|
// storm NEVER escapes the camera frame. Past uContainRadius a spring
|
||||||
|
// force pulls each particle back toward the target; the force ramps in
|
||||||
|
// smoothly (smoothstep-like) so the boundary reads as a glowing
|
||||||
|
// membrane, not a hard wall. The chaos attractor (Rössler) is
|
||||||
|
// unbounded by nature — this is what keeps mode 2 on-screen.
|
||||||
|
const distFromTarget = length(toTarget.negate()); // |pos - target|
|
||||||
|
const overflow = distFromTarget.sub(this.uContainRadius).max(0);
|
||||||
|
const pullBack = clamp(overflow.mul(0.012), 0, 0.6);
|
||||||
|
vel.addAssign(toTarget.normalize().mul(pullBack));
|
||||||
|
|
||||||
|
// Hard velocity clamp so no single step can shoot a particle across
|
||||||
|
// the frame even at peak ignition / chaos divergence.
|
||||||
|
const speed = length(vel);
|
||||||
|
const maxSpeed = float(1.2);
|
||||||
|
vel.assign(vel.mul(min(maxSpeed, speed).div(speed.max(0.0001))));
|
||||||
|
|
||||||
pos.addAssign(vel);
|
pos.addAssign(vel);
|
||||||
vel.mulAssign(0.95);
|
vel.mulAssign(0.95);
|
||||||
|
|
||||||
|
// Final safety net: if a particle still ends up beyond 1.35x the
|
||||||
|
// radius (extreme edge case), snap it onto the boundary shell so it
|
||||||
|
// can never be lost off-screen.
|
||||||
|
const finalToTarget = vec3(this.uTarget).sub(pos);
|
||||||
|
const finalDist = length(finalToTarget.negate());
|
||||||
|
const hardR = this.uContainRadius.mul(1.35);
|
||||||
|
const snapped = vec3(this.uTarget).sub(finalToTarget.normalize().mul(hardR));
|
||||||
|
pos.assign(mix(pos, snapped, finalDist.greaterThan(hardR).select(float(1), float(0))));
|
||||||
})().compute(this.count);
|
})().compute(this.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -236,6 +271,12 @@ export class SemanticComputeStorm {
|
||||||
this.uIgnition.value = 8.0;
|
this.uIgnition.value = 8.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Size the containment sphere (world units) so the storm always stays in
|
||||||
|
* frame. The sandbox derives this from the camera distance + fov. */
|
||||||
|
setContainRadius(radius: number): void {
|
||||||
|
this.uContainRadius.value = Math.max(8, radius);
|
||||||
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
if (this.mesh) {
|
if (this.mesh) {
|
||||||
this.scene.remove(this.mesh);
|
this.scene.remove(this.mesh);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue