**Note** from Bead: Taos Engine · [canonical source](https://redfish.acequia.io/guerin/.agents/c66cbd1d-453c-41f8-8440-179502f25de4/2026-06-09/notes/sphere-cap-plan.md) · session 2026-06-09 · discussion: Talk: Taos Engine
Stephen's design: the non-projective display = the image painted on the **sphere of directions at the camera center**. `p` toggles **projective (terrain drape)** vs **sphere (camera-centric)**. Sphere chosen over billboard because it generalizes — perspective = a cap, 360 = the full sphere. Radius R = **focus distance** (slider, default = live slant range).
## Why sphere (the principle) An image samples the light field **by direction** — each pixel is a ray from the optical center. Its natural domain is the sphere of directions, not a plane or the ground. Perspective→cap, fisheye→bigger cap, 360→full sphere. Only the **pixel→direction map** (lens model) changes per camera. Projective-on-terrain is the special case (maps pixels to ground hits; can't represent a 360's sky). The two **coincide at the center** ⇒ `h` (pose+fov match) fills the screen in either mode; `p` decides what happens as you move off-center.
## Grounded implementation (engine facts confirmed) - **Mesh:** `Mesh.fromData(device, vertices: Float32Array, indices: Uint32Array)`. Interleaved vertex = **12 floats**: position(3) + normal(3) + uv(2) + tangent(4), stride 48 B. - **Material:** `new PbrMaterial({ albedoMap: videoTex, albedo:[1,1,1,1], unlit: true, transparent: true })` (geo_photo.ts does exactly this) — sample the rVFC `videoTex`. - **Renderer:** `sphereGO.addComponent(new MeshRenderer(capMesh, capMat))` (import `Mesh, MeshRenderer, PbrMaterial` from `../src/engine/index.js`).
## Cap geometry (local sensor frame, unit radius) Grid of (N+1)² verts over image (u,v)∈[0,1]². Per vertex (sensor frame: look=−Z, up=+Y, right=+X): ``` x = (u-0.5)*2 * tan(hfov/2) y = (0.5-v)*2 * tan(vfov/2) // v=0 = image top = +Y up dir = normalize(x, y, -1) pos = dir // unit radius; R applied as GameObject scale uv = (u, v); normal = -dir; tangent = (1,0,0,1) ``` Triangulate the grid (wind so the front faces the center; if it renders blank, flip the index order or set the material non-culling). **R as GameObject scale** = `R = focusDistance` (default live slant range) → resizing is just a scale set, no rebuild. Bake hfov/vfov at the nominal sensor fov for v1 (gimbal-zoom variation is small); rebuild only if fov drifts a lot. **360 path:** swap the (u,v)→dir map to lon/lat and tessellate the full sphere — same mesh/material, different builder (the per-lens-model adapter = a `#catalog` concern).
## Per-frame update (sphere mode) Like the projector: `sphereGO.position = sensor world`, `sphereGO.rotation = lookRotation(frameCenter − sensor)`, `sphereGO.scale = focusDistance`. Texture auto-updates via the shared rVFC `videoTex`.
## `p` toggle + focus slider - `p` (move re-frame off `p` → `o`): show projector + hide cap, or hide projector + show cap. (Visibility: `scene.add/remove` the GO, or a renderer/GO enable flag — verify the engine's mechanism.) - **Focus-distance slider** (top-right collapsible): range in metres, default = live slant range, **"⟲ slant"** reset to re-snap to the live value. Drives `focusDistance` (= GameObject scale). R only affects parallax vs terrain; invisible at the center.
## Status Plan only. `h` fov-match (the fill foundation) is built + deployed. Cap is the next build.