Files
CodeNavigator/web/camera.js
T
Andy Dixon eae93d93dc Render 3D through a real perspective camera
Adds camera.js with perspective, lookAt and matrix helpers. The orbit
view is now a lookAt pose feeding one view-projection matrix shared by
the shaders and project(), replacing the hand-rolled projection with
its clamped depth. A node test checks the new camera reproduces the old
projection to within 0.05 px, so the landscape looks the same; this is
the base the city's walk and fly cameras need (near plane, correct w).
2026-05-03 09:41:31 +01:00

58 lines
2.7 KiB
JavaScript

// Minimal column-major 4x4 matrix helpers and the cameras built on them. World axes: x right,
// y "down" the 2D map, z up. Every 3D view renders and projects through one view-projection
// matrix, so WebGL, the overlay and picking agree exactly.
// The world is left-handed (y runs down the map, z up), so x is negated to keep the map unmirrored.
export function perspective(fovY, aspect, near, far) {
const f = 1 / Math.tan(fovY / 2), nf = 1 / (near - far);
return new Float32Array([-f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, -1, 0, 0, 2 * far * near * nf, 0]);
}
export function lookAt([ex, ey, ez], [tx, ty, tz], [ux, uy, uz] = [0, 0, 1]) {
let zx = ex - tx, zy = ey - ty, zz = ez - tz;
let len = Math.hypot(zx, zy, zz) || 1; zx /= len; zy /= len; zz /= len;
let xx = uy * zz - uz * zy, xy = uz * zx - ux * zz, xz = ux * zy - uy * zx;
len = Math.hypot(xx, xy, xz);
if (len < 1e-6) { xx = 1; xy = 0; xz = 0; len = 1; } // looking straight along up
xx /= len; xy /= len; xz /= len;
const yx = zy * xz - zz * xy, yy = zz * xx - zx * xz, yz = zx * xy - zy * xx;
return new Float32Array([
xx, yx, zx, 0, xy, yy, zy, 0, xz, yz, zz, 0,
-(xx * ex + xy * ey + xz * ez), -(yx * ex + yy * ey + yz * ez), -(zx * ex + zy * ey + zz * ez), 1,
]);
}
export function multiply(a, b) {
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;
}
// Clip coordinates of a world point.
export function transform(m, x, y, z) {
return [
m[0] * x + m[4] * y + m[8] * z + m[12],
m[1] * x + m[5] * y + m[9] * z + m[13],
m[2] * x + m[6] * y + m[10] * z + m[14],
m[3] * x + m[7] * y + m[11] * z + m[15],
];
}
// Unit view direction for a yaw (around z, 0 = looking toward -y) and pitch (up positive).
export function direction(yaw, pitch) {
return [-Math.sin(yaw) * Math.cos(pitch), -Math.cos(yaw) * Math.cos(pitch), Math.sin(pitch)];
}
// Orbit camera matching the original landscape projection: focal length 2.7, world scaled by
// 1/scale, with the view lifted by `lift` along the camera's up axis. `pitch` is the elevation.
export function orbitPose({ x, y, yaw, pitch, distance }, scale = 420, lift = .35) {
const cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch), d = distance * scale;
const up = [-sy * sp, -cy * sp, cp], shift = lift * scale;
const target = [x + up[0] * shift, y + up[1] * shift, up[2] * shift];
return { eye: [target[0] + sy * cp * d, target[1] + cy * cp * d, target[2] + sp * d], target, up };
}
export const ORBIT_FOV = 2 * Math.atan(1 / 2.7);