Environment Map
One 360° equirectangular map lights the whole scene: it is the background and every reflection on a mirror-metal cube floating in it.
import { perspectiveCamera, type SceneCamera } from 'vgpu/scene';
export const FOV_DEGREES = 42;
export const ORBIT_RADIUS = 3.35;
export interface CameraView {
/** View-projection consumed by the cube draw. */
readonly camera: SceneCamera;
readonly position: readonly [number, number, number];
/** Orthonormal basis + tangent, used by the background pass to rebuild primary rays. */
readonly forward: readonly [number, number, number];
readonly right: readonly [number, number, number];
readonly up: readonly [number, number, number];
readonly tanHalfFov: number;
readonly aspect: number;
}
/**
* Builds one camera in two forms that must agree: a view-projection matrix for
* rasterizing the cube, and a ray basis for sampling the environment behind it.
* `camera.test.ts` pins them together.
*/
export function cameraView(yaw: number, pitch: number, aspect: number): CameraView {
const clampedPitch = Math.max(-1.2, Math.min(1.2, pitch));
const cosPitch = Math.cos(clampedPitch);
const position: [number, number, number] = [
Math.sin(yaw) * cosPitch * ORBIT_RADIUS,
Math.sin(clampedPitch) * ORBIT_RADIUS,
Math.cos(yaw) * cosPitch * ORBIT_RADIUS,
];
const forward = normalize([-position[0], -position[1], -position[2]]);
const right = normalize(cross(forward, [0, 1, 0]));
const up = cross(right, forward);
return {
camera: perspectiveCamera({
fov: FOV_DEGREES,
aspect,
near: 0.1,
far: 40,
position,
target: [0, 0, 0],
}),
position,
forward,
right,
up,
tanHalfFov: Math.tan((FOV_DEGREES * Math.PI) / 360),
aspect,
};
}
/** Column-major rotation (Y then X) for the cube's own spin. */
export function spinMatrix(time: number): Float32Array {
const yaw = 0.85 + time * 0.35;
const pitch = Math.sin(time * 0.23) * 0.35 + 0.42;
const cy = Math.cos(yaw);
const sy = Math.sin(yaw);
const cp = Math.cos(pitch);
const sp = Math.sin(pitch);
return new Float32Array([
cy, 0, -sy, 0,
sy * sp, cp, cy * sp, 0,
sy * cp, -sp, cy * cp, 0,
0, 0, 0, 1,
]);
}
type Vec3 = readonly [number, number, number];
function cross(a: Vec3, b: Vec3): [number, number, number] {
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
}
function normalize(v: Vec3): [number, number, number] {
const length = Math.hypot(v[0], v[1], v[2]) || 1;
return [v[0] / length, v[1] / length, v[2] / length];
}