acdream/docs/research/2026-06-05-camera-collision-residual-a-handoff.md
Erik 2c7948a9f1 docs: handoff + kickoff for Render Residual A (camera collision verbatim port)
Session wrap: cellar-lip wedge fixed + visual-verified (cc4590f/9fdf6a5/41db027).
Next task per the plan = Render Residual A: keep the chase camera eye inside the
player's cell by porting retail SmartBox::update_viewer verbatim (fixes interior
walls going grey/transparent from inside).

- New canonical handoff with copy-paste fresh-session kickoff prompt, the retail
  update_viewer decode, the V1 current-state map, the gap to pin (faithful
  start-cell + AdjustPosition fallbacks + the no-wall-hit cause), and the
  evidence-first plan ([flap-sweep] capture → deterministic SweepEye test → port).
- Key finding recorded: find_valid_position (pc:273890) just calls
  find_transitional_position — the sweep function is faithful, NOT the divergence.
- CLAUDE.md banner updated to point at the new state + handoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 09:46:52 +02:00

180 lines
14 KiB
Markdown
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.

# Handoff — Render Residual A: camera collision (verbatim port of `SmartBox::update_viewer`) — 2026-06-05
## ▶ FRESH-SESSION KICKOFF PROMPT (copy-paste)
```
Continue acdream M1.5 render work: Render Residual A — CAMERA COLLISION (keep the 3rd-person camera
eye inside the player's cell so interior walls stop going grey/transparent from inside). This is a
VERBATIM port of retail SmartBox::update_viewer — no hybrids, no bandaids (master-plan mandate).
Branch claude/thirsty-goldberg-51bb9b (do NOT branch/worktree; do NOT push without asking; NEVER
git stash/gc). PowerShell on Windows; launch logs are UTF-16 (Select-String / rg --encoding utf-16le,
NOT GNU grep). Use superpowers:systematic-debugging; the user pre-approved the verbatim-port APPROACH
and the A→C→B order, so when you reach the design step use superpowers:brainstorming only to present
the concrete port design for sign-off before editing.
READ FIRST (in order):
1. docs/research/2026-06-05-camera-collision-residual-a-handoff.md (THIS file — canonical).
2. docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md (§3 residuals A/B/C; the
blue-hole DON'T-redo: never re-add a CurrCell write inside ResolveWithTransition/ResolveCellId).
3. docs/superpowers/specs/2026-06-03-verbatim-spatial-pipeline-port-master-plan.md (§C Camera: C1/C3).
4. memory/reference_render_pipeline_state.md + project_camera_visibility_coupling.md.
STATE: M1.5 "indoor world feels right." The cellar-lip step-up wedge is FIXED + visual-verified
(committed cc4590f/9fdf6a5/41db027 — check_other_cells now reads the LIVE sphere position). Per the
plan the next task is Render Residual A: camera collision. User-confirmed problem mapping: Residual A
= interior walls/seams go grey/transparent WHILE INSIDE (the chase eye drifts OUT of the player's
cell → near walls back-face/clip away); Residual C = outside-looking-in glass-box (separate, bigger
DrawPortal phase, do AFTER A); Residual B = particles (smallest, last).
GOAL: port retail SmartBox::update_viewer (0x453ce0, pc:92761) faithfully so [flap-cam] eyeInRoot=y
while inside and interior walls stay opaque. Retail behavior: pivot at player head → (indoor) pick the
PIVOT's cell via CPhysicsObj::AdjustPosition → SWEEP the 0.3 viewer_sphere pivot→sought-eye via a
CTransition, stop at first wall → viewer=curr_pos, viewer_cell=curr_cell → fallback AdjustPosition at
sought-eye → fallback snap-to-player.
KEY FINDINGS (do NOT re-derive):
- find_valid_position (pc:273890) is literally `return find_transitional_position(this)` (pc:273898).
So acdream's SweepEye→ResolveWithTransition→FindTransitionalPosition IS the faithful sweep. The
sweep FUNCTION is NOT the divergence — do not re-port it.
- The sweep + viewer_cell are ALREADY wired (V1): RetailChaseCamera.Update (damped eye → pivot →
CollisionProbe.SweepEye) + PhysicsCameraCollisionProbe.SweepEye (viewer sphere r=0.3, moverFlags
IsViewer|PathClipped|FreeRotate|PerfectClip, gated on CameraDiagnostics.CollideCamera).
- THE BUG (per handoff §3 + the [flap-sweep] probe comment): the sweep RUNS but finds NO wall
(pulledIn≈0, resolved=Y, bsp=ok) → the eye flies to full chase distance (eyeInRoot=n ~90%) in cells
like 0xA9B40174/0175. Root cause of the no-wall-hit is NOT yet pinned.
- GAPS per master-plan C1: (a) faithful START-CELL — retail uses AdjustPosition to find the PIVOT's
cell; acdream passes the player cellId straight in. (b) the two AdjustPosition FALLBACKS are missing.
(c) C3 find_visible_child_cell (pc:311397) is not ported (viewer cell uses the sweep curr_cell —
fine for now). Whether (a)/(b) actually cause the no-wall-hit is UNVERIFIED — pin it with evidence.
THE JOB (evidence-first; the saga lesson = do NOT guess):
1. Live capture: launch with ACDREAM_PROBE_FLAP=1 (+ CameraDiagnostics.CollideCamera on), stand inside
the Holtburg cottage, rotate the chase camera into a back wall. Capture [flap-sweep] (cell/resolved/
bsp/desiredBack/eyeBack/pulledIn/collNormValid) + [flap-cam] (root/eyeInRoot). Use the probe-comment
fork in PhysicsCameraCollisionProbe.cs to read WHY: pulledIn≈0 + bsp=ok ⇒ the sweep reaches no wall
geometry in the candidate set (clip/candidate-cell issue or wrong start cell); resolved=n/bsp=nobsp
⇒ collision can't run there (cell/BSP not loaded).
2. Diagnose the no-wall-hit from the capture (likely: the sweep's candidate-cell set doesn't include
the wall's cell, OR the start cell is wrong because AdjustPosition isn't seating the pivot). Confirm
against retail update_viewer before changing anything.
3. Port verbatim: the faithful start-cell (AdjustPosition for the pivot's cell, indoor branch) + the
two AdjustPosition fallbacks, plus whatever the capture proves is the no-wall-hit cause. Consider a
DETERMINISTIC SweepEye test (cell fixture + seed pivot/eye, assert the sweep stops at the wall) —
the CellarLipWedgeTests pattern made the stairs fix iterable in <200ms; do the same here.
4. VALIDATE: eyeInRoot=y inside; build + Core(1317p/4f/1s)/App green. VISUAL GATE: stand inside the
cottage + rotate — interior walls stay solid (no grey/transparent, no NPCs/particles through walls);
inside-looking-out still correct (don't regress the fixed flap); generic outdoor chase unaffected.
DO NOT: guess / speculative-edit (the saga's failure mode); re-add a CurrCell write inside
ResolveWithTransition/ResolveCellId (the blue-hole clobber — CurrCell is player-only via
UpdatePlayerCurrCell); conflate A (camera-eye containment, this task) with C (DrawPortal outside-
looking-in, next task); re-port find_valid_position/the sweep (it's faithful).
TEST BASELINE: Core 1317 pass / 4 fail (documented: Apparatus_Grounded_50cmOffCenter, 2×
DoorBugTrajectoryReplay LiveCompare_*, BSPStepUpTests.D4) / 1 skip. App green. Branch HEAD 41db027.
```
---
## 1. Session summary (2026-06-05)
**Shipped + visual-verified: the P2 cellar-lip step-up wedge.** Root cause = `Transition.CheckOtherCells`
collided the other cells against a STALE `footCenter` snapshotted before the primary collide; after a
step-up climbed the foot onto the cottage floor, the stale (pre-climb, penetrating) position spuriously
near-missed that floor → a doomed second step-up → revert → 0% advance. Fix: re-read
`footCenter = sp.GlobalSphere[0].Origin` in `RunCheckOtherCellsAndAdvance` (retail `check_other_cells`
reads the live `sphere_path.global_sphere`, pc:272735). 0/29 → 20/29 captured wedge frames climb; zero
regression. User visual-gate: **"Yes all works!"** (cellar smooth, door blocks, step-up climbs).
Commits `cc4590f` (fix + tests) / `9fdf6a5` (strip probes) / `41db027` (visual-gate note). Full writeup
+ the disproven prior framings: [`2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md`](2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md)
(top banner) + memory `project_p2_door_stepup_findings`.
**Then: picked + scoped the next task (this handoff).** Per the plan the next step after the collision
fix is Render Residual A — camera collision. Aligned with the user on the problem statement (the two
symptoms → residuals A/C) and the approach (verbatim port of `SmartBox::update_viewer`, A→C→B order).
Did the read-only investigation below; NO camera code changed (next session implements after the
evidence-first diagnosis).
## 2. The problem (user-confirmed)
| Symptom (user words) | Residual | Cause | Fix |
|---|---|---|---|
| Inside a building, walls/seams flicker grey/transparent; can see through walls | **A** (this task) | 3rd-person chase eye drifts OUTSIDE the player's cell → near walls seen from their back-faces → culled | camera collision: sweep the eye, stop at the wall, keep it in the cell |
| Outside looking in through a doorway, building is a see-through glass box; ground over the floor | **C** (next) | outdoor→interior portal render (retail `DrawPortal`) not built | build that render phase |
| Particles bleed through floor | **B** (last) | scene particles not cell-clipped (#104) | cell-link the emitters |
Order **A → C → B**: A is smaller + builds the shared "which cell is the viewpoint in" machinery that C
also needs (shrinks C). Mechanism for "transparent wall" = **back-face culling** (a wall is a one-sided
sheet facing into the room; from outside the room you see its culled back) + the renderer drawing from
the **viewer's cell** then flooding portals (so the viewer's cell must be right).
## 3. Retail target — `SmartBox::update_viewer` (0x453ce0, pc:92761)
Decoded this session (read the decomp directly for the verbatim port):
1. If `player->cell == 0``reenter_visibility`; still 0 → `set_viewer(player_pos, 1)`, `viewer_cell=null`, return.
2. Compute the desired eye (`viewer_sought_position`) from the pivot (head + `pivot_offset`).
3. **Start cell:** if player indoor (`objcell_id >= 0x100`), `CPhysicsObj::AdjustPosition(&var_90, &viewer_sphere, &cell_1, 0, 1)` to find the PIVOT's cell; success → `cell = cell_1`, else `cell = player->cell`. Outdoor → `cell = player->cell`.
4. **Sweep:** `makeTransition``init_object(player, 0x5c)``init_sphere(1, &viewer_sphere, 1.0)` (ONE sphere) → `init_path(cell_1, pivot, sought_eye)``find_valid_position`.
- success → `set_viewer(curr_pos, 0)`, `viewer_cell = sphere_path.curr_cell`, return.
- **fallback 1:** `AdjustPosition(sought_eye, &viewer_sphere, &var_170, 0, 1)``set_viewer(var_120, 0)`, `viewer_cell = var_170`, return.
- **fallback 2:** `set_viewer(player_pos, 1)`, `viewer_cell = null`.
- `0x5c` = `IsViewer | PathClipped | FreeRotate | PerfectClip` (PathClipped = hard-stop at first contact).
- **`find_valid_position` (pc:273890) = `return find_transitional_position(this)` (pc:273898)** — the
sweep is the ordinary transition; acdream's `ResolveWithTransition` is faithful to it. **The sweep
function is NOT the divergence.**
## 4. acdream current state (V1, partial)
- `RetailChaseCamera.Update` ([src/AcDream.App/Rendering/RetailChaseCamera.cs:102](../../src/AcDream.App/Rendering/RetailChaseCamera.cs)):
damps `_dampedEye`; `pivotWorld = playerPos + (0,0,1.5)`; if `CameraDiagnostics.CollideCamera &&
CollisionProbe != null``swept = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId)`;
`publishedEye = swept.Eye`, `ViewerCellId = swept.ViewerCellId`. (Collides into a LOCAL, leaves
`_dampedEye` clean to avoid wall-press oscillation — keep that.)
- `PhysicsCameraCollisionProbe.SweepEye` ([src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:24](../../src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs)):
shifts pivot/eye down by the radius (InitPath sphere-center convention), `ResolveWithTransition`
(viewer sphere r=0.3, height 0, isOnGround=false, body=null, moverFlags
`IsViewer|PathClipped|FreeRotate|PerfectClip`, `movingEntityId=selfEntityId`), returns swept eye +
`r.CellId`. **Passes the player `cellId` straight in — does NOT do retail's AdjustPosition pivot-cell;
has NO AdjustPosition fallbacks.**
- The `[flap-sweep]` probe (in SweepEye, gated `RenderingDiagnostics.ProbeFlapEnabled` = `ACDREAM_PROBE_FLAP`)
+ the builder's `[flap-cam]`/`[flap]`/`[shell]`/`[vis]` probes are the diagnosis apparatus — already
in the tree.
## 5. The gap to pin (next session, evidence-first)
The symptom is "sweep runs, finds no wall" (`pulledIn≈0`, `eyeInRoot=n ~90%`). Candidates, in order of
suspicion:
1. **Start cell** — acdream passes the player cell; retail seats the start cell at the PIVOT via
`AdjustPosition`. If the pivot/eye path's walls live in a cell that isn't the start cell and isn't
reached by the sweep's `check_other_cells` candidate set, the sweep misses them. (Most likely +
matches master-plan C1's "faithful start-cell" gap.)
2. **Candidate-cell tracking across the multi-step sweep** — the eye is ~2.6 m behind the player and the
sweep subdivides; if the carried cell doesn't advance into the wall's cell, the wall poly is never
in the per-cell BSP queried. (Related to the Stage-1 membership work; the player path now tracks the
carried cell correctly — verify the camera sweep does too.)
3. **AdjustPosition missing** — fallbacks aside, retail's start-cell AdjustPosition may be what seats
the sweep so it engages geometry; acdream has no AdjustPosition port at all (check
`CPhysicsObj::AdjustPosition`).
Pin with the live `[flap-sweep]` capture FIRST, then port. A deterministic `SweepEye` test (cottage
cell fixture, seed pivot inside + eye behind the back wall, assert the swept eye stops at the wall and
`ViewerCellId` stays the room) would make this iterable like the cellar-lip fix.
## 6. Apparatus + anchors
- **Probes:** `ACDREAM_PROBE_FLAP=1``[flap-sweep]` (PhysicsCameraCollisionProbe) + `[flap-cam]`/
`[flap]`/`[shell]`/`[vis]` (CellVisibility / the render builder). `CameraDiagnostics.CollideCamera`
toggles the spring-arm.
- **Decomp anchors:** `SmartBox::update_viewer` 0x453ce0 pc:92761 · `find_valid_position` pc:273890 →
`find_transitional_position` pc:273613 · `CPhysicsObj::AdjustPosition` (grep the decomp) ·
`CEnvCell::find_visible_child_cell` 0x52dc50 pc:311397 (C3, viewer child cell — not yet ported,
optional for A).
- **DON'T-redo:** the blue-hole fix (`UpdatePlayerCurrCell` player-only render-root write) — never
re-add a `CurrCell` write in `ResolveWithTransition`/`ResolveCellId`. Don't conflate A with C.
## 7. Brainstorming state (for the fresh session)
Approach + order are USER-APPROVED (verbatim port of `update_viewer`; A→C→B). The brainstorming design
step was NOT completed — resume by doing the evidence-first diagnosis (§5), then present the concrete
port design (start-cell + fallbacks + the no-wall-hit fix) for sign-off before editing (HARD-GATE:
no code until the design is approved).