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. */}
-
+
);
}