docs(A): spec — verbatim SmartBox::update_viewer completion (Render Residual A)
Live ACDREAM_PROBE_FLAP capture (Holtburg cottage/cellar) proved the V1 camera spring-arm already contains the eye (eyeInRoot=Y 99.75%, viewerCell never 0, indoor collide 97.6% in 0174). The dominant inside-cottage bluish void is the render-sealing residual C (DrawPortal), NOT the camera. This spec scopes the FAITHFUL completion of Residual A: port the two missing update_viewer pieces verbatim — the indoor start-cell seated at the pivot via CPhysicsObj::AdjustPosition (pc:280009) → CEnvCell::find_visible_child_cell (pc:311397), plus the two AdjustPosition/snap-to-player fallbacks — and land FindVisibleChildCell (which residual C also needs). Faithful layering (mirrors retail SmartBox→CPhysicsObj): primitives in Core (PhysicsEngine.AdjustPosition + CellTransit.FindVisibleChildCell + ResolveResult.Ok), orchestration in App PhysicsCameraCollisionProbe.SweepEye. Deterministic crux test (start-cell resolution) in Core.Tests with the cottage fixtures; SweepEye glue in App.Tests. Visible payoff is narrow (the cellar-corner, point 3); the cottage-room void stays for residual C. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c7948a9f1
commit
0ffc3f5be9
1 changed files with 114 additions and 0 deletions
|
|
@ -0,0 +1,114 @@
|
|||
# Render Residual A — Camera collision: verbatim `SmartBox::update_viewer` completion
|
||||
|
||||
**Date:** 2026-06-05 · **Phase:** M1.5 render residual A · **Branch:** `claude/thirsty-goldberg-51bb9b`
|
||||
|
||||
## 1. The finding (why this is a faithfulness completion, not a visible-bug fix)
|
||||
|
||||
A live `ACDREAM_PROBE_FLAP` capture this session (Holtburg cottage + cellar) proved the
|
||||
V1 camera spring-arm **already works**:
|
||||
|
||||
| Metric | Result | Meaning |
|
||||
|---|---|---|
|
||||
| `[flap-cam] eyeInRoot` | 186,349 `Y` / 470 `n` | eye inside the player's cell **99.75%** |
|
||||
| `viewerCell == 0` (eye in the void) | **0** of ~318k frames | the sweep never lands the eye in invalid space |
|
||||
| indoor collide rate, cell `0174` | **97.6%** | spring-arm engages cell-BSP walls hard |
|
||||
|
||||
The dominant inside-cottage **bluish void** (seeing other buildings / particles / NPCs through
|
||||
the walls) is the render-**sealing** residual **C** (`PView::DrawPortal`), NOT the camera — the
|
||||
eye is already in a valid cell, yet the renderer draws the GL clear colour past unsealed geometry.
|
||||
User-confirmed.
|
||||
|
||||
This task therefore **completes Residual A as a faithful verbatim port** and lands
|
||||
`FindVisibleChildCell`, which **C also needs**. Its one shot at a *visible* win is the
|
||||
**cellar-corner** (user point 3): there the player's feet are in the cellar but the pivot/head
|
||||
is up at cottage-floor level, so the pivot-seated start cell genuinely differs from the feet cell —
|
||||
the only configuration where the faithful start-cell changes the sweep's outcome.
|
||||
|
||||
## 2. Retail target (the oracle — port verbatim)
|
||||
|
||||
- `SmartBox::update_viewer` `0x453ce0` pc:92761 — start-cell → sweep → fallbacks.
|
||||
- `CPhysicsObj::AdjustPosition` `0x511d80` pc:280009 — indoor → `find_visible_child_cell`; outdoor → `LandDefs::adjust_to_outside`.
|
||||
- `CEnvCell::find_visible_child_cell` `0x52dc50` pc:311397 — `this`/portals/stab_list `point_in_cell`.
|
||||
- `CEnvCell::GetVisible` `0x52dc10` pc:311378 — cell-graph resolve.
|
||||
- `find_valid_position` pc:273890 = `return find_transitional_position(this)` pc:273613 — **the sweep is already faithful; do NOT re-port it.**
|
||||
- `init_object(player, 0x5c)` = `IsViewer | PathClipped | FreeRotate | PerfectClip`; `init_sphere(1, viewer_sphere, 1.0)` (ONE sphere, r=0.3 pc:93314).
|
||||
|
||||
Decoded `update_viewer` (indoor branch):
|
||||
```
|
||||
pivot = head frame · pivot_offset
|
||||
if player indoor (objcell_id >= 0x100):
|
||||
if AdjustPosition(pivot, viewer_sphere) -> cell_1: start = cell_1 # seat start at the PIVOT
|
||||
else: start = player->cell # fallback to feet cell
|
||||
else: start = player->cell
|
||||
sweep viewer_sphere pivot -> sought_eye, startCell=start, flags=0x5c # PathClipped = hard stop
|
||||
if find_valid_position: set_viewer(curr_pos); viewer_cell = curr_cell; return
|
||||
if AdjustPosition(sought_eye, viewer_sphere) -> var_170: # FALLBACK 1
|
||||
set_viewer(sought_eye); viewer_cell = var_170; return
|
||||
set_viewer(player->m_position); viewer_cell = null # FALLBACK 2: snap to player
|
||||
```
|
||||
|
||||
## 3. Design — faithful layering (Core primitives ← App orchestration)
|
||||
|
||||
Retail's `update_viewer` is a **`SmartBox` (camera) method** that calls *down* into physics
|
||||
(`CPhysicsObj::AdjustPosition`, `CTransition`). acdream mirrors that split exactly:
|
||||
|
||||
### Core (`AcDream.Core.Physics`) — the physics primitives
|
||||
- **`CellTransit.FindVisibleChildCell(IDataCache, uint startCellId, Vector3 worldPoint, bool useStabList)`** —
|
||||
sibling of the existing `FindCellList` (retail `find_cell_list`); both are cell-membership
|
||||
resolvers. Port of `find_visible_child_cell`:
|
||||
```
|
||||
start = cg.GetVisible(startCellId); if start == null: return 0
|
||||
if PointInsideCellBsp(start, toLocal(start, worldPoint)): return start.Id # point_in_cell
|
||||
ids = useStabList ? start.VisibleCellIds : start.Portals.Select(OtherCellId)
|
||||
foreach id in ids:
|
||||
c = cg.GetVisible(id)
|
||||
if c != null && PointInsideCellBsp(c, toLocal(c, worldPoint)): return c.Id
|
||||
return 0
|
||||
```
|
||||
Each candidate transforms `worldPoint` through its OWN `InverseWorldTransform` before the
|
||||
BSP test (matches `CellTransit.cs:520`).
|
||||
- **`PhysicsEngine.AdjustPosition(uint seedCellId, Vector3 worldPoint) -> (uint cellId, bool found)`** —
|
||||
port of `CPhysicsObj::AdjustPosition`, indoor branch: `FindVisibleChildCell(seed, point, useStabList:true)`.
|
||||
Outdoor branch (`seedLow < 0x100`) reuses the existing terrain-grid resolution.
|
||||
Retail's `seen_outside -> adjust_to_outside` sub-fallback is **deferred** (not on the cottage/cellar
|
||||
path; adding it unverified would be guessing — see §6).
|
||||
- **`ResolveResult.Ok` (new `bool`, default `true`)** — surfaces the `ok` already computed at
|
||||
`PhysicsEngine.cs:718` (`FindTransitionalPosition`), the faithful map of `find_valid_position != 0`.
|
||||
Default-true → existing callers unaffected.
|
||||
|
||||
### App (`AcDream.App.Rendering`) — the camera orchestration
|
||||
- **`PhysicsCameraCollisionProbe.SweepEye`** gains the verbatim `update_viewer` body:
|
||||
1. indoor (`cellId >= 0x100`) → `start = AdjustPosition(cellId, pivot)` else `cellId`;
|
||||
2. sweep `pivot → desiredEye` from `start` (existing `ResolveWithTransition`, viewer flags);
|
||||
3. `r.Ok` → return `(swept eye, r.CellId)`;
|
||||
4. `!r.Ok` → `AdjustPosition(cellId, desiredEye)` → return `(desiredEye, thatCell)` (fallback 1);
|
||||
5. else → return `(playerPos, 0)` (fallback 2, snap to player).
|
||||
`SweepEye` needs the player world position for fallback 2 → add a `Vector3 playerPos` parameter
|
||||
to `ICameraCollisionProbe.SweepEye` (passed by `RetailChaseCamera.Update`).
|
||||
|
||||
## 4. Tests
|
||||
|
||||
- **Core.Tests (`CellarLipWedgeTests` pattern, RED→GREEN):** load cottage fixtures `0171/0174/0175`.
|
||||
Seed the captured corner frame — player `(153.55, 9.32, 93.11)` in `0174`, pivot `(153.55, 9.32, 94.61)`.
|
||||
Assert `AdjustPosition(0174, pivot)` / `FindVisibleChildCell(0174, pivot, true)` resolves the pivot
|
||||
to its actual (floor-level) cell, not the cellar. <200 ms, iterable.
|
||||
- **App.Tests:** focused `SweepEye` orchestration test — start-cell seated, fallback-2 snaps to
|
||||
`playerPos` when the sweep fails. Fixtures loaded by the `SolutionRoot()` path-walk.
|
||||
|
||||
## 5. Validation / visual gate
|
||||
|
||||
- Core baseline **1317 pass / 4 fail (documented) / 1 skip** maintained (+ the new tests); App green.
|
||||
- **Visual gate:** stand in the cottage cellar, press into a corner, rotate — the **cellar-corner
|
||||
void should improve** (point 3). Inside-looking-out must be **unregressed**. The cottage-room
|
||||
bluish void is **NOT** in scope (Residual C).
|
||||
|
||||
## 6. No-shortcuts rules (per master plan §4)
|
||||
|
||||
1. Every ported behaviour cites its decomp anchor (address + `pc:line`) in a comment.
|
||||
2. No suppression flags / grace periods / `if (problem) return` guards. The two fallbacks are
|
||||
retail's own; fallback 2 (snap-to-player) is the faithful "never leave the eye invalid", not a band-aid.
|
||||
3. The `seen_outside → adjust_to_outside` sub-fallback inside `AdjustPosition` is deferred, not
|
||||
stubbed — documented as out-of-path; revisit if a capture shows the camera needs it.
|
||||
4. Do NOT re-add a `CurrCell` write inside `ResolveWithTransition`/`ResolveCellId` (the blue-hole
|
||||
clobber — `CurrCell` is player-only via `UpdatePlayerCurrCell`).
|
||||
5. Do NOT conflate A (eye containment) with C (`DrawPortal` outside-looking-in).
|
||||
Loading…
Add table
Add a link
Reference in a new issue