feat(core): add Vertex.TerrainLayer + LandblockMesh layer map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 20:16:25 +02:00
parent bc69f0cdf1
commit 324abed6eb
7 changed files with 64 additions and 29 deletions

View file

@ -133,7 +133,7 @@ public sealed class GameWindow : IDisposable
if (heightTable is null || heightTable.Length < 256)
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
var meshData = LandblockMesh.Build(block, heightTable);
var meshData = LandblockMesh.Build(block, heightTable, new Dictionary<uint, uint>());
_terrain = new TerrainRenderer(_gl, meshData, _shader);
_textureCache = new TextureCache(_gl, _dats);

View file

@ -58,6 +58,8 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
_gl.EnableVertexAttribArray(2);
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
_gl.EnableVertexAttribArray(3);
_gl.VertexAttribIPointer(3, 1, VertexAttribIType.UnsignedInt, stride, (void*)(8 * sizeof(float)));
_gl.BindVertexArray(0);

View file

@ -41,6 +41,8 @@ public sealed unsafe class TerrainRenderer : IDisposable
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
_gl.EnableVertexAttribArray(2);
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
_gl.EnableVertexAttribArray(3);
_gl.VertexAttribIPointer(3, 1, VertexAttribIType.UnsignedInt, stride, (void*)(8 * sizeof(float)));
_gl.BindVertexArray(0);
}

View file

@ -55,7 +55,7 @@ public static class GfxObjMesh
if (!bucket.Dedupe.TryGetValue(key, out var outIdx))
{
outIdx = (uint)bucket.Vertices.Count;
bucket.Vertices.Add(new Vertex(sw.Origin, sw.Normal, texcoord));
bucket.Vertices.Add(new Vertex(sw.Origin, sw.Normal, texcoord, TerrainLayer: 0));
bucket.Dedupe[key] = outIdx;
}
polyOut.Add(outIdx);

View file

@ -7,19 +7,20 @@ public sealed record LandblockMeshData(Vertex[] Vertices, uint[] Indices);
public static class LandblockMesh
{
// AC landblock geometry constants
private const int VerticesPerSide = 9; // 9x9 heightmap grid
private const int CellsPerSide = VerticesPerSide - 1; // 8x8 cells
private const float CellSize = 24.0f; // world units per cell edge
private const int VerticesPerSide = 9;
private const int CellsPerSide = VerticesPerSide - 1;
private const float CellSize = 24.0f;
// Phase 2b: tile terrain textures ~4x per landblock instead of stretching
// a single texture across the whole 192-unit patch.
private const float TexCoordDivisor = CellsPerSide / 4.0f;
/// <summary>
/// Build the CPU mesh for one landblock's heightmap. <paramref name="heightTable"/>
/// is the 256-entry non-linear height lookup from <c>Region.LandDefs.LandHeightTable</c> —
/// AC encodes per-vertex heights as indices into this table, not raw world-Z.
/// </summary>
public static LandblockMeshData Build(LandBlock block, float[] heightTable)
public static LandblockMeshData Build(
LandBlock block,
float[] heightTable,
IReadOnlyDictionary<uint, uint> terrainTypeToLayer)
{
ArgumentNullException.ThrowIfNull(heightTable);
ArgumentNullException.ThrowIfNull(terrainTypeToLayer);
if (heightTable.Length < 256)
throw new ArgumentException("heightTable must have 256 entries", nameof(heightTable));
@ -28,23 +29,23 @@ public static class LandblockMesh
{
for (int x = 0; x < VerticesPerSide; x++)
{
// Vertex buffer index (row-major, y*9+x) is internal to this mesh
// and what the index buffer below references.
int vi = y * VerticesPerSide + x;
// Height dat index is PACKED AS x*9+y — AC stores per-vertex
// heights in x-major order (see ACViewer's
// LandblockStruct: Height[x * VertexDim + y]). Using y*9+x here
// (as Phase 1 did) transposes the terrain along its diagonal,
// which is invisible for flat landblocks but leaves buildings
// buried by ~10+ units on real terrain like Holtburg.
int hi = x * VerticesPerSide + y;
float height = heightTable[block.Height[hi]];
// TerrainInfo raw ushort value used as the atlas-layer map key.
// The map is keyed on the raw terrain ushort (which encodes Road,
// Type, and Scenery fields), matching what the test and caller supply.
uint terrainType = (ushort)block.Terrain[hi];
if (!terrainTypeToLayer.TryGetValue(terrainType, out var layer))
layer = 0;
vertices[vi] = new Vertex(
Position: new Vector3(x * CellSize, y * CellSize, height),
Normal: Vector3.UnitZ,
TexCoord: new Vector2(x / (float)CellsPerSide, y / (float)CellsPerSide));
TexCoord: new Vector2(x / TexCoordDivisor, y / TexCoordDivisor),
TerrainLayer: layer);
}
}
@ -58,7 +59,6 @@ public static class LandblockMesh
uint b = (uint)(y * VerticesPerSide + x + 1);
uint c = (uint)((y + 1) * VerticesPerSide + x);
uint d = (uint)((y + 1) * VerticesPerSide + x + 1);
// two triangles per cell, CCW
indices[idx++] = a; indices[idx++] = b; indices[idx++] = d;
indices[idx++] = a; indices[idx++] = d; indices[idx++] = c;
}

View file

@ -2,4 +2,8 @@ using System.Numerics;
namespace AcDream.Core.Terrain;
public readonly record struct Vertex(Vector3 Position, Vector3 Normal, Vector2 TexCoord);
public readonly record struct Vertex(
Vector3 Position,
Vector3 Normal,
Vector2 TexCoord,
uint TerrainLayer);