import * as THREE from 'three/webgpu';
import { MeshBasicNodeMaterial } from 'three/webgpu';
import { float, hash, instanceIndex, mix, positionLocal, positionWorld, smoothstep, time, uniform, vec3 } from 'three/tsl';
import { mulberry32 } from '../../src/modes/mode';
import { Panel, Readouts, createStage, fatal } from './kit';
/**
* Demo — "one heartbeat for the whole reef".
*
* The reef's polyps don't own a timer. Their brightness is read out of a wave that lives in
* WORLD space, so every colony — every separate stroke, painted minutes apart — lights up
* in the right order automatically. The floor is shaded with the same expression, which is
* why you can see the wavefront arrive.
*
* Switch to per-colony phase and the illusion collapses into three unrelated blinkers.
*/
const uPulse = uniform(1);
const uSharp = uniform(2.5);
const uGlow = uniform(1.2);
const uDir = uniform(new THREE.Vector3(1.6, 1.1, 1.35));
const uGlowA = uniform(new THREE.Color(0x2ee6d6));
const uGlowB = uniform(new THREE.Color(0x4e8aff));
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-function-return-type */
const colorVec = (u: unknown) => vec3(u as any);
/** The production expression, verbatim apart from the direction being a uniform here. */
function colonyPulse() {
return positionWorld.dot(colorVec(uDir)).mul(2.6)
.sub(time.mul(uPulse.mul(2.1)))
.sin().mul(0.5).add(0.5).pow(uSharp);
}
/** The naive alternative: each colony keeps its own clock, in its own local space. */
function localPulse(phase: number) {
return positionLocal.dot(colorVec(uDir)).mul(2.6)
.sub(time.mul(uPulse.mul(2.1)).add(phase))
.sin().mul(0.5).add(0.5).pow(uSharp);
}
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-function-return-type */
const stage = await createStage({
cameraPos: [-0.5, 3.1, 6.6],
target: [-0.5, 0.3, 0],
fov: 44,
bloom: { strength: 0.7, threshold: 0.5 },
orbit: true,
}).catch((err) => {
fatal(err);
return null;
});
if (stage) {
const { scene } = stage;
// ---------- the floor, shaded with the same wave ----------
const floorMat = new MeshBasicNodeMaterial();
// Fade the floor out with distance instead of letting its far edge cut across the frame.
const horizon = float(1).sub(smoothstep(5, 20, positionWorld.length()));
floorMat.colorNode = vec3(0.03, 0.045, 0.07)
.add(colorVec(uGlowA).mul(colonyPulse()).mul(0.16))
.mul(horizon);
// The wave is evaluated per fragment from positionWorld, so one quad is enough — it just
// has to be big enough that its far edge never enters frame.
const floor = new THREE.Mesh(new THREE.PlaneGeometry(46, 46), floorMat);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
// ---------- three colonies ----------
const rnd = mulberry32(0x1eef);
const tipGeo = new THREE.IcosahedronGeometry(1, 1);
const worldMat = new MeshBasicNodeMaterial();
{
const blink = time.mul(0.8).add(hash(instanceIndex).mul(6.283)).sin().mul(0.15).add(0.85);
const c = mix(colorVec(uGlowA), colorVec(uGlowB), hash(instanceIndex.add(9)));
worldMat.colorNode = c.mul(colonyPulse().mul(2.6).add(0.2)).mul(blink).mul(uGlow);
}
interface Colony {
group: THREE.Group;
localMat: MeshBasicNodeMaterial;
mesh: THREE.InstancedMesh;
}
const colonies: Colony[] = [];
const centres: [number, number][] = [[-2.8, 0.3], [-0.5, -0.4], [1.8, 0.5]];
centres.forEach(([cx, cz], ci) => {
const group = new THREE.Group();
group.position.set(cx, 0, cz);
scene.add(group);
const localMat = new MeshBasicNodeMaterial();
const blink = time.mul(0.8).add(hash(instanceIndex).mul(6.283)).sin().mul(0.15).add(0.85);
const c = mix(colorVec(uGlowA), colorVec(uGlowB), hash(instanceIndex.add(9)));
localMat.colorNode = c.mul(localPulse(ci * 2.1).mul(2.6).add(0.2)).mul(blink).mul(uGlow);
// A scruffy little colony: polyps scattered on a few upright stalks.
const N = 70;
const mesh = new THREE.InstancedMesh(tipGeo, worldMat, N);
mesh.frustumCulled = false;
const m = new THREE.Matrix4();
const q = new THREE.Quaternion();
const s = new THREE.Vector3();
const p = new THREE.Vector3();
for (let i = 0; i < N; i++) {
const a = rnd() * Math.PI * 2;
const r = Math.pow(rnd(), 0.6) * 0.75;
p.set(Math.cos(a) * r, 0.06 + Math.pow(rnd(), 1.6) * 0.85, Math.sin(a) * r);
s.setScalar(0.028 + rnd() * 0.03);
mesh.setMatrixAt(i, m.compose(p, q, s));
}
mesh.instanceMatrix.needsUpdate = true;
group.add(mesh);
colonies.push({ group, localMat, mesh });
});
// ---------- panel ----------
let drift = true;
const out = new Readouts();
const readMode = out.add('Pulse source', 'positionWorld', 'good');
const readColonies = out.add('Colonies', '3 · one shared wave');
const ui = new Panel('Colony pulse');
ui.check({
label: 'World-space wave',
value: true,
onChange: (v) => {
for (const c of colonies) c.mesh.material = v ? worldMat : c.localMat;
floor.visible = v;
readMode(v ? 'positionWorld' : 'positionLocal + phase', v ? 'good' : 'bad');
readColonies(v ? '3 · one shared wave' : '3 · three private clocks');
},
});
ui.check({
label: 'Drift the middle colony',
value: true,
onChange: (v) => { drift = v; },
});
ui.slider({
label: 'Pulse speed',
value: 1,
min: 0,
max: 3,
onChange: (v) => { uPulse.value = v; },
});
ui.slider({
label: 'Wave sharpness',
value: 2.5,
min: 1,
max: 8,
onChange: (v) => { uSharp.value = v; },
});
ui.slider({
label: 'Wave heading',
value: 0,
min: -Math.PI,
max: Math.PI,
format: (v) => `${Math.round((v * 180) / Math.PI)}°`,
onChange: (v) => {
const d = uDir.value as THREE.Vector3;
d.set(Math.cos(v) * 1.9, 1.1, Math.sin(v) * 1.9);
},
});
ui.slider({
label: 'Bioluminescence',
value: 1.2,
min: 0,
max: 2.5,
onChange: (v) => { uGlow.value = v; },
});
ui.note(
'With positionWorld, moving a colony moves it through the wave. With a ' +
'local phase it carries its own beat around and nothing lines up.',
);
stage.onFrame((_dt, t) => {
const middle = colonies[1].group;
middle.position.x = drift ? -0.5 + Math.sin(t * 0.45) * 1.3 : -0.5;
});
}