acdream/memory/project_session_2026_04_16.md
Erik ff325abd7b feat(ui): debug overlay + refined input controls
Adds the first on-screen HUD for the dev client plus today's mouse-control
refinements. Also lands yesterday's scenery-alignment changes that were
left uncommitted in the working tree.

Overlay:
- BitmapFont rasterizes a system TTF via StbTrueTypeSharp into a 512x512
  R8 atlas at startup (Consolas on Windows, DejaVu/Menlo fallbacks)
- TextRenderer batches 2D quads in screen-space with ortho projection;
  one shader + two draw calls (rect then text) for panel backgrounds
  under glyphs
- DebugOverlay composes info / stats / compass / help panels on top of
  the 3D scene; toggles via F1/F4/F5/F6; transient toasts for key events
- DebugLineRenderer and its shaders (carried over from the scenery work)
  are properly committed in this commit

Controls:
- Per-mode mouse sensitivity (Chase 0.15, Fly 1.0, Orbit 1.0); F8/F9 to
  adjust the active mode multiplicatively (x1.2)
- Hold RMB to free-orbit the chase camera around the player; release
  stays at the new angle (no snap-back)
- Mouse-wheel zooms chase distance between 2m and 40m
- Chase pitch widened to [-0.7, 1.4] so mouse-Y tilts both ways from
  the default neutral angle

Scenery alignment (carried from yesterday's session):
- ShadowObjectRegistry AllEntriesForDebug + Scale field
- SceneryGenerator uses ACViewer's OnRoad polygon test + baseLoc +
  set_heading rotation
- BSPQuery dispatchers accept localToWorld so normals/offsets transform
  correctly per part
- TransitionTypes.CylinderCollision rewritten with wall-slide + push-out
- PhysicsDataCache caches visual-mesh AABB for scenery that lacks
  physics Setup bounds
2026-04-17 18:45:38 +02:00

8.1 KiB

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.csGfxObjVisualBounds + GetVisualBounds for mesh AABB fallback.
  • src/AcDream.Core/Physics/ShadowObjectRegistry.csAllEntriesForDebug() for the debug overlay, Scale on ShadowEntry.
  • src/AcDream.Core/Physics/BSPQuery.cslocalToWorld rotation parameter on all dispatcher methods so collision normals/offsets transform correctly.
  • src/AcDream.Core/Physics/TransitionTypes.csCylinderCollision 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.