vestige/apps/dashboard/src/lib/observatory/camera.ts
Sam Valladares a046c620c8 feat(observatory): raw-WebGPU living cognitive field with 5 deterministic demos
The Observatory is a full-bleed, zero-library WebGPU surface that renders
the memory graph as a living cognitive field. Five deterministic demo
moments driven by a URL contract (?demo=<name>&seed=...&frame=N):
recall-path, engram-birth, salience-rescue, forgetting-horizon, firewall.
Capture mode (?frame=N) freezes the sim deterministically so the same URL
produces identical pixels, the viral-clip primitive.

Architecture: bare-metal WebGPU engine (no Three.js), seeded demo clock,
per-demo plan + renderer modules, WGSL shaders (simulate, nodes, edges,
path, birth particles, rescue, forgetting, firewall) plus a post-processing
chain (tone mapping, MIP). DOM is instrument overlays only (telemetry strip,
timeline spine, rescue verdict); the layout gives /observatory the same
full-bleed bypass as marketing routes so recordings stay clean.

Reads the real memory graph. Verified live: svelte-check 939 files 0 errors,
96 observatory unit tests green, all 5 demos load at 108-119fps with zero
console errors against the live brain.

Known follow-up: engram-birth capture-mode particle cluster needs a render
fix pass before it is camera-ready; the other 4 demos are camera-ready.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:49:18 -07:00

139 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Cognitive Observatory — minimal column-major mat4 camera math.
*
* Spec §6: no new dependencies — small local helpers instead of three.js.
* All outputs are Float32Array(16) in WebGPU/WGSL column-major order.
*/
export type Mat4 = Float32Array;
/** Perspective projection (right-handed, depth 0..1 as WebGPU expects). */
export function perspective(fovYRad: number, aspect: number, near: number, far: number): Mat4 {
const f = 1 / Math.tan(fovYRad / 2);
const nf = 1 / (near - far);
// column-major
const m = new Float32Array(16);
m[0] = f / aspect;
m[5] = f;
m[10] = far * nf;
m[11] = -1;
m[14] = far * near * nf;
return m;
}
/** Right-handed lookAt view matrix. */
export function lookAt(
eye: [number, number, number],
target: [number, number, number],
up: [number, number, number]
): Mat4 {
const [ex, ey, ez] = eye;
let zx = ex - target[0];
let zy = ey - target[1];
let zz = ez - target[2];
let len = Math.hypot(zx, zy, zz) || 1;
zx /= len;
zy /= len;
zz /= len;
// x = up × z
let xx = up[1] * zz - up[2] * zy;
let xy = up[2] * zx - up[0] * zz;
let xz = up[0] * zy - up[1] * zx;
len = Math.hypot(xx, xy, xz) || 1;
xx /= len;
xy /= len;
xz /= len;
// y = z × x
const yx = zy * xz - zz * xy;
const yy = zz * xx - zx * xz;
const yz = zx * xy - zy * xx;
const m = new Float32Array(16);
m[0] = xx;
m[1] = yx;
m[2] = zx;
m[4] = xy;
m[5] = yy;
m[6] = zy;
m[8] = xz;
m[9] = yz;
m[10] = zz;
m[12] = -(xx * ex + xy * ey + xz * ez);
m[13] = -(yx * ex + yy * ey + yz * ez);
m[14] = -(zx * ex + zy * ey + zz * ez);
m[15] = 1;
return m;
}
/** out = a × b (column-major). */
export function multiply(a: Mat4, b: Mat4): Mat4 {
const out = new Float32Array(16);
for (let c = 0; c < 4; c++) {
for (let r = 0; r < 4; r++) {
out[c * 4 + r] =
a[r] * b[c * 4] +
a[4 + r] * b[c * 4 + 1] +
a[8 + r] * b[c * 4 + 2] +
a[12 + r] * b[c * 4 + 3];
}
}
return out;
}
export interface OrbitCamera {
viewProj: Mat4;
/** world-space camera right vector (billboarding) */
right: [number, number, number];
/** world-space camera up vector (billboarding) */
up: [number, number, number];
eye: [number, number, number];
}
/**
* Deterministic slow orbit camera: angle driven by the loop phase (frames),
* never wall clock — the same frame always yields the same view. Returns the
* view-projection plus the camera basis for GPU billboards.
*/
export function orbitCamera(
phase: number,
aspect: number,
distance: number,
elevation = 0.35
): OrbitCamera {
const angle = phase * Math.PI * 2;
const eye: [number, number, number] = [
Math.sin(angle) * distance,
distance * elevation,
Math.cos(angle) * distance
];
const proj = perspective((50 * Math.PI) / 180, aspect, 0.1, 4000);
const view = lookAt(eye, [0, 0, 0], [0, 1, 0]);
// camera basis: forward = normalize(target - eye), right = f × up, up = r × f
let fx = -eye[0],
fy = -eye[1],
fz = -eye[2];
let len = Math.hypot(fx, fy, fz) || 1;
fx /= len;
fy /= len;
fz /= len;
let rx = fy * 0 - fz * 1;
let ry = fz * 0 - fx * 0;
let rz = fx * 1 - fy * 0;
len = Math.hypot(rx, ry, rz) || 1;
rx /= len;
ry /= len;
rz /= len;
const ux = ry * fz - rz * fy;
const uy = rz * fx - rx * fz;
const uz = rx * fy - ry * fx;
return {
viewProj: multiply(proj, view),
right: [rx, ry, rz],
up: [ux, uy, uz],
eye
};
}