feat(client): animated terrain fluids (ocean/lava/rivers) on the SDF blend #222

Closed
opened 2026-07-29 20:25:47 +02:00 by panda · 1 comment
Collaborator

Design agreed via critical-design-review + owner handoff (handoff_claude_terreni_animazioni.docx). Display-only client rendering on top of the SDF ground blend (#221). Frame assets prepared in E:\Asset\Terrain Anim.

Model (owner handoff, aligned with the review)

  • Assets/frames/sequences are client-side; the server knows only terrain category + gameplay flags, never the frames.
  • Two families (owner-confirmed playback modes):
    • Cyclic-local → PING-PONG 0-1-2-3-2-1-0…: ocean, coast, lake, lava, dirty-water. The surface breathes back and forth, no pop at the loop seam. 4 or 8 frames.
    • Linear/directional → SEQUENTIAL LOOP 0-1-2-…-N-0…: rivers, four orientations (L→R, R→L, N→S, S→N), 8 frames each. Flow is one-way so frames must advance and wrap (ping-pong would reverse the current). Adjacent tiles share phase + speed; curves = per-tile local direction.
  • Hard rule: the blend is NOT re-run per frame — geometry, tile choice and transition masks are stable; only the frame index changes in the renderer.
  • Static-variant choice stays deterministic (coords + seed); animation phase is display-only and need not be identical across clients.

Assets (in E:\Asset\Terrain Anim, 1254² RGB → process to seamless 128² like the land tiles)

salt_water_ocean_anim (4) · salt_water_coast_anim (4) · dirty_water_anim (4) · water_lake_anim (8) · lava_anim (8) · water_river_{left_right,right_left,north_south,south_north}_anim (8 each).

Technical design

  • Client.Core TerrainAnimation (pure, unit-tested): FrameIndex(seconds, count, fps, mode)PingPong (cyclic surfaces) = triangle wave 0-1-2-3-2-1 (period 2·(count-1)); Loop (rivers, one-way flow) = ⌊t·fps⌋ % count. count=1 ⇒ always 0 (static biomes never offset).
  • Atlas: an animated biome lays its N frames as consecutive atlas cells (wrap-guttered like every tile).
  • Anim-params lookup keyed by atlas index (firstFrame, count, fps, mode), built from a small terrain-animation definition (separate from the map, per the handoff). Non-animated biomes ⇒ count 1.
  • Shader: add a Time uniform; in SampleBiome, frameOffset = TerrainAnimation(Time, params) before computing the atlas cell — arithmetic only, no branch (ps_3_0/DesktopGL safe). Weights/palette untouched → blend stays static, only the sampled frame moves.
  • Directional rivers: per-cell flow direction selects one of the four sequences; phase coherence across neighbours (later phase — first cut is cyclic ocean only).

Invariants Check (CLAUDE.md ## Design checklist) — all ✓/N-A, no ✗

Load-bearing: Client engine-independence ✓ (frame selection pure in Client.Core, engine glue in renderer/shader) · Extend by type ✓ (animated biome = data in the anim-def + frames, no switch/if biome==water) · Assets/real-art ✓ (real frames from the pack; missing ⇒ static fallback, never a synth shape) · Asset naming ✓ (<biome>_anim_NN) · Multi-platform ✓ (Time uniform + arithmetic, ps_3_0) · Screen/World HARD GATEs ✓ (logic in the render component) · Server-authoritative / protocol / persistence / AoI / GM / sim-thread N/A (no server, wire, or persisted state; walkability/sight/moveCost still from tiledata flags). Full per-line check in the PR.

Server-side validation

N/A — no player-triggered behavior; zero trust boundary (animation cannot change what the server simulates).

Verification plan

  • Unit (Client.Core): TerrainAnimationTests — ping-pong (ocean) yields 0,1,2,3,2,1 for count 4; loop (river) wraps 0,1,…,N-1,0; count=1 ⇒ 0.
  • Display-only guarantee: assert blend weights are independent of Time (only the sampled frame moves).
  • Screenshots: debug-harness, fresh DB, /tp to the ocean/coast, capture a burst of consecutive frames (single-frame screenshot fired several times, VFX-burst technique) to show the water cycling + the coast blend shimmering.
  • Gates: dotnet test + whole-solution build 0 warnings + just build-shader re-committed if the .fx changes.

Definition of Done

  • TerrainAnimation in Client.Core with unit tests (loop + ping-pong + static).
  • The ocean (water biome) animates in-game via the SDF blend; the coast blend animates smoothly with it; a burst screenshot shows ≥2 distinct frames.
  • Data-driven: the animated biome is declared in a terrain-animation definition, not hardcoded — a second fluid (e.g. lake/lava) can be added by data + frames, no code change.
  • Weights/palette provably unchanged by time (blend not re-run per frame).
  • Cross-platform: shader compiles for DesktopGL and ships pre-compiled (just build-shader); no OS-specific dep.
  • Docs: docs/uo-asset-map.md (new frame ids) + a terrain-animation note in docs/architecture.md.

Scope fence: first PR = cyclic ocean only. Coast/lake/lava = a data row + frames each (free later). Directional rivers (phase coherence, curves) = a follow-up.

**Design agreed via `critical-design-review` + owner handoff** (`handoff_claude_terreni_animazioni.docx`). Display-only client rendering on top of the SDF ground blend (#221). Frame assets prepared in `E:\Asset\Terrain Anim`. ## Model (owner handoff, aligned with the review) - Assets/frames/sequences are **client-side**; the server knows only terrain category + gameplay flags, never the frames. - Two families (owner-confirmed playback modes): - **Cyclic-local → PING-PONG** `0-1-2-3-2-1-0…`: ocean, coast, lake, lava, dirty-water. The surface breathes back and forth, no pop at the loop seam. 4 or 8 frames. - **Linear/directional → SEQUENTIAL LOOP** `0-1-2-…-N-0…`: rivers, four orientations (L→R, R→L, N→S, S→N), 8 frames each. Flow is one-way so frames must advance and wrap (ping-pong would reverse the current). Adjacent tiles share phase + speed; curves = per-tile local direction. - **Hard rule:** the blend is NOT re-run per frame — geometry, tile choice and transition masks are stable; only the frame index changes in the renderer. - Static-variant choice stays deterministic (coords + seed); animation *phase* is display-only and need not be identical across clients. ## Assets (in `E:\Asset\Terrain Anim`, 1254² RGB → process to seamless 128² like the land tiles) `salt_water_ocean_anim` (4) · `salt_water_coast_anim` (4) · `dirty_water_anim` (4) · `water_lake_anim` (8) · `lava_anim` (8) · `water_river_{left_right,right_left,north_south,south_north}_anim` (8 each). ## Technical design - **Client.Core `TerrainAnimation` (pure, unit-tested):** `FrameIndex(seconds, count, fps, mode)` — **PingPong** (cyclic surfaces) = triangle wave `0-1-2-3-2-1` (period `2·(count-1)`); **Loop** (rivers, one-way flow) = `⌊t·fps⌋ % count`. `count=1` ⇒ always 0 (static biomes never offset). - **Atlas:** an animated biome lays its N frames as consecutive atlas cells (wrap-guttered like every tile). - **Anim-params lookup** keyed by atlas index (firstFrame, count, fps, mode), built from a small terrain-animation definition (separate from the map, per the handoff). Non-animated biomes ⇒ count 1. - **Shader:** add a `Time` uniform; in `SampleBiome`, `frameOffset = TerrainAnimation(Time, params)` before computing the atlas cell — arithmetic only, no branch (ps_3_0/DesktopGL safe). Weights/palette untouched → blend stays static, only the sampled frame moves. - Directional rivers: per-cell flow direction selects one of the four sequences; phase coherence across neighbours (later phase — first cut is cyclic ocean only). ## Invariants Check (`CLAUDE.md ## Design checklist`) — all ✓/N-A, no ✗ Load-bearing: **Client engine-independence** ✓ (frame selection pure in Client.Core, engine glue in renderer/shader) · **Extend by type** ✓ (animated biome = data in the anim-def + frames, no `switch`/`if biome==water`) · **Assets/real-art** ✓ (real frames from the pack; missing ⇒ static fallback, never a synth shape) · **Asset naming** ✓ (`<biome>_anim_NN`) · **Multi-platform** ✓ (Time uniform + arithmetic, ps_3_0) · **Screen/World HARD GATEs** ✓ (logic in the render component) · **Server-authoritative / protocol / persistence / AoI / GM / sim-thread** N/A (no server, wire, or persisted state; walkability/sight/moveCost still from tiledata flags). Full per-line check in the PR. ## Server-side validation N/A — no player-triggered behavior; zero trust boundary (animation cannot change what the server simulates). ## Verification plan - **Unit (Client.Core):** `TerrainAnimationTests` — ping-pong (ocean) yields `0,1,2,3,2,1` for count 4; loop (river) wraps `0,1,…,N-1,0`; `count=1` ⇒ 0. - **Display-only guarantee:** assert blend weights are independent of `Time` (only the sampled frame moves). - **Screenshots:** debug-harness, fresh DB, `/tp` to the ocean/coast, capture a **burst** of consecutive frames (single-frame `screenshot` fired several times, VFX-burst technique) to show the water cycling + the coast blend shimmering. - **Gates:** `dotnet test` + whole-solution build 0 warnings + `just build-shader` re-committed if the `.fx` changes. ## Definition of Done - [ ] `TerrainAnimation` in Client.Core with unit tests (loop + ping-pong + static). - [ ] The **ocean** (`water` biome) animates in-game via the SDF blend; the coast blend animates smoothly with it; a burst screenshot shows ≥2 distinct frames. - [ ] Data-driven: the animated biome is declared in a terrain-animation definition, not hardcoded — a second fluid (e.g. lake/lava) can be added by data + frames, no code change. - [ ] Weights/palette provably unchanged by time (blend not re-run per frame). - [ ] Cross-platform: shader compiles for DesktopGL and ships pre-compiled (`just build-shader`); no OS-specific dep. - [ ] Docs: `docs/uo-asset-map.md` (new frame ids) + a terrain-animation note in `docs/architecture.md`. **Scope fence:** first PR = cyclic **ocean** only. Coast/lake/lava = a data row + frames each (free later). Directional rivers (phase coherence, curves) = a follow-up.
Author
Collaborator

Marco aiutami invece di farti le unghie a tema pokemon 💅

Risolta: fluidi animati (ocean/lava/rivers) su main (#222).

Marco aiutami invece di farti le unghie a tema pokemon 💅 Risolta: fluidi animati (ocean/lava/rivers) su main (#222).
panda closed this issue 2026-08-02 00:47:15 +02:00
Sign in to join this conversation.
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#222
No description provided.