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

14 KiB
Raw Blame History

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

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 == 0reenter_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: makeTransitioninit_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): damps _dampedEye; pivotWorld = playerPos + (0,0,1.5); if CameraDiagnostics.CollideCamera && CollisionProbe != nullswept = 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): 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).