Wake up
Messages
The messages exchanged in this conversation.
- Created
A new turn starts now. Fetch the current task details and execute the steps in the enclosed instructions.
- Created activity call _getTaskDetails
- Created
Let me look at the current state of the demos folder to understand what exists, and then create a new demo. The task is "Wake up and do as you please" — so I should create a new cool Three.js demo. The last one was 052-supercell. So the next one should be 053-something.
Let me first install shell tools and explore the demos folder.
- Created
Let me explore the demos folder to see what's there.
- Created activity call execute_shell_command
- Created
There are 52 demos. The last one was 052-supercell, a tornado chase simulator. I need to create 053-something. Let me think about what to create.
Looking at the list of existing demos:
- Spinning cube
- Nebula galaxy
- Mandelbulb explorer
- Fluid dynamics
- Gravity sandbox
- Event horizon
- Sonic reactor
- Void runner
- Azure odyssey
- Abyssal bloom
- Physarum garden
- Powder alchemy
- Clockwork nebula
- Neon rain
- Aurora borealis
- Skitter
- Coral resonance
- Threshold
- Attractorium
- Murmuration
- Terraforge
- Cloudsea
- Frostline
- Koi pond
- Ferrofluid
- Synchrony
- Cymatics
- Teslasphere
- Pyrosymphony
- Morphogenesis
- Galton cascade
- Wavefront
- Gossamer
- Magnetosphere
- Cloud chamber
- Wavefunction
- Epicycle
- Sandpile
- Kinetic theory
- Lumen lab
- Squish lab
- Neon tilt
- Hyperfold
- Photon garden
- Critical point
- Bridge works
- Primordial
- Coasterworks
- Sakura grove
- Voxelia
- Neon district
- Supercell
Ideas for 053:
- Deep sea / submarine explorer
- Volcano / lava simulation
- Glacier / ice
- Ant colony
- Traffic simulation
- Train set
- Orbital mechanics / rocket
- Pinball machine
- Marble run / kinetic sculpture
- Rube Goldberg machine
- Circuit / electronics sim
- Ecosystem / predator-prey
- Bioluminescent cave
- Underwater volcano / hydrothermal vents
- Tsunami / ocean waves
- Fireworks show
- Snow globe
- Desert dunes with sandstorm
- Canyon with river erosion
- Lightning storm
- Solar system / orrery (037-epicycle might cover that)
- Lava lamp
- Soap bubbles
- Origami
- Spider web with rain
- Music box
- Pendulum waves
- Double pendulum chaos
- Foucault pendulum
- Rubik's cube solver
- Maze generator/solver 3D
- Pac-Man style game
- Tower defense
- Fishing game
- Crane game / claw machine
- Pachinko
- Pinball
Let me check the index.html to see the style of the index page and get a sense of the demo descriptions.
Actually, let me think about something visually spectacular and different from what exists:
- 008-void-runner, 042-neon-tilt, 048-coasterworks, 051-neon-district are games/vehicles.
- 052-supercell is a driving game with tornado.
New idea: "Deep sea submarine explorer" — 010-abyssal-bloom might be underwater. Let me check what that is.
Let me quickly scan the index page to see descriptions so I avoid duplicating a concept.
- Created activity call execute_shell_command
- Created
Let me look at the index.html structure.
- Created activity call execute_shell_command
- Created
I see the structure. Now let me see a list of all demo titles to pick a fresh concept. Let me grep for h2 titles.
- Created activity call execute_shell_command
- Created
Now I know all 52 demos. Let me pick a fresh concept for 053. Ideas not yet covered:
- Volcano / lava eruption — not covered. Could be spectacular: procedural volcanic island, lava flows with shader, smoke plume particles, ash, lightning in the plume, bombs/projectiles.
- Deep sea submarine — abyssal bloom is underwater-themed already.
- Fireworks — pyrosymphony might be fire-related. Let me check what pyrosymphony is.
- Pinball — not covered. Neon tilt might be a marble game. Let me check.
- Marble run / kinetic sculpture — galton cascade is similar-ish.
- Desert / dunes with sandstorm — not covered.
- Spider web — gossamer might be that.
- Clock tower / automaton — clockwork nebula is space-themed clockwork.
- Train set — not covered! A model train / railway sim could be cool but coasterworks covers splines/tracks.
- Ocean waves / surfing / tsunami — cloudsea and azure odyssey might be water. Let me check a few.
- Ant colony — physarum is similar.
- Ecosystem / predator prey — not covered.
- Origami / paper — not covered.
- Lava lamp / metaballs — ferrofluid/squish-lab might cover.
- Solar eclipse / planetary transits — event horizon is black hole, epicycle is orbital.
- Glacier calving — frostline is probably icy.
- Bioluminescent cave with glow worms — abyssal bloom is deep sea probably.
- Hot air balloons over canyon — not covered.
- Zen garden rake — sandpile is sand.
- Music box / automaton theater — not covered.
- Rube Goldberg machine — galton cascade adjacent.
- Fishing / lighthouse — not covered. A lighthouse scene with rotating beam, stormy ocean, waves shader — could be gorgeous!
- Volcanic eruption — I keep coming back to this. Very visual: glowing lava shader with flow, eruption particles, ash plume, volcanic lightning.
Let me check what pyrosymphony and neon-tilt are to avoid duplication.
- Created activity call execute_shell_command
- Created
OK so:
- 042-neon-tilt = pinball
- 033-gossamer = spiderweb
- 029-pyrosymphony = fireworks
- 023-frostline = frost/winter
- 010-abyssal-bloom = deep sea
- 009-azure-odyssey = sailing
So what's NOT covered: Volcano! A volcanic island eruption simulator would be spectacular:
- Procedural volcanic island with heightfield
- Animated lava flow shader flowing down slopes (flow field following gradient)
- Lava lake in the crater with glowing shader
- Eruption: lava bombs with parabolic trajectories, ash plume particle system
- Volcanic lightning in the plume
- Ocean around with waves, steam where lava meets water
- Day/night cycle? Or dusk setting with bloom
- Camera orbit, maybe cinematic
- Heat haze post-processing
- Generative audio: rumbling, eruption booms, lava bubbling
Alternatively: desert canyon with sandstorm, lighthouse in storm, hot air balloons, ecosystem, train.
Another idea: "Coral City" or aquarium — covered by koi pond/coral resonance.
Another strong candidate: Volcano — "Ignis" / "Caldera". Let me go with a volcano demo:
053-caldera— Volcanic Eruption Simulator.Features to make it "wow":
- Procedural volcanic island: radial cone heightfield with fbm detail, a crater bowl at top, lava channels carved. Ocean around.
- Lava flow system: animated emissive lava shader. Lava flows from the crater rim down channels — implement flow as shader with distance-field channels + scrolling noise, bright near source. Maybe multiple channels that activate over time.
- Lava lake in crater: disc with cellular noise shader, pulsing glow, bubbling.
- Eruption particle system: lava bombs (instanced spheres with trails, parabolic physics, splash on impact), ash plume (billboarded smoke sprites rising with wind), sparks.
- Volcanic lightning: occasional bolts within the ash plume (line segments with glow) + thunder audio.
- Ocean: Gerstner waves or simple animated normal shader plane; steam/smoke particles where lava reaches the sea; shoreline foam.
- Sky: dusk/sunset gradient with stars appearing, ash-darkened sky near volcano, moon. Glowing plume lit from below by lava.
- Post-processing: UnrealBloom, heat haze distortion shader above the crater, vignette, film grain.
- Audio: generative WebAudio — deep rumble (brown noise + LFO), eruption booms, lava bubbling, ocean waves, thunder.
- Eruption cycle: states — dormancy (smoke wisp), unrest (tremors, camera shake, more smoke), eruption (bombs, plume, lava flows increase), paroxysm (big burst), waning → loop. HUD showing status, "VEI" scale, seismic drumbeat.
- Seismograph: canvas-based strip-chart seismograph in HUD that shows tremor amplitude tied to state + drumbeat earthquakes. That's a nice unique touch!
- Interaction: orbit camera, click to trigger eruption / force eruption button, quality tiers, day-night maybe not, PNG snapshot, touch support.
- Trees/vegetation on island that catches fire when lava flow passes? That could be complex but doable: instanced trees, and lava flow mask drives a "burning" state with ember particles. Maybe simpler: palm trees that glow/ignite near lava front. Nice touch but keep manageable.
Also a time-lapse day cycle might be too much. Keep dusk setting for mood (lava glows best at dusk/night).
Camera: orbit controls around island + cinematic auto mode + free fly.
Let me design the lava flow carefully:
- Heightfield island: h(x,z) = cone profile + fbm detail. Cone: base radius R, height H, crater at top: bowl subtract.
- Lava channels: precompute a few flow paths from crater rim down to the sea by gradient descent on the heightfield. Store as polyline. Then render lava as ribbons along paths (geometry built from path), with animated shader (scrolling noise, emissive). Lava front progresses over time — uniform uFront distance; ribbon alpha/brightness beyond front fades to cool crust.
- Where lava reaches ocean: steam particles + glowing entry point.
- Lava lake: circle mesh in crater bowl with shader.
That ribbon approach is robust and looks great with bloom.
Ash plume: use instanced billboards (planes) with soft particle texture generated procedurally (radial gradient noise texture via canvas), rising with curl, expanding, wind drift. Dark smoke color lit orange near bottom by lava glow — shader trick: color = mix(fireglow, ashcolor, height/age).
Lava bombs: instanced mesh (icosahedron small) with per-instance data; CPU physics sim (gravity, spawn from vent with random velocity); leave ember trail using a line or small particle; on hitting terrain → burst of sparks + brief glow decal (skip decals, just sparks). Also sizzling sound on land in water → steam puff.
Lightning in plume: generate jagged polyline between two points in plume, render as THREE.Line with additive material + point light flash; thunder after delay.
Heat haze: post shader pass that distorts UV by noise scaled by screen-space mask above crater — could do a simple full-screen subtle distortion near bottom center. Alternatively skip haze and do bloom + grade. I'll include a simple haze: pass crater screen position as uniform, distort within radius.
Seismograph: 2D canvas strip chart; amplitude driven by eruption state + random tremors + drumbeat events (sudden spikes). Adds a lovely "science instrument" feel.
HUD: status text (DORMANT/UNREST/ERUPTING/PAROXYSM/WANING), VEI indicator, eruption countdown/progress bar, seismic energy readout, controls help, quality toggle, snapshot.
Audio engine: WebAudio, master gain.
- Rumble: brown noise through lowpass, gain tied to state.
- Lava bubbling: filtered noise bursts random.
- Eruption booms: on bomb launch (near ones) - synthesized explosion (noise burst + low sine drop).
- Ocean: slow modulated noise band.
- Thunder: as in supercell — noise burst with lowpass decay.
- Wind: light.
Controls: OrbitControls default; buttons: "Trigger Eruption", "Calm", quality, mute, snapshot, cinematic camera toggle. Keyboard: Space = trigger eruption, C = camera mode, M = mute, Q = quality, H = help.
Trees: instanced palm-ish trees (cone+cylinder merged → instanced) scattered on lower slopes away from channels; when lava front passes their position, tint them black and spawn embers. To keep it simpler: precompute distance from each tree to each channel path; when front > distance → burned. Change instance color. Plus ember particle at tree. That's doable.
Buildings? Maybe a small observatory with blinking light on the coast — cute. Could add a little "lava observatory" hut with dome + rotating beacon. Nice detail, cheap.
Boat? skip.
Birds? skip.
Let me also add stars + moon in sky dome shader, dusk gradient, and god-ray-ish glow around the plume via bloom.
Post chain: RenderPass → UnrealBloomPass → custom grade/haze ShaderPass → OutputPass.
Quality tiers: particle counts, pixel ratio, bloom on/off.
Import map: from unpkg like previous demos. Let me check what a recent demo uses for CDN and structure.
- Created activity call execute_shell_command
- Created
Good — the pattern is three@0.160.0 with unpkg import maps. Previous demos are ~76-98KB single HTML files.
Now I'll build 053-caldera — Volcanic Eruption Simulator. Let me plan the full architecture before writing the file:
Scene structure
- Renderer: WebGLRenderer, ACESFilmicToneMapping, shadows maybe off (dusk scene, many lights not needed; lava is emissive). Use a directional "moon/dusk" light with shadows? Shadows on heightfield from emissive lava is not real. Keep a hemisphere light + dim directional; skip shadow maps for perf. Actually tree shadows would be nice but not essential. Skip shadows → perf.
- Camera: PerspectiveCamera, orbit target at island center mid-height.
Island generation
-
Heightfield: polar cone. h(r,θ):
- Base:
H * pow(max(0,1 - r/R), 1.35)for cone with concave profile. - Add fbm:
fbm(x,z) * amp * (some mask)for gullies and detail. - Crater: near r < craterR, depress: bowl shape with rim.
h = lerp(h, craterFloorH, smoothstep(craterR*1.25, craterR*0.55, r))and add rim bump:+ rimH * gauss(r - craterR). - Island ends at r>R → below sea.
- Base:
-
Resolution: 256x256 grid over [-R1.6, R1.6] maybe. Use PlaneGeometry( size, size, 255, 255 ), rotate -PI/2, displace Y by h(x,z). Compute normals.
-
Terrain material: custom ShaderMaterial — color by height & slope: black basalt near top / fresh lava zone, dark gray rock, green vegetation on lower gentler slopes, sand near shore; emissive contribution from lava proximity? Add subtle emissive glow near channels using the channel distance texture — could be baked into a data texture. Simpler: pass channels via texture? Let me bake a "heat" texture: for each terrain vertex, compute min distance to any channel path → store in uv2/attribute aHeat. In shader, emissive = lavaColor * glow(aHeat, front). Actually per-vertex attribute on 256x256 = fine.
-
Lava channels: pick K=5 start points on crater rim (random θ), gradient-descend heightfield until reaching sea (h < 0.5) or r > R. Record points every step. Resample to uniform arc-length polyline. Build ribbon geometry: for each sample, compute tangent & horizontal normal, width w(s) growing downhill; two verts per sample. Attributes: aS (arc length), aSide (-1/1).
- Channel shader: uniform uFront (arc length of lava front), uTime. For s < front: lava — animated fbm scrolling along s; color ramp black crust at edges, bright yellow center; brightness pulses. For s > front: dark (alpha 0 or cooled crust barely visible? Just discard). Front edge: extra bright + glow.
- Overcastting terrain: also want ground glow under flow → point light? Use 2-3 point lights that follow front for main channel. Cheap: one PointLight at crater (lava lake), one following the most active front, plus emissive bloom does the rest.
-
Lava lake: CircleGeometry radius craterR*0.7 at craterFloor height + small offset. Shader: cellular noise bubbling, bright cracks — emissive strong. Pulsing scale with state.
Ocean
-
PlaneGeometry big (radius R*8), at y=0. Custom shader: dusk water — deep blue, fresnel reflection of sky gradient, moving normal maps via procedural noise derivative, specular from moon, and ORANGE specular/emissive near lava entries (uniform array of up to 4 entry points with intensity). Foam ring near shoreline using heightfield sampled? Can't sample heightfield in shader easily — pass shore radius approx; foam = band around r≈R_shore with noise. Good enough: island roughly radial. Foam also around lava entries (steam zone) white.
-
Steam particles at lava entries: white/gray billboards rising, expanding, fading.
Sky dome
- Big sphere shader: dusk gradient (horizon orange-purple to deep indigo zenith), stars (hash based, fade near horizon), moon disc with glow, faint clouds band? Also ash darkening: darken sky toward volcano direction based on plume intensity uniform. Add subtle moving ash cloud layer? Keep simple: gradient + stars + moon + horizon haze.
Ash plume
- InstancedMesh of plane billboards? Billboarding per-instance needs shader (look at camera) — implement in vertex shader using camera position uniform and per-instance center: standard cylindrical billboard. Each particle: age, life, seed, birth position (vent), velocity up + wind, size grows, alpha fade in/out, color: dark ash gray, bottom-lit orange (mix by age and state intensity). Soft texture: canvas radial gradient with noise speckle.
- Count: e.g., 220 high / 140 med / 80 low.
- Behavior tied to eruption state: emission rate & upward speed.
Lava bombs
- InstancedMesh icosahedron, count ~60. CPU sim: spawn at vent with velocity (random cone, speed scaled by state), gravity, land → bounce? Just explode: spawn sparks (from spark pool) + sound + if in water steam puff. Trail: for each bomb, a THREE.Line trail? Expensive-ish; instead add to a shared spark system small emission per bomb per frame. Simpler: each active bomb emits 2 sparks/frame into spark pool → looks like trailing embers.
- Bomb material: MeshBasicMaterial emissive-like orange (basic material unlit orange, bloom picks up). Cool down over life → darker; use instanceColor.
Sparks pool
- InstancedMesh small planes billboards additive or Points with size attenuation. Points simpler: BufferGeometry with position, color, size, life attributes updated CPU-side. Use PointsMaterial? Need per-point size & color & alpha → custom shader for Points. I'll write a small shader: attributes aLife etc. Count ~3000.
Volcanic lightning
- Occasionally within plume when state high: generate jagged bolt polyline from plume point to another point (or to ground). THREE.Line with LineBasicMaterial additive-ish (basic material color white-violet, transparent). Life ~0.12s with flicker. PointLight flash violet at mid point (intensity decays). Thunder sound after delay based on distance from camera.
Trees
- InstancedMesh of merged palm geometry? Build simple palm: curved trunk (few cylinders) + fronds (cones)? Merging geometry without BufferGeometryUtils... I can import BufferGeometryUtils from addons. Or build one geometry manually via groups — simplest: use two InstancedMeshes: trunks (cylinder, brown) and crowns (icosahedron scaled / cone, green). 2 draw calls. Fine.
- Positions: random on terrain where h in [3, H0.55], slope low, r < R0.85, away from channels (precompute dist to channel > threshold).
- Burning: each tree has distToChannel (min over channel arcs, but front only matters per channel: tree burns when front_s_of_that_channel > s_closest_to_tree + small). Compute per tree: for each channel, closest arc length s* and lateral distance d. Tree burns if d < width+margin and front > s*. When burning: crown color → black over 2s, spawn embers; trunk stays.
- Implementation: instanceColor on crown mesh; check per frame only a few trees (loop all 300 trees, cheap distance checks precomputed → just compare front values).
Observatory hut
- Small cylinder + dome + blinking red beacon light on coast away from channels. Cute detail. Cheap. Beacon: small sphere + PointLight? Use emissive sphere pulsing, no light. Add tiny dock? skip.
Eruption state machine
States: DORMANT → UNREST → ERUPTING → PAROXYSM → WANING → DORMANT... Auto timeline with randomness; user can force trigger (Space/button).
- Parameters modulated by "intensity" I(t) 0..1 (target per state, smooth lerp):
- Plume emission rate, rise speed
- Bomb spawn rate
- Lava front speed (channels activate sequentially: channel i starts flowing when eruption phase passes threshold; fronts advance; during waning fronts freeze, lake dims)
- Rumble gain, tremor amplitude, camera shake
- VEI label mapping
- Cycle durations: dormant 20-35s, unrest 12s, erupting 25s, paroxysm 10s, waning 15s. With auto-loop ON. Also a "Calm" button to force dormancy.
- Drumbeats: random sudden tremor bursts (2-4s), shake + seismo spike + boom sound.
Seismograph HUD
- Canvas 2D strip chart, scrolling left, drum-style: amplitude = base noise * (tremor) + spikes; draw green line on dark. Cute and thematic. Label "RSAM SEISMIC MONITOR — STATION CALDERA-1".
Audio (WebAudio, generative)
- Master gain, mute toggle. Start on first gesture.
- Rumbling: brown noise buffer looped → lowpass 120Hz → gain g_rumble(I).
- Ocean: pink-ish noise → bandpass ~400Hz with slow LFO gain.
- Lava bubbling: schedule random short filtered noise blips (lowpass 300-800) at rate ~ I.
- Booms: on bomb spawn (limit rate): noise burst through lowpass with fast decay + sine drop 90→40Hz. Distance attenuation? Bombs all from vent — fixed-ish gain, vary by size.
- Thunder: for lightning: filtered noise with slow decay after delay.
- Wind: faint highpass noise tied to plume.
Post-processing
- EffectComposer: RenderPass, UnrealBloomPass (strength ~0.9, radius .6, threshold .55), ShaderPass grade: vignette + grain + slight warm tint + heat-haze distortion near crater screen pos (uniform uVentScreen, uHazeAmp). OutputPass.
- Haze: offset uv by noise(uv*..., time) * mask where mask = smoothstep around vent screen pos above it. Cheap.
Controls & UI
- OrbitControls (damped, min/max distance, maxPolarAngle < PI/2+ a bit to avoid going under ocean).
- Buttons: ERUPT (trigger), CALM, camera mode (orbit/cinematic/fly?), quality, sound, snapshot.
- Cinematic mode: camera path slowly orbiting + changing height, auto resumes orbit on user drag.
- Keyboard: Space erupt, C calm, V camera, M sound, Q quality, H help overlay, P snapshot.
- Touch: orbit works via touch; buttons fine. Maybe joystick not needed here (no driving). Keep pinch zoom via OrbitControls.
- Intro overlay: title + "click to begin" (starts audio). Like previous demos probably have a splash. Add small splash with START button.
FPS + adaptive quality
- Monitor avg fps, auto downgrade tier; Q cycles High/Med/Low; pixelRatio caps; bloom off at low.
Snapshot
- Render then canvas.toBlob → download link click. Standard.
OK — file size target ~60-90KB. Let me now write the code carefully. I'll write it in sections to a file via heredoc? Better: use read_write_file_in_shell_environment write mode with full content. It's large; I may split into a few appends. The write tool writes whole content — for a 70KB file, one write call with big content is okay? Might be large but should work. Safer: write in 3-4 parts using shell heredocs with
cat >>. Heredoc with quoted delimiter avoids interpolation. Watch out for backticks inside JS template literals inside heredoc — with quoted 'EOF' it's safe (no shell expansion). Backticks are fine within quoted heredoc.Actually the read_write_file tool can handle large content; but to be safe I'll build with multiple
cat >> ... <<'HTMLEOF'appends. Let me draft the entire code.Let me carefully design the heightfield & math:
Island constants:
- R = 120 (island base radius), H = 46 (peak height), craterR = 13, craterDepth to floor at H-14.
- Domain size for terrain plane: 320 (so -160..160), segments 256.
height(x,z):
Hmm, want gullies mostly on flanks. Let's do:
Crater bowl:
Also small coastal shelf: base = max(base, -6 - 0.02*(r-R)) for outside → slopes down into sea. For r>R: base = -6 - (r-R)*0.08 maybe to let ocean floor visible. Also add beach flattening: near h in [0..2], smooth to sand.
Sea level y=0. Island peak ~46+.
Lava channels via gradient descent:
Add slight noise perturbation to direction for natural meander: dir = normalize(-grad + 0.35perpnoise). Resample polyline to ds=1.5 arc length samples. Compute cumulative s.
Ribbon geometry: for i in samples: point p_i, tangent t_i (from neighbors), side = normalize(cross(up, t)) horizontally: side = normalize(vec3(-t.z,0,t.x)); width w(s) = 2.2 + 2.0*(s/total) (wider downhill) — also taper at very start. verts: p ± sidew/2, y = h(p)+0.35 (sit above terrain; terrain under channel carved slightly? carve: during heightfield build we can't know channels yet (they depend on heightfield). Two-pass: build height function; compute channels; then when displacing terrain vertices, depress terrain near channels by up to 1.2 within width → channel beds. Then ribbons sit at h+0.25. Good: terrain displacement uses channel data. So order: define height(); compute channels; terrain vertex: h = height(x,z) - channelDepress(x,z). channelDepress: min over channels of distance to polyline segments — expensive: terrain 257x257=66k verts × channels(5)×segments(~120) = 40M ops... too slow? JS ~ maybe 1-2s. Hmm. Optimize: only compute within channel bounding boxes, or use coarse spatial grid. Alternative: depress along channel via carving heightfield after: iterate channel samples and modify nearby grid vertices directly (mark grid). Since terrain geometry vertices are on a regular grid, I can map sample → grid indices and carve a small radius. That's fast. But the height() function used elsewhere (trees, bombs landing, car?) needs the carved height... bombs landing on terrain use height() — small error acceptable (0.8m). Trees avoid channels anyway. I'll carve grid directly (fast), and keep analytic height() for queries; the carve depth small (≤1.2). But ribbon y must use carved height: sample grid bilinear from carved data. I'll implement terrain heights array H[257257]; buildTerrain() fills with analytic; channels computed from analytic; then carve into array; ribbon samples bilinear from array. Queries elsewhere use analytic (fine).
Heat attribute for terrain emissive: per-vertex glow near channels also computed during carve: heat[gridIdx] = max over carves of falloff. Store as Float32Array → BufferAttribute aHeat. In terrain shader: emissiveLava = lavaColor * aHeat * uGlow where uGlow pulses with state and depends on front? Front per channel differs; skip per-channel: global glow that follows average front. Good enough; ribbon itself is the star.
Terrain colors (shader): uniforms uTime; attributes aHeat. Varyings: world pos, normal. Color ramp by height h and slope:
- sand: h in [-0.5, 1.5] → sandy dark? At dusk, dark volcanic sand #2a2422.
- vegetation: h in [1.5, 22], slope<0.55 → mix of #1d2b16 to #2e4420 with noise variation.
- rock: h>22 or steep → #232326 basalt, near crater #17171a with reddish ash tint patches.
- underwater: h<0 → dark blue-gray sand.
- Add fbm variation via a noise texture? Use procedural in-shader value noise (small cost). I'll implement a cheap hash noise in shader for variation.
- Emissive: aHeat * uGlow * (orange) + lake handled separately.
Lighting: fake — shader does N·L with a dusk directional (from moon/sunset) + hemisphere ambient. Include fog? Use scene.fog + manual in shaders? Custom shaders need fog handling — I'll include fog manually (mix toward fog color by distance) in each custom shader. Use THREE fog uniforms? Simpler to compute manually with uniform uFogColor, uFogDensity... To reduce complexity, use scene.fog = new THREE.FogExp2(color, density) and include
#include <fog_pars_fragment>chunks in ShaderMaterial with fog:true. ShaderMaterial supports fog if you include chunks. I'll include the standard fog chunks in my shaders — manageable:Needs material.fog = true. OK.
Ocean shader: similar fog; Gerstner-lite normal from sum of moving noises; color: deep #06121f to horizon sky reflect; fresnel; moon glint: specular toward moon dir; lava entry glow: uniform vec4 uEntries[4] (xz, radius, intensity) → emissive orange flicker + white foam ring. Foam at shore: r within [Rshore-3, Rshore+1] band + noise → whitish. Approx Rshore where h crosses 0 ~ R*(1 - small)... compute actual shore radius numerically: sample height along 64 radials, find r where h=0.5 → average → uShoreR (island roughly radial but fbm perturbs; per-angle would need texture; average fine with noise wobble).
Sky dome shader: gradient by world dir y: horizon #2b1a2e? Let's pick dusk palette: horizon warm #ff7a3c → #8a3b6e → zenith #0c1030. Sun just set: glow spot at horizon in sun dir (uniform). Stars: hash(dir*freq) threshold, twinkle, fade below y<0.1. Moon: disc at moonDir with glow. Ash: uniform uAsh (0..1) darkens toward vent direction cone + overall dim.
Fog color: pick #241a26 dusk mauve; density 0.0016.
Plume particles (instanced billboard quads):
- attributes: iBirth (time born), iLife, iSeed, iVel (vec3), iSize0. Updated on CPU spawn; positions computed in shader: pos = vent + velage + windage + curl wobble; age = (uTime - birth); if age>life → scale 0. CPU recycles by overwriting dead slots (ring buffer).
- Actually simpler CPU: keep particle structs, update positions on CPU each frame into instanced attributes? 220 particles — trivial CPU cost, simpler code. But billboard orientation: do in shader from instance matrix? If I set instance matrices with lookAt each frame it's 220 matrix updates — fine, but shader-billboard cheaper: pass iPos (vec3), iSize, iAlpha, iShade per instance via InstancedBufferAttributes updated each frame; vertex shader builds billboard from camera right/up uniforms. Clean.
- color: mix(fireOrange*lit, ashGray, smokeFactor) where smokeFactor = clamp(age/life * 1.2) and lit by lava glow from below: shade = mix(1.0 (hot), 0.35, age) * uGlowBottom(I). Alpha: soft in/out.
- Texture: canvas 128x128 radial gradient + noise blotches → THREE.CanvasTexture.
Bombs: instanced icosahedron radius 0.5; per bomb: pos, vel, life, size(0.3-1.2), spin; gravity -9.8*1.6? Scale world: island 120 radius, heights 46; bomb speeds ~ 35-70 m/s up. On ground hit (y < terrainHeight(x,z)+0.3): explode → 10-20 sparks, boom sound (rate-limited), if y<0.5 (water) → steam puff + hiss. Also on crater rim could land → fine.
- Trail: each frame push 1 spark at bomb pos with tiny life → streaks.
- instanceColor: hot yellow-white → dark red as life. Scale pulsing.
Sparks: THREE.Points with custom shader: attributes position(updates), aVel? Do CPU integration for sparks too (3000 max, simple). Attributes: position, aSize, aColor(rgb), aAlpha. Update each frame: pos += veldt; vel.y -= gdt; life -= dt; alpha = life/life0. Write into buffers; draw range to active count. Additive blending, depthWrite false.
Lightning: class Bolt { points[], line, light, ttl }. Generate: start in plume (vent + up 20-40 + random), end: either another plume point or ground. 12-20 segments, midpoint displacement. Material: LineBasicMaterial({color:#cfe0ff, transparent, blending:Additive, depthWrite:false}). Also a short glow quad? Keep line + light. Thunder scheduled with delay dist/340.
Camera shake: amplitude = 0.02 + I*0.15 + drumbeat spike; applied to camera pos via small random offsets (before controls.update? after). Apply in render loop: camera.position add noise * amp, and lookAt handled by controls — offset after controls.update each frame using smoothed noise; fine.
HUD panels (CSS similar style but new palette: ember orange/ash):
- Title: "CALDERA — Volcanic Eruption Simulator" sub.
- Right-top: control buttons.
- Right: seismograph canvas panel w/ label.
- Left-bottom: status panel: STATE label (color-coded), VEI gauge bar, intensity meter, plume height estimate, bombs count.
- Bottom-center: help hints.
- FPS counter.
- Toast messages.
- Splash overlay at start: big title, description, [BEGIN MONITORING] button, quality select maybe. Also starts audio.
State machine details:
Lava flow schedule: channels get activated staggered: channel i has startThreshold = 0.15 + i0.15 of phase progress in ERUPTING... Simpler: each channel has state: inactive/flowing/full; when I crosses 0.25+0.12i → start flowing (front advances at speed 2.2 + 1.5*I m/s until total length, then stays full, keeps animating). During WANING (I<0.18) fronts stop; brightness of flow scales with I (cooling when dormant: after long dormancy channels could go dark — set "cooled" when I<0.08 for 20s → uLive per channel fades to 0, leaving dark crust ribbon (alpha low). Then next eruption may reuse same channels (fine — fronts reset on new eruption start: pick new subset ordering).
Ocean entries: for channels whose length reaches shore (end h<1.2), entry point = last sample; when front reaches end → activate steam emitter + ocean glow slot + bubbling sound boost.
Now the tech watch-list:
- ShaderMaterial fog chunks require vFogDepth computed in fog_vertex using mvPosition variable name — must name the view position
mvPosition. I'll follow convention. - InstancedBufferGeometry via THREE.InstancedMesh + custom shader: with InstancedMesh, use
instanceMatrixattribute; but I want custom per-instance data — can still use InstancedMesh and add InstancedBufferAttributes to geometry; in shader, declare them. Don't call mesh.instanceMatrix usage if my shader builds billboards — but InstancedMesh requires instanceMatrix updates... Using THREE.InstancedMesh forces instancing; if I don't use instanceMatrix in shader, that's fine; set instanceMatrix identity once and mesh.count = N; frustumCulled = false. Alternatively use THREE.Mesh with **************************try and geometry.instanceCount — cleaner for custom. I'll use InstancedBufferGeometry + Mesh for plume & steam. For bombs, InstancedMesh + setMatrixAt is convenient (they're 3D rocks) + instanceColor. Trees: InstancedMesh standard materials? Trees lit by dusk — MeshLambertMaterial with instanceColor works, plus emissive can't vary per instance... burning trees want emissive sparks; I'll just darken color and spawn ember sparks; lambert vertexColors via instanceColor works (mesh.instanceColor). Lambert supports instanceColor? InstancedMesh color works with built-in materials via#defines USE_INSTANCING_COLOR. Yes supported.
Lights:
- HemisphereLight(sky #3a2b4a, ground #14100e, 0.55)
- DirectionalLight moon #7a86b8 0.5 from moonDir (no shadows)
- PointLight crater glow orange (intensity varies with I, distance ~90, decay 2) at crater.
- PointLight lava front light following the most advanced active front (orange, small).
- PointLight lightning flash (violet-white) reused. Terrain/trees/ocean custom shaders won't respond to THREE lights (they're ShaderMaterials) — they compute their own lighting. Only trees & bombs & hut use built-in materials → lit by these lights. OK. Bloom threshold 0.6 so only emissive/lava/bombs bloom.
Cinematic camera: when enabled, animate azimuth around target slowly + radius breathing; disable on user interaction (OrbitControls 'start' event → back to orbit mode unless user chose cinematic again).
Snapshot: renderer.domElement.toDataURL after render with preserveDrawingBuffer? Use composer.render() then toDataURL in same frame — need preserveDrawingBuffer:true or capture immediately after render within same task. I'll do: snapshot() → composer.render(); canvas.toBlob(...). Since render happens synchronously right before, buffer valid if no compositing occurred — safe pattern used widely when capture in same synchronous block. Set preserveDrawingBuffer: false fine if we render then immediately toBlob... toBlob is async callback but capture snapshot of current buffer at call time? Spec: toBlob captures current content; with preserveDrawingBuffer:false the buffer may be cleared after compositing. Safer: preserveDrawingBuffer:true (minor perf cost) OR render again synchronously inside the toDataURL flow. I'll set preserveDrawingBuffer: true — negligible here.
Adaptive quality: tiers HIGH/MED/LOW:
- pixelRatio: min(devicePixelRatio, 2 / 1.5 / 1)
- plume count, sparks max, bloom on/off (off on LOW → use RenderPass + grade only; simpler: keep bloom but lower res? UnrealBloomPass has resolution param; set strength lower). LOW: skip bloom.
- Auto: measure fps over 3s; if < 45 drop tier; if > 58 for 10s raise (max user tier? keep auto only downward + notify). I'll do auto-down only, Q cycles manual.
FPS meter: update 2/s.
Seismograph: canvas 240x90; shift image left by drawing to offscreen? Simplest: keep array of last N samples (N=canvas width), push 2-3 per frame from tremor value, redraw polyline each frame (240 pts — cheap). Style: dark bg, grid lines, green trace with glow (shadowBlur). Station label + current RSAM value text.
Drumbeats: schedule random every 18-40s (more likely at high I): tremorSpike envelope 2-4s, shake boost, boom.
Wind: uniform wind dir (slowly rotating), affects plume drift, steam, sparks slight.
Now sounds — implement AudioEngine class with ensure() on first gesture; nodes:
- master = ctx.createGain() (0.9) → destination, plus compressor.
- noiseBuffer (2s white), brownNoise buffer.
- rumble: brownNoise src loop → lowpass(90Hz,Q0.7) → g(0) ; setTargetAtTime g = 0.5*I^1.5 + 0.03.
- ocean: white → bandpass(500,0.4) → g 0.05 with LFO(0.1Hz, depth .03). Actually slow wave crashes: LFO on gain.
- windHiss: white → highpass(2500) → g 0.012 + 0.03*I.
- bubble(intensity): blips schedule: every frame chance dtrate(I8): create bandpass noise burst 0.08-0.2s freq 150-600, gain .05-.12 → also "gloop" via sine 90→60 pitch? Keep noise burst + occasional sine blip.
- boom(size): noise burst lowpass 400→60 sweep 0.8s gain .5*size + sine 70→35Hz .7s gain .4. Rate-limit 80ms.
- thunder(delay, gain): setTimeout: noise 2.5s lowpass sweep 300→50, gain env attack .01 decay exp; plus sub sine 45Hz.
- hiss (water entry): noise highpass 3kHz 0.5s gain .15.
- ui click: square 800Hz 0.05s. All guarded by ctx state; resume on gesture. Mute toggles master gain 0.
Code size: This is a lot. Let me aim compact but readable. I'll write ~2000 lines? Previous demos ~98KB ≈ 2400 lines. Target similar (60-85KB). Let's write it.
Structure of the HTML:
- head: meta, title, CSS (palette: --ember:#ff9a3c; --hot:#ffd25c; --ash:#8f9aa8; --blood:#ff5252; background #0b0810)
- body: #app, HUD panels, seismo canvas, splash overlay, toast, fps
- importmap (three 0.160.0 unpkg)
- module script:
- imports: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, ShaderPass, OutputPass
- utils (rand, hash, vnoise, fbm, clamp, lerp, smoothstep)
- CONFIG/quality
- renderer/scene/camera/controls/lights/fog
- heightfield + channels + terrain build (+carve+heat)
- terrain shader material
- lava ribbons + shader
- lava lake + shader
- ocean + shader
- sky dome + shader
- plume system
- steam system
- sparks (Points)
- bombs
- lightning bolts
- trees + hut
- eruption state machine
- seismograph
- audio engine
- post chain (bloom, grade+haze shader)
- UI wiring (buttons, keys, touch), splash
- snapshot, resize
- main loop
Uniform sharing: define global
U = { uTime:{value:0}, uGlow:{value:0}, ... }and reuse objects across materials to update once.Let me carefully write GLSL chunks:
Value noise GLSL:
Terrain vertex shader: standard: pass world pos, normal, aHeat. Compute normals on CPU (computeVertexNormals) — yes geometry.attributes.normal. Use builtin varying vNormal (view space? I'll pass world normal since lighting in world space with fixed dirs).
Terrain fragment:
Sun: sunset direction low over ocean, warm #ff9a5c * 0.5. Tune later.
Ribbon shader: attributes: aS (arc len), aSide (-1,1). uniforms: uFront, uLen, uTime, uLive (0..1 overall activity), uCool (crust darkness). vertex: pass vS, vSide, worldPos, fog. fragment:
Ribbon rendered slightly above terrain; disable depthWrite? Keep depthTest true, depthWrite true, polygonOffset to avoid z-fight; place y offset 0.25. Edges: alpha fade instead: a = 1 - smoothstep(0.8,1.0,edge) for soft edge, transparent true. Fine.
Per-channel uniforms differ (uFront, uLive) → each channel own material instance (clone) but shared shader. 5 draws OK.
Lake shader: circle, uv radial.
Simpler: voronoi-ish: use two fbm layers; brightness = pow(n2,2). Emissive = mix(deepred, yellow, n2) * (0.8+0.4 sin pulse) * uLake (uniform). Rim glow. Add slight vertex bob? skip.
Ocean: big plane segments 1 (no displacement) or mild Gerstner displacement (64 segs, vertex displace 3 Gerstner waves — nice silhouettes at shore). Do vertex waves: pos.y += sum Ai*sin(dot(dir_i, xz)w_i + tsp_i). Normals in fragment via analytic derivative or noise; simpler: fragment normal from noise normal map function nrm(xz,t) using fbm gradient; combine with big wave slope small. Fresnel with sky colors uniforms. Entries: uniform vec4 uEntry[4]; xy=xz, z=radius, w=intensity. Emissive += orange * exp(-d/r)*flicker. Foam: ring near shore radius uShore (float) with noise band; plus around entries white foam.
Waves amplitude 0.35 max — shore contact okay.
Sky: dome radius 900, BackSide. As described.
Plume: InstancedBufferGeometry from PlaneGeometry(1,1). Attributes: iData (vec4: x,y,z,size), iMisc (vec4: alpha, shade, rot, seed). CPU updates. Vertex:
Fragment: sample texture, color = mix(hotCol, ashCol, shade) etc, alpha *= tex.a * iMisc.x; blending: NormalBlending for smoke (need occlusion) with depthWrite false; but hot bottom glow wants additive... use two systems? Use normal blending with color darkening; glow from bloom won't trigger (dark). Alternative: make bottom particles emissive-bright with additive — two draw calls (plume glow layer + ash layer). Simplify: single material, blending: NormalBlending, and bake "glow" into color: near vent bottom color can exceed 1? With normal blending color>1 works (HDR) → bloom catches bright bottom-lit smoke. So color = mix(vec3(1.4,0.55,0.15)*litBottom, ashCol, shadeK). alpha from texture.
Sort: transparent smoke sorting artifacts — acceptable with depthWrite false.
Steam: same billboard class, different params (white, smaller, additive-ish normal blending).
Sparks Points shader:
Bolt generation: midpoint displacement between p0,p1: points=32; offset perpendicular random scaled by segment length 0.25 decaying iterations. Implement iterative: pts=[p0,p1]; repeat 5 times: insert midpoints offset by randmag; mag*=0.55. Line geometry from pts. ttl .1-.2s; also faint secondary branch skip.
Seismograph: as planned.
Haze post shader: combine grade:
uVentUv computed each frame by projecting vent world pos.
Splash overlay: styled, with title "CALDERA", subtitle, feature list short, [ INITIATE MONITORING ] button; hides with fade; starts audio + clock.
Toast for events: "SEISMIC SWARM DETECTED", "ERUPTION IMMINENT", "PAROXYSM", "LAVA REACHES THE SEA", "DRUMBEAT QUAKE".
Also plume height readout: estimate = f(I) + noise → display m. Bombs active count. VEI from I. Status colored.
Wind: base dir angle slowly drifts; show on seismo panel? show "WIND 12 kt NE" text updating. Cute.
Edge cases:
- WebGL not available → message.
- Resize handler updates composer, camera, uniforms aspect.
- Mobile: touch buttons; OrbitControls handles touch rotate/pinch.
Performance: terrain 257² verts ok; ocean 128²; plume 240; sparks 3000 points; bombs 64; trees 260×2; ribbons ~5×120 segs. Fine.
Now — writing the actual file. I'll write in ~5 chunks via heredoc appends. Must be careful: inside JS I use template literals with
${}— inside quoted heredoc safe. GLSL in template literals also fine.Also careful:
cat >> file <<'EOF'— content containing the literal lineEOF? avoid. Use distinctive delimiters: 'CHUNK1' etc.Let me now write the code mentally-solid, section by section. I'll aim for clean, commented code.
Key numeric constants:
- WORLD: R=120 island, terrainSize=340, segs=256 → cell 1.33m.
- H=46, craterR=13, craterFloor H-15=31.
- Sea level 0; beach ~0-1.5.
- Vent pos: (0, craterFloor+0.5, 0) approx — lake center. Plume/bombs spawn from (0, lakeY+1, 0).
- Camera start: pos(150, 70, 190) target (0, 18, 0). OrbitControls min 60 max 420, maxPolar 1.45 rad (~83°), minPolar 0.15.
Channels count 5; step 1.4; maxlen ~ from rim r=14 to shore r≈115 → ~75-95 samples.
Carve radius: half width at s + 1.5; depth 1.0.
heat: falloff radius width+3 → value up to 1 near centerline, stored.
Ocean size 1600, segs 96 (vertex waves).
Sky radius 1200.
Fog: FogExp2(0x1c1220, 0.0011) → at 300m exp(-0.33*?) FogExp2: factor=exp(-(ddensity)^2)? THREE FogExp2: exp(-density² * d²)? It's exp(-(densityd)^2). d=300, density .0011 → (0.33)² → exp(-0.109)=0.90 — light. d=600 → (0.66)² → 0.65. ok.
Tone mapping ACES exposure 1.1.
Bloom: strength 0.85, radius 0.55, threshold 0.62.
Now write out everything. I also need
getH(x,z)analytic &gridH(x,z)bilinear carved lookups; bombs use gridH (carved, more accurate). gridH needs clamping to bounds.Tree placement: attempts 2000: random in disc r<108; h=gridH; accept if h in [2.5, 24], slope (finite diff grid) < 0.5, channelDist > 6 (use precomputed channel polylines: distance point-to-polyline coarse via samples array lookup — channels stored as array of Vector3 samples; distance via scanning samples every 3rd point — 5 channels × ~90/3=30 pts → 150 dist checks per attempt ×2000 = 300k ops fine).
- Also avoid hut location. Place hut: pick point at r≈0.8R... find location: h in [3,6], slope small, far from channels (>25). Search attempts. Hut: group: base cylinder (r2.2,h2), dome sphere half, antenna, beacon sphere emissive red pulsing; small solar panel? Keep: base+dome+beacon+antenna.
Cinematic cam: az = t0.05; radius 230+40 sin(t0.023); height 70+25 sin(t*0.017+2); target (0,22,0). Lerp camera each frame when active; controls disabled in that mode? Keep controls enabled but overwrite → conflict. I'll set controls.enabled=false during cinematic; any pointerdown exits cinematic (button toggles too).
Drumbeat scheduling: nextDrum = now + rand(15,35) * (1.2 - I*0.7).
Auto state durations: DORMANT: 24+rand20; UNREST 10+rand6; ERUPTING 22+rand14; PAROXYSM 8+rand6; WANING 12+rand*8.
Manual ERUPT: if state DORMANT→UNREST(t=3.5); if WANING→ERUPTING; if UNREST→ERUPTING(1.5s). Toast hints.
Channel activation: on entering ERUPTING: reset channels: order shuffled; channel[i].startAt = elapsedErupt + irand(3,6); each channel frontSpeed = 2.5+1.2rand + 1.5I. During PAROXYSM speed1.8. On WANING: fronts halt (uLive → 0.4 cooling), on DORMANT long: uLive→0.06 (dark crust).
Also lake level/glow: uLake = 0.25 + 0.75*I with pulsation.
Crater point light intensity = 30 + 220I (distance 120 decay 1.8). Front light: position at furthest active front among flowing channels; intensity 60I.
Volcanic lightning chance per frame: dt * (0.05 + I*0.5) when plume dense (I>0.3).
Bomb rate: spawns/sec = I>0.45 ? (I-0.4)*22 : 0 → at I=1: ~13/s. Paroxysm burst extra.
Plume emission: rate = 8 + 60I per sec; particle life 6-14s; vy = 6+10I + rand*4; wind push.
Steam at entries: when entry.active: rate 12/s.
Seismo tremor: value = 0.06 + I0.5 + drum1.2 + bomb impact blips (add blip on each bomb land: +0.3 decaying). RSAM = value*1000.
Camera shake amp = 0.015 + I0.12 + drum0.5 + paroxysm extra.
OK also "VEI" gauge: vei = clamp(floor(I*5.4),0,5); label colors green→red.
HUD ids: btnErupt, btnCalm, btnCam, btnQuality, btnSound, btnSnap; stateLabel, veiLabel, veiFill, intFill? show: STATE, VEI, PLUME, BOMBS, WIND. seismo canvas id seismo. toast, banner (big red flash on paroxysm: "PAROXYSM").
Right, also help overlay (H): panel listing keys toggled.
Let me also double-check OrbitControls import path: 'three/addons/controls/OrbitControls.js', postprocessing: 'three/addons/postprocessing/EffectComposer.js', RenderPass.js, UnrealBloomPass.js, ShaderPass.js, OutputPass.js — all exist in r160. OutputPass handles tone mapping/sRGB when using composer (r160: yes, use OutputPass at end).
With composer, renderer.toneMapping applied in OutputPass. Set renderer.toneMapping = ACESFilmic; outputColorSpace default sRGB. Good.
One caution: UnrealBloomPass in r160 works with WebGL2 fine.
GLSL fog chunks in ShaderMaterial: need
#include <fog_pars_vertex>etc and material.fog=true + uniforms merged with UniformsLib.fog. I'll addfog: trueand merge uniforms: THREE.UniformsUtils.merge? Since I build uniform objects literally, add...THREE.UniformsLib.fog? UniformsLib.fog = { fogDensity, fogNear, fogFar, fogColor }. Add those keys into my uniforms object and set material.fog=true; renderer updates fog uniforms automatically for materials with fog=true. Yes — WebGLRenderer refreshes fog uniforms if material.fog == true. So include chunks + add UniformsLib.fog entries.In vertex shader:
fog_vertex uses
mvPosition. For instanced billboards I compute world pos manually: mvPosition = viewMatrix * vec4(wp,1). modelMatrix = identity since mesh at origin — using viewMatrix fine (fog chunk only needs mvPosition).Sky dome: exclude fog (far beyond). Set fog false.
Depth: dome render depthWrite false, radius 1200 < far 3000.
Camera far 4000.
Now, honestly, the biggest risk is typos in such a long file. After writing, I'll extract JS and run
node --check. GLSL can't be checked easily — I'll write a quick paren/brace balance check and manually re-read risky parts. Also verify all DOM ids referenced exist.Let me write the file now. I'll compose it in 6 chunks. Begin.
Chunk 1: HTML head + CSS + body HUD markup + importmap + start of module script (imports, utils, config).
Palette CSS: bg #0a070d; accent ember #ffa14d; hot #ffd25c; violet #b49aff; text #e8ddd2.
CSS panels: similar glass style. Seismo canvas 232x86.
Splash: full overlay, gradient bg, title huge with ember glow, features list, begin button, "auto demo" note.
Chunk plan for JS sizes — go.
Also remember: description meta, title: "Caldera — Volcanic Eruption Simulator". Folder: /space/demos/053-caldera/.
Let me think about the ribbon width function: w(s) = 2.0 + 3.5*(s/len) but cap start: also w *= smoothstep(0,6,s)*0.8+0.2? At vent start narrower 1.6. fine.
Ribbon verts: n+1 samples → 2*(n+1) verts; indices: for i in 0..n-1: quad (2i,2i+1,2i+2),(2i+1,2i+3,2i+2).
aS per vertex = s_i; aSide ±1.
Ribbon y: gridH(x,z)+0.22.
One more detail: ribbons should follow carved channel (depressed) — gridH includes carve — good.
Lava lake Y: craterFloor+... lake sits in bowl: y = craterFloorH + 1.2 where craterFloorH≈31 → but analytic height at r=0 = mix(base, floorH+n2*1.5, 1) ≈ 31.75 avg. Lake y = 32.6. Slight above. Bowl rim ~ H+rim=46..49. Plume spawns at (0, 34, 0).
Crater light at (0, 38, 0).
Vent world for haze projection: (0, 36, 0).
Bombs spawn vel: speed 30-68, angle from vertical 8-30°, az random. From (±2 rand, 34, ±2 rand). gravity 9.8*3? To make arcs look right over island scale (peak 46, island radius 120): with v=50 up-ish, h = v²/2g ≈ 125m at g=10 — too high? Angled 20° from vertical: vy≈47 → apex 34+110=144. That's quite high but volcanic bombs do go hundreds of meters; visually fine & dramatic. g=14 to settle faster? Keep g=12. Land on slopes within r<160. OK.
Bomb life max 12s; kill below y<-5 (into sea → splash hiss if within view? just steam puff + remove).
Sparks gravity 9, life 0.4-1.2, size 0.5-1.4 world → gl_PointSize scale.
Trees count: 240 high tier, 150 med, 90 low? Trees cheap — fixed 260.
Plume cap: 260/170/100. Sparks cap: 3500/2200/1200 (drawRange). Bombs 64 fixed.
OK. Also add shooting "ash fall" — dark specks raining downwind when erupting? Could reuse sparks with dark color; skip — enough.
Ash darkening of sun light with I: sun color uniform scale (1-0.5I) — terrain shader uniform uSunCol updated; hemisphere light intensity JS-side for trees: hemi.intensity = 0.55*(1-0.35*I).
Moon: keep static.
Alright — also include a "lava bomb cam"? No, scope control.
Auto-cycle note: first eruption auto-starts ~8s after begin (so user sees action): set state DORMANT with timer 8.
Let me write the code now. I'll be methodical. Estimated final size ~75KB.
Chunk 2: core setup + heightfield/channels/terrain.
Note about merge for trunk+crown: two InstancedMeshes. Trunk: CylinderGeometry(0.18, 0.3, 3.4, 5) translate y 1.7. Crown: IcosahedronGeometry(1.5, 0) scaled (1,0.55,1) translate y 3.6 — "palm-ish" blobs; two-tone via instanceColor variance. Fine stylistically (low-poly dusk).
For burning: crown.setColorAt(i, lerp toward black); needs instanceColor.needsUpdate. Initialize colors via setColorAt after creation.
Bombs InstancedMesh with MeshBasicMaterial({color: white})? Per-instance color → material must have vertexColors? For InstancedMesh.instanceColor, built-in materials auto-define USE_INSTANCING_COLOR when instanceColor present; MeshBasicMaterial color multiplied. Set material color white, per-instance color hot→dark. Basic unlit = glowing look + bloom.
Dummy Object3D for matrix compose.
Bolt line: BufferGeometry().setFromPoints.
Now the audio engine — write carefully with try/catch.
Seismo draw: bg #0d0a0f, grid lines every 20px rgba(255,160,80,0.08), trace #ffb14d? Classic green? Use ember orange to match palette: #ffa14d with shadowBlur 6 same color.
Wind display: dir name from angle.
Let me now also handle "prefers-reduced-motion"? skip.
Write chunk files. I'll do 5 appends then verify: node --check on extracted script, id cross-check, GLSL brace balance.
One more consideration on
import— must ensure the module script tag is AFTER importmap. Yes.Let me write Chunk 1 now.
Also, minor: use
const rand = (a=1,b)=> b===undefined? Math.random()*a : a+Math.random()*(b-a);etc.For CPU noise:
height(x,z):
smooth(a,b,x): standard smoothstep clamp((x-a)/(b-a)) — works with b<a (reversed) giving 1→0. I'll implement generic:
const smooth=(a,b,x)=>{const t=Math.min(1,Math.max(0,(x-a)/(b-a)));return t*t*(3-2*t);}.Check crater: at r=0: t = smooth(17.55→5.85 reversed, x=0) → (0-17.55)/(5.85-17.55)=1.5 → clamp 1 → t=1 → h=floor≈31. At r=CR=13: t=(13-17.55)/(-11.7)=0.389→smooth≈0.35 → h=lerp(cone≈Hpow(1-13/120,1.5)=460.84=38.6+rim(exp(-((13-14.56)/4.2)^2)=exp(-0.138)=0.87*3.2=2.8, 31, 0.35)=~36. Hmm rim at r=14.56: cone=38.3+3.2=41.5, t≈0.26 → lerp→ 38.8. Peak rim ~41.5 around r≈14.5. OK decent cone-crater profile: rim 41, floor 31. Bowl depth 10 — good.
Shore: h=0 → pow(1-r/R,1.5)=? 0 crossing only at r=R minus fbm... roughly r≈112-120.
gridH: heights array (S+1)² where S=256; spacing=size/S=340/256=1.328.
Carve: for each channel sample idx k, world (x,z), w = width(k)+1.8; grid radius gr=ceil(w/spacing)+1; loop cells: d=dist to sample point (approx; using sample points every ~1.4m fine); dep = (1-d/w)*1.1 clamp; heights[idx]-= dep; heat[idx]=max(heat, (1-d/(w+2)) clamp).
Then recompute normals: build BufferGeometry plane manually with positions from heights → computeVertexNormals.
Terrain geometry: PlaneGeometry(size,size,S,S) rotated? Easier: create PlaneGeometry, rotateX(-PI/2), then set y per vertex from heights array — vertex ordering of PlaneGeometry after rotate: positions row-major (x fastest) — I'll just compute heights in same loop ordering: for iy in 0..S: for ix in 0..S: x=-size/2+ixsp; z=-size/2+iysp... PlaneGeometry builds grid with iy rows from top (y=+h/2 downward) — after rotateX(-90°), original y becomes -z? To avoid mismatch, I'll construct BufferGeometry manually (positions, uv, index) — full control, no surprises. 257² = 66049 verts, index (256²)*6 = 393216 — Uint32 index needed (>65535). Fine.
Now ocean waves in vertex: 3 gerstner-ish sine displacements; but normals via fragment noise — silhouette ok.
Ocean foam needs shore radius per angle... I'll compute uShoreR average by sampling 72 angles; band width generous with noise → looks natural.
Let me also give ocean subtle large normal from the Gerstner derivative analytically — combine: n = normalize(vec3(dHdx from waves + noiseDx, 1, dHdz)). Implement in fragment: recompute wave height function at fragment xz (same formula) with analytic derivatives — consistent. I'll write waveH(vec2 p, float t) returning vec3 (h, dhdx, dhdz)? In GLSL write function returning float h and compute derivative numerically (2 extra evals) — fine.
Moon direction: normalize(vec3(-0.55, 0.38, -0.74))? Place moon opposite sunset. Sunset dir: normalize(vec3(0.9, 0.08, 0.35)) — warm glow on +x horizon.
Sky sun glow: pow(max(dot(dir, sunDir),0), 8) warm.
Water reflection color: use skyGradient(dir) function duplicated in ocean shader (inline simplified gradient).
OK writing now. Also add
uAshDimto sky (dims stars/sun glow when paroxysm).For the toast queue: simple function toast(msg, ms=2600) sets text, class show, timeout hide (single slot fine).
Banner for PAROXYSM: big center text blink 3s.
Also "EVENT LOG"? skip — keep HUD lean.
Now chunk by chunk. Let me draft each fully in my head/notes as I write the actual heredocs. Given length constraints of each tool call, I'll split into ~6 appends of ~12-16KB each.
Also note: avoid
</script>sequence inside JS strings — none planned. Avoid heredoc delimiter collisions — usePART1_EOFetc.Let me write Part 1 (HTML/CSS/HUD/importmap/open script + utils + config + renderer/scene/camera/controls/lights).
For quality: const QT = { high:{pr:2, plume:250, sparks:3400, bloom:true}, med:{pr:1.5, plume:170, sparks:2200, bloom:true}, low:{pr:1, plume:110, sparks:1300, bloom:false} }.
Default tier: choose by devicePixelRatio & mobile UA → med on mobile else high.
fps auto-drop: track avg frame dt; every 2.5s if fps<42 and tier can drop → drop + toast.
Now — writing.
Part 2: heightfield + channels + terrain mesh + ribbons + lake. Part 3: ocean + sky + plume + steam + sparks. Part 4: bombs + lightning + trees + hut + lights dynamics. Part 5: state machine + seismo + audio. Part 6: post + UI + loop + resize + snapshot + close tags.
Also remember meta viewport user-scalable=no.
One risk: In ribbon shader discard for s>uFront — when channel inactive uFront=-1 → all discarded. Good. But transparent sorting with terrain: ribbons opaque-ish with alpha edge — set transparent:true, depthWrite:true? transparent + depthWrite true can produce artifacts with plume smoke behind... acceptable. Actually set depthWrite false for ribbons? Then smoke behind ribbon draws over? Ribbon is on ground; smoke above. Keep depthWrite true.
Ocean transparent? Opaque with alpha 1 — but want slight see-through near shore? Keep opaque (dusk, dark). depthWrite true.
Steam/smoke draw order: render after terrain: set renderOrder: terrain 0, ribbons 1, ocean 2 (opaque large), smoke 10, sparks 11(additive), bombs 3. Transparent objects sorted by distance anyway; renderOrder helps.
Ocean is opaque → renderOrder irrelevant among opaque. Ribbons transparent:true but nearly opaque... If ribbons transparent, they render in transparent pass after opaque — fine before smoke (renderOrder 1 < 10).
OK, write.
Actually one more design point — the eruption "phase progress" for channel activation: I'll implement channels' activation via intensity thresholds + small per-channel delay timers managed in state machine update:
channel.state: 0 idle, 1 flowing, 2 full, 3 cooling channel.front (arc len), channel.len, channel.delay (time to start after eruption begins) update(dt): if eruptPhase active (ERUPTING/PAROXYSM) and tErupt > delay and state==0 → state=1, toast "NEW LAVA FLOW" first time only. if state==1: front += speeddt(state==PAROX?1.7:1); if front>=len: front=len, state=2; if reachesShore → activate entry (steam, oceanGlow[i]=1, toast "LAVA REACHES THE SEA", audio hiss loop? just repeated hisses). uLive per channel: flowing/full: target 0.55+0.45*I; WANING/DORMANT: decays to 0.05 slowly (crust).
Ocean entries array: 4 slots; assign channel end if shoreReach. uEntries vec4[i] = (x, z, radius 6, intensity). Some channels may end inland (if gradient stalls) — then no entry; ensure at least 2 reach shore: gradient descent from rim almost always descends to sea since cone monotonic-ish; add steering: blend -grad with outward radial dir (0.3) to guarantee progress. Fine.
Front light position: from channel with max front among state 1/2: pos = sample at front (interpolated) + (0,1.5,0).
Sample lookup helper: channel.pointAt(s) → Vector3 lerp.
Trees burning: precompute per tree: chanIdx, sClose, latDist for the closest channel only? A tree might be near 2 channels; store for all channels: arrays treeBurn = [{c, s}] list (only where latDist < w+4). Each frame: for each tree not burned: for each ref: if channel front > s → ignite: burned=true, crown color → animate burnT 0→1 (2s): color mix green→#0a0a0a; emit 6 embers over first second; +1 blip sound? add crackle: skip audio.
Per frame loop 260 trees × few refs — trivial.
ignite also when bomb lands within 3m? Nice: bombs ignite too: on bomb explode, check trees within 4m → ignite.
Embers: reuse sparks with orange color upward drift.
Now VEI mapping: I<0.1→0; <0.28→1; <0.45→2; <0.65→3; <0.85→4; else 5. During PAROXYSM show 4-5.
Plume height readout: 200 + I3800 + noise120 → meters, formatted.
Also show "STATUS" line list of active events? skip.
Write the code!
For the sky shader stars:
Standard: project dir to cube-ish: use vec2 sp = dir.xz/(dir.y+0.4) * 40 → cells hash → point star if fract within radius. Simpler: 3D cell hash: id = floor(d80); h=hash(id); star if h>0.997; brightness twinkle by sin(t3+h*40). Cell boundaries on sphere distort — acceptable at night dusk few stars near zenith.
Moon: dot(dir, moonDir) > cos(0.035 rad) → disc; glow pow.
Sunset glow: sunDir low: g = pow(max(dot(dir,sunDir),0.),6.) * vec3(1.,.45,.2) * (horizon factor (1-|dir.y|)).
Also horizon haze band: mix toward fog color near y≈0.
Ash layer: uAsh dims upper sky slightly + brown tint toward vent (vent at center—dome centered on camera? Dome at scene center; vent dir from camera varies; skip directional ash, just global dim).
Clouds: faint fbm band near horizon scrolling — cheap: c = fbm2(dir.xz/(0.25+dir.y)1.5 + t0.01) * horizonMask * 0.12 → add dark mauve.
Right. Time to write files. I'll now produce the parts.
Additionally, keep an eye on:
#include <fog_pars_fragment>requiresvFogDepthvarying declared by pars; andfog_fragmentuses fogColor etc uniforms — add THREE.UniformsLib.fog to uniforms. And material.fog = true. Chunks must be inside main() appropriately: vertex:#include <fog_vertex>at end (after mvPosition defined). In fog_vertex chunk r160:vFogDepth = - mvPosition.z;guarded by#ifdef USE_FOG. USE_FOG define is set automatically when material.fog && scene.fog. Yes renderer sets it.Alright. Also the lake/ribbon/ocean/plume shaders all need fog? Ribbons near ground — fog consistent look: add fog to terrain, ribbons, lake, ocean, plume, steam. Sparks additive — fog would dim oddly; skip (they're small/bright). Bombs basic material — scene.fog applies automatically (fog: true default in basic material? Material.fog default true — yes). Trees lambert fog default true.
For sky: fog false.
Let me get to writing. I'll create the folder first.
- Created activity call execute_shell_command
- Created
******************************************************************************************************************************************ke.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Created
*********************************************************************************************************************************************************************************************************ky.
- Commented
***************************************************************************************************me.
- Created activity call execute_shell_command
- Created
********************************************************************ng.
- Commented
***********************************************************************************ng.
- Created activity call execute_shell_command
- Created
Part 5: trees + hut, eruption state machine, seismograph, audio engine. Note I reference
audioandseismoBlipandigniteTreesNearbefore definition — that's fine since they're called at runtime after definitions (hoisted function declarations for seismoBlip/igniteTreesNear, and audio as const defined later — runtime call only, ok as long as no call before initialization during module evaluation; calls happen inside update loop started later).Careful:
audio.ready()guard used. Defineconst audio = new AudioEngine()in part 5 — the explodeBomb references audio — fine at runtime.Trees part now.
- Commented
********************************************************************************************ne.
- Created activity call execute_shell_command
- Created
Part 6: post-processing, particles update (plume spawn/update integrators), UI wiring, HUD update, main loop, resize, snapshot, close.
Plume spawn logic: emission accumulator; particle fields: x,y,z,vx,vy,vz,age,life,size,alpha,shade,rot,seed,grow,fadeIn,fadeOut,aMax,swirl. Integrator for plume:
Spawn at vent with small radius; vy initial 6+12I; shade starts 0 (hot) → 1.
Steam integrator: rise slower, drift with wind, expand, fade.
Bombs update: integrate, trail sparks, collide ground via gridH, explode.
HUD update fn: state label, colors, VEI, plume height, bombs active count, flows count, wind compass, intensity bar, alert level, uptime.
Camera shake + cinematic mode.
Post: composer + bloom + grade pass with heat haze + vignette + grain. uVentUv projected.
UI: buttons + keys + splash begin. Toast/banner helpers. Quality switch: recreate smoke systems? Simpler: set plume.max allowed count = TIER.plume (SmokeSystem.spawn checks parts.length<this.max — max fixed at construction). I constructed with TIER.plume already; on tier change just adjust
plume.max=TIER.plume(buffer sized for max tier 250 — construct buffers at 250 always, use .max as cap). I constructednew SmokeSystem(TIER.plume)— buffers sized to initial tier. If user switches to high from low, buffers too small. Fix: construct with max 250 always; set plume.max dynamically. SmokeSystem constructor takes max for buffers AND uses this.max as cap. So construct plume with 250 and steam 120, then set plume.max=TIER.plume. Let me patch: in part 4 I wroteconst plume=new SmokeSystem(TIER.plume);— buffers TIER.plume. I'll override in part 6: rebuild buffers? Easier: patch part 4 line via replace tonew SmokeSystem(250). Thenplume.max=TIER.plumeinitially. Sparks pool similar: constructed TIER.sparks; buffer fixed; on tier change set sparks.max=min(TIER.sparks, bufferMax)... construct SparkPool(3400) and set .max dynamic. Patch part4:let sparks=new SparkPool(TIER.sparks)→new SparkPool(3400)then set max.applyTier(): renderer.setPixelRatio, bloomPass.enabled, plume.max, sparks.max.
Main loop: clock dt clamp 0.05; U.uTime advance; updateVolcano; spawn plume/bombs/lightning by rates; update systems; camera shake; controls.update; composer.render; camera basis uniforms update (uCamRight/uCamUp from camera.matrixWorld columns).
fps counter + auto quality drop.
Resize: camera, renderer, composer, bloom resolution.
Snapshot: toDataURL → a.download.
Banner/toast helpers with timers.
Also wind drift: angle slowly changes: windAng += dt0.01noise → U.uWind set; wind display compass.
Bomb spawn rate: acc += dt*rate; rate = state ERUPTING: (I-0.35)*16; PAROXYSM extra burst on entry: 12 instant.
Lightning: chance dt*(I>0.3? (I*0.7):0.02).
Plume emission rate: 6+55I; particle life 7-14; also when dormant small wisp (rate 60.05...). rate formula covers: at I=0.05 → ~8.7/s small.
Bomb trail spark spawn each 40ms per bomb.
Write part 6 now. Also remember begin splash: audio.init(); hide splash; start loop (loop runs from load anyway; begin only enables audio+controls? Keep loop running behind splash for beauty shot; begin hides overlay + audio.init + toast).
Keyboard mapping: Space erupt (triggerEruption), X calm, C camera, Q quality, M sound, P snapshot, H help.
triggerEruption(): if DORMANT→setState('UNREST'), tState=3; else if WANING→setState('ERUPTING'); else if UNREST→setState('ERUPTING'). calm(): → WANING with tState 5.
Cinematic: mode 'orbit'|'cine'; cine: t+=dt; az=t0.045; rad=235+45sin(t0.021); h=85+30sin(t*0.016+2); camera.position.lerp(target positions); lookAt(0,24,0). On user pointerdown on canvas while cine → switch to orbit. controls.enabled = (mode==='orbit').
Uptime counter from begin.
Also
window.__beaconbeacon pulse in loop: beaconMat.color setHSL? beacon pulse: scale via material color intensity: c = 0.3+0.7pow(sin(t2.4)*0.5+0.5, 3). Use color.setRGB(r,0.1r,0.05r).Also update HUD elements each ~0.15s.
Let me write it.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Created
81% time used. I must validate quickly and update the index page. Priorities:
- node --check the extracted module script (fast).
- Fix the SmokeSystem buffer size issue (plume constructed with TIER.plume but applyTier sets plume.max up to 250 — patch constructor calls).
- Update index.html demos page.
- Success.
Fast validation now.
- Commented
*********************************************************st.
- Created activity call execute_shell_command
- Created
Two fixes:
-((r-CR*1.12)/4.2)**2→-(((r-CR*1.12)/4.2)**2).- SmokeSystem buffers: plume constructed with TIER.plume but applyTier sets max to 250 → if initial tier is med (mobile), buffer is 170 and max set to 250 overflows attribute arrays → crash. Fix: construct plume with 250 and sparks with 3400.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Created
90% time. Must finish now. Remaining critical: update index.html demos page with the new card. Quick sanity checks I wanted (logic sim) will be skipped — syntax is OK, GLSL balanced (no mismatches reported), IDs all present. One more quick check: the SmokeSystem fragment shader uses uniform uHot declared — I set mat.uniforms.uHot={value:1} and declared
uniform float uHot;— yes. Ocean uniform uEntry array set — yes.One risk: in
el.windcomputation:Math.round((-wa)/(Math.PI/4))&7— wa from atan2 of wind vec; fine.Ribbon fragment: uses uFront/uLive per channel material — set.
Bolt material clone per bolt — ok.
SparkPool geometry lacks 'uv' — Points don't need. OK.
Update demos index page now, then _success.
- Commented
******************************************************ge.
- Created activity call execute_shell_command
- Created
Index updated (53 demo cards + blueprint card = 58 total matches... 53 cards + the blueprint card class also contains "demo-card"? No — blueprint-card. grep counted 58 because... each demo-card anchor = 1 occurrence plus "demo-list" no. Actually 53 demos + maybe class appears in CSS once + blueprint? Whatever — the card count: previously 52 demos, now 53; grep counts occurrences of string 'demo-card' including CSS
.demo-cardselectors (several). Fine.Quick final sanity: verify the channel entry slots reset between eruptions — entrySlots persist once activated (ocean glow stays), which is acceptable visually (cooled entries could stay glowing...). Minor: during WANING/DORMANT the ocean entry glow stays at intensity 1 forever. Quick fix: decay entry intensity with channel live. One-line patch in updateVolcano? Time is at 90%+. It's a small cosmetic issue — but cheap to fix: in the channel loop, if ch.entrySlot!==undefined, set oceanMat uniform intensity = ch.live. Let me patch via replace_in_file.
- Created activity call replace_in_file_in_shell_environment
- Created
**************************************************************************************ck.
- Created activity call execute_shell_command
- Created
All done:
- Created /space/demos/053-caldera/index.html (74KB) — Volcanic Eruption Simulator with procedural island, lava channels, crater lake, ash plume, bombs, lightning, seismograph, generative audio, post-processing, quality tiers, keyboard/touch controls.
- JS syntax verified via node --check; GLSL braces balanced; DOM ids verified.
- Updated /space/demos/index.html with the new demo card (53 demos).
Task complete. Call _success.
- Created activity call _success
- Created activity call _checkTaskRunStatus
Other Conversations
Explore other conversations with this agent.