feat(client): terrain Z rendering, foot points, height-aware picking + fixture (Slice B of #224) #227

Merged
panda merged 2 commits from feat/terrain-z-render into main 2026-07-31 20:14:39 +02:00
Collaborator

Slice B of #224 (UO heightfield) — the visible slice. Stacked on Slice A (#225); base is feat/terrain-z-domain, so this diff is only Slice B. Merge #225 first.

Summary

Makes terrain Z actually render and interact, integrated into the SDF windowed renderer (not the per-tile dual-tile mesh the original prompt assumed — that renderer no longer exists):

  • Windowed grid mesh. The ground is now a triangulated grid mesh instead of a full-screen quad: one shared vertex per grid point at IsoProjection.ToScreen(x,y,GroundZAt), texcoord = the flat (x,y), two triangles per cell; ground_blend.fx is unchanged. Shared vertices ⇒ no cracks. Cost stays bounded by the viewport. On a flat (all-Z0) map the mesh is coplanar and renders pixel-identical to before.
  • Foot points ride the terrain. Statics, ground items, mobiles and corpses draw at ToScreen(x,y,GroundZAt) so nothing floats or sinks (a foot uses the same GroundZAt as the mesh vertex, so it sits exactly on the surface).
  • Depth-sort audit. Everything sorts by the flat foot Y, never the Z-lifted Y — so a mobile on a plateau never sorts behind the lower terrain in front of it. A mobile's flat position and ground Z glide separately, so walking a ramp raises the body smoothly; facing stays grid-X/Y (a steep climb doesn't spin it).
  • Height-aware picking. TerrainPicker (pure, in Client.Core) tests the cursor against the map's real Z-lifted triangles and returns the frontmost hit; TileUnderCursor uses it instead of the flat inverse, so clicking a raised plateau selects the plateau, not the tile it's drawn over.
  • Fixture. A committed rocky/mountain test region (content/maps/fixtures/, scripts/genzfixture.py) — ramps (X/Y/diagonal), a non-coplanar saddle, a salt_plain plateau, an over-MaxStepZ cliff, and a static + spawn on the plateau. Never touches the real world map (loaded by copying the three fixture files over the bin-output world.* for a screenshot).

No wire change (ProtocolVersion untouched). The Map Editor's own Z tools + server walkability are Slice C.

Screenshots / recording

Piloted via the debug harness on the fixture (fresh DB), plus a flat-world regression check. The plateau is a bright salt_plain mesa (Z20) on dark rocky_ground for legibility; our SDF blend feathers the cliff into a stretched gradient (that softness is the blend, not the geometry — the mesh is a hard step, proven below).

  • On the plateau (zC_plateau) — the player + a boulder stand on the raised salt_plain; the gold desert_sand ramp climbs at left; nothing floats.
  • Front view (zC_front) — the mesa and its ramp from flat ground in front.
  • Ramp walk (zC_ramp) — after goto up the 2-Z/cell ramp; each step glided up smoothly.
  • Flat regression (zC_flat) — the shipped 3× world (Z0) renders identically to before the mesh conversion.

How it was tested

  • Unit (Client.Core): IsoProjectionTests (ToScreen(x,y,0)==flat, +1 Z==Y−4); TerrainPickerTests — point-in-triangle (interior/exterior/either winding), flat pick, elevation-aware pick selects the raised cell, frontmost wins on overlap, null off-map.
  • Whole solution builds 0 warnings / 0 errors; dotnet test all green (Client.Core 178, Shared 122, GameServer 271, …).
  • Explicit verification on the fixture (all pass): no cracks between triangles (shared vertices; ground continuous); no floating/sunken objects (player + statics on the surface); smooth mobile movement up a ramp (goto 24 13 traversed it, Z interpolated); correct ordering around the plateau/cliff (player on the plateau edge not hidden by the lower ground in front); picking selects the visible elevated tile (a click on the plateau logged tile z=20, a flat click z=0); flat Z0 rendering pixel-identical (redwood spawn matched); Z clamped −225..255 (rejected in ElevationTests).
  • On the ClassicUO diagonal: the original prompt asked to verify the stretched-land diagonal against ClassicUO. In the SDF renderer the diamond UVs / exact diagonal are not load-bearing — vertices carry world (x,y) as texcoord and the shader blends per-pixel, so a fixed v00–v11 diagonal + shared vertices already guarantee no cracks and correct texture. (ClassicUO source wasn't reachable via the tooling here; noted rather than faked.)

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (DesktopGL DrawUserIndexedPrimitives, pure Client.Core math; no OS-specific API; shader stays the pre-compiled .xnb)
  • Tests added/updated for this change
  • Linked the related issue (#224) — Slice B; the Map Editor Z tools + server MaxStepZ walkability are Slice C
**Slice B of #224** (UO heightfield) — the visible slice. **Stacked on Slice A** ([#225](https://git.homelab.devncode.it/marco/IsoMmo/pulls/225)); base is `feat/terrain-z-domain`, so this diff is only Slice B. Merge #225 first. ## Summary Makes terrain Z actually render and interact, integrated into the **SDF windowed renderer** (not the per-tile dual-tile mesh the original prompt assumed — that renderer no longer exists): - **Windowed grid mesh.** The ground is now a triangulated grid mesh instead of a full-screen quad: one **shared** vertex per grid point at `IsoProjection.ToScreen(x,y,GroundZAt)`, texcoord = the flat `(x,y)`, two triangles per cell; `ground_blend.fx` is **unchanged**. Shared vertices ⇒ no cracks. Cost stays bounded by the viewport. On a flat (all-Z0) map the mesh is coplanar and renders **pixel-identical** to before. - **Foot points ride the terrain.** Statics, ground items, mobiles and corpses draw at `ToScreen(x,y,GroundZAt)` so nothing floats or sinks (a foot uses the *same* `GroundZAt` as the mesh vertex, so it sits exactly on the surface). - **Depth-sort audit.** Everything sorts by the **flat foot Y**, never the Z-lifted Y — so a mobile on a plateau never sorts behind the lower terrain in front of it. A mobile's **flat position and ground Z glide separately**, so walking a ramp raises the body smoothly; **facing stays grid-X/Y** (a steep climb doesn't spin it). - **Height-aware picking.** `TerrainPicker` (pure, in `Client.Core`) tests the cursor against the map's real Z-lifted triangles and returns the **frontmost** hit; `TileUnderCursor` uses it instead of the flat inverse, so clicking a raised plateau selects the plateau, not the tile it's drawn over. - **Fixture.** A committed rocky/mountain test region (`content/maps/fixtures/`, `scripts/genzfixture.py`) — ramps (X/Y/diagonal), a non-coplanar saddle, a `salt_plain` plateau, an over-`MaxStepZ` cliff, and a static + spawn on the plateau. **Never touches the real world map** (loaded by copying the three fixture files over the bin-output `world.*` for a screenshot). No wire change (`ProtocolVersion` untouched). The Map Editor's own Z tools + server walkability are Slice C. ## Screenshots / recording Piloted via the debug harness on the fixture (fresh DB), plus a flat-world regression check. The plateau is a bright `salt_plain` mesa (Z20) on dark `rocky_ground` for legibility; our SDF blend feathers the cliff into a stretched gradient (that softness is the blend, not the geometry — the mesh is a hard step, proven below). - **On the plateau** (`zC_plateau`) — the player + a boulder stand on the raised `salt_plain`; the gold `desert_sand` ramp climbs at left; nothing floats. - **Front view** (`zC_front`) — the mesa and its ramp from flat ground in front. - **Ramp walk** (`zC_ramp`) — after `goto` up the 2-Z/cell ramp; each step glided up smoothly. - **Flat regression** (`zC_flat`) — the shipped 3× world (Z0) renders identically to before the mesh conversion. ## How it was tested - **Unit (Client.Core):** `IsoProjectionTests` (`ToScreen(x,y,0)`==flat, `+1 Z`==`Y−4`); `TerrainPickerTests` — point-in-triangle (interior/exterior/either winding), flat pick, **elevation-aware pick selects the raised cell**, **frontmost wins on overlap**, null off-map. - **Whole solution builds 0 warnings / 0 errors; `dotnet test` all green** (Client.Core 178, Shared 122, GameServer 271, …). - **Explicit verification on the fixture (all pass):** no cracks between triangles (shared vertices; ground continuous); no floating/sunken objects (player + statics on the surface); smooth mobile movement up a ramp (`goto 24 13` traversed it, Z interpolated); correct ordering around the plateau/cliff (player on the plateau edge not hidden by the lower ground in front); **picking selects the visible elevated tile** (a click on the plateau logged `tile z=20`, a flat click `z=0`); flat Z0 rendering pixel-identical (redwood spawn matched); Z clamped −225..255 (rejected in `ElevationTests`). - **On the ClassicUO diagonal:** the original prompt asked to verify the stretched-land diagonal against ClassicUO. In the SDF renderer the diamond UVs / exact diagonal are **not** load-bearing — vertices carry world `(x,y)` as texcoord and the shader blends per-pixel, so a fixed `v00–v11` diagonal + shared vertices already guarantee no cracks and correct texture. (ClassicUO source wasn't reachable via the tooling here; noted rather than faked.) ## Checklist - [x] `just lint` passes (CSharpier + analyzers, zero warnings) - [x] `just test` is green - [x] The whole solution builds (client and tools included) - [x] Multi-platform preserved (DesktopGL `DrawUserIndexedPrimitives`, pure `Client.Core` math; no OS-specific API; shader stays the pre-compiled `.xnb`) - [x] Tests added/updated for this change - [x] Linked the related issue (#224) — Slice B; the Map Editor Z tools + server `MaxStepZ` walkability are Slice C
Replace the full-screen blend quad with a windowed triangulated grid mesh: one shared vertex per
grid point at IsoProjection.ToScreen(x,y,GroundZAt), texcoord = flat (x,y), two triangles per cell
(fixed v00-v11 diagonal); ground_blend.fx unchanged. Shared vertices => no cracks. On a flat (all-Z0)
map the mesh is coplanar and renders pixel-identical to the old quad (verified on the 3x world).
Foot points, fixture and picking follow in this slice.
feat(client): terrain Z foot points, depth sort + height-aware picking, fixture — Slice B (#224)
All checks were successful
ci / Lua content lint (pull_request) Successful in 22s
ci / Lint & Test (pull_request) Successful in 4m52s
7aa491bbd2
Statics, ground items, mobiles and corpses draw at ToScreen(x,y,GroundZAt) so they sit on the
terrain; the depth sort keys off the FLAT foot Y (never the Z-lifted Y) so a mobile on a plateau
never sorts behind the lower terrain in front of it; a mobile's flat position and ground Z glide
separately, so walking a ramp raises it smoothly, and facing stays grid-X/Y. Height-aware picking
(TerrainPicker, TDD in Client.Core) tests the cursor against the map's real Z-lifted triangles and
picks the frontmost, replacing the flat inverse for TileUnderCursor. A committed rocky/mountain
fixture (content/maps/fixtures, scripts/genzfixture.py) exercises ramps, a saddle, a plateau, an
over-MaxStepZ cliff and static/mobile placement — verified in the client (no cracks, nothing floats,
smooth ramp walk, correct ordering, picking selects the elevated tile). Flat Z0 stays pixel-identical.
Author
Collaborator

Screenshots (harness — fixture + flat regression)

On the plateau — player + boulder on the raised salt_plain; gold ramp at left; nothing floats

plateau.png

Front view of the mesa + ramp from flat ground

front.png

After walking up the 2-Z/cell ramp (Z interpolated per step)

ramp.png

Flat regression — the shipped Z0 world renders identical to before the mesh

flat.png

## Screenshots (harness — fixture + flat regression) **On the plateau — player + boulder on the raised salt_plain; gold ramp at left; nothing floats** ![plateau.png](https://git.homelab.devncode.it/attachments/865906e3-bbae-4f31-9f37-a9e7a77f5435) **Front view of the mesa + ramp from flat ground** ![front.png](https://git.homelab.devncode.it/attachments/f0fa3014-2185-41c5-8b82-90cd180829ab) **After walking up the 2-Z/cell ramp (Z interpolated per step)** ![ramp.png](https://git.homelab.devncode.it/attachments/a7abbc59-6b41-4c89-946d-9e0e6784a5d6) **Flat regression — the shipped Z0 world renders identical to before the mesh** ![flat.png](https://git.homelab.devncode.it/attachments/5213bcbb-3e05-413b-b153-3f99970a344e)
panda scheduled this pull request to auto merge when all checks succeed 2026-07-31 20:13:05 +02:00
panda merged commit 4fb146e9a2 into main 2026-07-31 20:14:39 +02:00
panda deleted branch feat/terrain-z-render 2026-07-31 20:14:40 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!227
No description provided.