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);
}
}