acdream/src/AcDream.Core/Terrain/SurfaceInfo.cs
Erik a6cd56663f feat(core): terrain surface recipe + cell data packing (Phase 3c.3)
Ports WorldBuilder's full BuildTexture / FillCellData pipeline as
pure CPU functions in TerrainBlending.cs, along with the SurfaceInfo
recipe record and a TerrainBlendingContext input struct that carries
the atlas index lists the algorithm needs.

This is still pure algorithm work — no GL, no shaders, no mesh gen
changes. Visual Phase 3c.4 next commit wires it into LandblockMesh
and rewrites the terrain shaders to consume Data0..3.

Added (all ports of WorldBuilder LandSurfaceManager methods):
  - ExtractTerrainCodes: inverse of GetPalCode terrain bits
  - PseudoRandomIndex: deterministic hash over palette code for alpha
    variant selection; overflow-dependent int math matches WorldBuilder
    byte-for-byte
  - RotateTerrainCode: *2 with wrap (1→2→4→8→1, multi-corner patterns
    handled in tests)
  - GetRoadCodes: decodes the 8-bit road mask into up to two canonical
    road patterns + allRoad flag; magic 0xE/0xD/0xB/0x7 switch kept verbatim
  - FindTerrainAlpha: picks corner vs side alpha map, walks the 4
    rotations looking for a TCode match, returns (alphaLayer, rotation)
    or (255, 0) for "not found"
  - FindRoadAlpha: same idea for road maps, iterates all maps from a
    pseudo-random offset
  - BuildSurface: composes the above into a SurfaceInfo, handling the
    all-road, all-duplicate-terrain, and distinct-terrain cases via
    BuildOverlayLayers + BuildWithDuplicates (ports GetTerrainTextures +
    BuildTerrainCodesWithDuplicates)
  - FillCellData: packs a SurfaceInfo + CellSplitDirection into the 4
    uint32 vertex attributes Data0..Data3. Byte layout documented in
    XML comment and matches WorldBuilder's Landscape.vert uvec4 byte
    unpacking exactly.

SurfaceInfo record carries resolved atlas byte layers directly (base +
3 terrain overlays + 2 road overlays, each with optional alpha layer
and 0-3 rotation). Sentinel 255 = "slot unused".

Tests (14 new, 75/75 total):
  - ExtractTerrainCodes round-trip with GetPalCode
  - RotateTerrainCode single-corner cycle + multi-corner patterns
  - GetRoadCodes: no-road, all-road, single-corner road
  - PseudoRandomIndex: range, count=0 guard, determinism
  - BuildSurface: all-grass → base only; all-road → road as base;
    two-grass-two-dirt → base + overlay
  - FillCellData: full round-trip bit layout with recognizable
    byte values in every slot, plus a no-road1 case that verifies
    the texRd1 slot collapses to 255 when road1 alpha is absent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:53:32 +02:00

62 lines
3 KiB
C#

namespace AcDream.Core.Terrain;
/// <summary>
/// Per-cell terrain blending recipe derived from a palette code. Holds atlas
/// layer indices (terrain atlas + alpha atlas) and rotation values for a base
/// texture plus up to three terrain overlays and two road overlays. Layer
/// indices use the sentinel <c>255</c> for "not present" — the fragment shader
/// skips any layer with index 255.
///
/// Ported from WorldBuilder's TMI structure with the field set adapted to
/// acdream's atlas layout: where WorldBuilder stores <c>TerrainTex</c>
/// pointers, we store resolved byte atlas layers so the shader never has to
/// resolve anything at render time.
/// </summary>
public readonly record struct SurfaceInfo(
byte BaseLayer,
byte Ovl0Layer, byte Ovl0AlphaLayer, byte Ovl0Rotation,
byte Ovl1Layer, byte Ovl1AlphaLayer, byte Ovl1Rotation,
byte Ovl2Layer, byte Ovl2AlphaLayer, byte Ovl2Rotation,
byte RoadLayer, byte Road0AlphaLayer, byte Road0Rotation,
byte Road1AlphaLayer, byte Road1Rotation)
{
public const byte None = 255;
/// <summary>An empty recipe where only the base is present and all overlays are None.</summary>
public static SurfaceInfo BaseOnly(byte baseLayer) => new(
baseLayer,
None, None, 0, None, None, 0, None, None, 0,
None, None, 0, None, 0);
}
/// <summary>
/// Read-only inputs to <see cref="TerrainBlending.BuildSurface"/> sourced from
/// the Region dat / TerrainAtlas at load time. Lets BuildSurface stay pure
/// (no dat or GL dependencies) so it can be unit-tested with synthetic data.
///
/// <para>
/// Fields:
/// <list type="bullet">
/// <item><c>TerrainTypeToLayer</c>: map from (uint)TerrainTextureType → terrain-atlas layer.</item>
/// <item><c>RoadLayer</c>: atlas layer for TerrainTextureType.RoadType, or 255 if missing.</item>
/// <item><c>CornerAlphaLayers</c>: alpha-atlas layer indices for TexMerge.CornerTerrainMaps, in original order.</item>
/// <item><c>SideAlphaLayers</c>: alpha-atlas layer indices for TexMerge.SideTerrainMaps.</item>
/// <item><c>RoadAlphaLayers</c>: alpha-atlas layer indices for TexMerge.RoadMaps.</item>
/// <item><c>CornerAlphaTCodes</c>: parallel to CornerAlphaLayers — each alpha map's TCode.</item>
/// <item><c>SideAlphaTCodes</c>: parallel to SideAlphaLayers.</item>
/// <item><c>RoadAlphaRCodes</c>: parallel to RoadAlphaLayers — each road map's RCode.</item>
/// </list>
/// The *Codes arrays let <see cref="TerrainBlending.FindTerrainAlpha"/> pick
/// which alpha map to use for a given terrain/road code via the rotation loop
/// lifted from WorldBuilder.
/// </para>
/// </summary>
public sealed record TerrainBlendingContext(
IReadOnlyDictionary<uint, byte> TerrainTypeToLayer,
byte RoadLayer,
IReadOnlyList<byte> CornerAlphaLayers,
IReadOnlyList<byte> SideAlphaLayers,
IReadOnlyList<byte> RoadAlphaLayers,
IReadOnlyList<uint> CornerAlphaTCodes,
IReadOnlyList<uint> SideAlphaTCodes,
IReadOnlyList<uint> RoadAlphaRCodes);