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
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:
- 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.
- 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— addedScalefield for scenery.src/AcDream.Core/Physics/PhysicsDataCache.cs—GfxObjVisualBounds+GetVisualBoundsfor mesh AABB fallback.src/AcDream.Core/Physics/ShadowObjectRegistry.cs—AllEntriesForDebug()for the debug overlay,Scaleon ShadowEntry.src/AcDream.Core/Physics/BSPQuery.cs—localToWorldrotation parameter on all dispatcher methods so collision normals/offsets transform correctly.src/AcDream.Core/Physics/TransitionTypes.cs—CylinderCollisionrewritten with wall-slide + push-out,FindObjCollisionsrewritten 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.cFUN_005311a0 (scenery loop) and FUN_00530d30 (OnRoad). - Setup field layout:
docs/research/decompiled/chunk_00510000.cgetter thunks ~line 7563-7662 (CylSpheres at +0x48, Radius +0x64, etc.).
Collision pipeline overview (current state)
- Scenery generation (
SceneryGenerator.Generate) — 64 cells per lb, LCG-deterministic displacement, OnRoad + building cell filter, slope filter, returnsScenerySpawn{ObjectId, LocalPosition, Rotation, Scale}. - Scenery hydration (
BuildSceneryEntitiesForStreaming) — samples terrain Z via_physicsEngine.SampleTerrainZ, buildsWorldEntity. - 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 atentity.PositioninShadowObjectRegistry. - Collision query (
Transition.FindObjCollisions) — player sphere sweeps viaGetNearbyObjectswhich searches player's lb + 8 neighbors. Per-object dispatch: BSP →BSPQuery.FindCollisions(retail 6-path), Cylinder →CylinderCollision(wall-slide + push-out).
Good night.