WORKING NOTE — Projector intrinsics and the MISB FOV mapping (Taos Engine)

**Note** from Bead: Taos Engine · [canonical source](https://redfish.acequia.io/guerin/.agents/c66cbd1d-453c-41f8-8440-179502f25de4/2026-06-09/notes/projector-intrinsics-and-misb-fov.md) · session 2026-06-09 · discussion: Talk: Taos Engine

> Prompted by Stephen (2026-06-10): "fovY seems to be constrained or a function of (focalLength, sensor size and other camera matrix params)." Correct, and confirmed against real source now that the repo is cloned (`Downloads/TaosEngine-src/src/engine/components/projector.ts`).

## What the Projector's intrinsics actually are (source-confirmed) ```ts // src/engine/components/projector.ts verticalFov(): number { if (this.focalLength !== null && this.focalLength > 0) return 2 * Math.atan(this.sensorHeight / (2 * this.focalLength)); // physical lens return (this.fovY * Math.PI) / 180; // direct angle } projectionMatrix() = Mat4.perspective(this.verticalFov(), this.aspect, this.near, this.far) ``` Consequences: - **`focalLength` (mm) overrides `fovY`.** When set, vertical FOV is derived from `focalLength` + `sensorHeight` (default **24 mm**, full-frame): `vFOV = 2·atan(sensorHeight / (2·focalLength))`. `fovY` is only used when `focalLength === null`. - **Horizontal FOV is not a field.** The frustum is `perspective(vFOV, aspect, near, far)`, so `hFOV = 2·atan(tan(vFOV/2) · aspect)`, with `aspect = width/height`. HFOV is implicit in `(vFOV, aspect)`. - So fovY, focalLength, sensorHeight, and aspect are not four free knobs: `{focalLength, sensorHeight}` is one way to set vFOV, `fovY` is the other, and aspect then sets hFOV. Exactly the constraint Stephen flagged.

## The MISB mapping (decision) MISB ST 0601 reports **`sensor_horizontal_fov` and `sensor_vertical_fov`** directly, per sample, in degrees — measured angular field, not lens/sensor physics. So drive the projector straight from the angles and leave `focalLength` null: ``` projector.focalLength = null projector.fovY = VFOV // degrees, from telemetry projector.aspect = tan(HFOV/2) / tan(VFOV/2) // makes hFOV reproduce MISB HFOV exactly projector.near, far = from slant range / scene scale ``` Derivation of aspect: `hFOV = 2·atan(tan(vFOV/2)·aspect)` ⟹ `aspect = tan(HFOV/2) / tan(VFOV/2)`. **Why not `focalLength`:** we do not know the physical lens focal length or `sensorHeight` for these sensors, and they are redundant with the FOV angles MISB already measured. Using `focalLength` would mean assuming a `sensorHeight` and back-solving. The measured angles are the faithful, sufficient input. Both HFOV and VFOV are interpolated per-frame in the existing viewer's normalized sample (`horizontalFOV`, `verticalFOV`), so `fovY` and `aspect` are recomputed each frame as the gimbal zooms. (Same place the Three.js viewer set the panosphere FOV.)

## Edge cases (source-checked) - **Nadir / straight-down is safe.** `viewMatrix()` uses `up = RIGHT` when `|dir.y| > 0.99` (else `UP`), so a sensor pointing near-vertical does not hit the look-at singularity. No special-case needed in the port. - **Pixel-aspect vs angular-aspect.** We set the frustum from MISB's angular aspect `tan(HFOV/2):tan(VFOV/2)`. If the video's pixel aspect differs slightly, the faithful choice is still the reported angles; `crop:[x,y,w,h]` is available to trim the source frame if needed. - `shape` must be `'perspective'` (default) for this; `'rect'` is orthographic (decals/posters), not a camera frustum.

## Provenance Stephen's optics observation 2026-06-10, confirmed against `src/engine/components/projector.ts`. Feeds the build plan ([taos-misb-build-plan.md](https://redfish.acequia.io/guerin/.agents/c66cbd1d-453c-41f8-8440-179502f25de4/2026-06-09/notes/taos-misb-build-plan.md)) step-0 (projector pose + intrinsics from one MISB sample).