From 0c73a570746d754cbeac7e00523936649033a473 Mon Sep 17 00:00:00 2001 From: Fraggersheep Date: Mon, 27 Apr 2026 19:06:01 +0200 Subject: [PATCH] Fix mushroom cloud cutoff: taller grid, open top, better shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MarchingCubes grid was a 12×12×12 cube centered at Y=0, so the simulation space ran from Y=-6 to Y=+6. The explosion injection at N*0.12 mapped to world Y≈-4.5 (underground), and the rising cloud hit the ceiling at Y=+6 where _setBoundary zeroed its density, literally clipping the mushroom cap. - Switch MC to a tall non-square box (16 wide × 40 high), shifted up so the grid bottom sits at ground level and the cap has headroom. - Open top boundary on density/temp so gas flows out instead of being killed at the ceiling. - Add entrainment force that pulls low-altitude gas toward the central axis, forming a narrow rising stem. - Tune vortex ring to form higher (52-74% of grid height) and wider for a recognizable mushroom cap. - Lower buoyancy and initial velocity so gas rises steadily rather than rocketing into the boundary. - Reframe the camera and tracking target to follow the cap up to world Y≈28, and thin the fog so the top stays visible. Co-Authored-By: Claude Sonnet 4.6 --- src/ExplosionSim.js | 43 ++++++++++++++++++++++++------------------- src/FluidSolver.js | 21 +++++++++++++++++++-- src/main.js | 20 ++++++++++---------- 3 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/ExplosionSim.js b/src/ExplosionSim.js index 3a19fc0..a593ca6 100644 --- a/src/ExplosionSim.js +++ b/src/ExplosionSim.js @@ -60,8 +60,8 @@ export class ExplosionSim { // 3-step cel-shading quantization float stepped = floor(NdotL * 3.0 + 0.5) / 3.0; - // Height-based + global temperature color ramp - float h = clamp(vWorldPos.y / 12.0, 0.0, 1.0); + // Height-based + global temperature color ramp (world Y 0..40) + float h = clamp(vWorldPos.y / 38.0, 0.0, 1.0); float t = clamp(uGlobalTemp * (0.5 + 0.5 * h), 0.0, 1.0); vec3 c; @@ -92,9 +92,10 @@ export class ExplosionSim { }); this.mc = new MarchingCubes(this.N, this.celMat, false, false, 200000); - this.mc.isolation = 0.12; - this.mc.scale.set(12, 12, 12); - this.mc.position.set(0, 0, 0); + this.mc.isolation = 0.14; + // Non-square: 16 wide × 40 tall — world Y goes 0 → 40 (grid bottom = ground) + this.mc.scale.set(8, 20, 8); + this.mc.position.set(0, 20, 0); this.scene.add(this.mc); } @@ -160,31 +161,35 @@ export class ExplosionSim { _updateTimeline(t) { const s = this.solver, N = this.N; - // Detonation — inject massive initial energy + // Detonation — narrow initial fireball near ground if (!this.detonated && t > 0.05) { this.detonated = true; - s.injectSphere(N / 2, N * 0.12, N / 2, N * 0.15, 80, 50); + s.injectSphere(N / 2, N * 0.08, N / 2, N * 0.09, 80, 40); } - // Sustained fireball energy - if (t > 0.1 && t < 3.0) { - const strength = (1 - remap(t, 0.1, 3.0)) * 20; - s.injectSphere(N / 2, N * 0.12, N / 2, N * 0.08, strength, strength * 0.5); + // Sustained narrow stem injection at base + if (t > 0.1 && t < 4.5) { + const strength = (1 - remap(t, 0.1, 4.5)) * 22; + s.injectSphere(N / 2, N * 0.07, N / 2, N * 0.05, strength, strength * 0.4); } - // Physics parameters - s.buoyancyAlpha = 4 + 6 * clamp01(remap(t, 0, 2)); + // Physics parameters — lower buoyancy so gas rises steadily, not rockets + s.buoyancyAlpha = 2 + 3 * clamp01(remap(t, 0, 2)); - const vp = remap(t, 2.0, 6.0); - s.vortexStrength = easeOutCubic(vp) * 3.0; - s.vortexCenter = N * (0.3 + easeOutCubic(vp) * 0.35); - s.vortexRadius = N * (0.1 + easeOutCubic(vp) * 0.15); + // Vortex ring starts later and forms much higher → wide mushroom cap + const vp = remap(t, 1.5, 7.0); + s.vortexStrength = easeOutCubic(vp) * 5.5; + s.vortexCenter = N * (0.52 + easeOutCubic(vp) * 0.22); // 52% → 74% height + s.vortexRadius = N * (0.10 + easeOutCubic(vp) * 0.22); // grows from narrow to wide cap - s.turbulenceStrength = 1.0 + 2.0 * clamp01(remap(t, 0.5, 3.0)); + // Entrainment: inward pull creates narrow stem below the cap + s.entrainStrength = t > 1.0 ? Math.min(3.0, (t - 1.0) * 1.2) : 0; + + s.turbulenceStrength = 0.8 + 1.5 * clamp01(remap(t, 0.5, 3.0)); s.cooling = 0.998 - remap(t, 5, 20) * 0.008; // Global temperature for coloring - this.globalTemp = 1.0 - remap(t, 1.0, 12.0) * 0.85; + this.globalTemp = 1.0 - remap(t, 1.0, 14.0) * 0.86; this.celMat.uniforms.uGlobalTemp.value = this.globalTemp; } diff --git a/src/FluidSolver.js b/src/FluidSolver.js index ee79d03..c1c3a8c 100644 --- a/src/FluidSolver.js +++ b/src/FluidSolver.js @@ -99,6 +99,7 @@ export class FluidSolver { this.turbulenceTime = 0; this.jacobiIters = 20; this.cooling = 0.995; // temperature decay per step + this.entrainStrength = 0; // inward pull to form narrow stem } step() { @@ -128,7 +129,13 @@ export class FluidSolver { // 5. Cool temperature for (let i = 0; i < N*N*N; i++) { g.temp[i] *= this.cooling; - // No density dissipation — let advection+boundaries handle mass loss naturally + } + + // 6. Open top: let gas flow out instead of being killed at the ceiling + for (let k = 0; k < N; k++) + for (let i = 0; i < N; i++) { + g.density[g.idx(i, N-1, k)] = g.density[g.idx(i, N-2, k)] * 0.90; + g.temp[g.idx(i, N-1, k)] = g.temp[g.idx(i, N-2, k)] * 0.90; } } @@ -170,6 +177,16 @@ export class FluidSolver { vz[idx] += vPol_r * nz * dt; } + // --- Entrainment: inward pull at low altitude → narrow stem --- + if (this.entrainStrength > 0 && j < N * 0.50) { + const edx = i - cx, edz = k - cz; + const er = Math.sqrt(edx * edx + edz * edz) + 0.001; + const altFrac = j / (N * 0.50); + const inward = this.entrainStrength * Math.max(0, 1 - altFrac); + vx[idx] -= (edx / er) * inward * dt; + vz[idx] -= (edz / er) * inward * dt; + } + // --- Curl-Noise Turbulence (Kolmogorov micro-scale) --- if (this.turbulenceStrength > 0) { const s = 0.15, t2 = this.turbulenceTime; @@ -286,7 +303,7 @@ export class FluidSolver { g.density[idx] += densityVal * falloff; g.temp[idx] += tempVal * falloff; // Initial upward velocity - g.vy[idx] += 3.0 * falloff; + g.vy[idx] += 2.0 * falloff; } } } diff --git a/src/main.js b/src/main.js index 7c06832..7ef2fb4 100644 --- a/src/main.js +++ b/src/main.js @@ -15,12 +15,12 @@ renderer.setClearColor(0x0a0a0f); // ---- Scene ---- const scene = new THREE.Scene(); -scene.fog = new THREE.FogExp2(0x0a0a0f, 0.005); +scene.fog = new THREE.FogExp2(0x0a0a0f, 0.003); // ---- Camera ---- -const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 500); -camera.position.set(18, 8, 24); -camera.lookAt(0, 5, 0); +const camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, 0.1, 600); +camera.position.set(22, 10, 32); +camera.lookAt(0, 10, 0); // ---- Post-processing (Sobel outlines + bloom) ---- let postProcess = null; @@ -44,8 +44,8 @@ restartBtn.addEventListener('click', () => explosion.reset()); const clock = new THREE.Clock(); let frameCount = 0, fpsTime = 0; -const camBase = new THREE.Vector3(18, 8, 24); -const camLook = new THREE.Vector3(0, 5, 0); +const camBase = new THREE.Vector3(22, 10, 32); +const camLook = new THREE.Vector3(0, 10, 0); // ---- Flash overlay (DOM-based for compatibility) ---- const flashEl = document.createElement('div'); @@ -58,12 +58,12 @@ function animate() { explosion.update(dt); const t = explosion.time; - // Camera: slow pull-back + rise with the cloud - const pull = 1 + Math.min(t * 0.012, 0.2); + // Camera: slow pull-back + rise tracking the mushroom cloud + const pull = 1 + Math.min(t * 0.010, 0.18); camera.position.x = camBase.x * pull; - camera.position.y = camBase.y + Math.min(t * 0.25, 4); + camera.position.y = camBase.y + Math.min(t * 0.30, 8); camera.position.z = camBase.z * pull; - camLook.y = 5 + Math.min(t * 0.35, 5); + camLook.y = 10 + Math.min(t * 0.90, 18); // follows cap to world-y ≈ 28 camera.lookAt(camLook); // Flash overlay