diff --git a/src/ExplosionSim.js b/src/ExplosionSim.js index b6f2a78..c075d12 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); // Smooth color transitions @@ -96,9 +96,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); } @@ -165,16 +166,16 @@ 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); } // Nachlauf-Stem (Mushroom stem injection) @@ -190,11 +191,14 @@ export class ExplosionSim { s.vortexStrength = easeOutCubic(vp) * 8.0; s.vortexRadius = N * (0.1 + easeOutCubic(vp) * 0.15); - 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 2669c98..72e93be 100644 --- a/src/FluidSolver.js +++ b/src/FluidSolver.js @@ -99,6 +99,7 @@ export class FluidSolver { this.turbulenceTime = 0; this.jacobiIters = 15; this.cooling = 0.995; // temperature decay per step + this.entrainStrength = 0; // inward pull to form narrow stem } step() { @@ -148,7 +149,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; } } @@ -190,6 +197,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; 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