feat: derive atlas tile size from loaded texture dimensions

This commit is contained in:
MDietermann
2026-05-31 21:43:53 +02:00
parent 50f2bc4221
commit c81cd3c561
4 changed files with 46 additions and 13 deletions

View File

@@ -44,7 +44,7 @@ public sealed class Game : GameWindow
this._world.SetChunk(new Vector3i(x, 0, z), this.BuildChunk()); 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) foreach ((Vector3i pos, Chunk chunk) in this._world)
{ {
Vector3i origin = pos * Chunk.Size; Vector3i origin = pos * Chunk.Size;

View File

@@ -7,6 +7,9 @@ public sealed class Texture : IDisposable
{ {
private readonly int _handle; private readonly int _handle;
public int Width { get; }
public int Height { get; }
public Texture(string path) public Texture(string path)
{ {
this._handle = GL.GenTexture(); this._handle = GL.GenTexture();
@@ -15,6 +18,8 @@ public sealed class Texture : IDisposable
StbImage.stbi_set_flip_vertically_on_load(1); StbImage.stbi_set_flip_vertically_on_load(1);
using FileStream stream = File.OpenRead(path); using FileStream stream = File.OpenRead(path);
ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
this.Width = image.Width;
this.Height = image.Height;
GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data); image.Width, image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data);

View File

@@ -16,7 +16,7 @@ public class ChunkMesherTests
[Fact] [Fact]
public void EmptyChunk_ProducesNoFaces() 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); Assert.Empty(mesh);
} }
@@ -26,7 +26,7 @@ public class ChunkMesherTests
var chunk = new Chunk(); var chunk = new Chunk();
chunk[8, 8, 8] = 1; // stone 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)); Assert.Equal(6, FaceCount(mesh));
} }
@@ -40,7 +40,7 @@ public class ChunkMesherTests
for (int z = 0; z < Chunk.Size; z++) for (int z = 0; z < Chunk.Size; z++)
chunk[x, y, z] = 1; // stone 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 // 6 sides * 16 * 16 visible faces
Assert.Equal(6 * Chunk.Size * Chunk.Size, FaceCount(mesh)); Assert.Equal(6 * Chunk.Size * Chunk.Size, FaceCount(mesh));
@@ -52,10 +52,32 @@ public class ChunkMesherTests
var chunk = new Chunk(); var chunk = new Chunk();
chunk[0, 0, 0] = 1; // corner block: all 6 neighbors are air/out-of-bounds -> 6 faces 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. // Pins the layout: 6 faces * 6 vertices/face * 6 floats/vertex.
Assert.Equal(6 * VerticesPerFace * FloatsPerVertex, mesh.Length); Assert.Equal(6 * VerticesPerFace * FloatsPerVertex, mesh.Length);
Assert.Equal(0, mesh.Length % FloatsPerVertex); 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");
}
}
} }

View File

@@ -5,9 +5,15 @@ namespace VoxelEngine.World.Chunk;
public sealed class ChunkMesher : IChunkMesher public sealed class ChunkMesher : IChunkMesher
{ {
private const int TileSizePixels = 32; // each tile is 32x32 pixels 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 // Per-face geometry: base corner + two in-plane edge vectors (u, v) with
// cross(u, v) == outward normal, guaranteeing CCW winding from outside. // 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); : (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false);
if (neighborOpaque) continue; 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(); return verts.ToArray();
} }
private static void AddFace(List<float> verts, int x, int y, int z, Face face, TileCoord tile) private void AddFace(List<float> 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. // 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) 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 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 pz = z + face.Base.z + face.U.z * cu + face.V.z * cv;
float u = uBase + cu * ChunkMesher.TileSize; float u = uBase + cu * this._tileU;
float v = vBase + cv * ChunkMesher.TileSize; float v = vBase + cv * this._tileV;
verts.Add(px); verts.Add(py); verts.Add(pz); verts.Add(px); verts.Add(py); verts.Add(pz);
verts.Add(u); verts.Add(v); verts.Add(u); verts.Add(v);