From 4e1478666811e6259eb0c5e4feb719fd566d6d3f Mon Sep 17 00:00:00 2001 From: MDietermann Date: Sun, 31 May 2026 20:32:46 +0200 Subject: [PATCH] New Atlas + Texture Pipeline --- Assets/textures/VoxelShadeAtlas.png | Bin 0 -> 4679 bytes Core/Game.cs | 2 +- VoxelEngine.Tests/BlockRegistryTests.cs | 6 +++--- VoxelEngine.csproj | 1 + World/Blocks/AirBlock.cs | 2 +- World/Blocks/DirtBlock.cs | 2 +- World/Blocks/GrassBlock.cs | 8 ++++---- World/Blocks/IBlock.cs | 4 ++-- World/Blocks/StoneBlock.cs | 2 +- World/Chunk/ChunkMesher.cs | 19 ++++++++++++------- 10 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 Assets/textures/VoxelShadeAtlas.png diff --git a/Assets/textures/VoxelShadeAtlas.png b/Assets/textures/VoxelShadeAtlas.png new file mode 100644 index 0000000000000000000000000000000000000000..bb6dcce1c099a0d1060cc73db92eaf5b471ef999 GIT binary patch literal 4679 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 + 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); } }