feat(core): add LandblockMesh flat-terrain generator
Pure CPU mesh generator: takes a DatReaderWriter LandBlock DBObj and produces 81 vertices + 128 triangles covering 192x192 world units. Vertices are a readonly record struct (position, normal, texcoord) so the upcoming GPU upload in Task 8 can sizeof() them directly. Height byte -> world z uses a simple 2x scale; the real AC height lookup table is a Phase 2+ concern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f6a57cbc6c
commit
baf0db303d
3 changed files with 131 additions and 0 deletions
50
src/AcDream.Core/Terrain/LandblockMesh.cs
Normal file
50
src/AcDream.Core/Terrain/LandblockMesh.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
using System.Numerics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.Core.Terrain;
|
||||
|
||||
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 float HeightScale = 2.0f; // byte height -> world z
|
||||
|
||||
public static LandblockMeshData Build(LandBlock block)
|
||||
{
|
||||
var vertices = new Vertex[VerticesPerSide * VerticesPerSide];
|
||||
for (int y = 0; y < VerticesPerSide; y++)
|
||||
{
|
||||
for (int x = 0; x < VerticesPerSide; x++)
|
||||
{
|
||||
int i = y * VerticesPerSide + x;
|
||||
float height = block.Height[i] * HeightScale;
|
||||
vertices[i] = new Vertex(
|
||||
Position: new Vector3(x * CellSize, y * CellSize, height),
|
||||
Normal: Vector3.UnitZ,
|
||||
TexCoord: new Vector2(x / (float)CellsPerSide, y / (float)CellsPerSide));
|
||||
}
|
||||
}
|
||||
|
||||
var indices = new uint[CellsPerSide * CellsPerSide * 6];
|
||||
int idx = 0;
|
||||
for (int y = 0; y < CellsPerSide; y++)
|
||||
{
|
||||
for (int x = 0; x < CellsPerSide; x++)
|
||||
{
|
||||
uint a = (uint)(y * VerticesPerSide + x);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return new LandblockMeshData(vertices, indices);
|
||||
}
|
||||
}
|
||||
5
src/AcDream.Core/Terrain/Vertex.cs
Normal file
5
src/AcDream.Core/Terrain/Vertex.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Terrain;
|
||||
|
||||
public readonly record struct Vertex(Vector3 Position, Vector3 Normal, Vector2 TexCoord);
|
||||
76
tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs
Normal file
76
tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Terrain;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Terrain;
|
||||
|
||||
public class LandblockMeshTests
|
||||
{
|
||||
private static LandBlock BuildFlatLandBlock(byte heightIndex = 0)
|
||||
{
|
||||
var block = new LandBlock
|
||||
{
|
||||
HasObjects = false,
|
||||
Terrain = new TerrainInfo[81],
|
||||
Height = new byte[81],
|
||||
};
|
||||
for (int i = 0; i < 81; i++)
|
||||
{
|
||||
block.Terrain[i] = (ushort)0;
|
||||
block.Height[i] = heightIndex;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FlatBlock_Produces81VerticesAnd128Triangles()
|
||||
{
|
||||
var block = BuildFlatLandBlock();
|
||||
|
||||
var mesh = LandblockMesh.Build(block);
|
||||
|
||||
Assert.Equal(81, mesh.Vertices.Length);
|
||||
Assert.Equal(128 * 3, mesh.Indices.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Vertices_Cover192x192WorldUnits()
|
||||
{
|
||||
var block = BuildFlatLandBlock();
|
||||
|
||||
var mesh = LandblockMesh.Build(block);
|
||||
|
||||
var minX = mesh.Vertices.Min(v => v.Position.X);
|
||||
var maxX = mesh.Vertices.Max(v => v.Position.X);
|
||||
var minY = mesh.Vertices.Min(v => v.Position.Y);
|
||||
var maxY = mesh.Vertices.Max(v => v.Position.Y);
|
||||
|
||||
Assert.Equal(0.0f, minX);
|
||||
Assert.Equal(192.0f, maxX);
|
||||
Assert.Equal(0.0f, minY);
|
||||
Assert.Equal(192.0f, maxY);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_FlatBlock_AllVerticesSameZ()
|
||||
{
|
||||
var block = BuildFlatLandBlock(heightIndex: 10);
|
||||
|
||||
var mesh = LandblockMesh.Build(block);
|
||||
|
||||
var zs = mesh.Vertices.Select(v => v.Position.Z).Distinct().ToArray();
|
||||
Assert.Single(zs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_HeightValues_ScaleByTwo()
|
||||
{
|
||||
var block = BuildFlatLandBlock(heightIndex: 5);
|
||||
|
||||
var mesh = LandblockMesh.Build(block);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue