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>
62 lines
3 KiB
C#
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);
|