diff --git a/Assets/textures/VoxelShadeAtlas.png b/Assets/textures/VoxelShadeAtlas.png new file mode 100644 index 0000000..bb6dcce Binary files /dev/null and b/Assets/textures/VoxelShadeAtlas.png differ diff --git a/Core/Game.cs b/Core/Game.cs index 64047f5..85c8706 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -34,7 +34,7 @@ public sealed class Game : GameWindow GL.Enable(EnableCap.CullFace); this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag"); - this._texture = new Texture("Assets/textures/atlas.png"); + this._texture = new Texture("Assets/textures/VoxelShadeAtlas.png"); this._world = new World.World(this._registry); diff --git a/VoxelEngine.Tests/BlockRegistryTests.cs b/VoxelEngine.Tests/BlockRegistryTests.cs index e614d5b..612993a 100644 --- a/VoxelEngine.Tests/BlockRegistryTests.cs +++ b/VoxelEngine.Tests/BlockRegistryTests.cs @@ -34,9 +34,9 @@ public class BlockRegistryTests var registry = new BlockRegistry(); IBlock grass = registry.Get(3); - Assert.Equal(0, grass.GetTextureIndex(BlockFace.Top)); - Assert.Equal(1, grass.GetTextureIndex(BlockFace.Bottom)); - Assert.Equal(3, grass.GetTextureIndex(BlockFace.North)); + Assert.Equal(new TileCoord(0, 1), grass.GetTextureTile(BlockFace.Top)); + Assert.Equal(new TileCoord(0, 0), grass.GetTextureTile(BlockFace.Bottom)); + Assert.Equal(new TileCoord(1, 1), grass.GetTextureTile(BlockFace.North)); } [Fact] diff --git a/VoxelEngine.csproj b/VoxelEngine.csproj index 5db1f8f..6deb50b 100644 --- a/VoxelEngine.csproj +++ b/VoxelEngine.csproj @@ -10,6 +10,7 @@ + diff --git a/World/Blocks/AirBlock.cs b/World/Blocks/AirBlock.cs index 0ee446b..9697dda 100644 --- a/World/Blocks/AirBlock.cs +++ b/World/Blocks/AirBlock.cs @@ -6,5 +6,5 @@ public sealed class AirBlock : IBlock public string Name => "Air"; public bool IsOpaque => false; public bool IsSolid => false; - public int GetTextureIndex(BlockFace face) => 0; + public TileCoord GetTextureTile(BlockFace face) => new(0, 0); } diff --git a/World/Blocks/DirtBlock.cs b/World/Blocks/DirtBlock.cs index ebb3e83..b2d4a21 100644 --- a/World/Blocks/DirtBlock.cs +++ b/World/Blocks/DirtBlock.cs @@ -6,5 +6,5 @@ public sealed class DirtBlock : IBlock public string Name => "Dirt"; public bool IsOpaque => true; public bool IsSolid => true; - public int GetTextureIndex(BlockFace face) => 1; + public TileCoord GetTextureTile(BlockFace face) => new(0, 0); } diff --git a/World/Blocks/GrassBlock.cs b/World/Blocks/GrassBlock.cs index f64c421..fe4d8c8 100644 --- a/World/Blocks/GrassBlock.cs +++ b/World/Blocks/GrassBlock.cs @@ -7,10 +7,10 @@ public sealed class GrassBlock : IBlock public bool IsOpaque => true; public bool IsSolid => true; - public int GetTextureIndex(BlockFace face) => face switch + public TileCoord GetTextureTile(BlockFace face) => face switch { - BlockFace.Top => 0, // grass top - BlockFace.Bottom => 1, // dirt - _ => 3 // grass side + BlockFace.Top => new(0, 1), // grass top + BlockFace.Bottom => new(0, 0), // dirt + _ => new(1, 1) // grass side }; } diff --git a/World/Blocks/IBlock.cs b/World/Blocks/IBlock.cs index 6f5de68..6e0c77e 100644 --- a/World/Blocks/IBlock.cs +++ b/World/Blocks/IBlock.cs @@ -11,8 +11,8 @@ public interface IBlock // Reserved for future collision (not used in the baseline). bool IsSolid { get; } - // Atlas tile index for the given face. - int GetTextureIndex(BlockFace face); + // Atlas tile coordinate for the given face. + TileCoord GetTextureTile(BlockFace face); // Interaction hooks — capability present now, system built post-baseline. void OnInteract() { } diff --git a/World/Blocks/StoneBlock.cs b/World/Blocks/StoneBlock.cs index 97aa47a..6d69561 100644 --- a/World/Blocks/StoneBlock.cs +++ b/World/Blocks/StoneBlock.cs @@ -6,5 +6,5 @@ public sealed class StoneBlock : IBlock public string Name => "Stone"; public bool IsOpaque => true; public bool IsSolid => true; - public int GetTextureIndex(BlockFace face) => 2; + public TileCoord GetTextureTile(BlockFace face) => new(1, 0); } diff --git a/World/Chunk/ChunkMesher.cs b/World/Chunk/ChunkMesher.cs index ac333c7..2167528 100644 --- a/World/Chunk/ChunkMesher.cs +++ b/World/Chunk/ChunkMesher.cs @@ -4,7 +4,10 @@ namespace VoxelEngine.World.Chunk; public sealed class ChunkMesher : IChunkMesher { - private const int AtlasTiles = 4; // horizontal strip of 4 tiles + 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 // Per-face geometry: base corner + two in-plane edge vectors (u, v) with // cross(u, v) == outward normal, guaranteeing CCW winding from outside. @@ -69,17 +72,18 @@ public sealed class ChunkMesher : IChunkMesher : (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false); if (neighborOpaque) continue; - ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureIndex(face.Kind)); + ChunkMesher.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, int texIndex) + private static void AddFace(List verts, int x, int y, int z, Face face, TileCoord tile) { - float tileWidth = 1f / ChunkMesher.AtlasTiles; - float uBase = texIndex * tileWidth; + float uBase = tile.X * ChunkMesher.TileSize; + // 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; foreach (int corner in ChunkMesher.Triangle) { @@ -89,10 +93,11 @@ 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 * tileWidth; + float u = uBase + cu * ChunkMesher.TileSize; + float v = vBase + cv * ChunkMesher.TileSize; verts.Add(px); verts.Add(py); verts.Add(pz); - verts.Add(u); verts.Add(cv); + verts.Add(u); verts.Add(v); verts.Add(face.Shade); } }