feat(core): add Vertex.TerrainLayer + LandblockMesh layer map
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bc69f0cdf1
commit
324abed6eb
7 changed files with 64 additions and 29 deletions
|
|
@ -133,7 +133,7 @@ public sealed class GameWindow : IDisposable
|
||||||
if (heightTable is null || heightTable.Length < 256)
|
if (heightTable is null || heightTable.Length < 256)
|
||||||
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
|
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);
|
_terrain = new TerrainRenderer(_gl, meshData, _shader);
|
||||||
|
|
||||||
_textureCache = new TextureCache(_gl, _dats);
|
_textureCache = new TextureCache(_gl, _dats);
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ public sealed unsafe class StaticMeshRenderer : IDisposable
|
||||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||||
_gl.EnableVertexAttribArray(2);
|
_gl.EnableVertexAttribArray(2);
|
||||||
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
_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);
|
_gl.BindVertexArray(0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
||||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
||||||
_gl.EnableVertexAttribArray(2);
|
_gl.EnableVertexAttribArray(2);
|
||||||
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
|
_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);
|
_gl.BindVertexArray(0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public static class GfxObjMesh
|
||||||
if (!bucket.Dedupe.TryGetValue(key, out var outIdx))
|
if (!bucket.Dedupe.TryGetValue(key, out var outIdx))
|
||||||
{
|
{
|
||||||
outIdx = (uint)bucket.Vertices.Count;
|
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;
|
bucket.Dedupe[key] = outIdx;
|
||||||
}
|
}
|
||||||
polyOut.Add(outIdx);
|
polyOut.Add(outIdx);
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,20 @@ public sealed record LandblockMeshData(Vertex[] Vertices, uint[] Indices);
|
||||||
|
|
||||||
public static class LandblockMesh
|
public static class LandblockMesh
|
||||||
{
|
{
|
||||||
// AC landblock geometry constants
|
private const int VerticesPerSide = 9;
|
||||||
private const int VerticesPerSide = 9; // 9x9 heightmap grid
|
private const int CellsPerSide = VerticesPerSide - 1;
|
||||||
private const int CellsPerSide = VerticesPerSide - 1; // 8x8 cells
|
private const float CellSize = 24.0f;
|
||||||
private const float CellSize = 24.0f; // world units per cell edge
|
// 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>
|
public static LandblockMeshData Build(
|
||||||
/// Build the CPU mesh for one landblock's heightmap. <paramref name="heightTable"/>
|
LandBlock block,
|
||||||
/// is the 256-entry non-linear height lookup from <c>Region.LandDefs.LandHeightTable</c> —
|
float[] heightTable,
|
||||||
/// AC encodes per-vertex heights as indices into this table, not raw world-Z.
|
IReadOnlyDictionary<uint, uint> terrainTypeToLayer)
|
||||||
/// </summary>
|
|
||||||
public static LandblockMeshData Build(LandBlock block, float[] heightTable)
|
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(heightTable);
|
ArgumentNullException.ThrowIfNull(heightTable);
|
||||||
|
ArgumentNullException.ThrowIfNull(terrainTypeToLayer);
|
||||||
if (heightTable.Length < 256)
|
if (heightTable.Length < 256)
|
||||||
throw new ArgumentException("heightTable must have 256 entries", nameof(heightTable));
|
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++)
|
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;
|
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;
|
int hi = x * VerticesPerSide + y;
|
||||||
|
|
||||||
float height = heightTable[block.Height[hi]];
|
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(
|
vertices[vi] = new Vertex(
|
||||||
Position: new Vector3(x * CellSize, y * CellSize, height),
|
Position: new Vector3(x * CellSize, y * CellSize, height),
|
||||||
Normal: Vector3.UnitZ,
|
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 b = (uint)(y * VerticesPerSide + x + 1);
|
||||||
uint c = (uint)((y + 1) * VerticesPerSide + x);
|
uint c = (uint)((y + 1) * VerticesPerSide + x);
|
||||||
uint d = (uint)((y + 1) * VerticesPerSide + x + 1);
|
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++] = b; indices[idx++] = d;
|
||||||
indices[idx++] = a; indices[idx++] = d; indices[idx++] = c;
|
indices[idx++] = a; indices[idx++] = d; indices[idx++] = c;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,8 @@ using System.Numerics;
|
||||||
|
|
||||||
namespace AcDream.Core.Terrain;
|
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);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ public class LandblockMeshTests
|
||||||
private static readonly float[] IdentityHeightTable =
|
private static readonly float[] IdentityHeightTable =
|
||||||
Enumerable.Range(0, 256).Select(i => i * 2f).ToArray();
|
Enumerable.Range(0, 256).Select(i => i * 2f).ToArray();
|
||||||
|
|
||||||
|
private static readonly IReadOnlyDictionary<uint, uint> EmptyTerrainMap =
|
||||||
|
new Dictionary<uint, uint>();
|
||||||
|
|
||||||
private static LandBlock BuildFlatLandBlock(byte heightIndex = 0)
|
private static LandBlock BuildFlatLandBlock(byte heightIndex = 0)
|
||||||
{
|
{
|
||||||
var block = new LandBlock
|
var block = new LandBlock
|
||||||
|
|
@ -36,7 +39,7 @@ public class LandblockMeshTests
|
||||||
{
|
{
|
||||||
var block = BuildFlatLandBlock();
|
var block = BuildFlatLandBlock();
|
||||||
|
|
||||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap);
|
||||||
|
|
||||||
Assert.Equal(81, mesh.Vertices.Length);
|
Assert.Equal(81, mesh.Vertices.Length);
|
||||||
Assert.Equal(128 * 3, mesh.Indices.Length);
|
Assert.Equal(128 * 3, mesh.Indices.Length);
|
||||||
|
|
@ -47,7 +50,7 @@ public class LandblockMeshTests
|
||||||
{
|
{
|
||||||
var block = BuildFlatLandBlock();
|
var block = BuildFlatLandBlock();
|
||||||
|
|
||||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap);
|
||||||
|
|
||||||
var minX = mesh.Vertices.Min(v => v.Position.X);
|
var minX = mesh.Vertices.Min(v => v.Position.X);
|
||||||
var maxX = mesh.Vertices.Max(v => v.Position.X);
|
var maxX = mesh.Vertices.Max(v => v.Position.X);
|
||||||
|
|
@ -65,7 +68,7 @@ public class LandblockMeshTests
|
||||||
{
|
{
|
||||||
var block = BuildFlatLandBlock(heightIndex: 10);
|
var block = BuildFlatLandBlock(heightIndex: 10);
|
||||||
|
|
||||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap);
|
||||||
|
|
||||||
var zs = mesh.Vertices.Select(v => v.Position.Z).Distinct().ToArray();
|
var zs = mesh.Vertices.Select(v => v.Position.Z).Distinct().ToArray();
|
||||||
Assert.Single(zs);
|
Assert.Single(zs);
|
||||||
|
|
@ -76,12 +79,36 @@ public class LandblockMeshTests
|
||||||
{
|
{
|
||||||
var block = BuildFlatLandBlock(heightIndex: 5);
|
var block = BuildFlatLandBlock(heightIndex: 5);
|
||||||
|
|
||||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap);
|
||||||
|
|
||||||
// AC's Land::LandHeightTable scales height byte index by 2.0f for the simple ramp case.
|
// AC's Land::LandHeightTable scales height byte index by 2.0f for the simple ramp case.
|
||||||
Assert.Equal(10.0f, mesh.Vertices[0].Position.Z);
|
Assert.Equal(10.0f, mesh.Vertices[0].Position.Z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_PerVertexTerrainLayer_UsesMappedLayerIndex()
|
||||||
|
{
|
||||||
|
var block = BuildFlatLandBlock();
|
||||||
|
// TerrainInfo is a struct with implicit conversion from ushort. The low 5 bits
|
||||||
|
// of the ushort encode TerrainTextureType via TerrainInfo.Type.
|
||||||
|
// Set vertex at x-major index (x=2, y=3) to terrain type 7.
|
||||||
|
block.Terrain[2 * 9 + 3] = (ushort)7; // low 5 bits = 7
|
||||||
|
|
||||||
|
var map = new Dictionary<uint, uint>
|
||||||
|
{
|
||||||
|
[0] = 0u, // default type → atlas layer 0
|
||||||
|
[7] = 4u, // type 7 → atlas layer 4
|
||||||
|
};
|
||||||
|
|
||||||
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, map);
|
||||||
|
|
||||||
|
// Vertex buffer internal order is y*9+x, so vertex at world (x=2, y=3) is at
|
||||||
|
// index 3*9+2 = 29.
|
||||||
|
Assert.Equal(4u, mesh.Vertices[3 * 9 + 2].TerrainLayer);
|
||||||
|
// An untouched vertex still has type 0, maps to layer 0.
|
||||||
|
Assert.Equal(0u, mesh.Vertices[0].TerrainLayer);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_HeightmapPackedAsXMajor_NotYMajor()
|
public void Build_HeightmapPackedAsXMajor_NotYMajor()
|
||||||
{
|
{
|
||||||
|
|
@ -98,7 +125,7 @@ public class LandblockMeshTests
|
||||||
var block = BuildFlatLandBlock();
|
var block = BuildFlatLandBlock();
|
||||||
block.Height[2 * 9 + 0] = 5; // x=2, y=0 in x-major packing
|
block.Height[2 * 9 + 0] = 5; // x=2, y=0 in x-major packing
|
||||||
|
|
||||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap);
|
||||||
|
|
||||||
// Find vertices by position. Vertex buffer uses y*9+x internally.
|
// Find vertices by position. Vertex buffer uses y*9+x internally.
|
||||||
var vAt_x2_y0 = mesh.Vertices[0 * 9 + 2]; // world (48, 0)
|
var vAt_x2_y0 = mesh.Vertices[0 * 9 + 2]; // world (48, 0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue