using VoxelEngine.World.Blocks; namespace VoxelEngine.World.Chunk; public sealed class ChunkMesher : IChunkMesher { private const int TileSizePixels = 32; // each tile is 32x32 pixels private readonly float _tileU; // normalized tile width private readonly float _tileV; // normalized tile height public ChunkMesher(int atlasWidthPixels, int atlasHeightPixels) { this._tileU = (float)TileSizePixels / atlasWidthPixels; this._tileV = (float)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. private readonly struct Face { public readonly BlockFace Kind; public readonly (int x, int y, int z) Normal; // also the neighbor offset public readonly (float x, float y, float z) Base; public readonly (float x, float y, float z) U; public readonly (float x, float y, float z) V; public readonly float Shade; public Face(BlockFace kind, (int, int, int) normal, (float, float, float) @base, (float, float, float) u, (float, float, float) v, float shade) { this.Kind = kind; this.Normal = normal; this.Base = @base; this.U = u; this.V = v; this.Shade = shade; } } private static readonly Face[] Faces = { new(BlockFace.Top, (0, 1, 0), (0, 1, 1), (1, 0, 0), (0, 0, -1), 1.0f), new(BlockFace.Bottom, (0, -1, 0), (0, 0, 0), (1, 0, 0), (0, 0, 1), 0.6f), new(BlockFace.North, (0, 0, -1), (1, 0, 0), (-1, 0, 0), (0, 1, 0), 0.8f), new(BlockFace.South, (0, 0, 1), (0, 0, 1), (1, 0, 0), (0, 1, 0), 0.8f), new(BlockFace.East, (1, 0, 0), (1, 0, 1), (0, 0, -1), (0, 1, 0), 0.8f), new(BlockFace.West, (-1, 0, 0), (0, 0, 0), (0, 0, 1), (0, 1, 0), 0.8f), }; // Quad corners as (uCoord, vCoord) and the triangle order (0,1,2,0,2,3). private static readonly (float u, float v)[] Corners = { (0, 0), (1, 0), (1, 1), (0, 1) }; private static readonly int[] Triangle = { 0, 1, 2, 0, 2, 3 }; public float[] Build(Chunk chunk, BlockRegistry registry, Func? neighborOpaqueOutside = null) { List verts = new(); for (int x = 0; x < Chunk.Size; x++) for (int y = 0; y < Chunk.Size; y++) for (int z = 0; z < Chunk.Size; z++) { IBlock block = registry.Get(chunk[x, y, z]); if (!block.IsOpaque) continue; foreach (Face face in Faces) { int nx = x + face.Normal.x; int ny = y + face.Normal.y; int nz = z + face.Normal.z; // Out-of-chunk neighbors are treated as non-opaque (single chunk). bool neighborOpaque = Chunk.InBounds(nx, ny, nz) ? registry.Get(chunk[nx, ny, nz]).IsOpaque : (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false); if (neighborOpaque) continue; this.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind)); } } return verts.ToArray(); } private void AddFace(List verts, int x, int y, int z, Face face, TileCoord tile) { 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) * this._tileV; foreach (int corner in Triangle) { (float cu, float cv) = Corners[corner]; float px = x + face.Base.x + face.U.x * cu + face.V.x * 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 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); verts.Add(face.Shade); } } }