fix(terrain): use real LandHeightTable from Region dat

Phase 1 simplified per-vertex height as byte * 2.0f, but AC stores
heights as byte indices into a 256-entry non-linear float lookup
(Region.LandDefs.LandHeightTable). Static object placements in
LandBlockInfo use the real table, so terrain rendered with the
simplified scale left buildings floating or buried.

LandblockMesh.Build now takes an explicit float[] heightTable so
the core code stays testable without a DatCollection. GameWindow
loads Region id 0x13000000 once at startup and passes its
LandDefs.LandHeightTable into every landblock mesh build. The
Phase 1 tests use an identity table (i * 2f for i in 0..255) so
their expectations remain unchanged.

Addresses the 'buildings buried and floating' issue the user
observed after the Phase 2a visual checkpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 19:09:27 +02:00
parent 1d1e668a2f
commit 4763b973da
3 changed files with 34 additions and 8 deletions

View file

@ -123,7 +123,17 @@ public sealed class GameWindow : IDisposable
Console.WriteLine($"loaded landblock 0x{landblockId:X8}");
var meshData = LandblockMesh.Build(block);
// Load the non-linear LandHeightTable from the Region dat. AC encodes
// per-vertex heights as byte indices into this 256-entry float table,
// not as a simple * 2.0 ramp — building placements depend on the real
// table, so terrain rendered with the simplified scale would leave
// buildings floating or buried.
var region = _dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
var heightTable = region?.LandDefs.LandHeightTable;
if (heightTable is null || heightTable.Length < 256)
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
var meshData = LandblockMesh.Build(block, heightTable);
_terrain = new TerrainRenderer(_gl, meshData, _shader);
_textureCache = new TextureCache(_gl, _dats);

View file

@ -11,17 +11,25 @@ public static class LandblockMesh
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)
/// <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)
{
ArgumentNullException.ThrowIfNull(heightTable);
if (heightTable.Length < 256)
throw new ArgumentException("heightTable must have 256 entries", nameof(heightTable));
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;
float height = heightTable[block.Height[i]];
vertices[i] = new Vertex(
Position: new Vector3(x * CellSize, y * CellSize, height),
Normal: Vector3.UnitZ,