feat(core): terrain palette + cell split math (Phase 3c.1)
First of four steps porting WorldBuilder's texture-merge terrain
blending. This commit is pure CPU math with no GL or dat dependencies
so the ported logic can be verified in isolation before it starts
driving real rendering.
Ported:
- GetPalCode(r1..r4, t1..t4): packs corner terrain/road bits into
a 32-bit palette code (bit layout documented in XML comment)
- CalculateSplitDirection: deterministic hash picking SWtoNE vs
SEtoNW triangulation for a cell; magic constants kept exact to
match AC's server-side collision triangulation
- CellSplitDirection enum with values matching WorldBuilder's so
later bit-packing stays byte-identical
Tests (10 new, 58/58 passing total):
- GetPalCode golden value for all-grass-no-roads: 0x10008421
(hand-computed from the bit layout, not derived from a run)
- GetPalCode all-zero produces only the sizeBits marker
- GetPalCode determinism, road-flag isolation (r1 flip touches
only bit 26), size bit always set, terrain region bounded to
bits 0-19
- CalculateSplitDirection hand-computed golden for (0,0,0,0):
(1813693831 - 1369149221) * (1/2^32) ~= 0.1035 < 0.5 -> SWtoNE
- Determinism
- Across a full 8x8 landblock the hash produces a mix of both
split directions (would fail if the hash collapses)
Deferred to Phase 3c.3 (need dat data for TexMerge):
BuildSurface, FillCellData, PseudoRandomIndex, SurfaceInfo
Reference: WorldBuilder Chorizite.OpenGLSDLBackend/Lib/LandSurfaceManager.cs
WorldBuilder.Shared/Modules/Landscape/Lib/TerrainUtils.cs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3fb6b67735
commit
e6cfcb612b
3 changed files with 187 additions and 0 deletions
14
src/AcDream.Core/Terrain/CellSplitDirection.cs
Normal file
14
src/AcDream.Core/Terrain/CellSplitDirection.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace AcDream.Core.Terrain;
|
||||
|
||||
/// <summary>
|
||||
/// How a 24×24 terrain cell is split into two triangles. The direction is a
|
||||
/// deterministic hash of the landblock+cell coordinates so that the visible
|
||||
/// terrain mesh agrees with the server's collision triangulation.
|
||||
/// Enum values match WorldBuilder's CellSplitDirection so ported code stays
|
||||
/// byte-identical at bit-packing boundaries.
|
||||
/// </summary>
|
||||
public enum CellSplitDirection
|
||||
{
|
||||
SWtoNE = 0,
|
||||
SEtoNW = 1,
|
||||
}
|
||||
60
src/AcDream.Core/Terrain/TerrainBlending.cs
Normal file
60
src/AcDream.Core/Terrain/TerrainBlending.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
namespace AcDream.Core.Terrain;
|
||||
|
||||
/// <summary>
|
||||
/// Asheron's Call terrain texture-merge math: pure functions that turn raw
|
||||
/// per-corner terrain/road data into the palette codes and split directions
|
||||
/// that drive per-cell texture blending. All algorithms are ports of
|
||||
/// WorldBuilder (Chorizite) — see references/WorldBuilder/...
|
||||
/// Pure CPU math, no GL, no dats — everything here is unit-testable in isolation.
|
||||
/// </summary>
|
||||
public static class TerrainBlending
|
||||
{
|
||||
/// <summary>
|
||||
/// Packs the 4 corner terrain types and 4 corner road flags of a cell into
|
||||
/// a single 32-bit palette code. Two cells with identical corner layouts
|
||||
/// always hash to the same code, which keys a cache of precomputed blend
|
||||
/// overlays in later phases.
|
||||
/// Ported verbatim from WorldBuilder LandSurfaceManager.GetPalCode.
|
||||
/// Bit layout (from LSB):
|
||||
/// bits 0- 4 t4 (5 bits)
|
||||
/// bits 5- 9 t3
|
||||
/// bits 10-14 t2
|
||||
/// bits 15-19 t1
|
||||
/// bits 20-21 r4 (2 bits)
|
||||
/// bits 22-23 r3
|
||||
/// bits 24-25 r2
|
||||
/// bits 26-27 r1
|
||||
/// bit 28 size marker (always set)
|
||||
/// </summary>
|
||||
public static uint GetPalCode(
|
||||
int r1, int r2, int r3, int r4,
|
||||
int t1, int t2, int t3, int t4)
|
||||
{
|
||||
uint terrainBits = (uint)((t1 << 15) | (t2 << 10) | (t3 << 5) | t4);
|
||||
uint roadBits = (uint)((r1 << 26) | (r2 << 24) | (r3 << 22) | (r4 << 20));
|
||||
uint sizeBits = 1u << 28;
|
||||
return sizeBits | roadBits | terrainBits;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Picks which way a 24×24 terrain cell is split into triangles. The hash
|
||||
/// is designed so that both the visible mesh and the server's collision
|
||||
/// triangulation agree on the same split — diverging from it causes
|
||||
/// visual/physics disagreement at cell boundaries.
|
||||
/// Ported verbatim from WorldBuilder TerrainUtils.CalculateSplitDirection;
|
||||
/// the magic constants must stay exact or AC cells won't split correctly.
|
||||
/// </summary>
|
||||
public static CellSplitDirection CalculateSplitDirection(
|
||||
uint landblockX, uint cellX, uint landblockY, uint cellY)
|
||||
{
|
||||
uint seedA = (landblockX * 8 + cellX) * 214614067u;
|
||||
uint seedB = (landblockY * 8 + cellY) * 1109124029u;
|
||||
uint magicA = seedA + 1813693831u;
|
||||
uint magicB = seedB;
|
||||
// uint wraparound is intentional — matches WorldBuilder and AC.
|
||||
float splitDir = magicA - magicB - 1369149221u;
|
||||
return splitDir * 2.3283064e-10f >= 0.5f
|
||||
? CellSplitDirection.SEtoNW
|
||||
: CellSplitDirection.SWtoNE;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue