# Session 2026-04-16 — Scenery Alignment + Collision Registration ## Headline result **Scenery alignment + collision finally in a working state.** - All outdoor entities get collision cylinders - Trees in roads mostly fixed - Trees snapped to terrain (triangle-aware Z sample matching player physics) - Visual-to-collision position alignment correct ## Critical root causes found this session ### 1. Scenery entity ID collision across landblocks **File:** `src/AcDream.App/Rendering/GameWindow.cs` `BuildSceneryEntitiesForStreaming` Old: `sceneryIdBase = 0x80000000 | (landblockId & 0x00FFFF00)` — the mask only captured bits 8-23 which always has `0xFF` in the low byte (from the lb id suffix). Result: every landblock with the same Y coord produced scenery with **identical IDs**. When streaming loaded the next landblock, `ShadowObjects .Register` called `Deregister(entityId)` first, wiping out the previous landblock's collision. Only the LAST-loaded landblock had collision for any given ID. Fix: `0x80000000 | (lbXByte << 16) | (lbYByte << 8)` — each landblock gets a unique ID namespace. Every scenery entity now uniquely identified. ### 2. Scenery generator using wrong terrain-Z formula **File:** `src/AcDream.App/Rendering/GameWindow.cs` `SampleTerrainZ` My earlier "triangle-aware" terrain sample had wrong barycentric math. Physics's `TerrainSurface.SampleZ` used different (correct) math derived from the WorldBuilder mesh index buffer. So player walked at one Z, scenery placed at another → trees hovered above ground. Fix: Ported WorldBuilder `TerrainUtils.GetHeight` exactly — same split direction formula, same barycentric triangle pair, same interpolation. Both scenery and player now use identical height sampling. ### 3. Collision cylinder position using AABB center, not entity root Multi-part scenery meshes can have parts offset from the mesh origin. The visual mesh AABB center is NOT the entity's rendered position. Using AABB center for the collision cylinder put collision meters away from the visible mesh. Fix: Collision cylinder center = `entity.Position` (the rendered origin). Radius from the retail Setup CylSphere when available, clamped to [0.3, 1.5] m. ### 4. Scenery iterated 81 vertices, retail iterates 64 cells **File:** `src/AcDream.Core/World/SceneryGenerator.cs` Decompiled FUN_005311a0 iterates `local_8c < 8 && local_94 < 8` — 64 cells. We iterated 0..8 on both axes (81 vertices). Extra scenery at landblock seams. Fix: `for (int x = 0; x < CellsPerSide; x++)` where `CellsPerSide = 8`. Note: WorldBuilder iterates 81, ACViewer iterates 81. Retail iterates 64. Currently matching retail decompiled — may need revisiting if we see seam gaps. ### 5. Road check was per-vertex nearest-neighbor, should be 4-corner polygonal **File:** `src/AcDream.Core/World/SceneryGenerator.cs` `IsOnRoad` ACE-server's simplified road test uses single-vertex `(terrain & 0x3)` check. That's wrong — retail uses a 4-corner polygonal test with 5m road ribbon (`FUN_00530d30`). Fix: Direct port of ACViewer's `Landblock.OnRoad` (lines 300-398) with 16-case corner-configuration dispatch and 5m `RoadHalfWidth`. Also kept an extra "origin-cell has road vertex" guard since retail's exact threshold constants (`_DAT_007c97cc/...`) weren't in dumped chunks. ### 6. Pre-displacement road check was wrong Retail doesn't skip a vertex based on its own road bit — it rolls displacement then tests the final position against OnRoad. My code had `if (IsRoadVertex(raw)) continue;` which silently dropped scenery retail would have kept. Fix: Removed. The post-displacement OnRoad test is the only road check. ### 7. Rotation missing baseLoc + (450-heading)%360 flip **File:** `src/AcDream.Core/World/SceneryGenerator.cs` Retail's `AFrame::set_heading(degrees)` transforms `yaw = -(450 - heading) % 360` before building the quaternion. It also composes with `BaseLoc.Orientation`. Fix: Applied both. Rotations now match retail for asymmetric scenery. ### 8. Stabs in buildingCells **File:** `src/AcDream.App/Rendering/GameWindow.cs` Was treating `lbInfo.Objects` (stabs) as buildings, over-suppressing scenery in town landblocks. Retail only suppresses scenery in cells with actual `lbInfo.Buildings`. Fix: Only `Buildings` contribute to `buildingCells`. ## Still imperfect (for tomorrow) - Some scenery still hovers slightly above ground in certain cells. Probably a minor split-direction edge case or BaseLoc.Z handling quirk. - User wants a real debug overlay (on-screen text/markers with positions, directions, distances, etc.). Currently has F3 console dump + cyan debug wireframes from the DebugLineRenderer. - Collision is now consistent but "feel" needs tuning against retail. - Large trees with BSP on canopy still rely on our CylSphere fallback rather than a retail-faithful dual-test (BSP + CylSphere). ## Pickup for next session User explicitly said next session: 1. **Debug overlay UI** — on-screen text for player pos, direction, heading in degrees, nearest object distances, "am I colliding" indicator. The current approach (F3 console dump, wireframes in 3D world) is insufficient because the user doesn't know "what 40m looks like" visually. 2. **Controls** — make working with the client easier. Presumably: keybind list, camera controls, debug toggles, maybe a tool palette. ## Files changed this session - `src/AcDream.App/Rendering/GameWindow.cs` — scenery ID namespace, OnRoad, visual-mesh-AABB collision, triangle-aware SampleTerrainZ, debug wireframes, F3 dump, building/stab separation, DebugLineRenderer wiring. - `src/AcDream.App/Rendering/DebugLineRenderer.cs` (new) — minimal GL line renderer for wireframe debug. - `src/AcDream.App/Rendering/Shaders/debug_line.{vert,frag}` (new) — debug line shader pair. - `src/AcDream.Core/World/SceneryGenerator.cs` — 64-cell iteration, ACViewer OnRoad, baseLoc+heading rotation, BaseLoc.Z passthrough, removed pre-displacement road check. - `src/AcDream.Core/World/WorldEntity.cs` — added `Scale` field for scenery. - `src/AcDream.Core/Physics/PhysicsDataCache.cs` — `GfxObjVisualBounds` + `GetVisualBounds` for mesh AABB fallback. - `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` — `AllEntriesForDebug()` for the debug overlay, `Scale` on ShadowEntry. - `src/AcDream.Core/Physics/BSPQuery.cs` — `localToWorld` rotation parameter on all dispatcher methods so collision normals/offsets transform correctly. - `src/AcDream.Core/Physics/TransitionTypes.cs` — `CylinderCollision` rewritten with wall-slide + push-out, `FindObjCollisions` rewritten per-object with retail 6-path dispatcher, TransitionalInsert retry loop. ## Key reference pointers (for tomorrow) - WorldBuilder scenery: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/ Lib/SceneryRenderManager.cs` + `SceneryHelpers.cs`. - WorldBuilder terrain: `references/WorldBuilder/WorldBuilder.Shared/ Modules/Landscape/Lib/TerrainUtils.cs` (GetHeight, GetNormal, OnRoad). - Retail decompiled collision: `docs/research/decompiled/chunk_00530000.c` FUN_005311a0 (scenery loop) and FUN_00530d30 (OnRoad). - Setup field layout: `docs/research/decompiled/chunk_00510000.c` getter thunks ~line 7563-7662 (CylSpheres at +0x48, Radius +0x64, etc.). ## Collision pipeline overview (current state) 1. **Scenery generation** (`SceneryGenerator.Generate`) — 64 cells per lb, LCG-deterministic displacement, OnRoad + building cell filter, slope filter, returns `ScenerySpawn{ObjectId, LocalPosition, Rotation, Scale}`. 2. **Scenery hydration** (`BuildSceneryEntitiesForStreaming`) — samples terrain Z via `_physicsEngine.SampleTerrainZ`, builds `WorldEntity`. 3. **Collision registration** (end of `ApplyLoadedTerrain`) — for every outdoor mesh entity: pick radius from Setup.CylSphere → Setup.Radius → mesh AABB fallback. Height from Setup.Height or mesh. Register cylinder at `entity.Position` in `ShadowObjectRegistry`. 4. **Collision query** (`Transition.FindObjCollisions`) — player sphere sweeps via `GetNearbyObjects` which searches player's lb + 8 neighbors. Per-object dispatch: BSP → `BSPQuery.FindCollisions` (retail 6-path), Cylinder → `CylinderCollision` (wall-slide + push-out). Good night.