feat(client): per-pixel relief terrain shading + cliff Z-rounding #249

Merged
panda merged 3 commits from feat/relief-texture into main 2026-08-02 00:17:04 +02:00
Collaborator

Summary

Terrain relief legibility, reworked end-to-end. The previous cliff shading (#247's per-vertex ambient
occlusion) produced hard serrated bands along the iso diamond, deep compounding grooves, a stripe per
road step, and — on a real 30-Z volcano — blocky "right-angle" cliff facets. Diagnosed each artefact to
a root cause (verified against the real map data: a 30-Z mountain_rock wall beside the volcanic
plain, with a road_dirt cut through) and fixed each with an established technique, tuned live to the
owner's sign-off:

  • Per-pixel relief shading from a precomputed window texture (TerrainReliefField, Client.Core,
    pure/unit-tested): a soft mid-scale gradient (hillshade), a two-scale cavity, and a true-cliff contact
    shadow that a walkable ≤MaxWalkStepZ step never triggers (so roads stop banding). Sampled LINEAR in
    ground_blend.fx → smooth, off the mesh grid. Replaces the per-vertex Hillshade()×TerrainOcclusion
    (both removed); also drops ~8M GroundZAt/frame.
  • Multidirectional hillshade (NW primary + W/N fills) — the cartographic fix for a single light's
    flat over-bright / flat-dark posterisation; additive with the cavity/cliff terms, floored once at
    MinimumShade so shading never multiplies toward black.
  • Boundary warp — jitters the biome palette+weight lookup cell by two-octave sub-cell noise
    (same offset cell for both, so palette/weights stay consistent) so a high-contrast biome border
    wobbles organically instead of stair-stepping on the diamond grid. Recovers the old dual-grid's
    curved-mask edge, per-pixel, without authored corner art.
  • Display-Z cliff rounding (TerrainDisplayHeight, Client.Core, pure/unit-tested) — the remaining
    "right-angle" teeth were the geometry (a 30-Z step is a band of steep mesh triangles). The mesh now
    draws vertices at a smoothed Z, gated to real cliffs (relief > the walk step) so walkable terraces
    stay bit-exact crisp. Gameplay Z is untouched — the server still validates walkability on integer
    GroundZAt; this only moves where a vertex is drawn.
  • WorldRules.MaxWalkStepZ 2 → 5 — a natural slope now climbs without a wall while a real cliff
    still gates (and rounds); walkable ≤5 stays crisp, consistent with the cliff gate. Server-authoritative
    rule, single source (the editor grader and the relief/display gates all read the const).

Client-only rendering + one world-model constant. No wire/protocol change; no World, save, or text.

Screenshots / recording

Piloted via the debug harness (docs/debug-harness.md) on a fresh DB, admin login test then
/tp to the volcano (7509,4972) and the road (7453,4876), screenshot each.

Volcano — before (#247 AO): hard serrated groove down the middle, stepped bands.

volcano before

Volcano — mid (relief + multidirectional + boundary warp): grooves/banding gone, biome edges
organic, but the sheer 30-Z wall still shows triangular mesh facets ("right-angle" teeth).

volcano teeth

Volcano — after (+ display-Z cliff rounding): the teeth are gone; the cliff reads as a continuous
rocky slope.

volcano after

Road on grass — after: organic dirt/grass border (boundary warp), gentle multidirectional shade;
the walkable road stays crisp (no over-smoothing).

road after

How it was tested

  • New unit tests (Client.Core, pure, MonoGame-free): TerrainReliefField (5) — flat → neutral
    gradient / no cavity / no cliff; a ΔZ ≤ MaxWalkStepZ step → cliff channel 0 (no road banding); a real
    cliff → a cliff signal that grows with the drop; a uniform ramp → a steady gradient (no per-step
    spikes); a valley → cavity at its floor. TerrainDisplayHeight (4) — flat unchanged; a walkable step
    stays crisp (bit-exact 2-Z edge, not a ramp); a 30-Z cliff rounds into a monotone ramp (no sheer
    adjacent jump); ground far from any cliff untouched.
  • Gates: whole solution builds with 0 warnings; dotnet test green (761); CSharpier clean.
  • Manual/visual: piloted the client on asterra at the problem zones (volcano, staircased road,
    road cut, coast, flat plain), before/after, tuned live to owner approval.

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (DesktopGL/SM3 shader, one extra tex2D, RGBA8 texture; pure CPU math in Client.Core — no OS-specific dependency)
  • Tests added/updated for this change
  • Linked the related issue (#247) — supersedes its ambient-occlusion cliff shading
## Summary Terrain relief legibility, reworked end-to-end. The previous cliff shading (#247's per-vertex ambient occlusion) produced hard serrated bands along the iso diamond, deep compounding grooves, a stripe per road step, and — on a real 30-Z volcano — blocky "right-angle" cliff facets. Diagnosed each artefact to a root cause (verified against the real map data: a 30-Z `mountain_rock` wall beside the `volcanic` plain, with a `road_dirt` cut through) and fixed each with an established technique, tuned live to the owner's sign-off: - **Per-pixel relief shading** from a precomputed window texture (`TerrainReliefField`, Client.Core, pure/unit-tested): a soft mid-scale gradient (hillshade), a two-scale cavity, and a true-cliff contact shadow that a walkable ≤`MaxWalkStepZ` step never triggers (so roads stop banding). Sampled LINEAR in `ground_blend.fx` → smooth, off the mesh grid. Replaces the per-vertex `Hillshade()`×`TerrainOcclusion` (both removed); also drops ~8M `GroundZAt`/frame. - **Multidirectional hillshade** (NW primary + W/N fills) — the cartographic fix for a single light's flat over-bright / flat-dark posterisation; additive with the cavity/cliff terms, floored **once** at `MinimumShade` so shading never multiplies toward black. - **Boundary warp** — jitters the biome **palette+weight lookup** cell by two-octave sub-cell noise (same offset cell for both, so palette/weights stay consistent) so a high-contrast biome border wobbles organically instead of stair-stepping on the diamond grid. Recovers the old dual-grid's curved-mask edge, per-pixel, without authored corner art. - **Display-Z cliff rounding** (`TerrainDisplayHeight`, Client.Core, pure/unit-tested) — the remaining "right-angle" teeth were the *geometry* (a 30-Z step is a band of steep mesh triangles). The mesh now draws vertices at a smoothed Z, **gated to real cliffs** (relief > the walk step) so walkable terraces stay bit-exact crisp. **Gameplay Z is untouched** — the server still validates walkability on integer `GroundZAt`; this only moves where a vertex is drawn. - **`WorldRules.MaxWalkStepZ` 2 → 5** — a natural slope now climbs without a wall while a real cliff still gates (and rounds); walkable ≤5 stays crisp, consistent with the cliff gate. Server-authoritative rule, single source (the editor grader and the relief/display gates all read the const). Client-only rendering + one world-model constant. No wire/protocol change; no `World`, save, or text. ## Screenshots / recording Piloted via the debug harness (`docs/debug-harness.md`) on a **fresh DB**, admin `login test` then `/tp` to the volcano `(7509,4972)` and the road `(7453,4876)`, `screenshot` each. **Volcano — before (#247 AO):** hard serrated groove down the middle, stepped bands. ![volcano before](https://git.homelab.devncode.it/attachments/21615740-cafe-49f0-92df-4ac138578d0a) **Volcano — mid (relief + multidirectional + boundary warp):** grooves/banding gone, biome edges organic, but the sheer 30-Z wall still shows triangular mesh facets ("right-angle" teeth). ![volcano teeth](https://git.homelab.devncode.it/attachments/441014d8-60ab-408e-877c-96b0e1a7386e) **Volcano — after (+ display-Z cliff rounding):** the teeth are gone; the cliff reads as a continuous rocky slope. ![volcano after](https://git.homelab.devncode.it/attachments/8532ca85-f9b9-4c44-82b6-15df80a7aeb1) **Road on grass — after:** organic dirt/grass border (boundary warp), gentle multidirectional shade; the walkable road stays crisp (no over-smoothing). ![road after](https://git.homelab.devncode.it/attachments/29c3cb4e-d705-4b2e-b085-841ea182b732) ## How it was tested - **New unit tests (Client.Core, pure, MonoGame-free):** `TerrainReliefField` (5) — flat → neutral gradient / no cavity / no cliff; a ΔZ ≤ `MaxWalkStepZ` step → cliff channel 0 (no road banding); a real cliff → a cliff signal that grows with the drop; a uniform ramp → a steady gradient (no per-step spikes); a valley → cavity at its floor. `TerrainDisplayHeight` (4) — flat unchanged; a walkable step stays crisp (bit-exact 2-Z edge, not a ramp); a 30-Z cliff rounds into a monotone ramp (no sheer adjacent jump); ground far from any cliff untouched. - **Gates:** whole solution builds with **0 warnings**; `dotnet test` green (**761**); CSharpier clean. - **Manual/visual:** piloted the client on `asterra` at the problem zones (volcano, staircased road, road cut, coast, flat plain), before/after, tuned live to owner approval. ## 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/SM3 shader, one extra `tex2D`, RGBA8 texture; pure CPU math in Client.Core — no OS-specific dependency) - [x] Tests added/updated for this change - [x] Linked the related issue (#247) — supersedes its ambient-occlusion cliff shading
panda merged commit cf50c6d8c2 into main 2026-08-02 00:17:04 +02:00
panda deleted branch feat/relief-texture 2026-08-02 00:17:04 +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!249
No description provided.