diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index f9c742d..b40a392 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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()); _terrain = new TerrainRenderer(_gl, meshData, _shader); _textureCache = new TextureCache(_gl, _dats); diff --git a/src/AcDream.App/Rendering/StaticMeshRenderer.cs b/src/AcDream.App/Rendering/StaticMeshRenderer.cs index 621cc31..12b7a8b 100644 --- a/src/AcDream.App/Rendering/StaticMeshRenderer.cs +++ b/src/AcDream.App/Rendering/StaticMeshRenderer.cs @@ -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); diff --git a/src/AcDream.App/Rendering/TerrainRenderer.cs b/src/AcDream.App/Rendering/TerrainRenderer.cs index e61c301..40ba538 100644 --- a/src/AcDream.App/Rendering/TerrainRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainRenderer.cs @@ -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); } diff --git a/src/AcDream.Core/Meshing/GfxObjMesh.cs b/src/AcDream.Core/Meshing/GfxObjMesh.cs index 31e6309..2e5bf00 100644 --- a/src/AcDream.Core/Meshing/GfxObjMesh.cs +++ b/src/AcDream.Core/Meshing/GfxObjMesh.cs @@ -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); diff --git a/src/AcDream.Core/Terrain/LandblockMesh.cs b/src/AcDream.Core/Terrain/LandblockMesh.cs index 9c3c100..934e754 100644 --- a/src/AcDream.Core/Terrain/LandblockMesh.cs +++ b/src/AcDream.Core/Terrain/LandblockMesh.cs @@ -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; - /// - /// Build the CPU mesh for one landblock's heightmap. - /// is the 256-entry non-linear height lookup from Region.LandDefs.LandHeightTable — - /// AC encodes per-vertex heights as indices into this table, not raw world-Z. - /// - public static LandblockMeshData Build(LandBlock block, float[] heightTable) + public static LandblockMeshData Build( + LandBlock block, + float[] heightTable, + IReadOnlyDictionary 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; } diff --git a/src/AcDream.Core/Terrain/Vertex.cs b/src/AcDream.Core/Terrain/Vertex.cs index b590ef2..d948d78 100644 --- a/src/AcDream.Core/Terrain/Vertex.cs +++ b/src/AcDream.Core/Terrain/Vertex.cs @@ -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); diff --git a/tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs b/tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs index b2853f6..d106fa2 100644 --- a/tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs +++ b/tests/AcDream.Core.Tests/Terrain/LandblockMeshTests.cs @@ -15,6 +15,9 @@ public class LandblockMeshTests private static readonly float[] IdentityHeightTable = Enumerable.Range(0, 256).Select(i => i * 2f).ToArray(); + private static readonly IReadOnlyDictionary EmptyTerrainMap = + new Dictionary(); + private static LandBlock BuildFlatLandBlock(byte heightIndex = 0) { var block = new LandBlock @@ -36,7 +39,7 @@ public class LandblockMeshTests { var block = BuildFlatLandBlock(); - var mesh = LandblockMesh.Build(block, IdentityHeightTable); + var mesh = LandblockMesh.Build(block, IdentityHeightTable, EmptyTerrainMap); Assert.Equal(81, mesh.Vertices.Length); Assert.Equal(128 * 3, mesh.Indices.Length); @@ -47,7 +50,7 @@ public class LandblockMeshTests { 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 maxX = mesh.Vertices.Max(v => v.Position.X); @@ -65,7 +68,7 @@ public class LandblockMeshTests { 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(); Assert.Single(zs); @@ -76,12 +79,36 @@ public class LandblockMeshTests { 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. 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 + { + [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] public void Build_HeightmapPackedAsXMajor_NotYMajor() { @@ -98,7 +125,7 @@ public class LandblockMeshTests var block = BuildFlatLandBlock(); 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. var vAt_x2_y0 = mesh.Vertices[0 * 9 + 2]; // world (48, 0)