Azure Odyssey 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 says "Wake up and do as you please." So I have freedom to do what I want in my space. My purpose is to create cool Three.js demos in
/space/demos/.The last execution created a "Void Runner" demo. Let me check the current state of the demos folder and then create a new impressive demo.
Let me first install shell tools and explore the current state.
- Created
Let me explore the current state of the demos folder to see what exists already.
- Created activity call execute_shell_command
- Created
The task is "Wake up and do as you please." So I have freedom. My purpose is to create cool Three.js demos. The last demo was 008-void-runner. Let me check the existing demos briefly to understand the style, and then create demo 009.
Let me look at the rest of the index and one demo to see the conventions used.
- Created activity call execute_shell_command
- Created
The task says "Wake up and do as you please." — this is a free-form wake-up task. My purpose is to create cool Three.js demos. The last demo created was 008-void-runner. So I should create demo 009.
Let me check one existing demo to understand the conventions used (import maps, three.js version, etc.), then create a new impressive demo 009.
Ideas for demo 009 — something different from what's been done:
- Spinning cube
- Nebula galaxy (particles)
- Mandelbulb (ray marching)
- Fluid dynamics (GPU Navier-Stokes)
- Gravity sandbox (N-body)
- Event horizon (black hole ray marching)
- Sonic reactor (audio visualizer)
- Void runner (endless runner game)
New ideas:
- Terrain/landscape generation with erosion — procedural terrain with hydraulic erosion simulation, fly-through
- Ocean waves — Gerstner waves water shader with islands, buoyancy
- City flyover — procedural cyberpunk city
- Volumetric clouds — ray-marched clouds
- Cloth simulation — Verlet cloth
- Fractal tree / L-systems garden
- Fireworks show — particle fireworks with physics and audio
- Snake 3D game
- Marble run / physics playground — cannon.js or custom physics
- Kaleidoscope shader
- Reaction-diffusion (Gray-Scott) — GPU compute, beautiful patterns
- Boids flocking — GPU flocking with thousands of boids
- Space station / docking sim
- Asteroids game
- Voxel world — Minecraft-like with chunked terrain
- Wormhole / tunnel shader
- Lightning storm — procedural lightning bolts, rain, thunder audio
I think a great pick would be "Neon Boids" — a GPU flocking simulation with tens of thousands of boids... but that may be similar to the galaxy particle demo.
Another strong candidate: Reaction-Diffusion playground — Gray-Scott model on GPU with ping-pong framebuffers, feeding patterns, mouse interaction. Very visual, distinct from prior demos. Could be rendered onto a 3D surface (displace a plane/sphere) for extra wow.
Or Procedural Ocean — Gerstner wave ocean with FFT-ish detail normals, floating islands, buoyancy on a boat you can steer, god rays... That would be really impressive.
Or Volumetric Clouds ray marching — beautiful but heavy.
Or Cyberpunk city flyover — procedural building generation with instancing, emissive windows, fog, flying cars with trails, rain, bloom. That's a strong "wow" demo and quite different from previous ones.
Let me think about what's most distinct and impressive. Previous demos: particles galaxy, fractal, fluid, N-body, black hole, audio visualizer, runner game.
A procedural cyberpunk city with:
- Infinite procedural city grid generated in chunks
- Instanced buildings with emissive window textures (canvas-generated)
- Fog + bloom
- Flying vehicles with light trails
- Rain particles
- Free-fly / orbit / cinematic camera modes
- Synthesized ambient soundtrack maybe (WebAudio)
- Day... night only probably
That's quite a bit of work but doable in a single file. Alternatively, Ocean Odyssey: Gerstner waves + islands + steering a boat. Also cool.
Another option: Gray-Scott reaction-diffusion — visually stunning and unique, and the "living patterns" are mesmerizing. Combined with 3D displacement it could be great. But interaction is mostly watching.
I think the city ("Neon Metropolis" / "Chrome City") is the strongest choice for variety and wow factor. Let me also add flying vehicles with light trails (using instanced elongated boxes or line trails), rain streaks, and a moody soundtrack.
Actually, let me reconsider: 008 was a synthwave runner with lots of neon. A neon city might feel similar in palette. An ocean + islands + golden hour demo would contrast nicely in palette (blues/oranges). Hmm, but the runner was more about gameplay.
Let me pick "Aqua Planet" / "Endless Ocean":
- Gerstner wave ocean shader with foam, subsurface scattering approximation, specular sun glints
- Procedural islands with beaches and vegetation? Vegetation is hard; maybe rocky islands with custom shading
- A controllable sailboat/hovercraft with wake/foam trail, buoyancy physics sampling wave heights
- Sky with sun, clouds (procedural), day/night cycle control
- Jumping fish? dolphins? Maybe particle splashes
- Bloom post-processing
- Draggable time-of-day, wave height, etc.
That's a solid demo. Wave height sampling on CPU must match the GPU Gerstner displacement — doable by implementing the same function in JS.
Alternatively volumetric clouds — ray-marched in a fullscreen shader with fly-through camera. Gorgeous but performance-heavy; quality controls can manage.
Another idea: "Crystal Cavern" — ray-marched cave with crystals, glow, fog, flight. Similar to Mandelbulb in technique though (ray marching). Avoid.
Fireworks festival — particle physics fireworks over a city/water skyline with reflections, synthesized launch/explosion audio, auto-show + click to launch. Fun, pretty, distinct. Maybe a bit simple compared to others but can be made rich: multiple shell types (peony, willow, strobe, ring, crossette), trails, smoke, water reflection, bloom.
I think I'll go with the ocean demo — "Azure Drift" or "Infinite Ocean". It offers:
- Custom GLSL water (Gerstner waves, foam, Fresnel, sun glints, depth-based color)
- Procedural island terrain (fbm) with sandy shores, rock, grass coloring
- Buoyancy-driven boat you can sail (WASD), with wake foam particles and spray
- Sky dome shader with sun, gradient, procedural clouds
- Day-night cycle slider
- Seagull boids? Maybe simple flocks circling
- Bloom + vignette
- Camera: chase cam / orbit / cinematic
- Sound: synthesized ocean ambience (filtered noise) + optional
That's ambitious but I can build it carefully in one file (~50-60KB like the others).
Actually, wait — let me also consider a "Planetary terrain with hydraulic erosion" demo... The erosion sim on GPU is complex. The ocean one is more tractable and visually rewarding.
Decision: Demo 009 — "Azure Frontier" (infinite procedural ocean + sailing). Hmm, name ideas: "Sea of Dreams", "Abyssal Drift", "Open Water", "Azure Odyssey". I'll go with "Azure Odyssey".
Let me first peek at an existing demo's structure to keep conventions consistent (import map, lil-gui, etc.). Let me look at 004-fluid-dynamics or 008 briefly for the header/import map conventions.
Then I'll write the new demo in parts (write file in chunks to avoid a single huge write failing), update index.html, and validate with node syntax check (extract module script and run
node --check).Plan:
- Read a bit of demo 008 for conventions (import map URLs, lil-gui CDN).
- Create /space/demos/009-azure-odyssey/index.html in chunks.
- Update /space/demos/index.html adding the card at top.
- Validate: extract JS and node --check; verify HTML structure.
Key technical details for the demo:
Water: A large plane (e.g., 600x600 segments over 1200x1200 units) that follows the camera (snapped to grid to avoid swimming vertices), with vertex shader Gerstner waves (6-8 waves), normal computation via partial derivatives, fragment: Fresnel, sun specular (Blinn), subsurface scattering approx, foam via wave crest factor + shore foam via depth to terrain... True depth needs depth texture; simpler: compute terrain height function in the water fragment shader too (same fbm) to get water depth → shore foam and color gradient. That's a classic trick: sample islandHeight(worldXZ) in water shader, depth = waterLevel - islandHeight; use for color absorption and foam lines.
Terrain fbm on GPU per-fragment for every water pixel might be expensive but fbm with 4-5 octaves is okay. Alternatively render island height to a texture once (islands are static? but camera moves infinitely...). Infinite ocean with islands scattered procedurally: islands placed on a hash grid — each grid cell may contain an island at a pseudo-random position; island height = sum of contributions of nearby cells. For the water shader, sampling 9 cells × island falloff + a few fbm octaves each is heavy.
Simpler approach: a finite world (e.g., 4000×4000) with several handcrafted islands positioned procedurally but fixed (seeded), and the ocean plane is big (8000×8000) with distant fog hiding the edge. The boat sails within this world; when nearing the edge, fog + "turn back" not needed — just let them sail into the open sea forever (islands are local interest). Ocean plane follows the camera via UV world coords... Actually if the world is finite, I can just make one giant ocean plane of 12000×12000 centered at origin, tessellated more near center? Non-uniform tessellation is complex.
Better: camera-following ocean patch (radius ~700, e.g., 512×512 segments = 260k verts — too many; 256×256 = 65k verts fine). Vertices snapped to world grid: position = cameraXZ snapped to gridStep + localOffset. Grid swimming artifacts avoided by snapping. Distant water beyond patch: a huge flat ring/plane with the same shader but waves dampened by distance (vertex normal flattening + fragment keeps sun glints via normal perturbation from detail normals). Common trick: fade Gerstner amplitude to 0 with distance from camera, blend to detail normal maps... I don't have textures unless I generate them via canvas. I can generate a tiling normal map procedurally on a canvas (or via DataTexture with simplex noise) — yes, generate a 256×256 normal map + foam/noise texture procedurally at boot with JS. Good.
Islands: heightmap-based terrain mesh, 256×256 resolution over 2000×2000, using seeded fbm + island masks (a few gaussian blobs at fixed positions). Same height function implemented in JS (for CPU: buoyancy, collision) and duplicated in GLSL (for water depth shading). To keep them in sync, write the noise in both languages carefully. Use a simple hash-based value noise with quintic interpolation, fbm 5 octaves. Island bases: define array of islands {x, z, radius, height, seedOffset}. Height = base seabed (-8) + sum over islands of falloff * (fbm ridge). Manageable.
Terrain coloring: sand near waterline, grass mid, rock steep slopes, snow? No snow — tropical. Add fog. Terrain mesh resolution: 512×512 verts over 2500×2500 might be 260k verts — fine as a single static draw (it's static geometry). Actually 512×512 = 262k vertices, 512k tris — okay for modern GPUs, but mobile hmm. I'll do 384 or make quality setting. Let's do 400×400.
For collision/buoyancy, CPU height function with 9 islands × fbm(5 octaves) per query — a few queries per frame (boat + camera) fine.
Boat: stylized low-poly sailboat built from primitives (hull from shaped geometry — could use a lathe or extruded shape; simpler: hull = scaled/inched box geometry deformed via code, or use a cone+capsule combo). I'll build a decent stylized boat: hull from a parametric shape (custom BufferGeometry via lofting sections), mast cylinder, sail = custom triangle mesh with slight curve (plane bent), flag, wake.
Sailing model: simple arcade — throttle (W/S), rudder (A/D), speed-dependent turn, drift/lateral friction, heel (roll) when turning and with wind. Buoyancy: sample wave height at bow/stern/port/starboard points → pitch/roll/heave. Wave height must match GPU: implement Gerstner in JS identical to shader (same wave params array shared as uniforms — define wave params in JS, upload to shader as uniform arrays, and use same math).
Wake: particle trail of foam quads spawned at stern, fading + expanding; plus bow spray particles when speed high. Instanced or pooled sprites.
Sky: big sphere with gradient shader: sun disc + glow, horizon haze, stars at night, procedural clouds via fbm in shader (domain-warped, on the dome). Day-night cycle via sun elevation parameter: colors lerp between day/sunset/night palettes. Sun direction shared with water + terrain lighting.
Extras:
- Seagulls: simple flapping birds (two triangles as wings) circling islands — instanced ~24, cheap.
- Fish/jump splashes: skip or simple.
- Dolphins: maybe skip to keep scope.
- Rain? no.
- Ambient audio: synthesized waves (filtered noise swells) + wind + occasional gull chirps. WebAudio, starts on user gesture. Keep it modest: noise buffer + bandpass + LFO gain swells; wind = filtered noise with slow LFO. Add mute button.
- Bloom (UnrealBloomPass) + optional vignette shader pass. Composer like previous demos.
- Controls: lil-gui panel: wave height, wind, time of day, clouds, foam, audio volume, quality, camera mode, bloom.
- HUD: speed, heading, mini help overlay; snapshot button (PNG export).
- Camera modes: chase (default), orbit (OrbitControls around boat), cinematic (slow island flyover), free? Keep chase + orbit + cinematic.
- Mobile: touch joystick? Maybe basic: drag left half = steer, right half = throttle... Could be scope creep; the previous demos had some mobile support. I'll add simple touch: left/right halves for rudder, and a throttle slider on screen? Simpler: touch drag = steer & throttle by vertical position. I'll add minimal touch controls.
Scope check — this is large but similar in size to 008 (65KB). I can do it. Write carefully to avoid bugs. Validate JS with node --check.
Performance considerations: water 256×256 (~65k verts) + terrain 400×400 (160k verts, static) + bloom at half res — should run fine.
Let me define the Gerstner wave set: 6 waves with direction, amplitude, wavelength, speed, steepness. In vertex shader: P.y += A * cos(dot(D, xz) * k - speed * t); and horizontal displacement x/z += steepness * A * D * sin(...). Normals computed analytically via derivatives. Standard GPU Gems formula.
JS side implements same to sample height at (x,z,t) (ignoring horizontal displacement for height sampling — acceptable, or include full Gerstner position solve? Height at the displaced point differs; typical approach: sample using the same formula ignoring horizontal offset; visually fine.)
Water fragment:
- normal from vertex (varyings) + detail normal perturb from generated normal map (two scrolling layers), detail fades with distance.
- depth = yWater - terrainHeight(worldXZ) — need terrainHeight in water fragment shader. That's fbm(4) + island falloffs for 9 islands — heavy-ish but fragment-only for water pixels. Alternatively precompute a heightmap texture of the terrain region (1024×1024 covering 2500×2500) at boot from the JS height function (1024×1024 = 1M evaluations × 9 islands × 5 octaves ≈ 46M noise evals... JS value noise ~ maybe 100ns each → 4.6s. Too slow. 512×512 = 262k evals → ~1.2s, acceptable-ish but noticeable boot delay. Could use 256×256 (65k → 0.3s) and rely on bilinear smoothing — height texture only used for water depth color/foam; 256 over 2500 units = ~10 units/texel, smooth bilinear, fine for soft color gradients but shore foam lines would be blobby. Foam line precision benefits from higher res. Compromise: 512×512 with 4 octaves for the texture bake (~0.5-0.8s). Or bake at 512 covering 2500, and terrain mesh itself 400×400 samples from... the terrain geometry should sample the exact height function for crispness; the texture is only for water shader. OK: bake 512×512 float texture (use DataTexture RFormat float or half; or pack into RG16F... simplest: R32F float texture, supported widely; fallback: encode in RGBA8). Actually for broad support, use HalfFloatType with linear filter — WebGL2 core supports float linear filtering? OES_texture_float_linear needed for R32F linear; HalfFloat linear is core in WebGL2? In WebGL2, float32 linear filtering requires extension, float16 linear filtering is core? Actually in WebGL2,
OES_texture_float_linearfor 32F; 16F linear filtering IS supported core in WebGL2? No — WebGL2 supports rendering to 16F with EXT_color_buffer_float, and texture filtering for half float is core (OES_texture_half_float_linear was folded into WebGL2 core). Yes: in WebGL2, half-float textures support linear filtering without extensions. So use DataTexture with HalfFloatType, RedFormat? R16F is filterable in WebGL2 core. I'll use RedFormat + HalfFloatType. To be extra safe I could just use RGBA + UnsignedByte with height packed... that loses precision (256 levels over ~40 units height = 0.16 steps, fine actually with bilinear). Simplest robust: R16F half float. Three r160+:new THREE.DataTexture(data, w, h, THREE.RedFormat, THREE.HalfFloatType). data must be Uint16Array with half floats — I'd have to convert float→half manually. THREE.DataUtils.toHalfFloat exists. OK do that. Or simpler: use FloatType (R32F) and set magFilter Linear — needs OES_texture_float_linear, present on ~all desktop; mobile mostly yes. To be safe use half float + DataUtils.
Alternatively skip texture: evaluate island height analytically in water shader with reduced octaves (3). Fragment cost: water pixels maybe 1M × 9 islands × 3 octaves = 27M noise evals per frame — GPU can do it (GPUs are fast at this), but on mobile might chug. The texture bake is safer for perf. I'll do the bake at 512² (262k × (9 islands × 4 octaves + seabed) ≈ 10M noise evals → JS ~0.3-0.6s boot). Acceptable with a loading indicator.
Hmm wait — terrain geometry also needs heights: 400×400=160k evals fine.
Let me also reconsider: simpler to make the whole terrain from a heightmap texture generated once (1024×1024 RGBA8 packed or R16F, covering the world 3000×3000), then terrain geometry displaces from the same texture (sample on CPU from the float array), water samples the same texture in shader, boat buoyancy samples the array via bilinear on CPU. One source of truth — cleaner! Bake 1024×1024 float array at boot: 1M evals... ~1-2s. Use 768×768 (590k) ~0.7-1s. Or 640×640=410k ~0.5s. Terrain geometry: instead of sampling the raw height fn, sample the baked array (bilinear). Terrain mesh 512×512 verts → 512² lookups from array — fast. Water shader samples the texture. Boat samples array.
World size 3000 units; 640 texels → 4.7 units/texel; bilinear-smooth shores. Water near shore foam: fine.
But wait — terrain detail: 4.7 units/texel is coarse for terrain geometry (mountains look smooth). Terrain mesh 512×512 over 3000 → 5.9 units/vertex, matching texture res — consistent, but visually smooth-lowpoly. I can add per-vertex fine detail noise (2 extra octaves evaluated directly, cheap: 262k×2 octave) on top of baked base — CPU only for geometry; water depth shading uses baked base only (fine detail irrelevant underwater shading). Boat buoyancy uses baked base (fine).
OK final data design:
bakeHeightmap(): Float32Array N=640, world SIZE=3200 (so 5 units/texel). height(x,z) = seabedDepth + island contributions. Islands: fixed seeded array of ~10 islands with center, radius (150-380), peak (12-40). Each island: falloff f = smoothstep(1 - d/r) shaped; h = peak * (fbm4(x,z,seed)*0.5+0.5)^1.5 * f... plus ring beach smoothing. Also add shallow shelf: islands raise seabed gradually (falloff with larger radius × 0.15). Reef ring maybe.- value noise: standard hash(ix,iz) → [0,1), quintic smooth interp. Seedable via island-specific offset (just offset coords by island seed).
CPU height sampling for gameplay: bilinear from baked array (outside → deep -30).
Terrain geometry: PlaneGeometry(SIZE, SIZE, 420, 420) rotated; each vertex: base = sampleBaked(x,z); detail = (base > waterline-ish ? fineNoise2(x,z) * min(1,(base+2)/6) * 1.2 : fineNoise*0.3 for seabed texture); final y. Normals via computeVertexNormals. Vertex colors? Use shader material for terrain with height/slope coloring + fog + sun — custom ShaderMaterial. Or MeshStandardMaterial with onBeforeCompile color ramp — simpler: custom ShaderMaterial with lambert-ish lighting + fog + optional shadows? Shadows: one directional light shadow over 3200 world — shadow map resolution insufficient for crisp shadows; use blob shadows? Skip real-time shadows on terrain; use slope-based AO-ish darkening + sun diffuse. Water receives no shadows. Fine — stylized look.
Simpler lighting approach overall: everything custom-shaded with the sun direction uniform; consistent stylized look. Boat: MeshStandardMaterial with DirectionalLight + ambient/hemisphere light — keep three's lighting for the boat and props (gulls), terrain/water/sky custom shaders using same sun dir. That mixes fine.
Sky shader: icosphere radius 6000 (inside-out), fragment:
- sunDir uniform
- base gradient: mix(horizonColor, zenithColor, pow(max(dir.y,0), 0.6))
- sun disc: smoothstep(cos(ang)) + halo
- clouds: 2D fbm in a plane projection: p = dir.xz/(dir.y+0.12) * scale + windOffset; coverage smoothstep; cloud color lit by sun (silver lining via dot with sundir), fade near horizon. Domain warp for nicer shapes. 3-4 octaves enough.
- stars: hash grid sparkle when night, fade by day.
- day cycle: uniform sunElevation (or timeOfDay 0-24). Palettes: day (zenith #2f6fd0...), sunset (orange/pink), night (dark blue + stars). Compute colors by mixing three keyframes.
Audio (modest):
- master gain; ocean = looping noise buffer → lowpass(400Hz) with gain LFO'd by ~0.08-0.15Hz swells + second layer bandpass ~800Hz "hiss" tied to swell; wind = noise → bandpass 300Hz with slow random gain wander; gull chirp occasionally: little FM chirps with random pan, every 7-20s. Engine? It's a sailboat — no engine; add sail flap / water lap at bow tied to speed: noise → bandpass 500-1200, gain ∝ speed². Nice.
- Mute button + volume slider.
Post: EffectComposer + RenderPass + UnrealBloomPass (strength ~0.35, threshold ~0.85) + OutputPass. Vignette: small ShaderPass. Quality setting: pixelRatio cap + bloom res.
HUD/UI conventions: follow previous demos' UI style — top-left title overlay, hint bar bottom, lil-gui top-right, buttons (sound, snapshot, camera). Let me peek at demo 008's UI/CSS quickly to match conventions.
Boat model:
- Hull: build via lofted sections: define half-breadth profile along length (e.g., 7 stations), each station a scaled U-shape; generate vertices grid + indices; flat shading for stylized look. Or cheat: use a stretched sphere cut in half? The loft is not hard: stations along z from -L/2..L/2, width w(t) = beam/2 * sin(pi * t)^0.7, depth d(t) similar; cross-section: ellipse arc from gunwale to keel: param angle θ 0..π: x = wsinθ, y = -dsin(θ/... ) hmm simpler: y = -d * sin(θ)^0.8? Actually section: points around half-ellipse: (wcos φ, -dsin φ) φ∈[0,π] gives U. Then deck cap: add top face? Open hull with gunwale rim + interior floor? Simplest good-looking: closed hull (mirror U above with small freeboard): points φ∈[0, 2π) ellipse with different top/bottom scale → lens/almond section; with stations tapering to bow point and flat transom. Compute normals flat-ish via computeVertexNormals + toNonIndexed for flat shading.
- Deck: slight inset ellipse plane, wood color.
- Mast: cylinder; boom: cylinder; sails: main sail = curved triangle (custom geometry: grid bent by y*x curvature), jib triangle; cloth color warm white with slight emissive? Sail curve: z offset = fullness * sin(πu)*sin(πv).
- Rudder, tiller; flag at masthead (small plane waving via vertex anim in onBeforeRender? or just static rotated — animate rotation slightly).
- Lantern at stern (emissive sphere + PointLight? point lights cost; make emissive + bloom does the rest).
- Wake: pool of ~120 foam sprites (PlaneGeometry billboarded? In three, use Sprite or instanced quads facing up). Foam on water: better as flat quads on water surface (rotated -90°, y set to wave height +0.05 each frame for the active head few). Use a pool with per-particle age/size/alpha updated in JS, drawn as InstancedMesh of plane with per-instance alpha? InstancedMesh per-instance alpha needs custom shader — use a ShaderMaterial with InstancedBufferAttributes (offset, scale, alpha, rotation). Or simpler: THREE.Points with size attenuation + soft circle texture, positioned on water surface — points always face camera; viewed from chase cam low angle, points look okay-ish but can look like blobs. I'll use instanced quads lying flat with a soft ring/blob texture (canvas-generated), custom shader with fade. ~150 instances, trivial.
- Bow spray: THREE.Points pool ~200, velocity + gravity, additive blending, small.
Gulls: 20 instanced "birds": each = two wing quads? Instancing flapping wings: use small custom geometry (two triangles) and flap via vertex shader using instance phase attribute + time uniform: wing tips rotate. Or simpler: each gull a Group of 3 meshes — 20×3=60 draw calls, meh. Use InstancedMesh with vertex-shader flap: geometry = 4 triangles (body + 2 wings), wing vertices flagged with attribute; instance attribute phase; shader rotates wing verts by sin(t*freq+phase). Position updated per-instance on CPU (circling paths around island centers with vertical bob). Write matrices per frame for 20 instances — cheap. I'll do InstancedMesh + onBeforeCompile? Custom ShaderMaterial with instancing support — with InstancedMesh + ShaderMaterial, need
#include <begin_vertex>... easier to write raw ShaderMaterial handling instanceMatrix manually:mat4 im = instanceMatrix;(available when used with InstancedMesh? three injects instanceMatrix attribute automatically for InstancedMesh in WebGLProgram if USE_INSTANCING defined — with ShaderMaterial you must define#ifdef USE_INSTANCING... Actually three sets USE_INSTANCING automatically when rendering InstancedMesh even with ShaderMaterial (defines are added by program based on object type). Yes — WebGLPrograms.getParameters sets instancing based on object.isInstancedMesh regardless of material; and common vertex chunks guard with #ifdef USE_INSTANCING. In raw ShaderMaterial I can write#ifdef USE_INSTANCING mat4 im = instanceMatrix; #endif— attribute instanceMatrix is auto-declared? The declaration comes from#include <common>? No — it's in vertex shader prologue injected by WebGLProgram:attribute mat4 instanceMatrix;is added when instancing. Yes, three injects#ifdef USE_INSTANCING attribute mat4 instanceMatrix; #endifin the prologue for non-raw ShaderMaterial. So ShaderMaterial (non-raw) works: I can use instanceMatrix inside #ifdef USE_INSTANCING.Similarly water/terrain ShaderMaterials fine.
Camera modes:
- Chase: behind boat, smoothed, slight speed FOV kick; mouse drag orbits offset (yaw/pitch around boat) with damping, wheel zoom.
- Orbit: OrbitControls targeting boat, auto-update target to boat position.
- Cinematic: slow spline flight around islands, boat autopilots? Simpler: cinematic detaches from boat — camera slowly orbits/drifts between islands while boat keeps sailing (or autopilot circles). I'll implement: camera follows a slow procedural path (lerp between island viewpoints), looking at nearest island/boat. Boat on gentle autopilot (auto throttle, steers away from islands using lookahead sample). Keep chase + cinematic + orbit via OrbitControls (import from examples jsm).
Collision: sample height ahead at bow; if land (h > -0.5), push boat back / kill speed, gentle slide along shore gradient, camera shake tiny, thud sound. Simple and fine.
Minimap? skip.
Snapshot: render then toDataURL — need preserveDrawingBuffer or render right before capture; do composer.render() then canvas.toBlob in same frame. Like previous demos presumably.
Performance scaling: quality select: High (water 256², terrain 420², PR min(dpr,2)), Medium (192²/320², PR 1.5), Low (128²/256², PR 1). Rebuild water geometry on change (terrain rebuild expensive-ish 180k× bilinear — okay ~100ms, acceptable on setting change).
Alright — also the index page gets a new card.
Naming: "009-azure-odyssey" — title "Azure Odyssey". Description: sailing across a procedural archipelago...
Let me check 008's head/import map + UI conventions quickly (first ~120 lines), then write the new demo.
I need to be careful about total size — 008 was 65KB; mine will be similar. Writing in ~5 chunks of ~13KB each via append. Let me make sure quoting is safe: use heredoc with 'EOF' quoted to avoid shell interpolation.
Let me look at 008 conventions first.
- Commented
***************************************************************************************************************************************************************************************************************************************st.
- Created activity call execute_shell_command
- Created
*********************************************************************************************************************************************on.
- Created activity call execute_shell_command
- Created
Conventions confirmed: three@0.160.0 jsdelivr import map, lil-gui, EffectComposer/RenderPass/UnrealBloomPass/OutputPass, OrbitControls.
Now I'll build demo 009: "Azure Odyssey" — a procedural sailing demo:
- Gerstner wave ocean (custom GLSL), foam, fresnel, sun glints, depth-based colors from baked heightmap texture
- Procedural archipelago (baked heightmap, custom terrain shader with sand/grass/rock ramp)
- Sky dome shader with day cycle, sun, procedural clouds, stars
- Sailable boat with buoyancy sampling the same Gerstner waves on CPU, wake foam trail, bow spray
- Seagulls (instanced, flapping in vertex shader)
- Synthesized WebAudio ambience (waves, wind, gulls, bow wash)
- Bloom + vignette, quality settings, camera modes (chase/orbit/cinematic), snapshot, autopilot, touch controls
- HUD: speed/heading/depth, time-of-day controls, etc.
Let me write this carefully. I'll write the file in chunks using heredocs with quoted delimiters. Total will be ~60KB. I'll structure it:
- HTML head + CSS + body UI + import map + start of module script
- JS: config, utils (noise, rng), waves definition, heightmap bake
- JS: sky, terrain, water shaders & meshes
- JS: boat build + physics, wake, spray, gulls
- JS: audio engine
- JS: camera modes, input, UI/gui, frame loop, boot
Then validate by extracting the module JS and running node --check (with imports — node --check on ESM?
node --checkassumes CJS unless .mjs; I can write to a .mjs file andnode --checkworks for ESM in .mjs. Node check validates syntax including import statements in .mjs.)Let me now carefully design the code.
Core constants
RNG / noise (JS)
Sin-based hash can have precision issues for large coords; islands are within ±1600, sin argument up to ~1600*311.7 ≈ 500k — sin of large args loses precision in float64? Math.sin on 5e5 is fine (double). fbm deterministic ok. In GLSL, sin hash with large coords is problematic on mediump — but I'm baking the heightmap on CPU, and GLSL never needs the island height function (water samples the baked texture). GLSL noise is only used for clouds/sky (dir-based, coords small) and fine water detail — but I planned detail normal from a generated DataTexture — also fine. So GLSL needs only small-coord noise.
Value noise fbm in JS:
For speed in the bake, 640×640=409,600 pixels × islands(≈9) each needing fbm4 → but I only need island fbm within falloff; still compute for all → 409600 × 9 × 4 vnoise = 14.7M vnoise calls. Each vnoise: 4 hash2 (each with Math.sin + floor) — maybe 60-100ns → ~1-1.5s. Acceptable with async bake chunked to show a progress bar (yield every N rows via await new Promise(r=>setTimeout(r))) — keeps page responsive and shows loading progress. I'll implement async bake with progress.
Optimization: skip islands whose influence radius doesn't cover the pixel (check distance before fbm). Most pixels only in 0-2 islands → big speedup.
Islands definition
Height function:
Hmm, need care to make beaches: body should cross sea level smoothly around t≈0.12-0.25. Let me define: body = lerp(-3.5, isl.peak, pow(t * (0.55 + 0.9*ridge), 1.3))? This could overshoot. Let me simplify:
At t=1 center: ridge = fbm ~0.5 avg → m = 0.45+0.55=1 → body = peak. At edge t→0 → body=-3.5 (below sea, joins shelf). Coast where body=0 → pow(mm,1.35) = 3.5/(peak+3.5) ~ 0.09-0.2 → mm ~0.17-0.28. With noise, coastlines wiggle nicely.
Beach flatness: I want relatively gentle near-shore slope — pow 1.35 gives derivative 0 at 0 — beaches gentle. good.
Also cap:
h = Math.max(h, body)with max blending gives islands union — islands shouldn't overlap given spacing; shelf max ensures shallow approaches (nice color gradients + warning shallows for sailing). Actually shelf: WORLD.maxDepth + (-3 - maxDepth)shelf = -26 + 23shelf → at shelf=1 → -3. Then max with body(-3.5 edge) — roughly consistent, smooth-ish junction (small crease acceptable underwater).Also add a tiny bit of coastline noise to shelf? skip.
And a barrier reef ring? skip.
Bilinear sampling of baked map
Gerstner waves (shared JS + GLSL)
Define array:
I'll define: each wave {dx, dz, amp, len, speed, steep}. k = 2π/len, ω = speed (phase velocity), phase = k*(D·xz) - ωkt? To keep units intuitive: phase = k*(D·xz - speed*t). So crest moves at
speedunits/s along D.JS sample:
GLSL vertex does same plus horizontal displacement + analytic normal:
Good enough visually (classic simplified Gerstner).
6 waves: lengths 90, 52, 31, 18, 11, 6.5; amps 1.6, 0.9, 0.5, 0.28, 0.15, 0.08 scaled by uWaveHeight (0..2); directions varied ±30°; speeds 8,6.5,5.5,4.5,3.5,2.5 roughly sqrt(len) scaled: real dispersion ω=√(gk) speed=√(g/k)=√(gL/2π)≈1.25√L → L=90→11.9, 52→9.0, 31→7, 18→5.3, 11→4.1, 6.5→3.2. Use those for realism, scaled by uWind? keep fixed, fine.
JS waveHeight ignores horizontal displacement — offset error small (< amp). Boat will look fine.
Water geometry: PlaneGeometry(1400, 1400, N, N) where N from quality (256 high). In vertex shader: worldXZ = (modelMatrix * pos) but we move mesh to camera each frame on CPU; to avoid swimming, quantize mesh position to grid step: step = 1400/N. mesh.position.x = Math.round(camX/step)*step etc. Vertex shader uses world pos for waves. Amplitude fade with distance from camera: fade = 1 - smoothstep(300, 650, dist(worldXZ, camXZ)) — far waves flatten; normals too (lerp toward up) — combined with detail normal fade for far. Beyond the patch, a giant skirt disc (circle r=6000, flat y=0) with same fragment-ish shading? The skirt must match water color. I'll make the skirt a big ring geometry (RingGeometry inner 650 outer 6000, 64 segments) with the SAME ShaderMaterial? The vertex shader would displace ring verts too — at distance fade=0, no displacement, so ring stays flat. Ring + patch share material; ring rendered slightly lower (y=-0.15) to avoid z-fight at overlap... patch at distance>650 is flat too, so z-fighting at 650 boundary region where both flat. Offset ring -0.25 and depthWrite... overlapping region 650..700 patch fades — the patch is above ring, fragment colors identical-ish (both flat, same normal≈up, same color math → seam invisible). Foam crest only where waves active (near), fine.
Actually simpler: skirt = CircleGeometry radius 6000 at y=-0.28 UNDER the patch; patch covers center 1400×1400. At patch edge (700), amplitude faded to 0 by 650, so patch edge flat at y≈0. Skirt at -0.28 below → tiny cliff at patch boundary, hidden by fog + same color. But skirt would show through patch? Patch is opaque (depthWrite true), skirt drawn first or after — with depth testing, patch covers. OK.
Hmm, but the water fragment for far pixels uses detail normal fade and up normal → pure fresnel+sun+depth color; depth from heightmap texture beyond 3200 coverage → deep color. Consistent. Good.
Fog: I'll use THREE.Fog (linear) manually in custom shaders (implement in shader with uniforms fogColor/fogNear/fogFar), and scene.fog for standard materials (boat, gulls). Match values. Fog color should change with time of day — drive uniform from sky horizon color each frame.
Water fragment shading
Inputs: vWorldPos, vNormal, vCrest (crest factor = accumulated amp fraction / jacobian-ish). Use
crest = clamp(y / maxPossibleAmp, 0..1)? Better foam trigger: use the "jacobian" determinant approx: J ≈ 1 - sum(q_ik_iA_icos(f_i))... I'll compute a cheap crest signal: c = sum over waves of (amp_i * (cos(f)+1)/2 * k_i * amp_i?) hmm. Simpler: pass vFoam = clamp( (1.0 - jacobian), ...) where jacobian computed approx in vertex: J = 1 + Σ -q_ik_iA_icos(f_i)... Standard: Jxx = 1 - Σ q_i k_i A_i D_x² cos; Jzz similar; Jxz... I'll approximate scalar: fold = Σ q_ik_iA_imax(cos(f_i),0) (k_i?) eh. Let me just do: crest = Σ (A_i * smoothstep(0.6,1.0,cos(f_i))) normalized → high at coincident crests. Plus whitecap noise modulation in fragment via detail texture's alpha channel (threshold breakup). Good enough — stylized.Fragment:
Sky function shared:
vec3 skyGradient(vec3 dir)with uniforms sunDir, colors (zenith, horizon, sunColor, sunI). I'll compose shader strings: const SKY_COMMON =...; used by sky material and water material.Clouds reflection? skip (too heavy) — horizon haze covers.
Night: darken water colors by uniforms lerped by day-night factor (drive via JS updating uniform colors — I'll compute palettes in JS per frame and set uniforms: zenith, horizon, sun, water deep/shallow, fog). This gives full palette control from JS — great: single source for day cycle in JS. GLSL sky = gradient + sun + clouds(tinted) + stars with those uniforms.
Day cycle
timeOfDay in hours [0,24). Sun elevation = sin curve: elev = sin((t-6)/12*π) for t in [6,18] day... azimuth rotates too. sunDir from spherical:
simplify: elevation = Math.sin((timeOfDay-6)/12Math.PI) → -1..1 across 0..24 (at 0h: sin(-π/2)=-1 midnight, 12h: 1 noon, 18h: 0 sunset). azimuth = (timeOfDay-12)/12π*0.8 (sun swings east→west). dir = normalize(cos(el)*sin(az)... with el asin). Fine.
Palette keyframes (hour → colors):
- 0 night: zenith #060a18, horizon #0d1526, sun(moon) #aebfe8, water deep #04101c shallow #0a2733, fog #0a1220
- 5 dawn-ish..., 6 sunrise: horizon #ff9a56, zenith #5a7ec2...
- 12 day: zenith #2a6fd4, horizon #bcd8ef, sun #fff3d6, deep #06324e, shallow #1e8f96, fog #a8c8e0
- 18 sunset: horizon #ff7847, zenith #3d3f7d ...
- 20 dusk: ... Implement palette as array of stops; lerp per frame → set uniforms. Also directional light (for boat/gulls) color/intensity + hemisphere. Also star opacity uniform = smoothstep of -elevation. Moon? The sun disc becomes moon-ish at night — just dim disc + stars; fine.
Auto-cycle toggle + speed; default start ~10:30 morning, cycle on at 24min/day? Make default speed slow (full day in ~6 min) and user-controllable, plus a "pause time" toggle. Also golden-hour button? Keep gui sliders.
Boat build
Loft hull:
Section shape: superellipse-ish: for s in [0..1] around half perimeter from port gunwale → keel → starboard gunwale: θ = s*π; x_sec = (B/2)wprofilecos(θ)... y: gunwale at FB, keel at -D: y = FB - (FB+D)*sin(θ)^0.9. At θ=0 → y=FB (gunwale), θ=π/2 → y=-D keel, θ=π → gunwale. x = -(B/2)*cos(θ)*halfW → port(+x?) fine.
Station profile along length t in [0,1] (stern→bow): halfW(t) = sin(πclamp(t1.06,0,1))^0.75 — stern not fully zero (transom): use halfW = 0.22 + 0.78sin(πpow(t,0.85))^0.8? At t=0 transom width 0.22B... At t=1 bow → sin(π1^0.85)=0 → point. Keel depth profile: keelT(t) = sin(πt0.9)^0.6, deeper amidships. Freeboard: sheer rises at bow: FB(t) = 0.45 + 0.35pow(t,3) + 0.1pow(1-t,2).
Generate grid (stations × around), indices, then computeVertexNormals, then toNonIndexed + computeVertexNormals again for flat facets? Flat shading on hull looks stylized-good. I'll toNonIndexed() then computeVertexNormals() → faceted.
Deck: ellipse-ish plane covering gunwale perimeter: build fan from gunwale loop points (the θ=0 and θ=π rails across stations) — simpler: ShapeGeometry from deck outline (station t → halfWB/2 points mirrored) at y=FB(t)-0.02... deck camber ignore. Use THREE.Shape with points, ShapeGeometry, rotate flat, wood color, slight roughness. Actually deck y varies along length (sheer) — ShapeGeometry is flat. Alternative: build deck strip grid from the same param function (stations × 2..3 across) as a separate BufferGeometry — straightforward: for each station, deck edge points at y=FB(t): quad strip between port rail and starboard rail. I'll generate small grid stations×5 across, y = FB(t) - 0.03(1-(x/halfw)^2) slight camber... keep flat y=FB(t)-0.04. Fine.
Cockpit: small dark inset box; cabin trunk: low rounded box amidships; mast at t≈0.45 from bow? mast position z: +0.6 (bow at +z?). Define boat forward = +z. Bow at z=+L/2.
Sails: main = triangle grid: tack(0,1.2,0.2 aft of mast), head(0,6.4,mastZ), clew(0,1.35,-2.6). Bent: offset x = 0.35sin(πu)sin(πv). Build as PlaneGeometry-like custom grid with 10×10 then reshape via barycentric on triangle: vertices (u,v) → p = A + u(B-A) + v(C-A) with u+v≤1... use standard right-triangle grid mapping. Material DoubleSide, MeshStandardMaterial white warm, flatShading false; add subtle sail number texture? skip. Jib: triangle to bow.
Also boom cylinder along clew-tack. Rigging: 2-3 LineSegments (forestay, backstay, shrouds) with LineBasicMaterial color #2b2f36.
Flag: small plane at masthead; animate by rotating/skewing in JS (simple sine on geometry? just swing rotation.z slightly and scale). Keep simple: a tiny triangle mesh fluttering via rotation.
Lantern: small sphere emissive amber at stern + one at masthead (white) — visible at night; intensity via material.emissiveIntensity driven by night factor.
Propulsion markers: none (sail).
Bob/heel: group hierarchy: boatGroup (position/heading) → hullGroup (pitch/roll/heave) → parts.
Boat physics
State: pos (x,z), heading θ, speed v, turn rate. Controls: throttle target sail trim [0..1] (W/S adjust, or keys set), rudder input [-1,1]. Wind: global windDir + windSpeed; sailing physics simplified: boat speed = f(wind)·trim − drag; heel from apparent wind side. Full sail physics (points of sail) is fun: boat accel depends on angle between heading and wind: efficiency = 0.35 + 0.65*max(0, sin(angleToWind)^?)... Let me do:
- windAngle = |wrapAngle(windDir - heading)| (0 = running downwind? define windDir as direction wind blows TOWARD). Best sailing at beam/broad reach: eff = 0.25 + 0.75 * pow(sin(windAngle*0.5 + 0.45), 2)? Hmm keep intuitive: eff = clamp(sin(windAngle), 0.15, 1)? sin(0..π): downwind (windAngle=π) → sin=0?? wrong.
Simplify to arcade: maxSpeed = 6 + windSpeed0.35, accel = trim * maxSpeed * (0.45 + 0.55reachEff), where reachEff = sin(clamp(windAngle,0,π))^0.5 with min 0.35 so never fully stuck; "in irons" zone when heading within 30° of wind source: eff *= smoothstep(0.1, 0.5, windAngleFromHead... ). I'll implement:
v += (target - v) * accelRatedt with target = trim * maxSpeed * pointEff. Turn: ω = rudder * (0.5 + 1.1min(v/6,1)) * (reversed if v<0) ; heading += ω dt; drift: velocity vector lerps toward heading direction (keel lateral resistance): vel = mix(vel, forwardv, 1-exp(-dt2.5)). Add leeway slight downwind push. Heel: roll = -heelSide * (windSpeed0.02 + v0.012) + rudderRoll; heelSide = sign of cross wind component... rollTarget = clamp(crossWind * windSpeed * 0.02, ±0.5 rad) — compute crossWind = sin(heading - windToward+π?) = component. Plus turn roll. Smooth via exp damp.
Buoyancy pose: sample waveHeight at 4 points (bow, stern, port, starboard) in world:
Smooth with exp damp; done relative to boatGroup which holds y at water level 0. Note boatGroup.position.y = 0; hullGroup offsets.
Collision: ahead probe: p = pos + fwd*(v*0.8 + 3); if sampleHeight(p) > -0.8 → shallow/land: push back along gradient: compute gradient via finite diff of sampleHeight, steer: v = 0.6, pos -= gradNorm * penetration... simple: pos.x -= g.xk; heading += turnAway. Plus throttle cut + soft shake + thud audio. Also if boat itself on land (h>-0.4) → stronger push. Works.
Wake
InstancedMesh planes (or instanced BufferGeometry with custom shader). Pool 160. Each particle: pos, age, life(2.2s), size grows 0.6→3.2, alpha (1-a)^1.7 * 0.55. Spawn at stern two points (port/starboard) rate ∝ speed (every 0.02s at speed 8 → pool covers ~4.4s… at 160 fine) plus bow wave specks when speed>4. Update CPU: write instanceMatrix (pos y = waveHeight(x,z,t)+0.06, rotation from age drift, scale) + per-instance alpha attribute; shader: soft radial texture (canvas 64×64 radial gradient with noise blotches), multiply alpha. Custom ShaderMaterial with instanceMatrix support + aAlpha attribute. transparent, depthWrite false, renderOrder above water? Water is opaque; wake drawn after with depthTest true → fine.
Foam texture: canvas radial gradient white→transparent + few random darker blotches cut? Actually foam = white blobs; generate 128×128, draw several overlapping soft circles, use as alpha map with luminance.
Spray (bow)
THREE.Points 240 pool, PointsMaterial with generated soft sprite map, additive? Water spray better normal blending white. Update in CPU (pos += vel*dt; vel.y -= 9.8dt; die on y<waveHeight). size attenuation. Spawn burst rate ∝ speed when v>3.5 & when pitching down into wave (relative vertical velocity of bow vs wave — compute hB now vs prev). Simple: spawn when v>3, rate (v-3)*6/s.
Gulls
18 gulls around 3-4 islands. InstancedMesh custom geometry:
- body: small elongated diamond (two tris), wings: two quads (4 tris). Attribute aWing = 0 body, ±1 wing side, and aSpan = distance along wing (for flap bend).
- Vertex shader: flapAngle = sin(uTime*7 + phase)*0.9; rotate wing verts around body axis (x): for wing verts: y += sin(flap)span... implement: wy = abs(pos.x); pos.y += sin(flap)wyside? Simplest: pos.y += sin(uTime7+phase) * abs(pos.x) * 1.1; also slight fold: x = 1 - 0.25abs(sin)*span. Looks flappy.
- Instance attributes: aPhase. Per-instance matrix updated CPU: circling: center island + radius r_i, angular speed ω_i, height base + bob; heading tangent. 18 matrix compose per frame — trivial cost.
- Fragment: flat color #f2f4f6 with slight dark wingtips (span attr), fog. MeshBasicMaterial-ish custom shader (no lighting needed) + fog. Gulls unlit looks fine as distant white birds.
Sky mesh
SphereGeometry(6000, 32, 16), BackSide, ShaderMaterial (depthWrite false, fog n/a). frag: gradient, sun disc+halo, clouds (fbm 4 octaves, domain warp 1, coverage uniform, tinted, silver lining via pow(max(dot(dir,sun)),?)), stars (hash sparkle, night only), horizon haze band. Colors from uniforms set by JS palette each frame. clouds projection: p = dir.xz / max(dir.y, 0.06) * 1.4 + wind drift; fade with dir.y<0.1. Also second higher cloud layer? one enough.
GLSL noise for sky: small coords (p ~ ± few) — sin hash fine, but mediump precision on mobile for sin(127.1*x) with x~5 fine. Use the classic hash without sin? Use fract(sin(dot)) standard — okay.
Fog
scene.fog = new THREE.Fog(color, 900, 5200)? Boat/gulls standard materials get fog automatically. Custom shaders (water/terrain/skirt) implement matching linear fog manually with same uniforms. Sky no fog. Terrain far edge (3200/2=1600) mostly inside fog range — fog near 600 far 4800 maybe. Tune: near 700, far 4500.
Lighting (standard materials)
- DirectionalLight sun (position = sunDir*100 relative to boat, target boat) — updates each frame; color/intensity from palette; no shadows (shadows off — cost/benefit low over water). Hmm boat without shadow fine on water.
- HemisphereLight sky/ground from palette (intensity 0.5).
- Ambient minimal at night.
Post
EffectComposer: RenderPass, UnrealBloomPass(strength 0.32, radius 0.6, threshold 0.82), VignettePass (custom ShaderPass: vignette + slight saturation lift), OutputPass.
Audio
WebAudio, master = GainNode → destination. Start on first gesture (button/key/click). Components:
- Ocean swell: looping noise buffer (2s pink-ish generated) → lowpass(320) → gain modulated by LFO(0.07Hz) depth — implement with two gain nodes crossfaded by slow LFOs for irregular swells; add second noise → bandpass(900, Q0.5) quiet "wash" gain tied to swell LFO via GainNode.gain automation? Easiest: schedule via setInterval-ish in frame loop: every frame set gains with smooth functions of t (using setTargetAtTime sparingly — direct .value sets each frame are fine, 60Hz param updates ok).
- Wind: noise → bandpass(240, Q=0.4) → gain = 0.05 + windSpeed*0.012 + gust(slow wander noise via JS).
- Bow wash: noise → bandpass(700 + v*60, Q=0.8) → gain = clamp((v-1.2)/9)^1.5 * 0.5.
- Gull cry: occasionally (rand 8-22s): two-three descending FM chirps: osc(sine) freq 1400→900 with vibrato, bandpass, gain envelope 0.12s, stereo pan random.
- Thud on grounding: lowpass noise burst + sine 70Hz pitch drop.
- UI: soft click on buttons. Mute toggle button 🔊/🔇, volume in gui. Suspend when tab hidden.
Noise buffer: create 2s buffer of white noise shaped pink-ish via 1/f filter approximation (Paul Kellet). Fine.
Input
Keys: W/↑ trim up, S/↓ trim down (sail trim 0..1 step 0.05, or hold = continuous), A/D rudder (hold, auto-center), Space = full/depower toggle? Space = brake (ease sheets). C camera mode cycle, P autopilot? Let: KeyV toggles autopilot ("vane"), KeyC camera, KeyM mute, KeyH hide UI? and snapshot button. Touch: drag left/right = rudder; drag up/down = trim. Show touch hint.
Rudder smoothing: rudderTarget ±1 while key held, else 0; rudder lerps.
Autopilot
When on: trim auto 0.85; rudder steers toward waypoint heading: target = cruise direction; probe land ahead 40 units; if shallow, turn toward deeper (sample left/right ±40° pick deeper). Wind no-go: if target heading inside no-go cone, offset by 45°. Good enough to watch hands-free; also used in cinematic camera.
Camera modes
- chase: pos = boat - fwddist(9.5) + up4.2, smoothed (exp damp 4/s); lookAt boat + fwd6 + up1.2; user drag adds yaw/pitch offsets that decay slowly; wheel zoom dist 6..16. FOV = 55 + speed*1.1 (lerp).
- orbit: OrbitControls enabled, target = boat position (lerp), damping; user free orbit; min 4 max 60, maxPolar 1.45 (above water).
- cinematic: camera path: pick anchor points slowly drifting around nearest island or boat vicinity: implement as smooth wandering: camTargetPos = boatPos + offset evolving via slow noise (radius 26, height 6-14), lookAt blends between boat and nearest island every ~12s crossfade. Autopilot auto-enabled in cinematic (store previous state to restore). FOV 50.
Transition: on mode switch, blend factor 0→1 over 0.8s using damped lerp anyway (keep persistent smoothed cam pos so transitions are smooth).
HUD
Top-left: title "AZURE ODYSSEY" gradient cyan→amber; subtitle; stats line: SPD kn, WIND kn arrow, TRIM %, DEPTH m, POS? speed in knots = v*1.94. Top-center pill: clock (HH:MM from timeOfDay) + mode + autopilot indicator. Bottom hint bar: key hints. Buttons bottom-right: 🔊 sound, 📷 snapshot, 🎥 camera, 🧭 autopilot. Toast for messages ("Grounding!", "Autopilot engaged"). Milestones? maybe distance sailed counter (nm). Fun: track total distance, show in stats; toast at 1/5/10 nm.
Snapshot
composer.render(); renderer.domElement.toBlob → download 'azure-odyssey.png'. Ensure render just happened in same tick.
Quality
gui: High/Medium/Low → setPixelRatio, rebuild water geometry segments (dispose & recreate geometry only — material persists), bloom resolution (pass.resolution? just strength) — bloom pass size set via setSize on composer; fine.
Structure & size
Estimate: HTML+CSS ~6KB, JS ~45-55KB. Write in 6 chunks.
Let me also double check three r160 APIs:
- THREE.DataTexture(data, w, h, format, type); .needsUpdate. RedFormat + FloatType — water shader texture().r. LinearFilter for float needs OES_texture_float_linear — widely supported; fallback to HalfFloat? To keep it simple and safe, I'll pack height into a RGBA8 texture? Precision 8bit over range -30..45 → 0.29 steps, bilinear ok but banding in depth color possible (depth color smooth function — banding barely visible). Hmm. Better: use FloatType + LinearFilter; if
renderer.capabilitieslacks extension, three handles? It won't error, just falls back? Actually WebGL2 with float linear without extension → texture incomplete → black. Risk. Safer: HalfFloatType with Uint16Array via THREE.DataUtils.toHalfFloat — R16F linear IS core WebGL2. I'll do that. In shader: texture(uHeightMap, uv).r returns float — fine. - InstancedMesh with ShaderMaterial: instanceMatrix auto — yes with
#ifdef USE_INSTANCINGguard (three defines it). I'll use it. - OrbitControls import path fine. lil-gui fine.
- OutputPass exists in r160 ✓.
renderer.outputColorSpacedefault SRGB in r160 ✓; with composer + OutputPass handles.- ACESFilmicToneMapping via renderer.toneMapping; OutputPass applies tone mapping? OutputPass performs tone mapping+sRGB based on renderer settings ✓. Custom shaders: tone mapping applied in OutputPass to everything (it operates on the composed buffer) ✓.
Sun specular with pow 700 — with bloom threshold 0.85 glints will bloom nicely.
One more consideration: water shader must avoid
cameraPositionissues — it's a built-in uniform in ShaderMaterial ✓.Terrain geometry: PlaneGeometry(3200,3200,seg,seg) rotateX(-π/2); then displace: pos.getX/getZ; y = sampleHeight + detail. detail: if y > -1: += (fbm(x0.05,z0.05,3)-0.5)1.6clamp((y+1)/8) (only on land); underwater: += (fbm(...)-0.5)*0.8. Then computeVertexNormals. Vertex count at 420 segs = 177k — computeVertexNormals on 177k verts fine (~50ms).
Terrain shader: lambert: col by ramp: underwater sand→rock dark; sand #d9c28a near 0..1.5 with jitter noise; grass #4e8a4a..#79a45b mid slope; rock gray steep; vegetation patches noise darker; snow none. slope = 1 - normal.y. wet band near y<0.3 darker sand. Slight ao by cavity? skip. Fog manual. Sun: max(dot(N,sun),0) * sunColor + ambient/hemisphere mix: upFacing * skyColor... Use hemi: mix(groundCol, skyCol, N.y*0.5+0.5)albedo0.4 + sun diffuse. Night dims via palette.
Terrain detail noise in shader for texture: cheap value noise 2 octaves at world scale 0.35 for color breakup + micro normal? skip micro normal, color breakup enough.
Terrain draws 420² = 352k tris — heavy for low mobile but quality setting reduces (Low: 220²). ok.
Skirt water + main water same material: but amplitude fade uses distance to uCamXZ uniform (update per frame) not cameraPosition (same thing) ✓.
Now let me think about the water UV for heightmap sampling: uv = (worldXZ + half)/size — needs worldSize uniform ✓ and beyond → clamped edge → deep (edges of map are deep? map edges may cut islands! Islands at |x|,|z| ≤ ~1000 with r≤330 → max extent 1330 < 1600 ✓ safe margin, edges deep.)
Detail normal texture: generate 256×256 RGBA DataTexture from fbm heightfield → normals via Sobel; wrap repeat. Two samples scrolling different dirs/scales in frag, perturb N. Also use its .a (store foam noise = another fbm) for foam breakup ✓.
Stars: hash(dirQuantized) — quantize dir by 200 grid on sphere: use 3D hash of floor(dir*180): sparkle = hash > 0.9985 → point; twinkle by time. Sun-independent ✓ night factor.
Moon: at night, second disc opposite-ish? Just make the "sun" the moon when elevation<0: use -sunDir as moon dir with pale disc, smaller halo. I'll compute moonDir = -sunDir... at midnight sun elevation -1 → moon up ✓. Moon disc: smoothstep on dot(dir, moonDir) ~0.9995 + halo 0.92; color pale #cdd8f0, drive via uniform uMoonI (night factor).
Clock UI updates.
Palette stops (7 stops × fields): fields: zen, hor, sun, sunI, deep, shallow, foam(#eef6f6 const), fog, hemiSky, hemiGround, lightI, starI, moonI. Stops at hours: 0, 4.5, 6.2, 8, 12, 16.5, 17.8, 19.2, 20.5, 24(=0). I'll write lerpStops(h).
Values (hex):
- 0.0 night: zen #050912, hor #0b1626, sun #9db4e6(moonlight), sunI 0.25, deep #03101c, shallow #0a2531, fog #0a141f, hemiS #16233a, hemiG #05070c, lightI 0.22, star 1, moon 1
- 4.5 pre-dawn: zen #0a1020, hor #1a2438, sun #b0c0e0 sunI 0.3, deep #041420, shallow #0c2c38, fog #101a28, lightI 0.28 star 0.6 moon 0.9
- 6.2 sunrise: zen #3a4d80, hor #ff9e5e, sun #ffd9a8 sunI 1.1, deep #06222f, shallow #14505a, fog #caa186→ hmm fog should match horizon #e8a37c, hemiS #7a7fb0 hemiG #2a2333, lightI 0.85, star 0, moon 0
- 8 morning: zen #2c68c4, hor #bcd9ee, sun #fff2d4 1.25, deep #07314a, shallow #1b8290, fog #b4d2e4, hemiS #9cc3e8 hemiG #3a4a42, lightI 1.15
- 12 midday: zen #2a6fd6, hor #c4e0f2, sun #fff6e0 1.35, deep #053350, shallow #1e95a0, fog #bcd9ea, lightI 1.3
- 16.5 afternoon: zen #2f66c0, hor #c3dcef, sun #ffedc8 1.2, deep #06304a, shallow #1c8a96, fog #b6d3e6, lightI 1.15
- 17.8 golden: zen #4a5aa8, hor #ffb070, sun #ffc27a 1.1, deep #07293d, shallow #16626e, fog #e0b490, lightI 0.95
- 19.2 sunset: zen #35346e, hor #ff7a4e, sun #ff9a5a 0.8, deep #061e30, shallow #104b58, fog #c98a72, lightI 0.6
- 20.5 dusk: zen #131a38, hor #3a2c50, sun #8a76b0 0.4, deep #041625, shallow #0c303e, fog #2a2438, lightI 0.35, star 0.35
- 24 → night stop again.
Wrap lerp across 24→0 ✓ (handle h<stop0 by treating +24).
sunDir: elevation: el = sin((h-6)/12π) — at h=0 → sin(-π/2) = -1 ✓; h=6→0 sunrise; 18→0 sunset. azimuth: az = (h-6)/12π → -π/2(east? whatever) … dir = (cos(el)cos(az), sin(el)? ... define: x = cos(el)*sin(az)*0.85, y = sin(el), z = -cos(el)cos(az) then normalize. At sunrise az=-π/2: sin=-1 → x negative (east = -x fine), z=0. Noon: el=1 → (0,1,~0). Sunset: az=π/2 → x+. Works. DirectionalLight position = boat + sunDir140, target boat. At night sunDir.y<0 → use moonDir=-sunDir for light (so light comes from above at night) ✓ and dim bluish.
Water sun spec uses sunDir when day; at night use moonDir with moon color — I'll just pass uniform uLightDir = (day? sunDir : moonDir) and uSunColor accordingly (palette sun field). Sky shader draws both sun disc (by sunDir, intensity fades as el→0, hide below horizon) and moon disc (moonDir, moonI). ✓
Also water "deep/shallow" colors darken at night via palette ✓.
Bloom threshold 0.82: sun disc + spec + lanterns + moon bloom ✓.
The frame loop order
dt clamp 0.05; t += dt; timeOfDay advance if cycle on; update palette+uniforms; boat physics; wake/spray; gulls; camera; water/terrain uniforms (time, camXZ); sky position follows camera (sky.position.copy(cam.position)); audio update; HUD update (throttle 4Hz); composer.render().
Wave uniforms: pass WAVE_DEFS as vec4 array uniform (dx,dz,amp,len) + vec4 array2 (speed,steep,0,0) or pack: uWavesA[6]=vec4(dx,dz,amp,len), uWavesB[6]=vec4(speed,steep,0,0). uWaveAmp global scale uniform (gui "swell" 0..2 multiplies amp) — but JS waveHeight must use same scale ✓. Also gui "wind" affects audio + heel + maxSpeed only (not wave shape) — fine, note label "wind". Also link swell→? keep independent.
Time uniform for waves: use global uTime (seconds) — same t as JS waveHeight(t). JS uses perf t. Avoid precision: GLSL t grows large (float mediump issues in vertex after hours: t=3600speed≈ phase k(... - 113600)=k39600 ≈ 2π6300 rad — sin precision degrades in mediump; vertex shaders usually highp on desktop, mediump on some mobile... risk of wave jitter after long sessions on mobile. Mitigate: wrap t: pass uTime = t % (2πL/k/speed...) — simplest: precompute per-wave phase offset in JS each frame: phase_i = k_i * speed_i * t mod 2π, pass as uniform (uWavesB.z = phase)! Then GLSL f = kdot(D,xz) - phase_i — no big numbers ✓. JS waveHeight uses f = k(D·xz) - phase_i too (same mod) ✓ identical. dot(D,xz) max |xz| ~ 6000(skirt)k(1 for len6.5: k=0.97) ≈ 5800 rad — sin of 5800 in mediump: precision ~2^-235800 ≈ 0.0007 rad fine.
Sky cloud time drift: small speeds fine.
GUI
lil-gui: folders:
- Ocean: swell (0..2), foam amount, water quality? no.
- Sky: time of day slider 0..24, cycle toggle, cycle speed, clouds coverage, stars? auto.
- Wind: wind speed 2..14, wind direction dial 0..360.
- Boat: autopilot toggle, reset position.
- FX: bloom strength, vignette, quality select, volume, mute. Buttons in HUD for sound/snapshot/camera/autopilot.
Distance tracker
dist += v*dt each frame; nm = dist/1852. Toasts at milestones.
OK — also "depth under keel" stat: sampleHeight(pos) → show m (negative→ depth = -h). Grounding toast cooldown.
Reset: place boat at (0, 0?) — island at (60,-60) r~? start pos: (-260, 240) heading toward open water; verify depth there: between islands… I'll pick start by scanning for deep spot: loop find pos with h<-8 within 600 of center — deterministic. Simple: try candidates ring around center island until deep & neighbors deep.
Edge of world: if |pos| > 2400 → wrap? Show toast "Edge of the chart — turning back" + auto turn? Just clamp push back gently like land collision using virtual wall. fine.
Now, let me write the code. I'll write to /space/demos/009-azure-odyssey/index.html in 6 append chunks. Then extract and node --check.
Careful bits:
- template literals inside heredoc: fine (no shell expansion with quoted 'HTML_EOF').
- GLSL: avoid backticks issues none.
- I'll define shaders as JS template strings.
Let me now write chunk 1: HTML head, CSS, body, import map, script start, config, utils, noise, waves, islands, bake (async with progress).
Actually — the loading overlay: bake takes ~0.5-1.5s + terrain gen ~0.2s. Show overlay with progress bar updated during bake; hide at first frame.
Let me write it all now. I'll be meticulous about matching function names/usage.
Plan the GLSL chunks:
WAVE_GLSL (vertex, shared by water patch & skirt):
I'll pass phase in uWavesB.z precomputed by JS. speed not needed in shader.
Main vertex (water):
Note: since mesh positioned at quantized offsets, position attr local — using modelMatrix includes translation ✓.
JS waveHeight mirror (no fade, no disp):
with phase_i = (kspeedt) % 2π. And in GLSL f uses same. ✓ Note fade not applied to JS (boat always near camera < 380) ✓.
SKY_GLSL fragment:
Cloud color at night: uHor dark → clouds dark ✓ moonlit slight via +uMoonCol*pow(max(md,0),4)0.2uMoonI. ok add.
hash3 for stars:
vnoise/fbm 2D standard with hash21:
WATER fragment (with shared sky-based reflection — simplified: reflect dir → gradient + sun spec + moon; skip clouds/stars in reflection except slight sparkle):
Hmm shore foam should also lighten water before foam: near shore add slight white wash — covered by shoreF foam. ok.
Note uSunDir here is the "light dir" (sun by day, moon by night) and uSunCol includes intensity — I'll just pass color premultiplied and drop uSunI in water; keep uniforms minimal: uLightDir, uLightCol. Sky keeps separate sun/moon.
SSS term uses uLightDir.
TERRAIN fragment:
Add cheap 2D vnoise in terrain frag (hash21) ✓.
Wake shader:
uColor = foam color uniform (palette) ✓.
Gull shader: described earlier with instanceMatrix + aPhase attr:
InstancedBufferAttribute aPhase per instance — instanced attr works with InstancedMesh? InstancedMesh geometry can have InstancedBufferAttribute ✓.
Spray: PointsMaterial with map, transparent, depthWrite false, size 0.9, sizeAttenuation, opacity dynamic? PointsMaterial global opacity — per-particle fade impossible without shader. Use ShaderMaterial for points with attribute aSize, aAlpha. Simple custom points shader. fine.
Now the JS for spray pool & wake pool — standard ring buffers.
Boat hull geometry code as planned.
Camera/FOV: PerspectiveCamera(55, ar, 0.1, 12000). Sky radius 6000 < far ✓ skirt 6000 → diameter 12000... far plane 12000 → skirt edge at 6000 from center; camera moves ±2400 → max dist 8400 ✓ within far. Set far 15000 safe.
renderer.setClearColor(fog) as backdrop.
Alright, HUD stats: speed knots (v*1.94384), wind kn, trim%, depth m (max(0,-h)), dist nm, clock.
Autopilot algorithm per frame:
Roam desired heading: changes slowly: desired += (vnoise(t*0.02)-0.5)0.4dt... and bias to stay within 1200 of origin: if r>1100 steer toward center blend.
Manual grounding: if depthAt(bow) > -1.2: v = (1-2.5dt clamped), push = normalize(grad) * (overlap), pos -= pushdt*3, throttle limited, thud if v>2 & cooldown, shake.
Also general world bound: if r>2350: push toward center.
Wind: windDir (toward, radians) uniform from gui; windSpeed kn display = windSpeed*1.94.
Boat speed: maxV = 4.5 + windSpeed0.42 (units/s ~ knots/1.94 → 8.7-21kn... too fast; maxV 3.5+wind0.35 → at wind 8 → 6.3 u/s = 12kn ok).
Time to write! Also remember: update index.html.
Total JS will be long; let me carefully write chunk by chunk, keeping a mental symbol table. I'll add section banners.
One more detail: OrbitControls import needed. Also
import GUI from 'three/addons/libs/lil-gui.module.min.js'.Buttons wiring + toast + floater-less (no score).
Snapshot filename 'azure-odyssey.png'.
localStorage: persist best distance? persist settings? Keep: persist nothing except maybe best distance "Farthest voyage" — nice touch: localStorage azureOdyssey.bestNm.
Mobile: media query CSS, touch handlers, quality default Medium if small screen.
Alright, writing chunk 1 now. I must be careful that when appending with
cat >> ... <<'EOF', content is literal.Let me draft the entire file mentally as I go, then validate.
Chunk sizes: I'll do 5 chunks. Let me start.
Also: directory name 009-azure-odyssey.
Important: three r160 ShaderMaterial + InstancedMesh: three defines USE_INSTANCING and declares attribute mat4 instanceMatrix in the prefix for ShaderMaterial (non-raw). Confirm: WebGLProgram builds prefixVertex including:
for all materials when parameters.instancing true (object.isInstancedMesh). Yes.
instanceColor only if used. fine.
For Points ShaderMaterial: attribute declarations: position is auto-declared; custom attributes declare manually.
For the water ShaderMaterial used by two meshes (patch + skirt) — both plain Mesh, fine.
fog uniforms shared object so one update hits all: create
shared = { uFogColor: {value: new Color}, ... }and spread into each material's uniforms (same object references) ✓.Now — writing.
Chunk 1: doctype→CSS→body UI→importmap→script open→config/palette/utils/noise/waves/islands/height+bake.
Let me write out full code carefully.
Also loading overlay div with bar.
Careful: in JS template strings for shaders, avoid
${unintended.Start writing chunk 1.
Note on
smoothstepJS helper,clamp,lerp,wrapAngle,smoothstepJS.Bake async: iterate rows, every 24 rows await frame. Progress bar width %.
Height function: (final)
Check coast: body=0 → (m)^1.5 = 3.4/(peak+3.4); peak 14..44 → 0.072..0.163 → m ≈ 0.17..0.26 → t*(0.42+1.450.25avg...) n avg 0.5 → ridge 0.33 → factor 0.9 → t≈0.2-0.29 → beach inset from r by 0.2r..0.29r ✓ reasonable width of shallow shelf (shelf extends to 2.1r — long shallow approaches: at d=r shelfH = -26+22.8smoothstep(2.1r→0.75r: at d=r: (2.1-1)/(2.1-0.75)=0.815 → smooth ≈ 0.93 → shelfH ≈ -4.8; at d=1.5r: 0.44→ ~-15). So at island edge (d=r) seabed ~-4.8 rising — nice gradual. Peak at center with m up to 1: body up to peak ✓. Mountains modest. Good.
But wait: smoothstepJS with edge0 > edge1 — standard implementation clamp((x-e0)/(e1-e0)) handles negative denominator fine ✓.
Terrain geometry detail: after bake, geometry samples baked via sampleHeight + fine detail noise direct.
sampleHeight outside range → -26.
Boat probe uses sampleHeight (fast) ✓.
Now ISLANDS layout: hand place ~9 as above but ensure spacing > sums of r*? to avoid overlap: positions spread ±1000, r 150-330. Distance between (-620,-420) and (-180,-900): dx=440,dz=480 → 651; r1+r2 up to 660 — possible slight overlap; overlap just merges islands (max) — acceptable, can look natural. Fine.
Make center island (60,-60) smaller scenic. Start position: scan.
Write chunk 1 now. I'll aim each chunk ≤ ~13KB to be safe with the tool.
Let me count chunks:
- HTML+CSS+UI+importmap+script start: config, utils, noise, waves, islands, analyticHeight, bake start (async)
- textures (heightmap tex, detail normal tex), sky, terrain, water(shaders+meshes)
- boat (build, sails, physics, collision), wake, spray, gulls
- audio engine, input, cameras, autopilot
- palette/daycycle, GUI, HUD, snapshot, frame loop, boot
Writing now. Also I must double-check every uniform used in GLSL exists in material uniforms.
Water uniforms list: uTime? (detail scroll + shore anim) yes, uWavesA, uWavesB, uSwell, uCamXZ, uFadeStart, uFadeEnd, uHeightMap, uWorldSize, uDeep, uShallow, uFoamCol, uLightDir, uLightCol, uZen, uHor, uFogColor, uFogNear, uFogFar, uDetailMap, uFoamAmt, uCrestLo, uCrestHi.
Skirt uses same material ✓ (its verts beyond fade → flat; fragment same; heightmap uv outside clamps → deep ✓).
Terrain uniforms: uLightDir, uLightCol, uHemiSky, uHemiGround, uFogColor, uFogNear, uFogFar.
Sky: uSunDir, uMoonDir, uZen, uHor, uSunCol, uMoonCol, uSunI, uMoonI, uStarI, uCloudCover, uTime, uCloudAlpha.
Wake: uMap, uColor, (instanceMatrix auto).
Gull: uTime, uFogColor/Near/Far, uColorA, uColorB, uLight? basic.
Spray: uMap, uColor.
Shared uniform objects pattern for palette-driven ones.
Alright, writing files now. Chunk 1:
- Commented
Conventions confirmed. Now I'll create demo 009 — "Azure Odyssey": an open-ocean sailing experience with a procedural archipelago — Gerstner-wave GLSL water, baked-heightmap islands, a full day/night sky dome with procedural clouds and stars, buoyancy-driven sailing physics, wake/spray particles, seagulls, synthesized WebAudio ambience, bloom post-processing and multiple camera modes. I'll write the file in chunks.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
Other Conversations
Explore other conversations with this agent.