From aad68b1638bdee3237b887348aa83fc5f36eb8de Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 25 Sep 2026 11:00:52 -0700 Subject: [PATCH] Let the plankton glow at night where the water is stirred: anything moving through it leaves a trail of cool sparks, and anything dropped in sends out a glowing ring, brighter where the plankton is rich. --- public/tide-pool.js | 54 ++++++++++++++++++++++++++++++++++++ src/components/tide-pool.tsx | 2 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/public/tide-pool.js b/public/tide-pool.js index feea907..7fc5ffb 100644 --- a/public/tide-pool.js +++ b/public/tide-pool.js @@ -113,6 +113,12 @@ async function initialize() { shared.uLamps = { value: Array.from({ length: LAMPS }, () => new T.Vector4(0, 0, 0, 0)) }; shared.uLampColor = { value: new T.Color(0xf0dcf6) }; shared.uNight = { value: 0 }; shared.uDusk = { value: 0 }; shared.uStorm = { value: 0 }; + // At night the plankton glows where the water is stirred: the plankton itself, and the bodies moving through the water. + const WAKES = mobile ? 12 : 24; + shared.uWakes = { value: Array.from({ length: WAKES }, () => new T.Vector4(0, 0, 0, 0)) }; + const planktonTexture = new T.DataTexture(new Uint16Array(COLS * ROWS), COLS, ROWS, T.RedFormat, T.HalfFloatType); + planktonTexture.magFilter = planktonTexture.minFilter = T.LinearFilter; planktonTexture.needsUpdate = true; + shared.uPlankton = { value: planktonTexture }; shared.uHaze = { value: new T.Vector4(0, 1, 0, 0) }; shared.uHazeZoom = { value: 7 }; shared.uHazeColor = { value: new T.Color(0x1a1524) }; // The simulation grid as a texture: r = water surface, g = ground, b = set stone, a = how damp the ground still is. @@ -306,6 +312,7 @@ async function initialize() { uniforms: { uTime: shared.uTime, uLight: shared.uLight, uRipples: shared.uRipples, uSun: shared.uSun, uView: shared.uView, uGrid: shared.uGrid, uGridBox: shared.uGridBox, uFlow: shared.uFlow, uOpen: { value: 0 }, uTide: shared.uWater, uLamps: shared.uLamps, uLampColor: shared.uLampColor, uNight: shared.uNight, uDusk: shared.uDusk, uStorm: shared.uStorm, + uWakes: shared.uWakes, uPlankton: shared.uPlankton, uHaze: shared.uHaze, uHazeZoom: shared.uHazeZoom, uHazeColor: shared.uHazeColor }, vertexShader: `${GRID} uniform float uOpen, uTide, uTime, uStorm; varying vec3 vW; varying float vDepth; @@ -322,6 +329,7 @@ async function initialize() { fragmentShader: ` varying vec3 vW; varying float vDepth; uniform float uTime, uLight; uniform vec3 uSun, uView; uniform vec4 uLamps[${LAMPS}]; uniform vec3 uLampColor, uHazeColor; uniform float uNight, uDusk, uHazeZoom, uStorm; uniform vec4 uHaze; + uniform vec4 uWakes[${WAKES}]; uniform sampler2D uPlankton; uniform sampler2D uFlow; uniform vec4 uGridBox; uniform float uOpen; ${WAVES} void main() { @@ -378,6 +386,31 @@ async function initialize() { col += vec3(.86, .84, 1.) * star; sparkle += star; } + // At night the plankton lights where the water is stirred: in a trail behind anything moving through it, and in + // the rings where something drops in. Brighter where the plankton is rich, dim where it has been grazed out. + if (uNight > .02) { + float stir = 0.; + for (int i = 0; i < ${WAKES}; i++) { + vec4 wk = uWakes[i]; + float sp = length(wk.zw); + if (sp < .02) continue; + vec2 dir = wk.zw / sp, dp = vW.xz - wk.xy; + float along = dot(dp, dir), across = dot(dp, vec2(-dir.y, dir.x)); + float trail = along < 0. ? exp(along / (.25 + sp * .6)) : exp(-along * along * 60.); + stir += trail * exp(-across * across / (.004 + max(-along, 0.) * .02)) * min(sp * 1.5, 1.); + } + for (int i = 0; i < 12; i++) { + float age = uTime - uRipples[i].z; + if (age <= 0. || age > 3.) continue; + float dd = length(vW.xz - uRipples[i].xy); + stir += exp(-pow((dd - age * .8) * 9., 2.)) * (1. - age / 3.) * 1.5; + } + float pl = texture2D(uPlankton, (vW.xz - uGridBox.xy) / uGridBox.zw).r; + float spark = fract(sin(dot(floor(vW.xz * 22.) + floor(uTime * 4.), vec2(12.9898, 78.233))) * 43758.5453); + float glow = min(stir, 2.) * (.25 + pl * 1.5) * uNight * (.3 + .7 * step(.7, spark)) * smoothstep(.02, .12, d); + col += vec3(.5, .6, 1.) * glow * .55; + sparkle += glow * .45; + } // At dusk and dawn the pools take the colour of the sky. col = mix(col, col * vec3(1.1, .95, 1.04) + vec3(.05, .02, .04), uDusk * .6); // Fast water carries thin streaks of foam in the direction it runs. @@ -419,6 +452,26 @@ async function initialize() { } gridTexture.needsUpdate = true; } + let planktonClock = 0; + function uploadPlankton(dt) { + planktonClock -= dt; + if (planktonClock > 0) return; + planktonClock = .25; + const data = planktonTexture.image.data, { plankton } = world; + for (let k = 0; k < cellCount; k++) data[k] = toHalf(plankton[k]); + planktonTexture.needsUpdate = true; + } + // Bodies moving through water, nearest the view first, for the glowing wakes. + function gatherWakes() { + const moving = []; + for (const c of world.creatures) { + if (c.speed < .05 || c.alt != null || depthAt(world, c.x, c.z) < .05) continue; + const vx = SPECIES[c.sp].kind === 'swimmer' ? c.vx : Math.sin(c.angle) * c.speed, vz = SPECIES[c.sp].kind === 'swimmer' ? c.vz : Math.cos(c.angle) * c.speed; + moving.push({ x: c.x, z: c.z, vx, vz, d: (c.x - view.x) ** 2 + (c.z - view.z) ** 2 }); + } + moving.sort((a, b) => a.d - b.d); + shared.uWakes.value.forEach((v, i) => { const m = moving[i]; if (m) v.set(m.x, m.z, m.vx, m.vz); else v.set(0, 0, 0, 0); }); + } // The current: water runs toward the sea as the tide falls and inland as it rises, faster where it is shallow and narrow. let flowClock = 0; function uploadFlow(dt) { @@ -1292,6 +1345,7 @@ async function initialize() { shared.uWater.value = waterLevel(world); uploadGrid(); uploadFlow(1 / 30); + if (night > .02) { uploadPlankton(1 / 30); gatherWakes(); } rippleVectors.forEach((v, i) => { const r = world.ripples[i]; v.set(r?.x || 0, r?.z || 0, r?.born ?? -10, 0); }); // Sun by day, a cool low moon by night. const dayAngle = world.time / 420 * Math.PI * 2; diff --git a/src/components/tide-pool.tsx b/src/components/tide-pool.tsx index 92f4923..556475a 100644 --- a/src/components/tide-pool.tsx +++ b/src/components/tide-pool.tsx @@ -141,7 +141,7 @@ export function TidePoolContent() { {/* Lets the three.js add-ons resolve the same pinned module the page already uses. */} + ); } -- 2.51.2