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>
This commit is contained in:
parent
a8459eecbb
commit
a6cd56663f
3 changed files with 680 additions and 3 deletions
62
src/AcDream.Core/Terrain/SurfaceInfo.cs
Normal file
62
src/AcDream.Core/Terrain/SurfaceInfo.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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);
|
||||
|
|
@ -2,13 +2,16 @@ 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/...
|
||||
/// per-corner terrain/road data into the palette codes, surface blend recipes,
|
||||
/// and per-cell vertex data that drive per-cell texture blending. All
|
||||
/// algorithms are ports of WorldBuilder (Chorizite) — see
|
||||
/// references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/LandSurfaceManager.cs.
|
||||
/// Pure CPU math, no GL, no dats — everything here is unit-testable in isolation.
|
||||
/// </summary>
|
||||
public static class TerrainBlending
|
||||
{
|
||||
// --- Phase 3c.1 algorithms ---
|
||||
|
||||
/// <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
|
||||
|
|
@ -57,4 +60,397 @@ public static class TerrainBlending
|
|||
? CellSplitDirection.SEtoNW
|
||||
: CellSplitDirection.SWtoNE;
|
||||
}
|
||||
|
||||
// --- Phase 3c.3 algorithms (surface recipe + vertex data packing) ---
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the 4 corner terrain types from a palette code, in the order
|
||||
/// [t1, t2, t3, t4]. Inverse of <see cref="GetPalCode"/>'s terrain packing.
|
||||
/// </summary>
|
||||
public static uint[] ExtractTerrainCodes(uint palCode) => new uint[]
|
||||
{
|
||||
(palCode >> 15) & 0x1Fu,
|
||||
(palCode >> 10) & 0x1Fu,
|
||||
(palCode >> 5) & 0x1Fu,
|
||||
palCode & 0x1Fu,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Pseudo-random index in [0, count) derived from the palette code. Same
|
||||
/// code always returns the same index, so neighbor cells that share a
|
||||
/// palette code pick the same alpha variant. Ported from
|
||||
/// WorldBuilder.GeneratePseudoRandomIndex.
|
||||
/// </summary>
|
||||
public static int PseudoRandomIndex(uint palCode, int count)
|
||||
{
|
||||
if (count <= 0) return 0;
|
||||
// The computation uses int overflow and double multiplication
|
||||
// exactly as in WorldBuilder — do NOT "clean up" the casts.
|
||||
int pseudoRandom = (int)Math.Floor((1379576222 * palCode - 1372186442) * 2.3283064e-10 * count);
|
||||
return pseudoRandom >= count ? 0 : pseudoRandom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates a terrain-code bit pattern one step (1→2→4→8→1). Used by
|
||||
/// <see cref="FindTerrainAlpha"/> and <see cref="FindRoadAlpha"/> to walk
|
||||
/// the 4 rotations of an alpha map looking for one that matches the
|
||||
/// target terrain/road code pattern.
|
||||
/// </summary>
|
||||
public static uint RotateTerrainCode(uint code)
|
||||
{
|
||||
code *= 2;
|
||||
return code >= 16 ? code - 15 : code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the road-bits region of a palette code into up to two road
|
||||
/// "codes" (corner patterns) and an <c>allRoad</c> flag. Ported verbatim
|
||||
/// from WorldBuilder.GetRoadCodes. The magic constants describe which of
|
||||
/// the 4 corners have roads (mask bit 0 = corner 1, bit 1 = corner 2,
|
||||
/// etc.) and which canonical patterns the result maps to.
|
||||
/// </summary>
|
||||
public static (uint road0, uint road1, bool allRoad) GetRoadCodes(uint palCode)
|
||||
{
|
||||
uint mask = 0;
|
||||
if ((palCode & 0x0C000000u) != 0) mask |= 1; // r1 in bits 26-27
|
||||
if ((palCode & 0x03000000u) != 0) mask |= 2; // r2 in bits 24-25
|
||||
if ((palCode & 0x00C00000u) != 0) mask |= 4; // r3 in bits 22-23
|
||||
if ((palCode & 0x00300000u) != 0) mask |= 8; // r4 in bits 20-21
|
||||
|
||||
if (mask == 0xF)
|
||||
return (0, 0, allRoad: true);
|
||||
|
||||
return mask switch
|
||||
{
|
||||
0x0 => (0, 0, false),
|
||||
0xE => (6, 12, false),
|
||||
0xD => (9, 12, false),
|
||||
0xB => (9, 3, false),
|
||||
0x7 => (3, 6, false),
|
||||
_ => (mask, 0, false),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an alpha-map layer and rotation for a single terrain overlay.
|
||||
/// <paramref name="terrainCode"/> is a 4-bit pattern indicating which
|
||||
/// corners the overlay applies to (1,2,4,8 = single-corner patterns →
|
||||
/// use corner maps; other patterns → use side maps). Returns
|
||||
/// <see cref="SurfaceInfo.None"/> and rotation 0 if no alpha map
|
||||
/// matches under any rotation, which is a valid "don't draw this
|
||||
/// overlay" state.
|
||||
/// Ported from WorldBuilder.FindTerrainAlpha.
|
||||
/// </summary>
|
||||
public static (byte alphaLayer, byte rotation) FindTerrainAlpha(
|
||||
uint palCode, uint terrainCode, TerrainBlendingContext ctx)
|
||||
{
|
||||
bool isCornerTerrain = terrainCode is 1 or 2 or 4 or 8;
|
||||
var maps = isCornerTerrain ? ctx.CornerAlphaLayers : ctx.SideAlphaLayers;
|
||||
var tCodes = isCornerTerrain ? ctx.CornerAlphaTCodes : ctx.SideAlphaTCodes;
|
||||
|
||||
if (maps.Count == 0 || tCodes.Count == 0)
|
||||
return (SurfaceInfo.None, 0);
|
||||
|
||||
int randomIndex = PseudoRandomIndex(palCode, maps.Count);
|
||||
uint currentCode = tCodes[randomIndex];
|
||||
int rotationCount = 0;
|
||||
while (currentCode != terrainCode && rotationCount < 4)
|
||||
{
|
||||
currentCode = RotateTerrainCode(currentCode);
|
||||
rotationCount++;
|
||||
}
|
||||
|
||||
if (rotationCount >= 4)
|
||||
return (SurfaceInfo.None, 0);
|
||||
|
||||
return (maps[randomIndex], (byte)rotationCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an alpha-map layer and rotation for a road overlay. Unlike
|
||||
/// terrain alphas, roads iterate through all maps (starting at a random
|
||||
/// offset) looking for one whose RCode matches the requested pattern
|
||||
/// under some rotation.
|
||||
/// Returns <see cref="SurfaceInfo.None"/> if no match exists.
|
||||
/// Ported from WorldBuilder.FindRoadAlpha.
|
||||
/// </summary>
|
||||
public static (byte alphaLayer, byte rotation) FindRoadAlpha(
|
||||
uint palCode, uint roadCode, TerrainBlendingContext ctx)
|
||||
{
|
||||
var maps = ctx.RoadAlphaLayers;
|
||||
var rCodes = ctx.RoadAlphaRCodes;
|
||||
if (maps.Count == 0 || rCodes.Count == 0)
|
||||
return (SurfaceInfo.None, 0);
|
||||
|
||||
int randomIndex = PseudoRandomIndex(palCode, maps.Count);
|
||||
|
||||
for (int i = 0; i < maps.Count; i++)
|
||||
{
|
||||
int index = (i + randomIndex) % maps.Count;
|
||||
uint currentCode = rCodes[index];
|
||||
for (int rotationCount = 0; rotationCount < 4; rotationCount++)
|
||||
{
|
||||
if (currentCode == roadCode)
|
||||
return (maps[index], (byte)rotationCount);
|
||||
currentCode = RotateTerrainCode(currentCode);
|
||||
}
|
||||
}
|
||||
|
||||
return (SurfaceInfo.None, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns a palette code into a full <see cref="SurfaceInfo"/> blend
|
||||
/// recipe. Handles three cases internally:
|
||||
/// <list type="bullet">
|
||||
/// <item>All-road cell → base = road texture, no overlays</item>
|
||||
/// <item>All corners same terrain → base only, no overlays</item>
|
||||
/// <item>2-4 distinct terrain types → base + up to 3 overlays per
|
||||
/// WorldBuilder's duplicate-handling logic</item>
|
||||
/// </list>
|
||||
/// Road overlays (up to 2) are added on top if any corner has a road bit.
|
||||
/// Ported from WorldBuilder.BuildTexture + ProcessTerrainOverlays +
|
||||
/// ProcessRoadOverlays + GetTerrainTextures + BuildTerrainCodesWithDuplicates.
|
||||
/// </summary>
|
||||
public static SurfaceInfo BuildSurface(uint palCode, TerrainBlendingContext ctx)
|
||||
{
|
||||
var terrainCorners = ExtractTerrainCodes(palCode); // t1..t4
|
||||
var (road0, road1, allRoad) = GetRoadCodes(palCode);
|
||||
|
||||
// All-road cell: base IS the road texture, no overlays at all.
|
||||
if (allRoad)
|
||||
{
|
||||
return SurfaceInfo.BaseOnly(ctx.RoadLayer);
|
||||
}
|
||||
|
||||
// Resolve base + up to 3 overlay terrain layers and the per-overlay
|
||||
// "which corners" code. For 4 distinct corners this produces layers
|
||||
// = [t1,t2,t3,t4] and codes = [2,4,8] (so overlays 0,1,2 cover corners
|
||||
// 1,2,3 respectively while the base covers corner 0). For cells with
|
||||
// duplicates the layout is rebuilt via BuildTerrainCodesWithDuplicates.
|
||||
byte[] terrainLayers = new byte[4];
|
||||
uint[] overlayCodes = new uint[3];
|
||||
BuildOverlayLayers(terrainCorners, ctx, terrainLayers, overlayCodes);
|
||||
|
||||
// Base is always terrainLayers[0]. Overlays 0..2 correspond to
|
||||
// terrainLayers[1..3] when present.
|
||||
byte baseLayer = terrainLayers[0];
|
||||
|
||||
// Find alpha layer + rotation for each of up to 3 overlays.
|
||||
// A zero terrain code means "no more overlays" (short-circuit loop).
|
||||
byte ovl0Layer = SurfaceInfo.None, ovl0Alpha = SurfaceInfo.None, ovl0Rot = 0;
|
||||
byte ovl1Layer = SurfaceInfo.None, ovl1Alpha = SurfaceInfo.None, ovl1Rot = 0;
|
||||
byte ovl2Layer = SurfaceInfo.None, ovl2Alpha = SurfaceInfo.None, ovl2Rot = 0;
|
||||
|
||||
if (overlayCodes[0] != 0)
|
||||
{
|
||||
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[0], ctx);
|
||||
if (a != SurfaceInfo.None)
|
||||
{
|
||||
ovl0Layer = terrainLayers[1]; ovl0Alpha = a; ovl0Rot = r;
|
||||
}
|
||||
}
|
||||
if (overlayCodes[1] != 0)
|
||||
{
|
||||
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[1], ctx);
|
||||
if (a != SurfaceInfo.None)
|
||||
{
|
||||
ovl1Layer = terrainLayers[2]; ovl1Alpha = a; ovl1Rot = r;
|
||||
}
|
||||
}
|
||||
if (overlayCodes[2] != 0)
|
||||
{
|
||||
var (a, r) = FindTerrainAlpha(palCode, overlayCodes[2], ctx);
|
||||
if (a != SurfaceInfo.None)
|
||||
{
|
||||
ovl2Layer = terrainLayers[3]; ovl2Alpha = a; ovl2Rot = r;
|
||||
}
|
||||
}
|
||||
|
||||
// Road overlays: share the road terrain layer, differ in alpha and rotation.
|
||||
byte roadLayer = SurfaceInfo.None;
|
||||
byte road0Alpha = SurfaceInfo.None, road0Rot = 0;
|
||||
byte road1Alpha = SurfaceInfo.None, road1Rot = 0;
|
||||
if (ctx.RoadLayer != SurfaceInfo.None && road0 != 0)
|
||||
{
|
||||
var (a0, r0) = FindRoadAlpha(palCode, road0, ctx);
|
||||
if (a0 != SurfaceInfo.None)
|
||||
{
|
||||
roadLayer = ctx.RoadLayer;
|
||||
road0Alpha = a0;
|
||||
road0Rot = r0;
|
||||
if (road1 != 0)
|
||||
{
|
||||
var (a1, r1) = FindRoadAlpha(palCode, road1, ctx);
|
||||
if (a1 != SurfaceInfo.None)
|
||||
{
|
||||
road1Alpha = a1;
|
||||
road1Rot = r1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SurfaceInfo(
|
||||
baseLayer,
|
||||
ovl0Layer, ovl0Alpha, ovl0Rot,
|
||||
ovl1Layer, ovl1Alpha, ovl1Rot,
|
||||
ovl2Layer, ovl2Alpha, ovl2Rot,
|
||||
roadLayer, road0Alpha, road0Rot,
|
||||
road1Alpha, road1Rot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills terrainLayers[0..3] and overlayCodes[0..2] based on the 4 corner
|
||||
/// terrain codes. If all 4 corners are distinct we use the straightforward
|
||||
/// mapping; otherwise we delegate to the duplicate-handling path. Combined
|
||||
/// port of WorldBuilder.GetTerrainTextures +
|
||||
/// BuildTerrainCodesWithDuplicates.
|
||||
/// </summary>
|
||||
private static void BuildOverlayLayers(
|
||||
uint[] terrainCorners,
|
||||
TerrainBlendingContext ctx,
|
||||
byte[] terrainLayers,
|
||||
uint[] overlayCodes)
|
||||
{
|
||||
// Find first duplicate pair — if any exists, use the duplicate path.
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
for (int j = i + 1; j < 4; j++)
|
||||
{
|
||||
if (terrainCorners[i] == terrainCorners[j])
|
||||
{
|
||||
BuildWithDuplicates(terrainCorners, ctx, terrainLayers, overlayCodes, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All 4 distinct: layers = [t1,t2,t3,t4], overlay codes = [2, 4, 8]
|
||||
for (int i = 0; i < 4; i++)
|
||||
terrainLayers[i] = LookupTerrainLayer(ctx, terrainCorners[i]);
|
||||
overlayCodes[0] = 2;
|
||||
overlayCodes[1] = 4;
|
||||
overlayCodes[2] = 8;
|
||||
}
|
||||
|
||||
private static void BuildWithDuplicates(
|
||||
uint[] terrainCorners,
|
||||
TerrainBlendingContext ctx,
|
||||
byte[] terrainLayers,
|
||||
uint[] overlayCodes,
|
||||
int duplicateIndex)
|
||||
{
|
||||
uint primaryTerrain = terrainCorners[duplicateIndex];
|
||||
uint secondaryTerrain = 0;
|
||||
|
||||
terrainLayers[0] = LookupTerrainLayer(ctx, primaryTerrain);
|
||||
terrainLayers[1] = SurfaceInfo.None;
|
||||
terrainLayers[2] = SurfaceInfo.None;
|
||||
terrainLayers[3] = SurfaceInfo.None;
|
||||
overlayCodes[0] = 0;
|
||||
overlayCodes[1] = 0;
|
||||
overlayCodes[2] = 0;
|
||||
bool foundSecondary = false;
|
||||
|
||||
for (int k = 0; k < 4; k++)
|
||||
{
|
||||
if (primaryTerrain == terrainCorners[k]) continue;
|
||||
|
||||
if (!foundSecondary)
|
||||
{
|
||||
overlayCodes[0] = (uint)(1 << k);
|
||||
secondaryTerrain = terrainCorners[k];
|
||||
terrainLayers[1] = LookupTerrainLayer(ctx, secondaryTerrain);
|
||||
foundSecondary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Another corner differs from primary.
|
||||
if (secondaryTerrain == terrainCorners[k]
|
||||
&& overlayCodes[0] == (1u << (k - 1)))
|
||||
{
|
||||
// Adjacent same-secondary corners merge into a 2-bit pattern.
|
||||
overlayCodes[0] += (uint)(1 << k);
|
||||
}
|
||||
else
|
||||
{
|
||||
terrainLayers[2] = LookupTerrainLayer(ctx, terrainCorners[k]);
|
||||
overlayCodes[1] = (uint)(1 << k);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte LookupTerrainLayer(TerrainBlendingContext ctx, uint terrainCode)
|
||||
{
|
||||
return ctx.TerrainTypeToLayer.TryGetValue(terrainCode, out var layer)
|
||||
? layer
|
||||
: SurfaceInfo.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Packs a <see cref="SurfaceInfo"/> and split direction into the 4 uint32s
|
||||
/// that appear as vertex attributes (<c>Data0..Data3</c>) on every vertex
|
||||
/// in a cell. Bit layout matches WorldBuilder's Landscape.vert so the
|
||||
/// shader port can be verbatim.
|
||||
///
|
||||
/// <para>
|
||||
/// Byte layout (little-endian, consumed by the shader as <c>uvec4</c>
|
||||
/// byte-attribute reads):
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// data0: [ baseTex, baseAlpha=255, ovl0Tex, ovl0Alpha ]
|
||||
/// data1: [ ovl1Tex, ovl1Alpha, ovl2Tex, ovl2Alpha ]
|
||||
/// data2: [ road0Tex, road0Alpha, road1Tex, road1Alpha ]
|
||||
/// data3: bit 0-1 rotBase (unused, 0)
|
||||
/// bit 2-3 rotOvl0
|
||||
/// bit 4-5 rotOvl1
|
||||
/// bit 6-7 rotOvl2
|
||||
/// bit 8-9 rotRd0
|
||||
/// bit 10-11 rotRd1
|
||||
/// bit 12 splitDirection (0=SWtoNE, 1=SEtoNW)
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static (uint d0, uint d1, uint d2, uint d3) FillCellData(
|
||||
SurfaceInfo s, CellSplitDirection split)
|
||||
{
|
||||
uint texBase = s.BaseLayer;
|
||||
uint alphaBase = SurfaceInfo.None; // unused, always 255
|
||||
|
||||
uint texOvl0 = s.Ovl0Layer;
|
||||
uint alphaOvl0 = s.Ovl0AlphaLayer;
|
||||
uint rotOvl0 = s.Ovl0Rotation;
|
||||
|
||||
uint texOvl1 = s.Ovl1Layer;
|
||||
uint alphaOvl1 = s.Ovl1AlphaLayer;
|
||||
uint rotOvl1 = s.Ovl1Rotation;
|
||||
|
||||
uint texOvl2 = s.Ovl2Layer;
|
||||
uint alphaOvl2 = s.Ovl2AlphaLayer;
|
||||
uint rotOvl2 = s.Ovl2Rotation;
|
||||
|
||||
// Road0 and Road1 share s.RoadLayer (same road texture, different alpha masks).
|
||||
uint texRd0 = s.RoadLayer;
|
||||
uint alphaRd0 = s.Road0AlphaLayer;
|
||||
uint rotRd0 = s.Road0Rotation;
|
||||
|
||||
uint texRd1 = s.Road1AlphaLayer == SurfaceInfo.None ? SurfaceInfo.None : s.RoadLayer;
|
||||
uint alphaRd1 = s.Road1AlphaLayer;
|
||||
uint rotRd1 = s.Road1Rotation;
|
||||
|
||||
uint d0 = (texBase | (alphaBase << 8)) | ((texOvl0 | (alphaOvl0 << 8)) << 16);
|
||||
uint d1 = (texOvl1 | (alphaOvl1 << 8)) | ((texOvl2 | (alphaOvl2 << 8)) << 16);
|
||||
uint d2 = (texRd0 | (alphaRd0 << 8)) | ((texRd1 | (alphaRd1 << 8)) << 16);
|
||||
uint d3 = 0u // rotBase in bits 0-1 (unused)
|
||||
| (rotOvl0 << 2)
|
||||
| (rotOvl1 << 4)
|
||||
| (rotOvl2 << 6)
|
||||
| (rotRd0 << 8)
|
||||
| (rotRd1 << 10)
|
||||
| ((((uint)split) & 1u) << 12);
|
||||
|
||||
return (d0, d1, d2, d3);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue