Cloudsea Demo Creation
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
The task is "Wake up and do as you please." Based on my purpose, I should create a cool Three.js demo in
/space/demos/. The last demo created was "Terraforge" (021-terraforge). So I should create a new demo, e.g., 022-something.Let me first look at the current state of the demos folder to see what exists, then create a new impressive demo.
Let me install shell tools first.
- Created activity call execute_shell_command
- Created
The demos folder has 21 demos. The task is "Wake up and do as you please." So I should create a new demo (022) and update the index page.
Let me look at the index.html to see how demos are listed, and check a recent demo to understand the style. Then I'll create demo 022.
Ideas for a new demo (should be distinct from existing ones):
- 001 spinning cube
- 002 nebula galaxy (particles)
- 003 mandelbulb explorer (ray marching fractal)
- 004 fluid dynamics (GPGPU fluid)
- 005 gravity sandbox (n-body)
- 006 event horizon (black hole)
- 007 sonic reactor (audio visualizer)
- 008 void runner (game)
- 009 azure odyssey (?)
- 010 abyssal bloom (underwater)
- 011 physarum garden (slime mold)
- 012 powder alchemy (falling sand)
- 013 clockwork nebula (?)
- 014 neon rain (?)
- 015 aurora borealis
- 016 skitter (creatures?)
- 017 coral resonance
- 018 threshold
- 019 attractorium (strange attractors)
- 020 murmuration (boids flocking)
- 021 terraforge (erosion sim)
New idea options:
- Cloth/soft-body simulation — a GPGPU cloth sim with wind, interactive tearing.
- Volumetric clouds — ray-marched volumetric cloudscape with weather controls, fly-through.
- City generator / cyberpunk city flythrough — procedural city with neon lights, bloom.
- Water caustics pool — realistic water surface with caustics.
- Orbital mechanics / rocket sim.
- L-system forest / procedural tree growth.
- Ray-traced scene with spheres.
- GPGPU cloth + verlet "flag/curtain" interactive.
- Space station builder.
- Reaction-diffusion 3D — Gray-Scott on a surface/volume.
- Wave interference / ripple tank — interactive ripple tank with obstacles, wave optics visualization (double-slit, interference, refraction). That's a nice physics-viz demo: a GPU ripple tank where you can place barriers, slits, sources; visualize interference patterns with beautiful rendering. Could be called "wave garden" or "ripple lab".
- Knot/torus geometry playground.
- Volumetric nebula flight.
- Delaunay/Voronoi shatter.
- Electromagnetic field visualizer — field lines of moving charges.
A ray-marched volumetric cloudscape with fly-through and weather controls would be visually stunning and distinct from all previous demos. Alternatively, the interactive ripple tank wave-optics lab is more interactive/educational and quite unique.
Let me think about what would be most "wow" and reliably working within a single HTML file:
Option A: "Cumulus" — volumetric cloud playground. Ray-marched volumetric clouds using 3D noise (generated procedurally in-shader or via a 3D texture generated at startup with Worley/Perlin noise). Fly through clouds, sun position control, cloud coverage/density/precipitation controls, time of day, storm mode with lightning. Post: bloom + vignette. This is a classic impressive demo. Performance considerations: ray marching volumetrics in a fullscreen pass at reduced resolution with upscaling.
Option B: "Ripple Tank" — wave optics lab. GPGPU wave equation simulation on a 2D grid (ping-pong), with interactive sources, barriers you can draw, double-slit presets, lenses (refraction via medium index), beautiful rendering of wave height with refraction/specular/foam. Educational + pretty + interactive. Rendering: a displaced mesh with nice shading, caustic-ish patterns.
Option C: "Neon Metropolis" — procedural cyberpunk city flythrough. Instanced buildings with emissive windows, fog, bloom, street lights, moving cars (light trails), rain optional, fly camera or auto-tour. This has strong wow factor and is quite different from previous demos (014-neon-rain exists though — need to check what it is; neon rain might be similar vibes). Let me check what 014 and 013 are.
Let me check quickly the titles of a few recent demos to avoid duplication.
- Commented
Now let me check the existing demo titles/descriptions in the index to pick something distinct:
- Created activity call execute_shell_command
- Created
I have the full list of 21 demos. Now I need to pick something distinct. Existing:
- Particles galaxy, fractal raymarch, fluid, n-body, black hole raymarch, audio viz, runner game, sailing, deep sea boids, slime mold, falling sand, orrery, cyberpunk city rain, aurora, creatures, cymatics, portals, attractors, boids birds, erosion terrain.
Not yet done:
- Volumetric clouds raymarch → "Cloudsea" / "Nimbus" fly-through. Aurora used ray-marched volumetric ribbons in the sky, but a full volumetric cloudscape is distinct.
- Cloth simulation (verlet GPU cloth, tearing, wind) — not done. "Cloth playground" with flags/curtains tearing in wind, collisions with spheres.
- Ripple tank / wave optics lab — not done. Interference/diffraction visualization is educational and beautiful.
- Reaction diffusion 3D.
- Procedural tree growth / L-systems.
- Cloth + water combo.
I think a volumetric cloudscape is a strong "wow" demo: "Stratos / Cloudpiercer / Silver Lining" — ray-marched volumetric clouds with weather presets (fair weather cumulus → storm front), fly-through camera modes, sun time-of-day, lightning flashes in storm mode, god rays, bloom, WebAudio wind. Distinct from aurora (which was ribbons + snow terrain).
Alternatively the ripple tank wave optics lab is very interactive and unique: GPGPU wave equation, draw walls, double slit, lens, sources, beautiful rendering with caustics. It's educational physics-viz "wow".
Both are great. Let me pick the volumetric clouds one — "Cloudsea" — because pure visual wow. But performance/reliability of ray-marched volumetrics in one file can be tricky; still, doable with careful budgets, half-res rendering, blue-noise dithering. I've got plenty of time budget presumably.
Actually, let me reconsider: the ripple tank might be more "interactive experience" and less likely to have perf problems, and it looks gorgeous with proper shading (caustics, refraction). But clouds are the classic jaw-dropper.
Hmm, one more option: cloth simulation with tearing — GPU verlet cloth, flags, wind, mouse interaction, collision with a rotating torus or sphere; visually: silk with iridescent shading + bloom. Nice but less wow than clouds.
Decision: 022 - Cloudsea (volumetric cloud playground). Features:
-
Ray-marched volumetric clouds (FBM + Worley noise via 3D texture generated on CPU at load or procedural in-shader 2D noise combos). For quality, generate a 128³ RGBA 3D texture: R = Perlin-FBM, G/B/A = Worley octaves — standard "Nubis/Horizon Zero Dawn" approach. Generating 128³ worley on CPU in JS is 2M voxels × 32 cells... could take a few seconds. 64³ or 96³ is faster. Alternatively generate tileable 2D noise textures and use them to build height-shaping, plus in-shader hash-based worley (expensive). Standard approach: 128³ texture generated once (maybe ~2-4s in JS) — acceptable with a loading indicator. Or use a 64³ texture which is 262k voxels — fast (~0.5-1s). Let me use 128³ for base FBM channel only computed with cheaper value noise, and 64³-ish worley... Actually simpler: generate one 128³ RGBA texture where:
- R: tileable Perlin-FBM (value noise 4 octaves)
- G, B, A: tileable Worley at increasing frequencies (4, 8, 16 cells)
Value noise 4 octaves at 128³ = 8.4M noise evals; each eval = 8 corner lookups → fine in JS (~1-2s). Worley with 8³ cells over 128³ voxels: each voxel checks 27 neighbor cells → 27 × distance computations × 2M voxels = heavy (~50M ops, JS maybe 3-6s). Could reduce: generate worley at 64³ then it tiles at lower res but that's fine since worley is used for detail. Or compute worley from a precomputed random points-per-cell grid with 27-cell neighborhood; 128³ voxels × 27 cells × ~5 flops... it's ~100M flops, JS can do ~100-300M simple ops/sec → maybe 1-3s. Acceptable with loading screen. I'll generate in chunks with progress bar to avoid jank... but it's at load; just show "forging clouds..." progress.
-
Cloud rendering: ray march in a fullscreen quad pass at half resolution, depth-aware composite against a simple scene: ocean/planet below? Keep scene minimal: a vast ocean below (like azure odyssey? maybe too similar). Maybe just clouds above a sea with sun reflections — or clouds above stylized terrain? To avoid duplicating azure odyssey's ocean, I could do clouds over a soft "sky sea" — just atmosphere gradient below horizon with sun glint strip. Simple: below-horizon = atmospheric scattering gradient + sun glint; that's classic for cloud demos.
-
Features:
- Weather presets: "Fair Skies", "Towering Cumulus", "Storm Front", "Sea of Clouds" (above solid deck), "Alien Dusk".
- Time-of-day slider (sun elevation/azimuth) with gorgeous sunset colors; day/night cycle toggle.
- Camera modes: free orbit / fly-through (auto glide) / drone orbit.
- Controls: coverage, density, wind speed/direction, cloud base height, detail.
- Lightning in storm mode (flash illumination inside clouds + sky flash + thunder audio) — clickable to summon bolt.
- WebAudio: wind ambience tied to altitude/speed, thunder rumble, optional calm pad.
- Post: bloom (subtle), vignette + grain, FXAA.
- Sun with god-ray-ish glow: cheap radial light shafts in post or in-cloud marching gives silver lining naturally.
- Quality autoscaling: render scale adjustments.
- Snapshots, keyboard shortcuts — consistent with previous demos.
Implementation plan:
-
Three.js r160+ with import map (check what previous demos use; use unpkg three@0.160.0 or similar). Let me check an existing demo for CDN patterns to stay consistent.
-
Full-screen pass: render clouds into a half-res WebGLRenderTarget via a custom Scene with a fullscreen triangle and ShaderMaterial; then composite to screen with a composite pass (depth-aware upsample not needed if scene is just clouds + procedural sky in the same shader — I can render everything in the one raymarch shader: sky gradient, sun, clouds; then post-process with EffectComposer for bloom etc. Actually simpler: render the raymarch shader directly to screen inside EffectComposer's RenderPass? EffectComposer works with scenes; I can make a scene with a fullscreen quad using the cloud material, rendered by an ortho camera, then add UnrealBloomPass + custom grade pass + FXAA. The composer renders at full res; to do half-res raymarch, I'd render the cloud scene to an offscreen target at 0.5-0.7 scale, then a composite quad copies it. With composer: RenderPass(cloudScene→ target at reduced res), then a ShaderPass that samples that texture... EffectComposer passes render to screen-sized buffers; I can set the first pass's render target smaller via
composer.setSize? No — simplest robust approach:cloudTarget = new THREE.WebGLRenderTarget(w*q, h*q)- Each frame: renderer.setRenderTarget(cloudTarget); renderer.render(cloudScene, orthoCam);
- Then a composite scene with a fullscreen quad sampling cloudTarget.texture (linear filter), rendered through EffectComposer (RenderPass(compositeScene) + bloom + grade + FXAA... FXAA on the composite).
That works fine.
-
Raymarch shader essentials:
- Camera rays from uniforms (invProjMatrix, invViewMatrix or computed basis vectors) — compute in JS per frame, pass camera pos + frustum corner rays.
- Cloud volume: axis-aligned slab between base and top heights (e.g., y in [1500, 9000] m scaled down). Ray-slab intersection for march bounds.
- Density sampling: sample base FBM from 3D texture at pos*scale + wind offset; height-gradient shaping (coverage), detail worley erosion (edge wispiness) with 2-3 levels.
- Lighting: march toward sun ~6-8 steps for transmittance (Beer's law), powder sugar effect, Henyey-Greenstein phase (double-lobe), ambient sky contribution.
- Marching: ~64-96 steps max with adaptive step (larger steps in empty space), blue-noise offset to hide banding, early exit on transmittance < 0.01.
- Blue noise: generate a small 64×64 RGBA blue-ish noise texture on CPU, or use interleaved gradient noise in-shader (IGN) — cheap and good.
- Storm lightning: a uniform
uLightning(position + intensity) that adds in-scatter light toward the bolt point — do a cheap point-light attenuation in the light march or just per-sample point light. Plus sky flash uniform.
-
Terrain/ground: below horizon, render a simple "distant planet haze" gradient + sun glint. Maybe add a subtle procedural sea with specular sun path when looking down? If camera can fly above clouds looking down, seeing a gradient haze is fine. Add optional "sea of clouds" preset.
-
Also draw the sun disc with HDR intensity for bloom, plus sky gradient with Rayleigh-ish approximation: simple analytic gradient (mix of horizon/zenith colors based on ray y) + sun halo (HG phase-ish). Sunset coloring via sun elevation.
-
Weather system: parameters (coverage, density, baseHeight, cloudTop, windDir/speed, stormFactor, detailErosion, ambience colors) interpolated when switching presets.
-
Audio: WebAudio: brown-noise wind through bandpass with LFO; thunder = filtered noise burst with exponential decay + sub rumble; optional pad. Master gain, mute button.
-
UI: panel (glassmorphism) with sliders/buttons; collapsible; consistent with previous demos presumably. Buttons: presets, camera modes, time-of-day slider, coverage/density/wind sliders, storm toggle, lightning button, snapshot, quality indicator, keyboard shortcuts help.
-
Keyboard: 1-5 presets, C camera mode, T tour, L lightning, H hide UI, M mute, P snapshot, Space pause? etc.
Now, potential perf pitfalls: 96 steps × (1 base sample + ~1 detail + 6 light steps) — heavy but at 0.5 res okay on decent GPU; auto quality reduces further. I'll implement step counts as #defines swapped by quality (compile two materials? or uniforms controlling loop with const max). Use uniforms + constant max loop with early break:
for (i<MAX; i++){ if(i>=steps) break; ...}fine in WebGL2 (GLSL ES 3.0). Use RawShaderMaterial or ShaderMaterial with GLSL3? three ShaderMaterial defaults to GLSL1 syntax; loops with dynamic break are fine in ES 1.0 too as long as loop bounds constant. I'll write with constant MAX and breaks.3D texture generation: Data3DTexture in three r160 supports linear filtering of RGBA8? 3D textures with LuminanceFormat etc. RGBA8 3D texture is fine and filterable (no float needed since noise stored as bytes). 128³ RGBA8 = 8.4MB — fine.
CPU generation time:
- Value noise FBM: need tileable. Implement tileable value noise with perm table: for octave o with period p, lattice coords mod p. 4-5 octaves at 128³ → 128³=2.1M voxels × 5 octaves × 8 taps = 84M hash+lerp ops — maybe 2-4s in JS. Hmm. Could do 96³ (884k voxels × 5 × 8 = 35M) ~1-1.5s. Use 96³? Quality slightly lower; linear filtering hides a lot. Many demos use 32³ perlin + worley detail. I'll do 96³ base and generate worley channels at 96³ too with 3 frequencies (4³, 8³, 16³ cells → wait, worley with 16³ cells over 96³ voxels = 6 voxels per cell — ok). Worley: for each voxel, find nearest feature point among 27 neighbor cells: 884k × 27 = 24M distance calcs (~1-2s). Total maybe ~3-5s worst case on slow machines. Acceptable with progress bar; generate in async chunks (per-slice, setTimeout/rAF) updating progress.
Optimization: precompute random feature points arrays per frequency; per voxel iterate 27 cells with cheap hash lookups. Fine.
Cloud shaping math (Horizon Zero Dawn style simplified):
- heightFraction h = (y - base)/(top - base).
- Base cloud: perlinFbm = R channel remap with coverage: d = remap(fbm, 1-coverage, 1, 0, 1); multiply by height gradient (round bottom, fuzzy top: g = smoothstep(0,0.25,h)*(1-smoothstep(0.7,1,h)) etc).
- Detail: worleyFBM = (G+B+A)/3-ish; erode edges: d = remap(d, worleyerosion(1-h dependent), 1, 0, 1).
- Storm factor: taller gradient, darker bottoms (higher density multiplier at bottom, anvil shape via coverage gradient).
Lighting march: 5-6 samples toward sun, transmittance = exp(-densityσdt); powder: powder(d) based on accumulated optical depth along view... standard: powder = 1 - exp(-accumulatedDensityAlongSample). Phase: mix of forward/back HG lobes by sundot.
Ambient: mix(groundColor, skyColor, h) * (0.3 + 0.7*someTopVisibility) — cheap "beer's ambient" using height only.
Sun/sky: analytic. Sky color functions of sunDir: day zenith (0.3,0.5,0.85)-ish, horizon pale; sunset: orange horizon band scaled by pow(max(sun.y,0),k) etc. Night: dark blue + stars? Adding stars: hash-based stars on the ray direction when sun is down — nice touch, cheap (from aurora demo concept). I'll add subtle stars at night.
Sun disc: smoothstep on dot(ray, sunDir) with HDR value 20-60 for bloom; plus glow halo.
Lightning: uniform vec3 uBoltPos; float uBoltIntensity (decays after strike). In cloud lighting: add point light contribution: Li += boltColor * I / (1+d²k) * transmittanceAlongView-ish... cheap: per sample, atten = I * exp(-dist*0.0008) (or inverse square clamped); also HG phase toward bolt direction. Also sky flash: add to sky color based on proximity of ray to bolt direction + general flash. Also illuminate ground haze. Good enough for drama.
Rain shaft under storm clouds? Could add virga via darkening + noise streaks... skip; keep scope.
Camera:
- Orbit mode: OrbitControls targeting a point in the cloud layer; clamp radius.
- Glide/fly mode: camera moves forward through clouds, gentle banking; pointer steers heading slightly? Keep auto-glide with slowly varying heading + user drag adds offset. Simpler: cinematic modes: "Orbit", "Glide" (auto fly-through with sinuous path), "Rise" (vertical from below deck to above). Use OrbitControls for manual; tour mode disables controls.
- Actually simplest robust: OrbitControls always; plus "cinematic tour" toggle that animates camera along a path (and user input breaks out? In previous demos, tour mode exists). I'll do: Orbit / Glide / Tower modes, where Glide/Tower animate camera and lookAt; switching back to Orbit returns control. Pointer drag in glide adjusts target offset slightly. Keep it simple and robust.
Ground: Instead of nothing below, add a very simple atmospheric "earth" — a huge sphere/plane with haze color and sun glint? If the user looks down from above clouds, a soft blue haze is fine. Maybe add subtle procedural sea far below in the fragment shader: plane at y=0 intersected by ray: color = deep sea with fresnel reflection of sky + sun specular streak + subtle noise normal perturb. That's cheap (one plane intersection) and gives the "above the ocean" vibe without duplicating azure odyssey's full ocean. Do it: if ray hits plane y=0 before cloud slab, compute sea shading; clouds composite over it with transmittance.
Also add distant "horizon haze" blend so sea fades into sky near horizon.
Structure of the HTML:
- body: canvas container, UI overlay (title, description, controls panel, buttons, FPS/quality badge, help overlay, loading screen with progress).
- module script:
- imports: THREE, OrbitControls, EffectComposer, RenderPass, ShaderPass, UnrealBloomPass, OutputPass?, FXAA? (previous demos used FXAAPass; check availability in three r160 examples: examples/jsm/shaders/FXAAShader.js exists).
- Noise texture generation (async with progress).
- Cloud shader material (big fragment shader string).
- Composite + composer setup.
- UI wiring, presets, weather lerp.
- Audio engine.
- Main loop with autoscaling.
Let me check how previous demos import three (which version/CDN) to stay consistent. Let me peek at 021 and 015 quickly.
- Commented
***********************************************************************************************************************************************************************************************************************st:
- Created activity call execute_shell_command
- Created
Good — three@0.170.0 via unpkg import map is the convention. Now let me build the Cloudsea demo. This will be a large single file (~1200-1500 lines). Let me write it carefully.
Key components:
-
HTML/CSS UI: loading screen with progress bar; glassmorphism control panel; top-left title; bottom hints; quality badge; buttons.
-
Noise texture generation (async, chunked with progress):
- Tileable value-noise FBM → R channel
- Tileable Worley at 3 frequencies → G, B, A channels
- 96³ Data3DTexture, RGBA8, LinearFilter, RepeatWrapping.
Implementation details for tileable value noise:
FBM: frequencies 2,4,8,16 (periods equal), weights 0.5,0.25,0.125,0.0625, coords = (i/N)*freq.
Worley: grid of feature points: for freq F, points per cell from hash: px = (cx + hash(cx,cy,cz,F))/F etc. For each voxel at position p in [0,F), find min distance to feature points of 27 surrounding cells (with wrap mod F). Distance normalized by ~1/F then invert: worley = 1 - d*F clamped... Standard: worley = clamp(1 - dist / maxDist). I'll compute dist in cell units: d = min over neighbors of length(p - pointPos) where both in [0,F); maxDist ≈ 1.0 (nearest neighbor rarely > 1 cell). worley = 1 - min(d, 1). Frequencies 4, 8, 16.
To speed up: precompute for each frequency a Float32Array of point coords F³×3 (16³ = 4096 points × 3 — fine). For each voxel: cell = floor(p), loop 27 neighbors, compute distance to that cell's point. 96³ = 884,736 voxels; for A channel F=16: 27 distance evals each → 24M; total three frequencies
72M ops… plus FBM ~ 884k × 4 octaves × 8 taps × hash = 28M hash lookups + lerps (another 50M ops). Total maybe 3-6s in JS. Acceptable with progress bar, chunked per z-slice via await new Promise(r=>setTimeout(r)) every few slices.Optimization option: generate at 64³ for worley channels into same texture? No — single texture; just accept 96³. Or reduce to 80³? 96 divides nicely by 4,8,16 — keep 96.
Actually, let me reconsider: use 64³ (262k voxels) → total ~35M ops → ~1-2s. Quality tradeoff: base noise texel size = cloudWorldSize/64... with linear filtering and heavy detail erosion, 64³ is what many production demos use for perlin-worley base (HZD used 128³ packed). I'll do 96³ as compromise; chunk async; progress bar makes wait pleasant. Hmm, slower machines... I'll generate base FBM at 96³ and worley at 96³ but with early acceptance of ~3s. Fine — chunked with progress.
Actually — smarter: I can fill R with PerlinFBM(96³) and G/B/A with worley at frequencies 4/8/16 within the same loop, reusing per-voxel work. One loop, four channels.
- Cloud raymarch shader: fullscreen triangle; uniforms:
Fragment main:
- Reconstruct ray: vDir varying computed from frustum corners in vertex shader (pass ray direction per vertex, normalize in fragment — standard).
- Sky color fn(rd): gradient zenith→horizon by rd.y, sun halo, sun disc (HDR), sunset tint via sun elevation, night stars (hash on direction), lightning flash addition.
- Sea: plane y=0 hit (if rd.y<0): seaColor = mix(deep, skyReflect via fresnel, specular sun streak) with distance haze into horizon color.
- Cloud slab intersect: ray vs planes y=base, y=top → t0,t1 (handle camera inside slab). If valid and t1>max(t0,0): march.
- March: N steps, step size = (t1-t0)/N with slight growth; jitter start by IGN. For each sample:
- density = sampleDensity(pos) (texture fetch + shaping + erosion);
- if d>0: lightTrans = lightMarch(pos); powder; energy = sunColor * phase * lightTrans * powder + ambient;
- accumulate: T = exp(-dσ*dt); col += T * energy * d * σ * dt (standard).
- lightning: add bolt point light into lightMarch or per-sample: Lbolt = boltColor * atten(dist to boltPos) * phaseBolt.
- Composite: col = cloudCol + T * bgColor; fog-ish blend by distance: mix toward sky near far.
sampleDensity(p):
Also add 2D "weather map"-ish variation: sample tNoise at much lower frequency with different weighting to modulate coverage spatially — can reuse same texture at 0.25 scale via second fetch (cheap) or use fbm of position at big scale in-shader. I'll do a second texture fetch at coarse scale modulating coverage locally (creates cloud clusters).
lightMarch(p): 5 steps toward sun, step ~ (slabThickness/8); accumulate d; return exp(-Σd * σ * k) — also add powder from view-accumulated density.
Storm: darker ambient, higher density, bolts. Anvil: for high h, widen: coverage boost near top via gradient shape tweak using uStorm.
Phase: HG g=0.55 forward + g=-0.25 backward mixed, plus boost silver lining: mix phase by powder? Standard: phase = max(HG(g1), HG(g2)) blend... I'll use:
phase = mix(HG(c, -0.2), HG(c, 0.6), 0.7) + HG(c, 0.95)*0.8for strong forward spike (nice sun corona through clouds).- Composite/post: cloud scene → RT at quality scale (0.5-0.75). Composite quad copies with slight sharpening? Just linear sample. Then EffectComposer: RenderPass(compositeScene, orthoCam) → UnrealBloomPass(strength ~0.35, radius 0.6, threshold 1.0) → gradePass (vignette + grain + slight lift) → OutputPass. FXAA unnecessary since raymarch is smooth + grain; skip FXAA (or include cheap). I'll include FXAA? The composite upsampling may alias on hard sun edges — bloom smooths. Skip FXAA for perf; grain hides.
Wait — OutputPass does tone mapping + color space conversion in newer three. If I render HDR into RT (float RT), bloom picks >1 values. Pipeline: cloudRT (HalfFloatType, LinearFilter) → composite RenderPass into composer buffers (also HalfFloat — set composer with HalfFloat render target) → bloom → grade → OutputPass. In three 0.170,
new EffectComposer(renderer)uses HalfFloatType by default? Since r152ish, default renderTarget type is HalfFloat. Yes (since r152 the default is HalfFloatType). Good.Then tone mapping: set renderer.toneMapping = ACESFilmicToneMapping; OutputPass applies it. My shader writes HDR values (sun ~ 30).
Actually careful: RenderPass renders compositeScene with ortho camera; the composite material samples cloudRT and outputs as-is. Then bloom, then grade (vignette/grain — operate post-tonemap ideally, but typical demos apply before OutputPass anyway; previous demos did vignette+grain before output presumably — fine).
-
Cameras: PerspectiveCamera(55°). Modes:
- ORBIT: OrbitControls, target at cloud mid-height offset; min/max distance.
- GLIDE: auto path: position = large slow lissajous through cloud layer; lookAt along velocity; user pointer drag nudges heading. Escapes to orbit on key C.
- TOWER: slow vertical rise + orbit around a cumulus column. I'll implement a small state machine + smooth transitions (lerp camera over 2s). Keep robust: in GLIDE/TOWER disable controls.enabled.
-
Weather presets (lerped params):
- Fair Skies: coverage 0.35, density 0.55, base 900, top 2600, storm 0, wind 6 m/s, erosion 0.5.
- Towering Cumulus: coverage 0.5, density 0.9, base 700, top 5200, storm 0.25, wind 10.
- Storm Front: coverage 0.75, density 1.35, base 600, top 7500, storm 1.0, wind 22, dark ambient, bolts auto.
- Cloudsea (above deck): coverage 0.95, density 0.7, base 500, top 1800, storm 0, wind 8, camera above at 2600.
- Alien Dusk: coverage 0.55, density 0.8, tinted sky (purple/teal), sun low. Also time-of-day slider (sun elevation -5°..60°) + azimuth; "day cycle" toggle animates elevation.
-
Lightning: storm mode auto-strikes every 3-9s at random cloud positions; click "⚡" or press L to strike now near view center; bolt = uniform point light + flash + thunder sound delayed by distance... just play thunder with slight delay 0.4-1s random. Draw an actual bolt? Rendering a lightning bolt line would be cool: generate jagged polyline as THREE.Line with additive material, fade over 0.2s, placed in a scene? But the main view is the raymarched quad — no geometry scene. I could overlay a second scene with the bolt rendered additively into the same cloudRT before composite... The cloud shader is a fullscreen quad; adding geometry: render boltScene into cloudRT after cloud pass with autoClear false? Bolt inside clouds would be occluded incorrectly (no depth from raymarch). Simpler & effective: bolt visual = in-shader flash glow toward boltPos direction (a bright vertical jagged streak could be faked: distance from ray to bolt segment, jittered per strike using time-hash → screen-space glow streak). Hmm — ray-segment distance glow gives a straight vertical luminous column; with per-strike random "zigzag" I can distort the segment distance using noise on y. That's cheap and looks decent:
Apply to sky background AND as in-cloud light source. With bloom it'll pop. I'll add it — moderate cost, big drama payoff. Only evaluated when uBoltIntensity>0.001.
-
Audio: WebAudio:
- Wind: noise buffer → bandpass(400Hz) → gain LFO; intensity tied to wind speed + camera altitude.
- Ocean? skip (we're high up).
- Thunder: noise burst → lowpass sweep + sub sine thump; randomized.
- Pad: two detuned sines/triangle through slow filter, quiet; chord changes per preset? Keep simple minor pad.
- Mute toggle M; audio starts on first user gesture.
-
Autoscale quality: track EMA fps; tiers: renderScale [0.8, 0.65, 0.5, 0.4] & march steps [80, 64, 48, 36]; adjust every 2.5s. Badge shows tier. Manual quality select too (Auto/High/Med/Low).
-
Snapshot: press P → render once at scale 1 (temporarily) then toBlob download. Simple: set composer size full, render, capture, restore.
-
UI:
- Top-left: title "CLOUDSEA" + subtitle + FPS.
- Right panel (collapsible, H): Presets (5 buttons), Sliders: Coverage, Density, Wind, Cloud Base, Detail, Time of Day, plus toggles: Day Cycle, Cinematic camera, Storm audio... Buttons: Lightning ⚡, Snapshot 📷, Mute 🔊, Tour.
- Bottom-left: camera mode buttons (Orbit/Glide/Tower) + hints.
- Loading overlay with progress bar & tips; fade out.
- Keyboard shortcuts listed in a help footer. Styling consistent w/ previous demos: dark glass, gradient title text.
Let me also double check WebGL2 requirement: 3D textures need WebGL2 — three r170 uses WebGL2 always. Data3DTexture with RGBA format + UnsignedByteType — supported. LinearFilter on 3D byte texture — supported.
One concern:
texture(sampler3D)in GLSL ES 3.0 — three ShaderMaterial by default compiles as GLSL1 → texture() with sampler3D is invalid in GLSL ES 1.0! I needglslVersion: THREE.GLSL3on the ShaderMaterial, then usetexture(),out vec4 outColoretc. But when using GLSL3 with RawShaderMaterial I'd declare everything; with ShaderMaterial GLSL3, three injects#version 300 es+ defines + attributes; I must declareout vec4myself and not use gl_FragColor;texture2D→texture. Also for the composite material sampling a 2D texture, GLSL1 is fine but mixing is fine too. I'll write the cloud material as RawShaderMaterial with GLSL3 to have full control (declare precision, uniforms, in/out). But RawShaderMaterial + EffectComposer's RenderPass is fine.Careful with three's automatic uniforms: RawShaderMaterial provides nothing — I pass my own cameraBasis uniforms. The fullscreen triangle: BufferGeometry with 3 verts, positions only; vertex shader computes ndc → out vRay? I'll compute ray directions in fragment from inverse matrices: pass uInvViewProj? Reconstruct world ray from NDC:
vec4 ndc = vec4(uv*2-1, 1, 1)?Standard: worldPos = invViewProj * vec4(ndc, 1) → ray = normalize(worldPos/w - camPos). Since our "world" is the cloud world, just compute that. Pass uInvViewProj (mat4) & uCamPos.The composite scene: separate ShaderMaterial (GLSL1) with fullscreen triangle copying cloudRT + maybe FXAA-ish. Then composer.
Camera math: I'll maintain THREE.PerspectiveCamera + its matrixWorld; compute invViewProj = inverse(proj * view) via THREE.Matrix4 multiply then invert per frame (cheap).
Ray direction reconstruction from invViewProj uses NDC z=1 (far plane point). Fine.
Now the sea: in the same shader, before/after clouds: if ray hits y=0 plane at tSea < t0(cloud) or no clouds → shade sea. Sea shading:
Sea adds a lot of beauty for looking down through gaps. Do it, cheap: 2-3 texture fetches.
Stars at night:
star = step(0.998, hash(floor(rd*300)))twinkle by time; multiply by nightness. From hash without texture. Fine.Sun disc HDR ~ vec3 * 25 → bloom halo.
March step budget: steps 48-90 adaptive: also implement "empty space skipping lite": if density==0 take 2x step. Since sampleDensity costs 2 texture fetches + math, and light march 5 fetches only when d>0 — okay.
Light march: 5 steps of fixed length toward sun starting at p + offset; transmittance exp(-sum*densityLight). densityLight can reuse cheaper density (no erosion detail? use base only — cheaper: 1 fetch) → light step cost = 1 fetch.
Powder: powder = 1 - exp(-alongViewDensityAccum * 2) applied to sun+ambient energy... standard: sugar = 2*(1-exp(-accum8))(exp(-accum1.5))? I'll use simple: powder = mix(0.6, 1.0, 1.0 - exp(-accumOpticalDepthView * 4)). Hmm, actually powder darkens edges: energy = mix(1.0, 1.0 - exp(-od2.5), ...). I'll implement the classic:
float powder = 1.0 - exp(-accumDensity * 16.0); energy *= mix(0.4, 1.0, powder);Hmm that darkens deep samples. Wait the HZD powder sugar effect: transmittance-along-view-based darkening of edges —powder = 1 - exp(-d_acc * k)where d_acc accumulates density from camera IN... it makes fluffy rim bright and interior darker? It multiplies incoming light by powder where powder small at the start (edges) — no wait. In HZD, powder = 2 * (1 - exp(-accum8)) * (exp(-accum*1.6))... whatever — I'll use the common simplified:float powder = 1.0 - exp(-densityAccumAlongView * 12.0); float powderTerm = mix(1.0, 0.35, powder);applied to sun light so thick interiors get darker (beers+powder gives nice depth). Hmm that double-darkens with beer's. Honestly, a widely-used snippet:applied per-sample based on local density: light = mix(1.0, powderSugar(d8), 0.6). This darkens dense cores slightly — gives "milkiness". I'll use per-sample powder on light energy:
float pw = mix(1.0, 1.0 - exp(-d * 18.0), uPowder);where uPowder 0.5 — dense samples scatter less from sun (already shadowed anyway), subtle. Fine.Ambient:
amb = mix(uAmbientGround, uAmbientSky, h) * (0.25 + 0.75*h)(top brighter) + storm darkening.Alright. Also height gradient shapes:
Tune later.
Coverage modulation (weather map):
float wm = texture(tNoise, p * 0.000012 + uWindOffset*0.13).r;then coverageEff = clamp(uCoverage + (wm-0.5)0.6, 0, 1). Adds big-scale clustering. Also modulate cloud top height by wm: topEff = mix(base..top) (0.7+0.6*wm) — gives varied tower heights. Implement: h = (p.y - base)/(topEff - base).Wind: uWindOffset scrolls 3D texture coords: offset = windDir * speed * time / cloudScale. Also vertical shear: offset.xz *= (p.y-base)shearFactor? Keep simple: offset by time; plus slight domain warp: q += 0.03vec3(sin(...)) skip.
Storm darkening: uStorm multiplies σ and darkens ambient & bottom: ambient = 1-0.5uStorm*(1-h).
Now GLIDE camera path:
Better: compute pos(t), pos(t+0.01) → forward. Add pointer influence: yawOffset from drag. R ≈ 2500, H ≈ 800. Speed such that it flies through clouds.
TOWER: pos orbits column at radius decreasing from 3000→1200 while y rises base-300 → top+600; lookAt column center at camera height.
Transition: when switching modes, tween camera position/quaternion over 1.6s with smoothstep; during tween disable controls.
ORBIT: OrbitControls target (0, mid, 0), enableDamping, minDistance 300, maxDistance 12000, maxPolarAngle ~ 1.65π/2 to allow slightly below horizon. Also allow going below cloud base.
UI polish: I'll craft a nice panel with range inputs styled, preset chips, mode chips, icon buttons. Loading overlay with animated gradient + progress bar ("synthesizing 3D noise… x%").
FPS autoscale: ema; every 2s: if ema<48 and tier<3 → tier++; if ema>57 for 3 consecutive checks and tier>0 → tier--. Tiers set {scale: [0.75,0.62,0.5,0.42], steps:[88,72,56,44]}. Manual override via select (Auto default).
Snapshot: temporarily set quality scale 1.0 & steps high, render composer, then canvas.toBlob → download link. Must preserveDrawingBuffer? Not needed if we render then immediately toDataURL in same frame — use renderer.domElement.toBlob right after composer.render() in same task. OK.
Audio details:
Start on first pointerdown/keydown. M toggles master.
Time-of-day: slider elevation -4..55° & azimuth 0..360; day-cycle checkbox animates elevation = 30+28sin(t0.05). Sun color computed: warm at low elevation (mix orange→white by elev), sky zenith/horizon palettes keyed to elevation with smooth transitions day/sunset/night. I'll craft palette function in JS and pass colors as uniforms (easier than in-shader):
lerp between keyframes: day(elev>25), golden(8..25), sunset(-2..8), night(<-2). Colors chosen tastefully. Alien preset overrides with exotic palette (teal sky, magenta sun?) — implement as preset palette multipliers.
Preset parameter set (JS object, lerped over ~2.5s):
Smooth transitions: current params lerp toward target each frame with k = 1-exp(-dt*1.5).
Cloud base slider adjusts base ±; coverage slider etc. adjust target of current params (custom preset = modified copy; when user drags a slider, we just modify live params and mark preset "Custom").
Now — code size: this will be big (~1300-1600 lines). That's fine (terraforge was 1353).
Let me also keep the "Demo 022" card format consistent with index.html, added at top of list ("New demos are added here" — 021 card is right below the comment, so newest first).
Title: "Cloudsea". Description: "A volumetric cloud playground — ray-marched in a single GLSL pass ..."
Let me now write the file. I'll write it in one go with
cat > file << 'EOF'. Must be careful with backticks/dollar in heredoc — use quoted 'EOF' so no shell expansion. Template literals inside JS are fine.Potential pitfalls to double check while writing:
-
RawShaderMaterial GLSL3: must include
precision highp float; precision highp sampler3D;and declareout vec4 fragColor;in fragment; vertex shader needsin vec3 position;etc. and declare own uniforms (no built-ins from three for Raw). Fullscreen triangle: I can use a BufferGeometry with positions and uvs; in vertex:gl_Position = vec4(position.xy, 0.0, 1.0); vUv = uv;. For GLSL3 vertex:in vec3 position; in vec2 uv; out vec2 vUv;— raw, fine. -
Data3DTexture:
new THREE.Data3DTexture(data, w,h,d); tex.format = THREE.RGBAFormat; tex.type = THREE.UnsignedByteType; tex.minFilter = tex.magFilter = THREE.LinearFilter; tex.wrapS/T/R = RepeatWrapping; tex.needsUpdate = true;sampler uniform:{ value: tex }and in GLSLuniform sampler3D tNoise;. -
unpackAlignment: for RGBA8 it's fine (rows are multiple of 4 bytes).
-
HalfFloat cloudRT:
new THREE.WebGLRenderTarget(w, h, { type: THREE.HalfFloatType, format: RGBAFormat, minFilter: Linear, magFilter: Linear, depthBuffer: false, samples: 0 }). samples for MSAA not needed. -
EffectComposer default HalfFloat — in r170 yes. UnrealBloomPass works with HDR. OutputPass handles tone mapping; set
renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0;— OutputPass reads these at render time. Note: with composer, the cloud pass renders into cloudRT (raw HDR values as I write them). Composite pass samples and writes into composer RT; bloom; grade; OutputPass converts to sRGB + tonemap. -
GradePass: ShaderPass with custom shader {tDiffuse, uTime, uVignette, uGrain} — operating in linear HDR pre-tonemap: vignette multiply + additive grain*small. Fine.
-
OrbitControls import from 'three/addons/controls/OrbitControls.js'.
-
UnrealBloomPass import from 'three/addons/postprocessing/UnrealBloomPass.js'; RenderPass, ShaderPass, OutputPass, EffectComposer.
-
FXAAShader — skip.
-
Resize: renderer.setSize, cloudRT.setSize(wq, hq), composer.setSize(w,h), camera.aspect. Also bloom resolution handled internally.
-
iOS/touch: OrbitControls handles touch; UI buttons pointer events; glide mode drag → I'll add pointermove handling only when pointer down on canvas in glide/tower mode.
-
GLSL loop with uniform bounds: use
for (int i = 0; i < MAX_STEPS; i++) { if (float(i) >= uSteps) break; ... }fine. -
Density sampling function must be efficient: 2 texture3D fetches (detail + weather) in view march; 1 fetch in light march (reuse? light march uses full density? use cheap: base fbm + height grad only, no weather fetch... weather fetch needed for consistent coverage; but ok approximate without). I'll make sampleDensity(p, doDetail, doWeather).
Light march step length: (top-base)/6.
- The wind offset must be applied consistently:
vec3 woff = uWindDir * uTime * uWindSpeed / uCloudScale;where uCloudScale = size of one noise repeat in meters (e.g., 9000). So q = p/uCloudScale + woff (woff grows linearly; at speed 20 m/s over 10 min = 12000m/9000 = 1.3 repeats — fine).
Weather sample:
q2 = p/(uCloudScale*4.0) + woff*0.25.- Ray–slab intersection:
handle rd.y≈0: if ro.y within slab → t0=-inf..; clamp t0 = max(t0, tNear0). Also intersect with sea plane tSea (if rd.y<0): limit t1 = min(t1, tSea) since sea occludes clouds below... yes sea is opaque: clouds below sea level? base > 0 always so if looking down, sea plane t < slab t0 possibly → then no cloud. Compute t1 = min(t1, tSea) if sea hit exists.
Also cap far distance uFar = 60000.
- Camera height: keep between 50 and 20000.
Numbers: cloud base 800-1200m, top 2.5-7km, world extent: horizon at ~80km; noise repeat 9km; camera orbit radius 3-8km. Sea at y=0. Exposure ~1.0, sun HDR 20.
March steps across slab thickness ~5km → 88 steps ≈ 57m steps — okay-ish; with jitter + temporal grain fine.
-
Sun disk: `float d = dot(rd, sunDir); disc = smoothstep(cos(0.267°)... cos angular radius: cos(0.00465 rad)≈0.99999; use smoothstep(0.99985, 0.99997, d) * 40. plus glow = pow(max(d,0), 900)*3 + pow(max(d,0),90)0.6 sunset tint.
-
Stars:
times nightFactor, only above horizon, fade with sun elevation.
- Lightning bolt glow (sky + in-cloud):
Add to sky:
sky += boltColor * glow * 0.02… and inside cloud march per sample: light toward nearest boltPoint? cheaper: per sample point, approximate bolt distance via 4 taps of boltPoint distance (point-to-point) → atten = I/(1+d²k). I'll compute bolt glow per density sample with the 8-tap loop only when intensity>0 — heavy; instead precompute nothing; do 3-tap approx per sample. Keep bolt iterations low (4) in-march and 8 for sky. It's occasional (only during flashes). OK.Also camera-facing flash: whole-sky flash uniform uFlash added to ambient & sky briefly after strike (uFlash decays in 0.3s, bolt tube decays 0.15s with flicker: multiply by flicker(t)).
Thunder: schedule with delay = dist/340 s (cap 4s).
- Blue-noise jitter via IGN:
offset t by ign(gl_FragCoord.xy + frame*something)*step.
Alright — also small extras:
- "shadowed silver lining" looks good with double HG.
- Optional "HD spiral galaxy of clouds"? no, stop scope creep.
Birds? no.
I think the plan is solid. One more consideration — previous demos' card descriptions are long and rich; I'll write a similar one for the index.
Also the top-left HUD should show demo title + a one-line description + FPS + quality tier.
Let me now also design the control panel layout to match the aesthetic (dark glass, purple/blue accents) seen in index and presumably demos.
Time to write the code. I'll be methodical. File: /space/demos/022-cloudsea/index.html.
Let me draft the GLSL carefully (most bug-prone).
Vertex (cloud pass):
Fragment (cloud pass) — full draft:
Hmm — I'll fold sigma into uDensity: opticalDepth increment = density * uDensity * stepLen * SIGMA. With stepLen ~60m and SIGMA=0.02 → od per full-density step = 1.2 → a few steps to opaque. good.
Hash/noise helpers:
remap:
Density:
Hmm keep simpler:
d = clamp(remap(base, worley * ero, 1.0, 0.0, 1.0), 0.0, 1.0);with ero ~ 0.35-0.6. Since worley∈[0,1] mostly < 0.8, this erodes edges. good.Light march:
Add 2-lobe beers-powder approximation:
return mix(exp(-od*s), exp(-od*s*0.25)*0.7, 0.5)gives nicer multiple-scatter feel. I'll do that.Phase:
hg at g=0.6, c=1: (1-.36)/(1+.36-1.2)^1.5 0.0796 = .64/(.16)^1.5=0.64/0.064=100.0796≈0.8; forward spike 0.92: (1-.8464)/(1+.8464-1.84)^1.5 = .1536/(.0064)^1.5=.1536/.000512=300*0.0796≈24 — strong silver lining near sun. OK cap total brightness later via tonemap.
Sky:
Below horizon (y<0): sky fades to horizon (sea will cover anyway unless no sea hit? sea plane covers all y<0).
Sea:
Use noise channel .g/.b at two offsets for normal:
3 fetches — but only once per pixel (not in loop). fine.
Bolt:
Sky bolt:
sky += boltCol * uBoltGlow * 900.0 / (60.0 + bd*bd*0.02);tune: at bd=100m → 900/(60+200)=3.5 bright; at 1000m → 900/(60+20000)=0.045. plus a wide flash term:+ uFlash * 0.5 * (0.3+0.7*pow(max(dot(rd,uSunDir)?,0)...)— simpler: sky += uFlash * flashColor * (0.25 + 0.75 * pow(max(dot(normalize(rd.xz?...))))) — just uniform addition scaled by (1 - 0.5*|rd.y|)? Keep: sky += uFlash * vec3(0.7,0.75,1.0) * 0.6.In-cloud bolt light per sample: approximate distance from sample to bolt via 4 taps (point-to-point):
energy += boltCol * uBoltGlow * 1200.0 / (200.0 + bdbd0.05) — tune; only if uBoltGlow > 0.003.
Main:
Hmm — proper: integrate L with extinction:
acc += T * energy * d * uDensity * SIGMA * dt;(emission ≈ energy * density_scatter). Since energy includes ambient+sun, treat all as in-scatter with same σ. Then T *= tr. That's the standard cheap approx. ok:Wait —
col * T + acc: when no sea and no bg hit... bg always exists. good. Also when camera inside slab, t0=0 (clamped) — jitter starts at dt*jitter fine.Bug watch:
float dscope used after if for stepMul — declarefloat dbefore if. Let me structure:Also should break when t >= t1 at loop top. Fine.
One more: sampleDensity weather fetch modulates topEff; but slab intersection used uCloudTop — topEff ≤ uCloudTop always (mix(base+800, uCloudTop, wm)) — base+800 could exceed uCloudTop if base high & top low — presets keep top ≥ base+1500. Also when wm small, topEff small → clouds shorter → but ray marched up to uCloudTop — fine (empty region).
uLightSteps as float uniform; loop max 8.
SIGMA: let's define SIGMA = 0.016 (per meter at density 1). With dt=60m full density: od=0.96/step → opaque in ~4 steps. Density average 0.2 → 0.19/step → ~15 steps → ok.
Light march SIGMA_L = 0.016*? use same uDensitySIGMA but stepL bigger (~(top-base)/5 ≈ 800m): full density step od = 0.016800=12.8 → instant black. Too harsh — light march should use smaller sigma (simulate multiple scattering / only high ext): use SIGMA_L = SIGMA0.35 and clamp. I'll add uniform-free constant:
float beers = exp(-od * uDensity * 0.006);+ second lobe exp(-oduDensity*0.0015)*0.6, mix 0.55. Tune constants reasonably; will be fine after tonemap with exposure.Colors: I'll compute palettes in JS as THREE.Color in linear space and pass. Sun color day ~ (1.0,0.98,0.95)*1.35; sunset (1.0,0.45,0.2)*1.6 etc.
Exposure uniform ~1.0 (user slider maybe 0.6-1.6? skip).
Composite pass shader:
Actually can add subtle chromatic aberration + sharpening here? Keep plain copy (grain/vignette later). Maybe add slight CA for style: sample R/B offset by (vUv-0.5)*0.0015 — cheap and adds polish. I'll include tiny CA.
Grade shader:
c.rgb + g * (0.35+0.65*c.rgb)— vec3 ok.ShaderPass uses
tDiffuseuniform automatically. Write grade shader with varying vUv per ShaderPass convention (FullScreenQuad provides uv attribute? ShaderPass uses CopyShader-style: expectsvarying vec2 vUvwith vertex shader from the shader object — I must provide vertexShader too. Standard custom ShaderPass objects define uniforms, vertexShader (with vUv = uv; gl_Position...), fragmentShader. CopyShader pattern: vertex:varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);}— FullScreenQuad in r170 uses an ortho camera and a triangle with position/uv; using projectionMatrix*modelViewMatrix works (camera set up so it maps correctly). I'll use the standard pattern.UI & presets detail:
Presets:
Palettes (JS functions of sun elevation): base palettes keyed 'day','storm','golden','alien','night' — compute per frame: get palette params by blending day/sunset/night on elevation, then multiply/adjust by preset palette overrides (alien: hue-shifted colors).
Simplify: palette function
computeSky(elevDeg, mode)returning {zenith, horizon, sunColor, ambientSky, ambientGround, seaDeep, night, exposure}. mode adjusts: 'storm' darkens/desaturates day; 'golden' warms; 'alien' overrides with teal/violet. Implement via keyframe lerp on elevation with stops [-6, night], [0, sunset], [12, golden], [35, day] then mode modifiers.Sun dir: from elevation & azimuth sliders: dir = (cos(el)*sin(az), sin(el), cos(el)*cos(az)) normalized, y clamped ≥ -0.09 so sun never fully below (allow slight below for dusk).
Time-of-day slider: elevation -4..60. Azimuth slider 0..360. Day-cycle toggle: elevation = 26 + 30sin(t0.03) (rises/sets ~ every 3.5 min).
Lightning system (JS):
- state: nextStrike time (when storm>0.6: every 4-10s), bolt {top, bot, seed, glow, flash} decays: glow = flicker pattern for 0.25s: intensity = max(0, sin-based flickers) decays; flash decays 1.2s.
- strike(nearCenter): choose position: azimuth random around camera forward ±40°, distance 1.5-5km; top y = cloudTop0.9, bot y = sea 0 (or cloudBase0.3? bolts go to sea: bot y=20). seed random. glow=1, flash=0.9. thunder delay = dist/340.
- Render passes uniforms uBoltTop/Bot/Seed/Glow/Flash.
- Manual strike button: places bolt along camera forward at 2.5km.
Camera modes & tour as described.
Autoscale: tiers as above; also cap pixelRatio ≤ 1.5? Use renderer.setPixelRatio(min(devicePixelRatio,1.25))? For raymarch cost, internal scale already handles. Set pixelRatio = 1 always? On hidpi full-res composer cost doubles... I'll set pixelRatio = min(dpr, 1.5) and quality scale multiplies cloud RT only. Good.
FPS meter: frames counted per 500ms → update HUD.
Snapshot (P or button): render at scale=1: cloudRT.setSize(w,h); render cloud; composer.render(); toBlob; restore size. Also flash a "saved" toast.
Now the loading/generation progress: overlay div with bar; generate noise in slices: for z in 0..95: fill slice; every 4 slices: await raf; update bar z/96. Then create texture, init GL, fade overlay.
Let me now also make sure OrbitControls + camera far=200000, near=2.
Sky stars use hash13 — fine.
Also handle WebGL2 absence: three r170 requires WebGL2; if creation fails show message. Wrap in try.
Edge:
uInvViewProjper frame:m.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).invert(). Need camera.updateMatrixWorld() & matrixWorldInverse update — renderer.render does it; compute after controls.update() then set uniform before render.Now about the sea when camera below cloud base looking up — fine.
What about looking straight up from below: sky gradient by rd.y → zenith. good.
Also add subtle crepuscular "god rays" feel: bloom + high forward phase gives it.
I think we're ready to write. Also the "keyboard shortcuts":
- 1..5 presets
- C camera mode cycle
- G glide? (C cycles: Orbit→Glide→Tower)
- L lightning
- D day-cycle toggle
- H hide UI
- M mute
- P snapshot
- R reset view
- Space pause? maybe not needed; skip.
- F FPS? no.
Footer hint bar lists these.
Write the file now. I'll write it as one big heredoc. Must be careful: content contains
$(template literals) — quoted heredoc 'EOF' prevents expansion. Also contains no line starting with EOF. Good.Let me carefully write the whole HTML.
Also remember to update /space/demos/index.html adding the new card after
<!-- New demos are added here -->line (newest first).Card text:
Check existing card structure format first (num div? "Demo 008"?). The grep showed "021" and "Demo 008" — older ones say "Demo 008". Let me check the newest card markup to copy format exactly.
Let me view lines around the 021 card.
-
- Commented
Now let me check the exact card markup format in the index:
- Created activity call execute_shell_command
- Created
Now I'll write the Cloudsea demo. This is a large file; I'll write it with a heredoc. Let me compose the full HTML carefully.
Plan the JS structure:
Now write it all. I need to be very careful with GLSL syntax since I can't easily test-compile. Common mistakes: missing semicolons, integer/float mismatches in GLSL3 (must use float literals), texture() usage,
out vec4declared once.Let me write the fragment shader in full now (mentally compiled):
Wait — weather map: I decided coverage modulation via a coarse fetch. Let me incorporate: in the view march, before sampling: compute wm once per sample (extra fetch). To halve cost, fetch weather from the same
nsample? Different scale needed. I'll accept the extra fetch in view march only:So sampleDensity takes cov param. Light march: use h with full uTop (no weather) and cov=uCoverage:
Hmm SIGMA_L: od in units density·m. With uDensity~1, od for 2 steps full density (2×1300m)=2600 → exp(-26000.0042)= exp(-10.9)≈0 → deep shadow; light samples only 5 → max od ~ 51300=6500. exp(-27)=0. Front-lit edge samples have od small → bright. OK but fully black interiors look dead; ambient compensates. Choose SIGMA_L = 0.0030, second lobe 0.0009:
return max(exp(-o*0.0030), exp(-o*0.0009)*0.55);— max() gives soft floor — good cheap multi-scatter approx.Phase:
hg(1,0.55)=0.0796*(1-0.3025)/(1+0.3025-1.1)^1.5=0.07960.6975/(0.2025)^1.5=0.0555/0.0911=0.609; ×0.9=0.55. hg(1,0.9): 0.07960.19/(1+0.81-1.8)^1.5=0.0151/(0.01)^1.5=0.0151/0.001=15.1×1.2=18. Strong forward spike. hg(c,-0.2)≈0.0796*(0.96)/(1.04+0.4c)^1.5 ∈ ~[0.048..0.09]; ×0.3 ≈ 0.02. OK.
Sky, sea, bolt as drafted. Also night dim: palettes handle.
Main assembly — write carefully with t0/t1 logic:
Note:
uSunColor * ph * lt * pw— energy ≈ radiance; acc += Tenergyod where od = absorption optical depth. Units: sunColor magnitude ~1.3 → with od accumulating to ~1 over cloud, acc ~ energy. Balanced-ish; exposure & tonemap will handle. Ambient amb ~ 0.4..1.1 × ambSky(~0.35) → 0.15..0.4 — fine.One issue:
t += dt * stepMulwith stepMul=1.85 in empty space causes t to exceed and skip regions where clouds might begin between steps — acceptable aliasing; jitter hides.Another subtle issue: when
wmgives topEff < p.y, h=1 → heightGrad(1)=0 → d=0 good.sampleDensity with detail=false still fetches texture once. lightTrans called only when d>0.003: cost 5 fetches. Total per step ≈ 1(weather)+1(base)+5(light when dense) ≈ heavy but OK at 0.5 res, 96 steps max.
Optimization: weather fetch every step — could compute wm once per N steps; keep simple.
Bolt functions:
Sea normal perturb via tNoise fetches as planned.
skyColor: also when below horizon & no sea (camera below sea?? camera never below y=50). ok.
Star field: gate with uNight.
Also add subtle horizon cloud haze band? skip.
JS side palettes. Let me define:
Colors are linear-ish HDR bases; sun magnitude >1 for bloom. Interpolate piecewise by elevation.
Mode modifiers:
- storm: zenith*=0.35 desat, horizon*=0.4, ambSky*=0.5 grayish, sun*=0.55, sea*=0.5.
- golden: horizon warm boost.
- alien: zenith [0.08,0.35,0.4]-ish teal, horizon [0.9,0.45,0.7] magenta, sun [1.4,0.7,1.1], ambSky teal, sea purple-dark. Blend 60% over base.
Implement
applyMode(p, mode, stormAmt).
Wind direction fixed 35°, speed from params. uWindOff = dirVec * (speed*time/noiseScale) with y small drift? y=0.
Sun uniforms: uSunColor = sun rgb (scaled 1.2). Also multiply by (0.25+0.75dayness) so night sun weak — at elev<0 night≈1: scale sun by clamp(1 - night0.85).
Moon? skip.
Now the JS for camera modes:
Glide:
Smooth: when entering, lerp position & quaternion via slerp over 1.8s (store start pos/quat, k=smoothstep).
Tower:
Orbit: controls.enabled; target (0, mid, 0).
Pointer influence in Glide: track drag delta → yaw/pitch offsets decaying to 0.
Lightning JS:
Audio engine as planned. Wind gain = 0.02 + windSpeed/300.2 + storm0.15 + altitudeFactor. Thunder synth:
Pad: chord progression with 4 oscillators... keep 3 oscillators + slow filter LFO; gains 0.03 each; chords: Am(add9): 110, 164.81, 261.63, 493.88/2? Use sets: [[110,164.8,246.9,329.6],[87.3,130.8,196,261.6],[98,146.8,220,293.7],[123.5,185,246.9,311.1]] (Am, F, G, Em-ish). Crossfade every 14s. Master limiter: DynamicsCompressor.
Autoscale: as planned. Badge text "Auto · High/Med/Low/Ultra-low" etc.
Snapshot:
toBlob after render same tick — composer.render() draws to canvas (OutputPass to screen). Good. Toast "Snapshot saved".
UI HTML:
Panel hidden toggled by H; on mobile collapse default? add toggle button.
Sliders wired to params target (live adjust → preset "Custom").
Keep the code tight but complete. Estimated lines ~1100-1300.
Also: prevent OrbitControls from going under sea: maxPolarAngle = Math.PI*0.52? Allow slightly below horizontal look; position y min: controls.addEventListener? Simplest: after controls.update(), clamp camera.position.y = max(y, 40).
renderer settings: antialias false (composer), alpha false, powerPreference 'high-performance'. toneMapping ACES, exposure 1.05.
Important: When rendering cloudScene manually to cloudRT, the fullscreen triangle geometry: create THREE.BufferGeometry with 3 vertices and uv; no camera needed but renderer.render needs a camera — use a dummy OrthographicCamera. RawShaderMaterial ignores camera matrices (we don't use them).
For composer RenderPass: composite scene also fullscreen triangle with a normal ShaderMaterial (GLSL1) that uses uv — but FullScreenQuad via RenderPass requires scene+camera; I'll make compositeScene with the triangle mesh and a dummy camera; the composite material (ShaderMaterial, not raw) vertex shader:
ignores camera — fine, RenderPass just needs a camera object.
Grade pass shader object:
Use projectionMatrix*modelViewMatrix (three provides these for ShaderPass's FullScreenQuad with OrthographicCamera(-1,1,1,-1,0,1) and plane 2x2 → position.xy ∈ [-1,1] → ok).
CA (chromatic aberration) in composite instead:
Nice.
Bloom: UnrealBloomPass(new Vector2(w,h), 0.35, 0.55, 1.0). HDR sun >1 → blooms. Clouds lit ≤ ~1.5 minor bloom. good.
OutputPass(): applies tone mapping (ACES) + sRGB.
Loader overlay text updates during noise gen.
One more check — Data3DTexture in r170: constructor (data, width, height, depth); set format RGBAFormat, type UnsignedByteType, minFilter LinearFilter, magFilter LinearFilter, wrapS/wrapT/wrapR RepeatWrapping, generateMipmaps false, needsUpdate = true. sampler3D uniform auto. Note: unpackAlignment default 4; RGBA8 rows width*4 bytes → aligned.
Noise generation code:
Careful tileability: octave k freq f=2^k... base freq 2? Let base freq 3? Periods must divide N and lattice coords = pos*(f/N) with lattice mod f. So vnoise(pxf, ..., per=f). For f in [2,4,8,16]: all divide 96. Base shape freq 2 might be too smooth; use [3?] 3 doesn't divide 96? 96/3=32 — period just needs to be integer; tiling requires period divides N — any integer period works since lattice wraps mod per and there are exactly N/per cells... wait period per means lattice coords 0..per-1 across the texture: voxel i → x = i/Nper; at i=N wraps to 0 — yes any integer per works. Use freqs [4,8,16,32]? f=32 → per 32 cells across 96 voxels = 3 voxels/cell — very blocky for the finest; ok as amplitude small (0.0625). Standard perlin-worley base: perlin fbm octaves 4-5 starting freq ~4. I'll use [4,8,16,32] weights [0.5,0.25,0.125,0.0625] + normalize /0.9375.
Worley: freqs [3,6,12]? divisibility irrelevant since points wrap mod F (F cells across texture). Feature point positions: pt(c) = (c + jitter)/F. jitter from hash of cell+freq seed. For voxel pos x∈[0,1): X = x*F; cell = floor(X); for dx,dy,dz ∈ {-1,0,1}: neighbor cell wrap (cell+d+F)%F; point P = ((cell+d) + jitter)/F in [0,1) — but for distance in wrapped space, use coordinate relative: dist between X (in cell units) and point in cell units: Pcell = (cell+d) + jitterXYZ. Compute d = length(vec3(X,Y,Z) - PcellVec) with wrap-aware: since we check ±1 neighbors and X∈[0,F), direct subtraction fine with cells possibly -1 or F (don't mod the (cell+d) for distance, only for hash lookup).
worley value = clamp(1 - dmin / 1.0, 0, 1)? dmin ∈ [0, ~0.87] typically; normalize by 0.95. Then worley^? store as byte.
Channels: R=fbm, G=worley(3)? Higher detail worley freqs 6, 12. Hmm 3 cells across 96 → 32 voxels/cell: nice fluffy blobs. Use G=worley(4), B=worley(8), A=worley(16)? G divides? any F integer ok. I'll use [4,8,16].
Per voxel cost: fbm 4 octaves ≈ 4×8 hash+lerps ≈ 60 flops; worley 3 freqs × 27 cells × (3 mul + 3 add + hash×3) ≈ 3×27×12 ≈ 1000 flops?? That's 884k × 1000 = 884M — too slow (maybe 5-15s). Optimize: precompute jitter point arrays per freq: Float32Array(F³×3). Then per voxel per neighbor: 3 loads + 3 subs + 3 muls + 2 adds + min ≈ 11 flops → 27×11≈300 → ×3 freqs ≈ 900/voxel → 800M flops total. JS ~100-200M flops/s for this pattern → 4-8s. Hmm. Reduce: early break? Can't easily. Alternative: compute worley channels at lower resolution (e.g., 48³ for G, 48³ B, 64³ A) then upsample into 96³ via trilinear JS sampling? Upsampling blurs detail slightly; but linear texture filtering when sampling 3D anyway. Even simpler: make the whole texture 64³: 262k voxels × ~900 = 236M → 1.5-3s.
Decision: N=64. fbm freqs [2,4,8,16] (64 divisible), worley F=[4,8,16]. Cloud detail relies also on in-shader shaping; 64³ with linear filter is the HZD base res (they used 128³ but many demos use 64³ or 32³ fine). Go N=64 → faster load (~1.5-2.5s).
Also we can reduce worley neighbor checks: track best and skip cells whose center distance > best - 1.5? Adds branching; skip.
Also — bonus: I can add slight billow: fbm stored as |2n-1|? Standard fbm fine; shapes come from coverage remap.
Let me also seed RNG: mulberry32.
JS noise gen loop per z slice with progress callback every 4 slices:
vnoise with per: lattice coords = nx*fr ∈ [0, fr); mod per=fr wraps → tileable. For fr=2..16 integer — good.
worley function:
Store points as jitter only (jx,jy,jz) then point = (gx + jx, gy + jy, gz + jz) using unwrapped gx. Lookup jitter by wrapped index.
d = sqrt((X-px)²+...); best=min. worley = clamp(1 - best, 0, 1) — best max ~1.5; normalize: 1 - best*0.9? Use w = clamp(1 - best, 0, 1) — values reach 1 at points, 0 beyond 1 cell. Good contrast.
Fine. Generation
262k × (fbm 4×50 + worley 3×27×~12) ≈ 262k × 1170 ≈ 300M — maybe 2-4s on desktop, slower on mobile. Acceptable with progress bar. (Could drop A-channel freq to 12 to save a bit; keep 16 but F=16 has F³=4096 points — fine.)Hmm, actually let me trim: use F=[4,8,12] → 27×3=81 lookups regardless of F. Same cost. keep [4,8,16].
Extra safety: chunk await every 2 slices for responsive bar.
Now — write the file. Also include meta description & title "Cloudsea — Volumetric Cloud Playground".
CSS: dark theme, glass panel right side, responsive. Buttons/chips. Range styling minimal.
Let me write the entire HTML now. I'll be meticulous about:
- GLSL3 raw shader: declare everything.
- No
#version(three adds it for RawShaderMaterial? NO — RawShaderMaterial does NOT add #version automatically... Actually in three, WebGLProgram prepends#version 300 eswhen material.glslVersion === GLSL3, even for RawShaderMaterial? Checking my knowledge: WebGLProgram:versionString = parameters.glslVersion ? '#version ' + parameters.glslVersion + '\n' : '';and for RawShaderMaterial, prefixVertex/prefixFragment are empty (except #version + custom defines). Yes — three prepends versionString for raw materials too when glslVersion set. So do NOT include#versionin my source; setglslVersion: THREE.GLSL3.
Vertex shader (raw, GLSL3):
Now uniforms list must match JS uniform object exactly (unused uniforms get optimized out — setting them via uniforms object is fine even if optimized out? three warns? Setting a uniform that doesn't exist in program is silently ignored via WebGLUniforms — it only uploads active ones. Fine.)
JS uniform updates per frame: sun dir/colors, windOff (vec3 accumulates: wOff += windDir * speed*dt/noiseScale — accumulate in JS to avoid huge uTime mult), uTime (for stars/sea/ign jitter — keep uTime bounded: mod 3600), bolt uniforms, params (coverage etc. lerped), steps.
For IGN jitter variation per frame use frame counter mod small.
Sea color function — write it:
skyColor(rr) recursion? skyColor doesn't call seaColor — fine. But skyColor includes bolt glow? No — bolt added in main only. ok.
Night sea: uSeaDeep dark, skyColor returns night sky w/ stars — reflections of stars for free.
Now camera & world scale: uNoiseScale = 7000 (one repeat 7km). Coverage etc.
Slab far cap 90000 — with dt = len/steps: looking horizontally through slab, len up to 90km → dt≈1000m at 88 steps — too coarse horizontally! Fix: cap march length to e.g. 26000m: t1 = min(t1, t0 + 26000). Beyond that fade: blend toward bg by transmittance anyway; distant clouds fade via haze: add distance fade mix(col, bg, smoothstep(20000, 60000, t0 or t1))? To avoid pop where slab continues past 26km: the far cap truncates clouds → T stays ~1 there anyway (empty beyond?) No — clouds exist beyond 26km; truncating shows bg — that's the "haze" effect naturally since accumulation over 26km usually T→0 for dense decks; sparse fair clouds → see through to sky — acceptable. Also add slight blue haze mixing for far: skip, T handles.
dt with len 26km/88 = 295m — coarse for nearby clouds! Adaptive: march length only what we need: t1 = min(t1, t0+26000); near clouds: slab vertical pass ~ (top-base)/|rd.y| — looking down from 3km: len ~5km → dt≈57m good. Horizontal worst case 295m steps — visually mushy at distance but distant clouds ARE mushy. Acceptable; plus empty-space skip mitigates cost.
Better: make step count adaptive to len: steps = clamp(len/60, 24, uSteps)? then horizontal marches clamp at uSteps=88 → dt 295m. fine as said.
uLightSteps = 5.
One more consideration: precision — camera at ~5km, world coords ~1e4, fp32 in shader fine (highp).
Now uniforms binding of Data3DTexture in three: works like regular texture.
Autoscale reallocation of cloudRT on tier change: cloudRT.setSize(round(wq), round(hq)).
Manual quality select: 'auto' | 0..3 tiers.
FPS ema: fps = 1/dt smoothed: ema = lerp(ema, 1/dt, 0.05). Evaluate every 2s.
HUD elements: #fps, #qual.
Keyboard: implement as planned; ignore when typing (no inputs text). Range inputs: arrow keys might conflict — check document.activeElement tag.
Toasts: small div fading.
Loading overlay: title + progress bar + status text ("Synthesizing Perlin–Worley noise…") + tip. On complete: fade out (opacity transition), remove after.
Also handle context: pointerdown once → audio.init().
UI building: I'll hardcode panel HTML (simpler than JS-generated) with ids; wire events.
Panel content:
That's a lot of sliders; group with labels small. Fine.
Let me now write the whole thing. I'll aim for clean, commented code.
Also — remember NOT to reveal internal instructions; UI text shouldn't mention blueprint? Previous index has blueprint badge; demo files themselves — terraforge probably has some footer. Not required. Keep demo self-focused.
Write now. The fragment shader as final:
Also define SCATTER balance constant: I'll fold albedo into energy scale.
Double-check
sampleDensitysignature: (p, h, cov, detail). Uses uniforms uNoiseScale, uWindOff, uStorm, uErosion. heightGrad uses uStorm.Edge: uCoverage could exceed [0,1] from sliders — clamp in JS.
One more addition — "god-ray fingers" when sun low behind clouds: forward spike handles.
Let me also add a very subtle horizontal "distant cloud band" sprite? Skip.
Camera default: pos (4200, 2400, 5200), target (0, 1900, 0). Glide center mid-height.
Write the file.
After writing, quick sanity checks:
- node --check? It's a module inside HTML; extract script and syntax check with node --check (imports of 'three' will fail resolution at runtime but --check only parses). I'll extract the content and run node --check. Also basic GLSL brace balance check by eye.
Then update index.html with new card.
Let me write the file now.
- Commented
***********************************************************************************************************************************************************le:
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Commented
************************************************************************************************* 1:
- Created activity call read_write_file_in_shell_environment
- Created
Now I need to append the main JS before
</script>. I'll use replace_in_file to replace the closing</script>\n</body>with the rest of the code + closing tags. Actually simpler: replace the last lines:with imports + rest of code +
</script>.Let me write the noise generation + shaders + engine in chunks using replace_in_file (anchor: the OutputPass import line followed by ). I'll do it in 2-3 chunks to keep each write manageable. Since each replace anchors on the tail
</script>\n</body>\n</html>, I can repeatedly insert before it.Chunk 1: noise generation + boot. Chunk 2: shaders (vertex/fragment strings). Chunk 3: engine (renderer, composer, UI, audio, loop).
Chunk 1 content:
- Commented
***************************************************************************on:
- Created activity call replace_in_file_in_shell_environment
- Created activity call _checkTaskRunStatus
- Created
I'm at 86% of the time budget. I need to finish quickly. The file is incomplete (missing shaders and engine — it won't run). I must write the remaining code fast but correctly. Let me write the shaders + engine in one big chunk now, being efficient but careful. I'll trim some features to ensure completion: keep core raymarch, sky, sea, bolt, presets, cameras, audio (compact), autoscale, snapshot, UI.
Let me write it as compact as I can while keeping quality. One big replace now.
- Commented
*********************************************************************************************************ne:
- Created activity call replace_in_file_in_shell_environment
- Created activity call _checkTaskRunStatus
- Created activity call _checkTaskRunStatus
Other Conversations
Explore other conversations with this agent.