diff --git a/Core/Game.cs b/Core/Game.cs index 85c8706..7130f27 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -44,7 +44,7 @@ public sealed class Game : GameWindow this._world.SetChunk(new Vector3i(x, 0, z), this.BuildChunk()); } - ChunkMesher mesher = new(); + ChunkMesher mesher = new(this._texture.Width, this._texture.Height); foreach ((Vector3i pos, Chunk chunk) in this._world) { Vector3i origin = pos * Chunk.Size; diff --git a/Rendering/Texture.cs b/Rendering/Texture.cs index 3a30ee8..cd69bb1 100644 --- a/Rendering/Texture.cs +++ b/Rendering/Texture.cs @@ -7,6 +7,9 @@ public sealed class Texture : IDisposable { private readonly int _handle; + public int Width { get; } + public int Height { get; } + public Texture(string path) { this._handle = GL.GenTexture(); @@ -15,6 +18,8 @@ public sealed class Texture : IDisposable StbImage.stbi_set_flip_vertically_on_load(1); using FileStream stream = File.OpenRead(path); ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + this.Width = image.Width; + this.Height = image.Height; GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data); diff --git a/VoxelEngine.Tests/ChunkMesherTests.cs b/VoxelEngine.Tests/ChunkMesherTests.cs index 88b3619..d11444b 100644 --- a/VoxelEngine.Tests/ChunkMesherTests.cs +++ b/VoxelEngine.Tests/ChunkMesherTests.cs @@ -16,7 +16,7 @@ public class ChunkMesherTests [Fact] public void EmptyChunk_ProducesNoFaces() { - var mesh = new ChunkMesher().Build(new Chunk(), new BlockRegistry()); + var mesh = new ChunkMesher(128, 128).Build(new Chunk(), new BlockRegistry()); Assert.Empty(mesh); } @@ -26,7 +26,7 @@ public class ChunkMesherTests var chunk = new Chunk(); chunk[8, 8, 8] = 1; // stone - var mesh = new ChunkMesher().Build(chunk, new BlockRegistry()); + var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry()); Assert.Equal(6, FaceCount(mesh)); } @@ -40,7 +40,7 @@ public class ChunkMesherTests for (int z = 0; z < Chunk.Size; z++) chunk[x, y, z] = 1; // stone - var mesh = new ChunkMesher().Build(chunk, new BlockRegistry()); + var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry()); // 6 sides * 16 * 16 visible faces Assert.Equal(6 * Chunk.Size * Chunk.Size, FaceCount(mesh)); @@ -52,10 +52,32 @@ public class ChunkMesherTests var chunk = new Chunk(); chunk[0, 0, 0] = 1; // corner block: all 6 neighbors are air/out-of-bounds -> 6 faces - var mesh = new ChunkMesher().Build(chunk, new BlockRegistry()); + var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry()); // Pins the layout: 6 faces * 6 vertices/face * 6 floats/vertex. Assert.Equal(6 * VerticesPerFace * FloatsPerVertex, mesh.Length); Assert.Equal(0, mesh.Length % FloatsPerVertex); } + + [Fact] + public void Mesher_UsesAtlasDimensions_ForTileSize() + { + var chunk = new Chunk(); + chunk[0, 0, 0] = 1; // stone, tile (1,0) + + // 128px atlas, 32px tiles => 4x4 grid, normalized tile size 0.25. + var mesh = new ChunkMesher(128, 128).Build(chunk, new BlockRegistry()); + + // Gather all U coords (offset 3) and V coords (offset 4); each must be a + // multiple of 0.25 within [0,1]. + for (int i = 0; i < mesh.Length; i += 6) + { + float u = mesh[i + 3]; + float v = mesh[i + 4]; + Assert.InRange(u, 0f, 1f); + Assert.InRange(v, 0f, 1f); + Assert.True(System.Math.Abs(u * 4f - System.MathF.Round(u * 4f)) < 1e-4f, $"u={u} not on 0.25 grid"); + Assert.True(System.Math.Abs(v * 4f - System.MathF.Round(v * 4f)) < 1e-4f, $"v={v} not on 0.25 grid"); + } + } } diff --git a/World/Chunk/ChunkMesher.cs b/World/Chunk/ChunkMesher.cs index 2167528..3ed2e2e 100644 --- a/World/Chunk/ChunkMesher.cs +++ b/World/Chunk/ChunkMesher.cs @@ -5,9 +5,15 @@ namespace VoxelEngine.World.Chunk; public sealed class ChunkMesher : IChunkMesher { private const int TileSizePixels = 32; // each tile is 32x32 pixels - private const int AtlasSizePixels = 64; // atlas is 64x64 pixels (2x2 grid) - private const float TileSize = (float)TileSizePixels / AtlasSizePixels; // normalized tile size + private readonly float _tileU; // normalized tile width + private readonly float _tileV; // normalized tile height + + public ChunkMesher(int atlasWidthPixels, int atlasHeightPixels) + { + this._tileU = (float)ChunkMesher.TileSizePixels / atlasWidthPixels; + this._tileV = (float)ChunkMesher.TileSizePixels / atlasHeightPixels; + } // Per-face geometry: base corner + two in-plane edge vectors (u, v) with // cross(u, v) == outward normal, guaranteeing CCW winding from outside. @@ -72,18 +78,18 @@ public sealed class ChunkMesher : IChunkMesher : (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false); if (neighborOpaque) continue; - ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind)); + this.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind)); } } return verts.ToArray(); } - private static void AddFace(List verts, int x, int y, int z, Face face, TileCoord tile) + private void AddFace(List verts, int x, int y, int z, Face face, TileCoord tile) { - float uBase = tile.X * ChunkMesher.TileSize; + float uBase = tile.X * this._tileU; // Image is flipped on load, so offset from the top to keep tile (0,0) = top-left. - float vBase = 1f - (tile.Y + 1) * ChunkMesher.TileSize; + float vBase = 1f - (tile.Y + 1) * this._tileV; foreach (int corner in ChunkMesher.Triangle) { @@ -93,8 +99,8 @@ public sealed class ChunkMesher : IChunkMesher float py = y + face.Base.y + face.U.y * cu + face.V.y * cv; float pz = z + face.Base.z + face.U.z * cu + face.V.z * cv; - float u = uBase + cu * ChunkMesher.TileSize; - float v = vBase + cv * ChunkMesher.TileSize; + float u = uBase + cu * this._tileU; + float v = vBase + cv * this._tileV; verts.Add(px); verts.Add(py); verts.Add(pz); verts.Add(u); verts.Add(v);