A week on the indoor render (Phase U.4 → U.4c → 2026-05-31) fixed the flap but
produced NO shippable progress: walls/ceiling don't seal, outdoor terrain is
visible from inside (#78), the enclosure reads grey/transparent. Root cause is
ARCHITECTURAL, not a bug.
Evidence this session (direct, via the new [shell] probe + screenshots) RULED OUT
every subsystem except the gating architecture: the interior cell shells render
fine (geometry/texture/opaque/depth all correct, zh=0 tr=0); the visibility
traversal computes correct sets + non-empty portal clips; cull mode is fine; the
camera/eye thread was a detour. The residual is that OUTDOOR geometry is not gated
to portal openings when indoors, and acdream enforces visibility THREE inconsistent
ways (TerrainClipMode / per-cell shell clip / entity ParentCellId filter with an
outdoor-stab bypass) instead of retail's ONE PView gate.
This commit is the reset handoff + documentation, not a code fix:
- docs/research/2026-05-31-render-architecture-reset-handoff.md — canonical: honest
state, evidence ledger (ruled-out / do-not-repeat), the mapped 3-gate patchwork,
the retail PView target (one traversal → one gate for ALL geometry), the reset
mission, and a copy-paste pickup prompt.
- docs/architecture/acdream-architecture.md — new "Render Pipeline" SSOT section
(current divergence + unified-PView target + the one rule: compute visibility
once, enforce it once). (Doc has pre-existing corruption below this section —
flagged for separate cleanup.)
- Apparatus: ACDREAM_PROBE_SHELL → [shell] (EnvCellRenderer per-cell prepared/drawn
geometry + flags) added to RenderingDiagnostics + EnvCellRenderer. Throwaway.
- docs/superpowers/specs/2026-05-31-camera-collision-indoor-engagement-design.md —
spec for e099b4c (camera collision; now parked as orthogonal to the seam).
Next session: STOP point-fixing; do the architecture reset to a single PView gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.9 KiB
Camera-collision indoor engagement — the shared-root residual fix (2026-05-31)
Phase: M1.5 "indoor world feels right" — post-flap-fix residuals.
Status: design (pre-approved by user 2026-05-31; "most thorough + retail-faithful").
Predecessors: flap fix 0ee328a; diagnosis docs/research/2026-05-31-camera-collision-indoor-diagnosis.md; camera-collision research docs/research/2026-05-29-a8f-camera-collision-handoff.md.
Problem (one shared root, three faces)
After the U.4c flap fix (indoor visibility rooted at the player's cell; the 3rd-person
camera eye still drives the per-frame projection), three residuals remain — transparent
outer walls (#2), terrain-through-floor (#78), "stairs everything grey" (#3). Evidence
(u4c-fix.log, current code state): the eye is outside the player's cell ~90% of frames,
at full chase distance (~3.4 m) — i.e. the shipped camera collision is not engaging in
interior cells. When the eye is displaced, it projects the player-traversed portal openings
to garbage/off-screen NDC (74% of frames), collapsing OutsideView to empty → terrain Skip
(grey, #3) or tripping the giant-scissor fallback → over-include (bleed, #78/#2). The projection
from the eye is correct (the OutsideView is a screen-space clip and the screen is the eye's
view) — the fault is the eye being in the wrong place. So the root fix is keeping the eye in
valid space, which resolves all three faces at once.
Why the shipped collision doesn't engage (code-verified)
PhysicsCameraCollisionProbe.SweepEye → PhysicsEngine.ResolveWithTransition(... cellId = playerCell, moverFlags = IsViewer|PathClipped|FreeRotate|PerfectClip ...). The camera eye
sweeps UP+BACK from the head-pivot (~2.6 m back, ~2.25 m up). In an acdream cottage the
enclosing geometry (walls/floor/roof) lives in a landblock-baked exterior-shell GfxObj
registered cellScope=0 (outdoor/landblock-wide shadow list) — established by issue #98
(the cottage floor that capped the player's head sphere is GfxObj 0x01000A2B, not cell BSP).
ShadowObjectRegistry.GetNearbyObjects has the issue-#98 indoor gate at
ShadowObjectRegistry.cs:480:
if ((primaryCellId & 0xFFFFu) >= 0x0100u) // primary cell is indoor
return; // skip the outdoor radial sweep entirely
So while the viewer sphere's primary cell is the indoor cottage cell, the sweep is gated away from the exterior-shell GfxObj — the only geometry that encloses the cottage in our data model. The eye therefore finds nothing to stop it and flies to full distance.
Retail behavior (decomp-verified — the faithfulness anchor)
SmartBox::update_viewer @ acclient_2013_pseudo_c.txt:92761-92892:
- roots the viewer
CTransitionat the player/pivot cell (init_path(t, cell_1, pivot, sought);cell_1=AdjustPosition's pivot cell, fallbackplayer->cell); init_object(player, 0x5c)=IsViewer|PathClipped|FreeRotate|PerfectClip(acdream matches);- sweeps
viewer_sphere(0.3 m) viafind_valid_positionand publishes the stoppedsphere_path.curr_pos+curr_cell; fallbacksAdjustPosition→ snap-to-player.
So retail bounds the viewer by whatever the swept transition hits inside the player's cell.
Retail's interior EnvCells are self-enclosing (walls + ceiling in the cell's own geometry),
so the viewer is stopped by interior geometry, and retail's "structural separation" (CObjCell:: find_cell_list :308751-308769 adds outdoor GfxObjs only to outdoor cells' shadow lists)
holds because the interior is self-contained.
acdream's divergence: our cottages are NOT self-enclosing — the enclosure is the landblock shell GfxObj (per #98). So for our data model, the shell GfxObj is the enclosure the viewer must collide. Letting the viewer reach it is the faithful analog of retail's viewer-bounded-by-enclosure.
The fix (Option A — viewer-exempt the #98 gate)
Thread the mover flags (or a derived isViewer bool) from the FindObjCollisions call site
(ObjectInfo.State is already on the Transition) down through to
ShadowObjectRegistry.GetNearbyObjects, and change the gate to:
// Issue #98 gate is correct ONLY for the player foot/head capsule (it stops the cottage-floor
// GfxObj from capping the head sphere from the cellar below). The camera viewer (IsViewer, a
// single 0.3 m sphere, no capsule, no contact-plane) must reach the landblock-baked building
// shell that forms the cottage enclosure — retail's update_viewer bounds the viewer by the
// cell enclosure (acclient_2013_pseudo_c.txt:92761); in acdream's data model that enclosure is
// the shell GfxObj. find_obj_collisions has no indoor gate (acclient_2013_pseudo_c.txt:308918).
if ((primaryCellId & 0xFFFFu) >= 0x0100u && !isViewer)
return;
GetNearbyObjects gains an isViewer parameter (default false → existing callers keep the
gate). Only PhysicsCameraCollisionProbe.SweepEye passes IsViewer, so only the camera sweep
changes behavior.
Why not the alternatives
- Re-scope the cottage shell registration to the indoor cell (
cellScope= cottage cell): requires per-GfxObj→cell adjacency we don't have and isn't how retail models it; would also re-expose the player to the #98 head-cap. Rejected. - Make interior cells self-enclosing (faithful hydration of ceiling/walls into cell BSP): the genuinely-retail-faithful end state, but a large, #98-adjacent, high-risk refactor of cottage hydration. Documented out-of-scope follow-on (file as a residual after the visual gate). Option A is the low-risk bridge that matches retail's observable behavior for our data model.
- Retail fallback chain (
AdjustPosition→ snap-to-player): faithful completeness, but not load-bearing for this residual (our sweep succeeds and returns a position; it doesn't fail). Out-of-scope follow-on.
Tests (TDD)
- GREEN the RED test
CameraCollisionIndoorTests.SweepEye_IndoorCellExteriorGfxObjWall_ NotReachedFromIndoorContext_CurrentlyFails: after the fix, the viewer sweep stops at the shell wall →pulledIn ≥ 0.5(rename to drop_CurrentlyFails). - #98 regression guard (new): an
IsViewersweep in the cellar toward the cottage-floor GfxObj behaves correctly (the viewer single-sphere is not a head-capsule, so the #98 cap does not apply); assert the player's gate behavior is untouched (anIsPlayersweep at the same site still hits the gate / keeps the #98 fix). Use the cottage GfxObj fixture (RegisterCottageGfxObj,0x01000A2B). - Full
dotnet build+dotnet testgreen; with-fix failure set ⊆ baseline (the pre-existing static-leak flakiness is independent).
Acceptance
- Build + tests green; RED→GREEN; #98 guard passes.
- Visual gate (the one user step): walk
+Acdreaminto a Holtburg cottage / cellar / stairs and a window room withACDREAM_PROBE_FLAP=1. Expect: the eye stops at walls/ceiling (no transparent outer walls, no grey, no terrain-through-floor);[flap-sweep]showspulledIn > 0eyeInRoot=Yfor indoor cells;[flap-cam]showsterrain=Planesthrough windows (notSkip/Scissor-fallback). The visual gate is the arbiter on real geometry — if the eye still flies free in the main-floor residual cells (0174/0175), the residual is a different cause (interior-BSP completeness) and we iterate with the live[flap-sweep]data.
Out of scope (documented residuals)
- U.5 outside-camera → building-interior peering, and the legitimate eye-through-an-open- doorway exit (both deferred in the Phase U spec).
- Interior-cell self-enclosure hydration (the deeper retail divergence; faithful end state).
- Viewer fallback chain (
AdjustPosition→ snap-to-player) for the find-valid-position-fails path. - The throwaway
[flap]/[flap-cam]/[flap-sweep]apparatus is kept for the visual gate and stripped once the indoor residuals close.