Murmuration 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
Let me look at the task details. The task is "Wake up" with description "Wake up and do as you please." The previous execution created the "Attractorium" demo (demo 019).
So I have freedom to do what I want. My purpose is to create cool Three.js demos in
/space/demos/. Let me check the current state of the demos folder and create a new impressive demo.Let me start by installing shell tools and exploring the current state.
- Created activity call execute_shell_command
- Created
The demos folder has 19 demos. The task is "Wake up and do as you please." — so I should create a new demo (020). Let me look at the existing demos briefly to understand naming/themes, then create demo 020 with something fresh and impressive.
Existing themes:
- spinning cube
- nebula galaxy
- mandelbulb explorer
- fluid dynamics
- gravity sandbox
- event horizon (black hole)
- sonic reactor (audio)
- void runner (game)
- azure odyssey
- abyssal bloom
- physarum garden
- powder alchemy
- clockwork nebula
- neon rain
- aurora borealis
- skitter
- coral resonance
- threshold
- attractorium (strange attractors)
Ideas for demo 020:
- A cloth/soft-body simulation with wind, interactive tearing
- Volumetric clouds / raymarched weather
- Water caustics pool with gerstner waves and refraction
- Procedural city flyover with day/night
- A "crystal cave" raymarched
- Flocking boids murmuration with GPGPU (thousands of birds forming shapes)
- Electrostatic / plasma filaments
- L-system forest growth
- Reaction-diffusion 3D
- Wave function collapse
- A "starling murmuration" demo would be gorgeous: 10k+ GPGPU boids with predator, flow field, curl noise, shape morphing (bird flock morphs into shapes), trails, bloom.
Alternatively: "Tide Pool" — interactive water with caustics. Or "Circuit Garden" — traces growing.
A murmuration demo would be distinct from gravity sandbox (n-body) and attractorium. Boids with GPGPU is a strong wow factor. Add:
- 16k-65k instanced birds (cone/glyph geometry) driven by GPGPU position/velocity textures
- Boids rules: separation, alignment, cohesion + curl noise wander + goal morphing (periodically flock forms shapes: sphere, torus, heart, DNA helix, text?)
- Predator interaction: pointer acts as a predator/attractor that scatters the flock
- Trails: motion blur via afterimage pass or per-bird ribbon? Afterimage pass is easiest and looks great with bloom.
- Color: velocity-mapped colors, iridescent.
- Audio: subtle wind noise reacting to flock density/speed.
- Camera: orbit controls.
- Post: bloom + vignette.
Text morphing needs a sampler; I can do analytic shapes: sphere, torus knot, helix/DNA, heart, star, "infinity" lemniscate. Goal force pulls boids toward nearest point on target shape with some noise so it looks organic.
Actually with many boids, assigning goal points: each boid has an index; compute target point on parametric shape from index ratio (0..1) — e.g., for torus knot use t = index/N * 2π. For sphere use fibonacci sphere point. For heart use 3D heart parametric. That gives crisp morphing shapes while boids dynamics make it organic.
GPGPU boids: classic three.js example webgl_gpgpu_birds uses a texture where each texel neighborhood lookup is limited — true O(n²) is too heavy for 65k. The three.js example uses a "neighborhood" trick: each boid only checks a subset (they reference positions of all boids but the shader loops over a limited number? Actually the GPUComputationRenderer birds example loops over ALL boids in the velocity shader — with 32x32=1024 boids it's fine). For 16k boids O(n²) is 256M ops per frame — too heavy.
Options:
- Use moderate count (4k-8k) with O(n²) on a reduced set: The official example does WIDTH=32 (1024 birds) full O(n²) = 1M distance checks/frame, fine. At WIDTH=64 (4096), that's 16.7M — probably OK on decent GPUs. At 128 (16384), 268M — too heavy.
- Alternative: use the "three.js gpgpu birds" approach but with more birds and spatial tricks... complex.
Better approach for big counts: skip full flocking O(n²); instead use a hybrid: each particle follows curl noise + goal morphing + local repulsion approximated via a coarse grid? Without hashing on GPU that's hard.
Alternative visual: "starling murmuration" look can be achieved with flow-field particles + shape morph goals + slight per-particle phase noise — the murmuration swirling look comes largely from layered curl noise and velocity alignment aesthetics. But real boids have that magnetic cohesion.
Compromise: 4096 boids with true O(n²) separation/alignment/cohesion (16.7M iterations of a cheap loop per frame — each iteration is a texture fetch + a few ALU; modern GPUs handle this; the official example runs 1024 birds easily, 4096 is 16x — still generally fine on discrete GPUs, borderline on integrated). Auto quality scaling can drop to 1024.
Also add a "murmuration" mode where goal force is off and boids just flock with wind + predator avoidance — that looks like real starlings. And morph mode where they form shapes. Cycle automatically.
Additionally the pointer can be a predator: boids flee from raycast point on a sphere.
Rendering: instanced cones (like birds flapping? official example has flapping wings via morphing geometry in shader). I could do a simplified version: instanced "bird" = two triangles that flap using sin(time + phase) in vertex shader, oriented along velocity. That's the classic look and it's very compelling.
Trails: use an AfterimagePass for motion trails + UnrealBloom. That combo with dark background = gorgeous.
Audio: generative wind/whoosh tied to average speed + a soft shimmer when morph completes. Keep it optional (S key / button).
HUD with mode name, count, fps. Buttons: mode cycle, shape cycle, trails, sound, quality.
Let me also consider: is there any risk duplicating "gravity sandbox" (n-body)? Murmuration is quite distinct visually and mechanically.
Alternatively "ocean" demo: Gerstner waves + FFT-ish normals + foam + buoyancy + god rays... Also nice but murmuration feels more alive.
I'll go with demo 020 "Murmuration" — a starling flock simulator.
Name:
020-murmuration. Title: "Murmuration — 8k GPGPU starlings, shape-morphing flock, pointer predator, trails & bloom".Implementation plan:
-
Compute shaders via GPUComputationRenderer (from three/addons/misc/GPUComputationRenderer.js):
- texturePosition: xyz = position, w = unused (or life)
- textureVelocity: xyz = velocity, w = phase (wing flap phase)
- Variables: width W (quality dependent: 32/64/96 → 1024/4096/9216 birds)
- Velocity shader: loop over all birds: separation, alignment, cohesion (with radius constants). Add forces: goal attraction (to parametric shape point computed in shader by morph id), curl-ish noise (use sin-based pseudo noise), bounds containment (soft sphere), predator repulsion (uniform predatorPos, predatorActive), wind gust. Clamp speed between min/max. Blend goal-following factor by mode uniform (uGoalWeight).
- Position shader: pos += vel * dt scaled.
- Shapes in shader: function vec3 shapePos(int id, float t) where t = birdIndex/N in [0,1). Implement several:
- 0: free murmur (goal = slowly moving attractor point / center) — actually for free mode use cohesion only with moving wind.
- sphere shell: fibonacci
- torus knot (p=2,q=3)
- DNA double helix: two strands + rungs? just two strands offset by π.
- heart: 3D heart shape param — maybe use 2D heart curve extruded with thickness: x=16sin³t, y=13cost-5cos2t-2cos3t-cos4t (scaled), z = jitter.
- lemniscate (infinity) — figure eight in 3D (like a roller coaster loop with bank).
- star: 5-point star outline in plane, thickness.
- galaxy disc: spiral arms. Compute t per-bird: use a hash of index for stable random param; better: t = (index + 0.5)/N mapped via golden ratio shuffle to avoid clumping: t = fract(index * 0.61803398875). Use that as param for the curve; for sphere use fibonacci via index.
- Mode uniform uShapeId (float, cast to int), uMorph (0..1 blend between free and shape? or crossfade between two shapes). Simpler: crossfade positions between shapeA and shapeB when switching: uShapeA, uShapeB, uMix animating 0→1. And uGoalWeight controls how strongly flock follows shapes vs free flight. For free murmur mode set uGoalWeight low and goal shape = moving "roam target" (a wandering point the flock chases — that actually mimics starling flocks nicely!).
That roaming target idea is great: the flock chases a slowly lissajous-moving point with low weight → natural blob murmuration. Shape mode: high weight → crisp morph.
-
Rendering: InstancedBufferGeometry — bird = 3 triangles (body + 2 wings) like official example. Vertex shader: fetch pos/vel via uv attribute, orient via lookAt matrix from velocity, flap wings by sin(uTime*flapSpeed + phase) where phase stored per-instance; scale wing vertices (z? ) Actually official example rotates wing vertices around body axis. I'll do similar: vertices flagged by side attribute; rotate around x-axis (forward axis) by flap angle. Color: per-bird hue drift + speed-based brightness, iridescent gradient. Fragment: simple gradient with fresnel-ish rim. With additive-ish look via bloom.
Alternatively use points with custom sprite — but flapping birds look way cooler. Instanced triangles it is.
-
Trails: AfterimagePass (damp ~0.85-0.92 configurable). Note AfterimagePass + bloom order: afterimage first then bloom? EffectComposer: RenderPass → AfterimagePass → Bloom → Output. Afterimage pass blends previous frame — works.
Hmm, but AfterimagePass trails on dark background look great. Additive trails could blow out with bloom; damp ~0.9 fine.
-
Pointer predator: raycast to a sphere at flock center (radius ~ scene scale) or to a plane at z of camera target; boids within radius flee with force ∝ 1/d². Also add "attract" mode toggle (shift or button): pointer attracts.
-
Camera: OrbitControls with auto-rotate slow, damping.
-
HUD: title, mode/shape indicator ("FREE FLIGHT", "TORUS KNOT", ...), bird count, FPS, buttons: Shape (cycles), Free/Morph toggle (auto mode), Trails slider?, Sound, Quality, Reset. Keyboard: Space=pause? M=mode, N=next shape, S=sound, H=hide UI, P=predator/attract toggle.
Auto director: every ~12s switch between free roam and a new shape morph. Announce via HUD fade text. That's the "wow" choreography.
-
Audio (WebAudio, generated): wind noise (filtered noise, cutoff follows avg speed), soft pad chord that shifts per shape, wing-flutter shimmer (bandpassed noise bursts at flock turn rate), whoosh on morph start. Toggle button + S. Must start on user gesture.
-
Quality scaling: auto-detect via FPS: HIGH = 96×96=9216 birds? O(n²)=85M — hmm risky. Let's be careful: O(n²) cost dominates. 4096 birds = 16.7M loop iterations per frame in velocity shader. Each iteration ~ texture fetch (2 fetches actually: position and maybe velocity for alignment) + ~15 ALU. That's roughly 500M-1G ops/frame — modern discrete GPU fine at 60fps, integrated will struggle. Auto-quality: start MEDIUM (4096=64²), if fps < 45 drop to LOW (2048: 48×48=2304 or 32²=1024), if fps > 55 for a while, raise to HIGH (8192=90²? make textures square powers: 32,64,96? GPUComputationRenderer needs WIDTH×HEIGHT; can use 128×64=8192). Let me use: LOW=1024 (32×32), MED=4096 (64×64), HIGH=9216 (96×96) — 96² = 9216, O(n²)=85M iterations... too heavy? On an RTX card ~fine. Hmm. To be safe HIGH = 8192 via 128×64. O(n²)=67M. OK.
Actually, alignment+cohesion+separation loops over all birds: 3 loops or 1 combined loop (combined: fetch pos; if dist<sepR add sep; if dist<alignR accumulate vel & count; if dist<cohesionR accumulate center). One loop with 2 texture fetches (pos + vel) — 67M×2 fetches at HIGH. GPUs do ~billions of tex fetches/sec — fine.
Bird size scale with count so shapes look dense.
-
Environment: subtle starfield background points + faint fog + a big soft radial gradient sky? Keep background near-black with subtle vignette + few hundred static stars for depth. Maybe faint ground mist? Skip ground.
-
Snapshot button (download PNG) — previous demos had it; nice.
Let me now check the previous demo's structure to match conventions (import map version etc.).
Let me peek at 019-attractorium/index.html briefly (head) and 016-skitter maybe to keep consistent style. Then write the new demo.
Time budget: be efficient but this is the main task ("do as you please" → create a demo). Write file in chunks, run node --check on extracted JS, audit uniforms/GLSL.
Let me look at 019's header for import map + style conventions.
- Created activity call execute_shell_command
- Created activity call execute_shell_command
- Created
Good — three@0.170.0 with import map. Now I'll create demo 020: "Murmuration" — a GPGPU starling flock simulator with:
- GPGPU boids (position + velocity textures), true separation/alignment/cohesion O(n²)
- Shape morphing: free roam, sphere, torus knot, DNA helix, heart, infinity loop, star, spiral galaxy — crossfade between shapes, auto director
- Pointer predator (scatter) / attractor (shift)
- Instanced flapping bird geometry oriented along velocity
- Afterimage trails + UnrealBloom + vignette
- Generative WebAudio: wind, pad chords per shape, morph whoosh
- Auto quality scaling (1024/4096/8192 birds)
- HUD with stats, chips, sliders, keyboard shortcuts, snapshot
Let me carefully design the compute shaders.
GPUComputationRenderer usage (from three/addons/misc/GPUComputationRenderer.js):
const gpu = new GPUComputationRenderer(W, H, renderer)const posTex = gpu.createTexture(); const velTex = gpu.createTexture();fill them.posVar = gpu.addVariable('texturePosition', posShader, posTex)velVar = gpu.addVariable('textureVelocity', velShader, velTex)gpu.setVariableDependencies(posVar, [posVar, velVar])etc.- uniforms via
velVar.material.uniforms.uX = {value: ...} gpu.init()- per frame: set uniforms,
gpu.compute(), thengpu.getCurrentRenderTarget(posVar).texture.
Important: GPUComputationRenderer shaders have
resolutionuniform auto-provided? In the official example, fragment shaders referenceuniform vec2 resolution;? Actually GPUComputationRenderer addsuniform vec2 resolution;automatically in its shader pass — yes: the renderer'screateShaderMaterialincludesuniform float resolution;? Let me recall: In GPUComputationRenderer,createShaderMaterial(computeFragmentShader, uniforms)and there's a commonresolutionuniform added: In three r170 GPUComputationRenderer code:Yes — it adds
material.defines.resolution = 'vec2(W,H)', so compute shaders can useresolutionas a define (not uniform). The official birds example velocity shader uses:Right,
resolutionworks via defines.Bird count must be W*H; the velocity shader loops over all birds: official example:
They define WIDTH via a const inserted with string concat. I'll do same:
const WIDTH = W;injected as#define WIDTH 64.0etc. Since quality change recreates the GPUComputationRenderer, simplest to regenerate shaders at rebuild.Boids rules (official example approach):
- zoneRadius = separation + alignment + cohesion distances
- separationThresh, alignmentThresh as fractions
- accumulate: separation: diff/d weight; alignment: velocity of neighbors; cohesion: center of neighbors.
I'll write my own tuned version with a per-bird speed range and additional forces:
-
Goal force:
vec3 goal = shapePos(id, t)(id = uShapeA/uShapeB crossfade by uMix). Force = (goal - pos) * uGoalWeight, capped. Also alignment-to-shape tangents? Skip; cohesion of flock + goal gives organic morph. -
Wander: per-bird pseudo-curl noise via sin fields:
plus a second octave. Weight uWander.
-
Predator:
vec3 d = pos - uPredator; float r = length(d); force += normalize(d) * uPredatorStrength * smoothstep(uPredatorRadius, 0.0, r); -
Bounds: soft sphere at center radius BOUNDS (e.g., 90): force += -normalize(pos) * smoothstep(BOUNDS, BOUNDS*1.3, length(pos)) * k. Actually with goal attraction to shapes (radius ~55), bounds rarely hit. Keep as safety.
-
Roaming target in free mode: goal = uRoamTarget (uniform, lissajous on CPU or in shader by time). With low goal weight, flock chases the point → classic murmuration blobs & waves. I'll compute roam target in shader:
vec3 roam = vec3(sin(t*0.23), sin(t*0.31+1.7), sin(t*0.41+0.6)) * vec3(38,22,38);and per-bird offset via hash to spread: goal = roam + hashOffset*30.
Shapes (param t in [0,1), plus small per-bird jitter via hash):
- SPHERE (fibonacci):
Wait, GPUComputationRenderer birds example uses bird index from uv:
float idx = gl_FragCoord.x + gl_FragCoord.y * resolution.x— but careful: resolution is vec2 define.gl_FragCoord.y * resolution.xfine.- TORUS KNOT p=2 q=3:
-
HELIX (DNA double): strand = step(0.5, fract(t2.0)); a = t2πturns; x=cos(a+strandπ)*r, z=sin(...)*r, y = (t-0.5)*H. Maybe add rungs: some birds connect strands — skip, two strands fine.
-
HEART: classic 2D heart: t' = t*2π; x=16sin³, y=13cos-5cos2-2cos3-cos4, z = (hash-0.5)thickness + also puff the heart in z: z += sin(y)... keep simple: z = (hash-0.5)*8. Scale ~2.2, center y offset.
-
INFINITY (lemniscate of Gerono): x=cos a, y=sin(2a)/2, z=(hash-0.5)*thin, scale 55. Or 3D "roller coaster": use x=cos a * 55, y=sin(2a)*18, z=sin(a)*20? Lemniscate in XY with z bank: z = sin(a)*15. Fine.
-
STAR: 5-point star outline: param by segment: a = t2π; r = mix(rOuter, rInner, abs(mod(a5/π,2)-1))? Use polar star function: r(θ) = r1r2 / sqrt( (r2 cos(5θ)...) hmm simpler: r = mix(ROUT, RIN, smoothstep-ish triangle wave of (θ5 mod 2π)/...). Standard: triangle wave: k = abs(mod(5*a/(2π)*2, 2.0)-1.0); r = mix(RIN, ROUT, k). Then x=r cos a, y=r sin a, z thin.
-
GALAXY: spiral: arm = floor(hash3); a = t2π0.9 + arm2π/3 + spin by radius: radius = pow(t,0.65)R; a2 = a + radius0.12; x=cos(a2)*radius, z=sin(a2)*radius, y=(hash-0.5)6(1-radius/R). Looks like spiral disc.
-
CUBE/LATTICE? Or "WAVE" — skip, 7 shapes + free is plenty. Actually let me add "RING" (torus): simple: a=t*2π; x=cos a * R, z=sin a * R, y=(hash-0.5)*10. Call it HALO. 8 total ids 0..7 with 0 = FREE (roam).
Shape count: FREE, SPHERE, KNOT, HELIX, HEART, INFINITY, STAR, GALAXY, HALO = 9 modes. Auto director: alternate free & morph: free 8s → shape 14s → free → next shape...
Shader crossfade: uShapeA, uShapeB ints, uMix float 0..1 with smootherstep on CPU. goal = mix(shapePos(A,t,...), shapePos(B,t,...), smooth). FREE as shape id 0 handled inside shapePos by returning roam target + spread. So crossfade free→shape works through same path.
For FREE, per-bird spread offset: hash3(idx)*44 - 22, so flock is a blob around roaming point, and boids rules shape the blob.
Hash functions in shader:
Velocity shader detail:
Careful: gl_FragCoord.x = x+0.5. Use
vec2 f = floor(gl_FragCoord.xy); float id = f.x + f.y * WIDTHF;.Separation/alignment/cohesion loop:
Then:
Hmm official approach: alignment:
selfVelocity += (avgVel) * alignmentStrengthwhere avgVel normalized-ish. I'll use steering: desired = normalize(avg)maxSpeed; steer = desired - vel; acc += steeruAli. Cohesion: center = coh/nCoh; desired = normalize(center-pos)maxSpeed; steer = desired - vel; acc += steeruCoh*0.5.Goal:
Wander:
Predator:
Negative strength → attractor.
Bounds:
(div by zero guard: max(br,1e-3))
Integrate:
phase in pos.w? I said posData.w = phase. Official stores bird phase in velocity w? They store wing phase via BIRD_ID? Doesn't matter: I'll store phase (0..1 random) in position texture w at init, keep it through position shader (copy w), and store nothing special in vel.w (maybe speed smoothed for audio? no, audio computed on CPU sparsely — skip CPU readback; audio follows a "chaos estimate" from uniforms: e.g., goalWeight, predator activity, time). Simpler: audio params driven by mode changes + a cheap FPS-avg. Fine.
Position shader:
dt clamp CPU side: uDelta = min(delta, 0.033) * timeScale (speed slider).
Units: scene scale ~ shapes radius 50; speeds ~ 24-40 units/s. Forces: sep 30, ali 8? Need tuning. Classic official example: BOUNDS 800, speed limit 9 (per frame at 60fps → multiplied by delta? official uses per-frame without delta: velocity added each frame). I'll use delta-based with tuning values that are sane; risk: untested tuning could look off. Mitigate with conservative choices from official example scaled: official: width 32; separation 15?, let me recall official values:
From memory of webgl_gpgpu_birds:
and forces weighted 0.01-ish per frame at 60fps. My delta-based: multiply per-frame-ish forces by delta*60 implicitly. To reduce tuning risk, follow official structure but with dt scaling: I'll implement steering-style with modest gains and clamp accel magnitude:
acc = clampMagnitude(acc, uMaxAccel)with uMaxAccel ~ 90. This guarantees stability regardless of gains. Good safety.Actually simplest robust: after computing acc,
float al = length(acc); if (al > uMaxAccel) acc *= uMaxAccel/al;.Speeds: minSpeed 14, maxSpeed 34 (free), during morph goal weight high & maxSpeed 46 to let them catch shape. CPU lerps uGoalWeight & uMaxSpeed by mode. Nice dynamics: morph phase = "tightening": goalWeight ramps 6→? Let me define per-mode targets:
- FREE: goalWeight 0.10 (roam target + per-bird offset 44 spread), maxSpeed 30, wander 9, coh 0.6.
- SHAPE: goalWeight 1.6 (strong), maxSpeed 44, wander 3, coh 0.3.
Note goal force = toGoal * goalWeight * smoothstep(0,30,d): at d=50 → 501.61 = 80 accel — near cap 90 ok. Near shape d<30 soft → birds settle with wander noise.
Predator strength: 260 (repel), radius 55. When attract mode: -120 (attract) but then birds might pile into point; add orbital swirl? Keep simple: attract strength -90 and they'll orbit due to speed floor + wander. OK.
Rendering — instanced bird geometry:
Base geometry via BufferGeometry (like official): triangles:
- body: elongated diamond: verts (0,0,-2.2) nose? Official uses:
I don't remember exactly; I'll construct my own bird: forward = -Z? I'll orient with a matrix built from velocity in the vertex shader (construct basis: forward = normalize(vel); right = normalize(cross(up, forward)); up2 = cross(forward, right)).
Geometry (in local space, forward = +Z toward nose):
- nose (0,0, 3.2)
- tail (0, 0.6, -2.6)
- left wingtip (-4.2, 0, -0.6)
- right wingtip (4.2, 0, -0.6)
- back center (0, 0, -0.2)
Triangles:
- body: nose, tail, backcenter? Make a slim body: nose→backcenter→tail with slight y on tail — actually a triangle (nose, tail, backcenter) is degenerate-ish. Use 3 triangles:
- T1: nose, leftWing, backcenter (left wing)
- T2: nose, backcenter, rightWing (right wing)
- T3: nose, tail, backcenter (body) — gives a little dorsal fin look with tail raised.
Wing flap: rotate wingtip verts around Z-axis? Flap = wings move up/down around body axis (Z forward). In vertex shader: attribute
aSide= -1 left, 0 body, +1 right. Flap angle θ = sin(uTime10 + phase6.2831)0.9 (radians). For wing verts: rotate around Z: x' = xcosθ_side... For side s: rotate (x,y) by θs: x' = xcos(sθ) ... let me think: rotation around Z axis by angle α: x' = x cosα - y sinα; y' = x sinα + y cosα. Wingtip at x=-4.2 (left, s=-1): with α = θs? We want left wingtip y to go up when right goes up: y' = x sinα: for left x=-4.2, to raise y need sinα negative → α = -θ for left. So α = θ * s works if s = sign(x)? s=-1 → α=-θ: y' = x sin(-θ) = (-4.2)(-sinθ) = +4.2 sinθ up. Right x=+4.2, α=+θ: y'=4.2 sinθ up. Both rise together. And x' = x cosα shrinks span. Good:float ang = flap * aSide; float c=cos(ang), s2=sin(ang); vec3 p = vec3(v.x*c - v.y*s2, v.x*s2 + v.y*c, v.z);Then world = basis * (p * aScale) + pos. aScale per-bird size ~ 0.9..1.4, scaled by 1/√(birdCount/4096) maybe: more birds → smaller. Also global birdSize uniform.
Color: varyings: vPhase, vSpeed. Fragment: base gradient palette by phase: iridescent starling — dark body with green/purple shimmer: color = mix(colA, colB, 0.5+0.5sin(phase6.28 + uTime*0.5)); brightness ∝ speed; add rim by |side| for wings? Add
vWing = abs(aSide)varying for brighter wingtips. Plus top/bottom shading via normal-ish: skip normals (flat glow look with bloom).Palette: deep violet→teal→amber cycle — matches prior demos' aesthetics. During morph "settle" maybe shift hue: uniform uHueShift.
Background: Points with 600 static stars in big sphere shell, additive, subtle. Plus a very faint radial gradient via big sphere shader? Skip; vignette + bloom enough. Add subtle fog: FogExp2(0x040309, 0.0028) — birds far away dim nicely. But trails pass operates on screen, fog fine.
Post chain: RenderPass → AfterimagePass(damp ~0.87 slider 0.80..0.96) → UnrealBloomPass(strength 1.1, radius 0.55, threshold 0.12) → OutputPass. Vignette: use small custom ShaderPass before OutputPass (copy from previous demo pattern: vignette+grain). Order: bloom then vignette/grain then OutputPass.
AfterimagePass in r170:
new AfterimagePass(damp)— uniforms['damp']. It renders previous frame blended; with additive bright birds gives ghost trails. Watch: AfterimagePass needscomposerping-pong — it manages internally. Note AfterimagePass uses its own internal render target and blends into readBuffer/writeBuffer... In r170 AfterimagePass.render(renderer, writeBuffer, readBuffer): it renders quad mixing textureComp with textureOld into readBuffer? It setsthis.uniforms['tOld'].value = this.textureComp.texture... The official example webgl_postprocessing_afterimage: composer.addPass(renderPass); composer.addPass(afterimagePass); works. OK.Trail slider maps damp 0.80..0.97.
Director logic (CPU):
Simplify: keep uShapeA = last settled, uShapeB = next, animate uMix 0→1; on complete uShapeA=uShapeB, uMix=0. Param targets (goalWeight etc.) lerp toward mode targets always by dt*rate. Mode target determined by uShapeB when mixing or current settled shape. FREE is id 0. Manual: N → skip to next shape; F → toggle free now; Auto toggle chip.
HUD elements: title panel (name + hint), stats panel right-top (mode name, birds, fps, quality), controls bottom (chips: Auto, Sound, Predator/Attract, Snapshot, Reset cam; sliders: Speed, Trails, Bloom, Volume), announcement center text (big, fades), and a shape-name toast. Loading overlay until ready. Keyboard: A auto, S sound? conflict — use: Space=next shape, F=free, A=auto, S=sound, X=predator mode toggle, C=snapshot, H=hud, R=reset camera. 1-9 jump shapes.
Predator interaction: pointermove raycast onto sphere (center 0, radius 60) — if no hit, project onto plane facing camera through origin. uPredator = point, strength active only while pointer down? Better: predator always active at pointer position with strength (repel 200) but smooth-faded when pointer idle >2s (strength → 0). Pointer down = burst scatter (temporary strength spike 600). Attract toggle flips sign. Touch: touchmove = predator.
Audio design (WebAudio):
- master gain → destination, compressor maybe.
- Wind: looping noise buffer (2s) → bandpass filter (freq 300→1200 by "energy"), gain 0.12; LFO on freq.
- Pad: 2 detuned triangles (root + fifth + octave shift per shape via freq table), lowpass 600, gain 0.05, slow attack. Change chord on shape change with ramp.
- Shimmer: high sine cluster (like sparkle) triggered on morph start: quick arpeggio of 5 sine pings (pentatonic from previous demo, fine).
- Whoosh on morph: noise → bandpass sweep 200→2400, 1.2s, gain 0.2→0.
- Wing flutter: amplitude mod of wind by avgSpeed? avgSpeed unavailable without readback; use energy proxy = goalWeight transitions & predator activity & time-based LFO. Keep honest: compute energy from simulation state: E = clamp(predatorActive0.5 + morphing0.6 + 0.3) → wind freq & pad brightness.
Snapshot: render one frame then canvas.toBlob → download. Need preserveDrawingBuffer? Instead: composer.render() right before toBlob in same task → works without preserveDrawingBuffer if called synchronously after render. I'll do:
renderFrame(); canvas.toBlob(...).Auto quality: fps EMA; every 2s evaluate: if < 45 and level > 0 → lower; if > 57 and level < max and stable 6s → raise. Rebuild GPGPU + geometry on change (dispose old). Levels: [{n:32,label:'1K'}, {n:64,label:'4K'}, {n:96,label:'9K'}]? 96×96=9216 "9K". Start at 1 (4K). Manual override chips LOW/MED/HIGH + AUTO. On change, rebuild with new WIDTH (shaders are string-generated with defines — rebuild all).
Alright — also
renderer.setPixelRatio(min(devicePixelRatio, 2)); resize handler updates composer & passes & bloom resolution.One more consideration: GPUComputationRenderer float textures — need WebGL2 (r170 default) with float render targets; supported broadly. Fallback: if init fails, show error overlay.
Now bird orientation basis in vertex shader:
Edge case vel≈0 guarded by speed floor.
Add banking: roll = clamp(lateral..., ...) skip.
vColor computation in vertex: pass vHue (phase), vSpeed = length(vel)/uMaxSpeedRef, vSide.
Fragment:
With bloom threshold 0.12, bright fast birds glow. Starlings at dusk — silvery-violet-teal. palette(h) with offset to keep dark-ish: multiply 0.8.
Blending: normal (opaque) birds with dark colors → trails still work. Additive blending would make flock center blow out into white blob with 4k triangles? With bloom maybe fine but opaque reads as "birds" better. I'll use opaque, dark iridescent — trails + bloom make it glow.
Hmm, dark birds on near-black bg may vanish. Speed-scaled brightness 0.25..1.6 with palette values 0..1 → visible. Plus slight emissive floor. Should be fine with bloom.
Let me also add faint "moon" disc? Skip — keep focus.
Camera: fov 55, pos (0, 26, 120), target (0,0,0). OrbitControls: enableDamping, autoRotate 0.4, min 40 max 320.
Scene scale: BOUNDS 110. Shapes radius ~48-58.
Now, shapePos GLSL — write carefully:
Hmm fibonacci needs per-bird index not t (t is shuffled by golden ratio — that's fine! tN is a permutation-ish). Use yy = 1.0 - 2.0t; r = sqrt(max(0.0, 1.0-yy*yy)); th = bid * 2.399963; pos = vec3(cos(th)*r, yy, sin(th)*r) * (46.0 + (h.x-0.5)*6.0);
Better: strand = mod(bid, 2.0); turns: yy = (t-0.5)2.0 * 52.0; ang = t * 6.2831 * 4.0 + strand3.14159; x=cos14, z=sin14; plus jitter 2.
Heart y range -17..~+12 2.1 → ~-36..+27: recenter y2.1+? fine add +6. Also puff z by shape: z = (h.z-0.5)10 + sin(y0.2)*3? keep simple.
5 points: θ mod (2π/5): r interpolates between ROUT at point tips and RIN at valleys:
At seg integer → tip (r=ROUT), fract 0.5 → valley (RIN): r = mix(ROUT, RIN, tri)? tri at fract=0 → abs(-1)=1 → wait tri=abs(0*2-1)=1 at tip, 0 at valley(fract .5). So r = mix(RIN, ROUT, tri). ROUT=52, RIN=22. x=r cos a, y=r sin a, z=(h-0.5)*6. Rotate so star faces camera-ish: it's in XY plane — fine, orbit shows it.
uTime used in shapes — declare uniform uTime in velocity shader (already). Galaxy slow spin nice.
BIRDSF define = float(W*H).
Velocity loop zone radius: ZR 26? With 4k birds in ~100 box... cohesion neighborhood zone radius 26 world units reasonable: birds flock locally. sep 6.0. Values: SEPR 7.0, ZR 24.0.
Performance of O(n²): 9216² = 85M iterations × (2 texfetch + ~20 flops). On mid GPUs ~ might be 10-20ms — borderline. Auto-quality will drop if slow. Start MED 4096 (16.7M iter — fine).
Also cap: devicePixelRatio ≤ 1.75 to protect fill rate (trails+bloom at 2x is heavy).
Now HUD stats: birds count, mode label, fps, sim quality. Announcement div center-top: "✦ TORUS KNOT ✦" fading.
Buttons row: [Auto ▶] [Next Shape] [Free] [Predator] [Sound] [Snapshot] [Reset Cam] + sliders Speed(0.2-2), Trails(0.80-0.97), Bloom(0-2), Volume(0-1) + quality chips Auto/Low/Med/High.
Title: "MURMURATION" sub: "eight thousand starlings · gpgpu boids". Hint text: "Move pointer to scatter the flock · hold to burst · Space next shape · F free flight · 1–9 jump".
Alright, also a loading overlay with title + "summoning the flock…" fade out on ready.
Edge: GPUComputationRenderer init can fail on no-float → catch error, overlay message.
Write the file now. Structure:
- importmap three 0.170.0.
- body: canvas #stage, panels, announcement, loading.
- module script:
- imports: three, OrbitControls, GPUComputationRenderer, EffectComposer, RenderPass, UnrealBloomPass, AfterimagePass, ShaderPass, OutputPass.
- Shaders as template strings (velocity with W/H defines injected at build: function buildSim(W) returns shaders).
- State object; init renderer/scene/camera/composer; buildSimulation(level); buildBirds(); director; audio; events; loop.
Let me be careful with AfterimagePass uniform name: in r170 AfterimagePass:
this.uniforms['damp'].value. Yes ('damp').UnrealBloomPass(resolution, strength, radius, threshold).
Custom grade pass (vignette+grain) — small:
Order: render → afterimage → bloom → grade → output. Note OutputPass does tone mapping/sRGB; grade before output operates linear — fine.
Renderer:
renderer.toneMapping = THREE.ACESFilmicToneMapping; toneMappingExposure 1.05. OutputPass handles color space.Audio module (~120 lines): class MurmAudio {ensure(), setEnergy(e), setShape(i), whoosh(), toggle(), setVolume(v)}.
Director details:
Params per shape: goalW, maxSpeed, wander, coh, spread handled in shader. Table:
Timeline: auto on: FREE 7s → pick next shape (cycle), announce, mix 2.8s, hold 12s, mix to FREE 2.8s, repeat.
mix transitions: uShapeB=target, uMix 0→1 (smootherstep), onDone: uShapeA=target; uMix=0.
Param lerp: current → target with rate 1.2/s toward mode of (mixing? uShapeB : uShapeA).
Predator: uniforms uPredator(vec3), uPredStr. Update each frame: if pointer recent (<2.5s) target str = down? 600 : 220 (repel) or attract flips; else 0. Smooth current toward target.
Pointer mapping: ray from camera through NDC; intersect sphere radius 70 at origin: solve |o + t d|² = r²; if no hit use closest approach point o + d*max(0, dot(-o,d)).
Energy for audio/wind: E = clamp01(0.25 + predStr/5000.5 + (mixing?0.35:0) + speedNorm0.2)... compute simple: E toward target E lerp.
FPS meter & auto-quality: frame times ring buffer; every 2.5s: avg; if <45 → level down (if auto) rebuild; if >57 for 3 consecutive → level up. Rebuild function: dispose old gpu/geometry/mesh; build new; also update stats label. During rebuild sim resets (flock re-scatters) — acceptable; morph state preserved (shapes ids kept, uMix kept).
Bird geometry instancing: InstancedBufferGeometry from base triangle list (9 verts). attributes: position (base), aSide per vertex, aTip (wingtip factor for color). Instanced attributes: aUV (vec2), aPhase (float), aScale(float). set instanceCount = N.
Bird material: ShaderMaterial with uniforms uTime, texturePosition, textureVelocity (set each frame from gpu targets), uHueShift, uMaxSpeedRef(46). side: DoubleSide, no depth issues? depthWrite true, depthTest true — opaque birds occlude properly (good, looks like flock mass).
trails + opaque + bloom → nice.
Starfield: BufferGeometry 700 pts, PointsMaterial size 1.2 sizeAttenuation false? With attenuation true size 2.5, opacity .55, color 0x8899bb, additive, depthWrite false. Slight per-star size variance via shader? Keep PointsMaterial simple. Rotate slowly.
Resize: renderer.setSize, composer.setSize, bloom.setSize? UnrealBloomPass has setSize handled by composer. gradePass uniform none for size. camera aspect.
Snapshot: function snapshot(){ render once: composer.render(); canvas.toBlob(blob=>{a.download='murmuration.png'; a.href=URL.createObjectURL(blob); a.click();}) }.
Loading overlay hide after first frames.
Error handling: try/catch around gpu.init(); on fail overlay text.
Now write the HTML. Sizeable (~950 lines). I'll write in 3-4 chunks using write + append via shell heredoc? Prefer read_write_file_in_shell_environment write mode full content — but content is large (~40-50KB). The tool should handle it; risk of truncation. Safer: write in 4 sequential appends with cat >> heredoc via execute_shell_command? Heredoc with quotes 'EOF' avoids interpolation; but content includes backticks and $ — with quoted delimiter it's literal. However command length limits? Should be OK for ~15KB chunks. I'll use read_write_file for chunk1 (create) then append via shell heredoc for others. Actually read_write_file has no append mode. I'll use execute_shell_command with
cat >> file <<'CHUNK_EOF'for appends. Fine.Let me draft the full code mentally then write.
Key GLSL pieces — full velocity shader (generated with defines WF, HF, BIRDSF as literals):
Torus knot orientation: standard (x,y,z) = ((2+cos3a)cos2a, (2+cos3a)sin2a, sin3a) — lies in XY plane, viewed from front (camera z) it's flat-ish; rotate: put sin3a on y so knot lies flat-ish in XZ plane (like a crown viewed from above)... Camera orbits everywhere; either fine. I'll use (x,z) circle, y height: vec3((2+cos3a)cos2a, sin3a, (2+cos3a)sin2a)*15.5 — nice.
sin³ can be negative — pow of negative undefined; use sss:
float s1=sin(a); float x=16.0*s1*s1*s1;Heart extent: x ±162.15=±34, y (−17..12.6)0.952.15 ≈ −34.7..+25.7 +8.6 offset... let me recompute: (y0.95+4.0)*2.15: y=−17 → (−16.15+4)*2.15 = −26.1; y=12.6 → (11.97+4)*2.15=34.3. ok centered-ish. Add z puff: (h.z-0.5)112.15=±11.8. fine.
Hmm cohesion: steer toward center with strength uCoh (0.4..0.9): acc += normalize(toC)*min(cl,30)0.06uCoh? Let me simplify: acc += toC * 0.035 * uCoh (linear pull capped by MAX_ACC anyway). Keep that.
Tuning sanity: goal at 60 units: 601.510.9 = 81 → capped 95 fine. sep at close range: 26(~1)=26. wander 9*(1.5)=13. align 0.55*(Δv up to ~60)=33. cohesion toC0.0350.6: cl 24 → 0.5. Weak-ish cohesion vs wander — flock may disperse in free mode... In free mode goal target (roam+spread) with goalWeight 0.12: at gd 40 → 400.120.9=4.3 pull — plus cohesion 0.5, wander 13, align strong — hmm flock might spread too thin. Official birds stay flocked via bounds + cohesion with zone 40. My ZONE 24 in spread 52-unit blob → local subflocks. To keep a coherent blob in free mode: raise free goalWeight to 0.35 and reduce spread to 44, and cohesion weight 0.08: acc += toC0.08uCoh? At cl 20 → 200.080.9=1.44 — still weak vs wander 13.
Rebalance: make wander lower (free 5.5, morph 2.5), sep 30, align 0.6, cohesion linear 0.06 (cl 20 → 1.1)... The real binding force should be cohesion; linear pull of 1.1 vs wander accel 5.5*~1.5=8 — wander dominates direction-wise but it's zero-mean oscillation; over time cohesion+goal drift wins. Birds speed min 14 max 30 in free: they'll swirl in blob. I think with goal pull 4-5 and cohesion ~1-2 + alignment, blob holds ~60-80 unit cloud. BOUNDS 118 safety. Morph shapes radius ~50 — travel across cloud at speed 44 takes ~2s. OK.
Free-mode roam target moves at |d roam/dt| ≈ 40*0.3 ≈ 12 u/s — flock at 14-30 speed keeps up.
One concern: goal force smoothstep(0,26,gd) → near shape (gd<26) force fades linearly-ish; birds orbit their goal point with wander → fuzzy shape ~ nice.
Separation SEP_R 7 with bird scale ~1.6 length 5.8 (nose 3.2 to tail -2.6) — ok.
Also for shape crispness at HIGH count it's denser — good.
uPredStr repel 240: at pr 20 → smoothstep(58,0,20)= (58-20)/58 smoothed ≈ 0.65²-smooth… smoothstep(58,0,x) with edge0=58 edge1=0 (reversed edges — GLSL smoothstep requires edge0<edge1? With edge0>edge1 result is undefined per spec but commonly works as 1-smoothstep(0,58,x)... risky! Use
1.0-smoothstep(0.0,58.0,pr). Same for others: I used smoothstep(58.0,0.0,pr) — must fix to 1-smoothstep(0,58,pr). Similarly bounds uses smoothstep(0,26,gd) fine.Predator accel 240*~1 at close → capped by MAX_ACC 95. Burst 700 → cap. ok.
Attract mode uPredStr negative: (pvec/pr)*(-150)*falloff → pulls toward point. With min speed they orbit. fine.
Position shader trivial.
Now bird vertex shader:
Wait modelViewMatrix with mesh at identity — fine.
Fragment:
Since OutputPass tone maps, values >1 bloom nicely.
Instanced geometry: InstancedBufferGeometry; copy 'position','aSide','aTip' from base; index none (non-indexed 9 verts? Let's list triangles):
verts (local): nose N(0, 0.1, 3.4) tail T(0, 0.75, -2.4) center C(0, -0.1, -0.4) left L(-4.4, 0, -1.1) right R(4.4, 0, -1.1) leftTail? keep 5 verts, 3 triangles: (N,L,C), (N,C,R), (N,T,C)?? body triangle N,T,C plus wings — total 9 verts: tri1: N, L, C (left wing) sides: N 0, L -1, C 0; tips: L 1. tri2: N, C, R (right wing): C 0, R +1. tri3: N, T, C (body): all side 0; T tip .4.
DoubleSide so winding doesn't matter.
aScale random 0.8..1.5 * global scale by count: countScale = sqrt(4096/N)*1.0 clamp 0.55..1.6 → at 9216 scale 0.67, at 1024 scale 2.0 clamped 1.6.
aUV: (x+0.5)/W etc.
Flap uFlap 0.85.
Now CPU JS structure (~450 lines). Let me write it all.
State:
MODES table with params:
Init renderer/scene/camera/controls/stars/composer.
buildSimulation(w): create gpu, textures:
Note addVariable wraps uniforms — material.uniforms accessible.
buildFlockMesh(w): dispose old mesh; create InstancedBufferGeometry:
Note: ShaderMaterial built-in attribute 'position' used; custom attributes aUV etc declared in shader — with WebGL2 & three, custom attributes must be declared in shader (done).
Each frame:
Director update(dt real): timers; transitions set uniforms uShapeA/B ints & uMix.
Param lerp: for each of goalWeight etc: cur += (target-cur)min(1,dt1.5).
Predator smoothing similarly. hueShift += dt*0.02.
Audio update with energy E.
Render composer.render().
fps tracking & auto quality.
Camera reset: set position/target.
Snapshot.
Keyboard & chips wiring. Chip active states.
Announcement: show text with CSS class 'show', timeout remove.
Shape chips row? 9 chips might crowd; instead stat panel shows mode name; Space/N next. Add small select? Keep chips: Auto, Predator/Lure, Sound, Snapshot, Reset + sliders. Shapes via keyboard/auto/announce only. Add "Next ▸" chip too.
Quality chips: Q Auto/1K/4K/9K small.
OK — also stats: birds label, fps, mode. Update 2/s.
Now the CSS/HTML skeleton mirroring 019 style (same fonts/colors for family resemblance): panels, .chip, .slider, #announce center, #stats top-right, #loading overlay, #err overlay hidden.
Mobile: touch-action none on canvas; panels scale; fine.
Audio implementation:
Call whoosh at morph start; setShape at settle.
Energy E computed in loop:
E = clamp(0.22 + Math.abs(state.predStr)/500*0.5 + (state.mixing?0.3:0) + (state.maxSpeed-30)/40, 0, 1).Fine. Now — write everything. Also remember resolution define: compute shaders use
resolution(vec2) — provided as define by GPUComputationRenderer. In my shaders I usegl_FragCoord.xy / resolution.xy— resolution is defined asvec2(64.0, 64.0)so.xyworks?#define resolution vec2(64.0, 64.0)→resolution.xy→vec2(64.0,64.0).xyvalid GLSL. Good.texture2D vs texture: three injects
#define texture2D texturefor WebGL2 in ShaderMaterial? GPUComputationRenderer material — it's a RawShaderMaterial? No — ShaderMaterial (not raw), three auto prefixes with common defines including texture2D alias when GLSL3? Default ShaderMaterial glslVersion null → compiled as WebGL1-style with auto conversion. Official example uses texture2D — safe.Bird ShaderMaterial: texture2D in vertex shader — needs vertex texture fetch (universal now). OK.
One more check — AfterimagePass shader:
In r170, AfterimagePass constructor(damp=0.96). Works with composer.
Edge: AfterimagePass uses NearestFilter & RGBA16F? Let me recall r170 source:
Yes HalfFloat — fine with HDR-ish values.
Also AfterimagePass.setSize exists (composer calls it).
Composer setPixelRatio: composer.setPixelRatio(renderer.getPixelRatio()) then setSize.
Alright, let me also make canvas element:
<canvas id="stage"></canvas>and renderer = new THREE.WebGLRenderer({canvas, antialias:false, powerPreference:'high-performance'}). antialias false since post chain; birds small triangles → aliasing visible but trails+bloom mask it. Use FXAA? Skip; trails soften.BG color: renderer clear #040309. scene.fog = new THREE.FogExp2(0x040309, 0.0026). Fog affects ShaderMaterial? Only if material.fog true and shader has fog chunks — my custom shader ignores fog → birds won't fog. Add manual distance fade in bird fragment: vDist varying = -mvPosition.z... I have gl_Position only; compute viewZ = -(viewMatrix*vec4(world,1)).z → varying; col = exp(-vDist0.004)? Fade to bg → col *= fade. OK do that (fade to 0 = bg-ish since bg near black). Stars DO get fog? PointsMaterial has fog:true default. fine.
Let me now also double-check
sign(sin(a))avoided; using s1s1s1 keeps sign. pow(sin,3) avoided. Good.mod(bid,2.0) with bid float — fine.
fract(bid*0.618...) uniform spread OK.
Potential GLSL strictness:
intuniforms uShapeA/uShapeB: set value as JS number — three sets int via uniform1i for declared int uniforms? Three.js infers setter from GLSL type — yes uniform1i for int. Shape ids:velVar.material.uniforms.uShapeA = {value:0}→ int type from shader declaration.Also
if(id==0)with int — fine.Loop
for(float y=0.0; y<HF; y++)HF=96.0 → 96×96=9216 iterations. Fine as defines.One perf tweak: HF/WF as
const float? defines simpler.Announce emojis per shape: ✦ sphere ●? Use glyphs: FREE 🕊? Keep typographic: '∿ FREE FLIGHT', '● SPHERE', '♾ INFINITY' etc. small map.
Write files now. Also update /space/demos/index.html: insert new card before "". Note previous cards appended after the comment? Looking at grep: comment at line ~99 then 019 card after. Actually "New demos are added here" then 019 listed — so newest at top? Check index more closely around list.
From earlier output: line 100 is 019 card right after comment (line ~99). So newest first directly under comment. I'll insert 020 card right after the comment line.
Description for 020 card — write rich blurb like others.
Let me write the demo file in chunks now. I'll aim ~1000 lines total. Chunk sizes ~ 250 lines each, 4 chunks.
Chunk plan:
- HTML head + CSS + body panels + importmap + script start (imports, constants, MODES, state, shaders part 1: velocity shader fn).
- Shaders: position, bird vert/frag, grade shader; renderer/scene/composer setup; stars.
- buildSim/buildFlock, director, predator, audio class.
- UI wiring, keyboard, resize, snapshot, loop, boot.
Careful with heredoc: content contains
$(template literals${}) — quoted heredoc 'EOF' prevents shell expansion. Backticks safe inside quoted heredoc. Must ensure no line equals EOF marker.Let me write chunk by chunk, then verify with node --check (extract module script to .mjs — imports won't resolve but --check only parses... node --check on ESM with bare specifiers: parsing fine).
Also validate GLSL by eye once more at end.
Go.
- Commented
The demos folder has 19 demos. The task says "Wake up and do as you please" — so I'll create demo 020. My pick: "Murmuration" — a GPGPU starling-flock simulator with true GPU boids (separation/alignment/cohesion), shape-morphing choreography (sphere, torus knot, helix, heart, infinity, star, galaxy, halo), a pointer predator, phosphor trails + bloom, and generative audio. That complements the existing collection nicely.
Let me create it:
- Created activity call execute_shell_command
- Created
***********************************************************************rs.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Commented
*********************************************************************************************************************************ly.
- Created activity call execute_shell_command
- Created
*************************************************************************************************************************************************ml.
- Created activity call execute_shell_command
- Created
Now verify: extract the module script and run node --check. Also check for issues:
-
$('c-pred').childNodes[1].textContent— chip HTML:<button class="chip" id="c-pred"><span class="k">X</span>predator</button>— childNodes[0] = span, childNodes[1] = text node "predator". OK. -
perfNowused inonMovebefore its definition (hoisting:const perfNow = () => ...is NOT hoisted — but onMove only runs on user events after script fully evaluated. OK.) -
state.predatorpassed as uniform value object reference —uPredator: {value: state.predator}then latervu.uPredator.value.copy(...)— good, same Vector3. -
AfterimagePass uniform name 'damp' — correct for r170.
-
bindChipwrapper pointless but fine. -
In
jumpToShape, if i === state.shapeA while not mixing: sets mode. Also if mixing: settle first. OK. -
beginMorphannounce only for shapes, not free. Good. -
Issue:
state.nextShapestarts 1; after first free → shape 1, nextShape = 1%8+1 = 2... cycles 1..8. Good. -
endMorphsets mode text; alsobeginMorphwhile auto and timer... In director: mixing branch decrements timer first; when timer<=0 endMorph sets phase & timer. Good. But note whenstate.autofalse and user triggers jumpToShape → beginMorph sets state.timer=2.8 but director returns early when !auto → mixing never progresses! BUG: director gated byif (!state.auto) return;— manual morphs won't animate.
Fix: director should always process mixing; only auto-scheduling gated. Restructure:
-
MODES[state.mixing ? state.shapeB : state.mode]— during mixing target = shapeB good. -
AfterimagePass +
composer.render()in snapshot: fine. -
gradePass.uniforms.uTime — ShaderPass exposes
.uniforms. Yes. -
Bird vertex shader:
attribute vec2 aUV;etc. — ShaderMaterial auto-declaresposition,normal,uvbut custom attributes declared manually — fine. But wait: declaringattribute vec2 aUV;in a ShaderMaterial with WebGL2: three prepends#version 300 es? No — default ShaderMaterial stays GLSL1 style and three handles conversion. Declaringattributeis standard for ShaderMaterial custom attributes. OK.
Also
viewMatrixavailable in vertex shader built-ins. Yes.-
resolutionin compute shaders: provided via defines by GPUComputationRenderer. Yes. -
sim.gpu.getCurrentRenderTarget(sim.posVar)— correct API. -
In disposeSim:
v.renderTargets— GPUComputationRenderer variable hasrenderTargetsarray (2). Yes, property exists. Alsogpu.createTexture()textures disposed with RTs. OK-ish. There's nogpu.dispose()in r170? Actually r170 GPUComputationRenderer hasdispose()method (added r151+). I could call it but manual disposal of RTs/materials is fine. Hmm — but callingsim.gpu.dispose()also needed to release... let me just also callsim.gpu.dispose()if exists. Minor: addif (sim.gpu.dispose) sim.gpu.dispose()after loop. Actually dispose() disposes render targets and pass-through materials itself — then my manual loop would double-dispose (harmless in three). Keep manual loop only; skip gpu.dispose to avoid double dispose. Fine as-is. -
Keyboard '1'..'9':
parseInt(k)-1gives 0..8. Good. -
camera.aspectinitial uses window dims — good. -
bloomPass.strength binding — ok. afterimage damp default state.trails=0.885 matches slider value 0.885 — good.
-
The
vu.uMinSpeednever updated after init (13) — fine. -
energyunused var warning? used in audio.setEnergy. ok. -
bindChip('c-snap', snapshot)— snapshot defined later via function declaration — hoisted. OK. -
In frame():
state.predatorcopy —vu.uPredator.value.copy(state.predator)where value IS state.predator (same ref) — copy of self, harmless. -
Loading overlay: hidden after 400ms regardless of first frame — fine.
-
Announce glyph uses '☰' for knot — fine.
-
AfterimagePass constructor param:
new AfterimagePass(damp)— r170 signature (damp = 0.96). ok. -
renderer.setPixelRatiothencomposer.setPixelRatio(renderer.getPixelRatio())— EffectComposer has setPixelRatio. ok. -
ShaderMaterial for birds lacks
depthTestissues — defaults true. DoubleSide set. Good. -
Potential GLSL problem: in velocityShader,
uMinSpeedused; declared.phasefrom pd.w — used in wander. ok. -
In birdVertexShader I declared
attribute float aPhase;but never use it (phase comes from texture w). Unused attribute — GLSL compilers may warn but fine; three will still bind it (getAttribLocation returns -1 if optimized out — three skips). Actually WebGL: setting attribute that doesn't exist in program: three checks location !== -1. Fine. Remove to be clean? Leave — harmless.
Also
aPhaseinstanced attribute: InstancedBufferAttribute — three handles.-
geo.instanceCount = N— InstancedBufferGeometry default Infinity? Setting explicitly good. Also needgeo.instanceCountonly; three renders instanced since it's InstancedBufferGeometry. -
One more:
floor(gl_FragCoord.x)— fine. -
mix(20.0, 52.0, tri)star: tri=1 at tips (fract 0), 0 at valley — rad = mix(RIN=20, ROUT=52, tri) → tips 52. Good. -
shapePoswhen id==0 roam: goal = roam + spread*(h-0.5)52 — big 52-unit cloud. goalWeight 0.340.9*smoothstep(0,26,gd)... far birds pulled ~ (gd up to ~80) * 0.3 ≈ 24 accel — flock holds. -
Helix height 104 > BOUNDS 118/2? Helix y ±52, x/z ±15 → max radius ~55 < 118 fine. Sphere 45+jitter ~48.5 fine. Infinity x ±54 fine. Star 52 in-plane. Galaxy 54. OK.
-
Velocity texture w stores speed (sp) — overwritten each frame with pre-clamp sp. unused elsewhere. fine.
-
compute shader uniforms: GPUComputationRenderer addVariable — uniforms param: I use
Object.assign(velVar.material.uniforms, {...})AFTER addVariable — the material's uniforms object replaced content — the shader program recompiles on first render anyway (init happens after). Actually gpu.init() compiles materials; I assign uniforms before init — good order.
But: does GPUComputationRenderer pass uniforms to material? addVariable creates material with
uniforms: {}? It creates via createShaderMaterial(frag, {}) — material.uniforms starts empty; I add mine. OK.-
posVar.material.uniforms.uTime/uDelta— used by positionShader (uTime unused but declared — fine). -
Edge:
state.trailsslider maps to afterimage damp — AfterimagePass in r170: uniform name 'damp' — yes. -
Announce initial call at 900ms — loading hides at 400ms. ok.
-
#announceuses white-space nowrap, top 16% — ok. -
iOS Safari: importmap OK modern.
pointerleaveon canvas — ok. -
In onMove:
perfNow()defined as const later in file but evaluated at event time — OK since events fire after full module eval... module evaluates synchronously; listeners attached before const perfNow line? canvas.addEventListener('pointermove') attached during eval — a pointermove can't fire mid-eval (JS single thread, events queued). Safe. -
camera near 0.5 far 1200; stars at 320-740 — within far. Good.
-
controls.autoRotate— good. -
jumpToShapewhen clicked free while already free: i===state.shapeA → set mode; fine. -
bindSlider('s-vol'...)setVolume before audio.ctx exists — guarded (this.ctx check). ok.
Fix needed: director auto-gating bug (item 9). Let me patch it.
Also small:
#st-dirinitial text "auto choreo" matches auto=true initial. good.Patch director function via replace.
-
- Commented
************************************************************************************************************************************************************************************************ed:
- Created activity call replace_in_file_in_shell_environment
- Created activity call _checkTaskRunStatus
- Created
*************************************************************************************************ne.
- Created activity call execute_shell_command
- Created activity call execute_shell_command
- Created
***************************************************************************************ne.
- Commented
All checks pass. Now update the demos index page:
- Created activity call replace_in_file_in_shell_environment
- Created
*************************************************************************************rd.
- Created activity call execute_shell_command
- Created activity call _checkTaskRunStatus
- Created activity call replace_in_file_in_shell_environment
- Created
Index updated. Final quick validation of the demo HTML closing tags and done.
- Created activity call execute_shell_command
- Commented
Demo 020 "Murmuration" is complete and registered on the index page. Summary of what was built:
/space/demos/020-murmuration/index.html(980 lines) — a GPGPU starling observatory:- True GPU boids: position + velocity textures via GPUComputationRenderer, O(n²) separation/alignment/cohesion loop, 1K/4K/9K flock sizes with auto quality scaling driven by FPS
- Nine flight programmes (free roam, sphere, torus knot, double helix, heart, infinity, star, galaxy, halo) implemented as a parametric GLSL sculpture library, cross-faded with golden-ratio slot morphing and an auto-choreography director with announcements
- Pointer predator / lure with raycast-to-sphere projection, hold for shockwave, shift-mode lure via toggle
- Instanced flapping birds: 3-triangle bird geometry, wing flap + velocity-aligned orientation + wingtip flash in custom shaders
- Post chain: AfterimagePass phosphor trails → UnrealBloom → vignette/grain grade → OutputPass
- Generative WebAudio: filtered-noise wind, detuned triangle pad with per-shape chords, morph whoosh, pentatonic chimes — all following a live "drama energy" signal
- HUD, sliders (tempo/trails/bloom/volume), full keyboard shortcuts, snapshot export, loading overlay, WebGL2 fallback error message
- JS syntax verified with
node --check, all DOM ids audited, a morph-state-machine bug fixed (manual morphs freezing when auto-choreo off) /space/demos/index.htmlupdated with the 020 card
- Created activity call _success
- Created activity call _checkTaskRunStatus
Other Conversations
Explore other conversations with this agent.