Files
VoxelEngine/World/Chunk/ChunkMesher.cs
2026-05-31 20:32:46 +02:00

105 lines
4.0 KiB
C#

using VoxelEngine.World.Blocks;
namespace VoxelEngine.World.Chunk;
public sealed class ChunkMesher : IChunkMesher
{
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.
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<int, int, int, bool>? neighborOpaqueOutside = null)
{
List<float> 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 ChunkMesher.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;
ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureTile(face.Kind));
}
}
return verts.ToArray();
}
private static void AddFace(List<float> verts, int x, int y, int z, Face face, TileCoord tile)
{
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)
{
(float cu, float cv) = ChunkMesher.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 * ChunkMesher.TileSize;
float v = vBase + cv * ChunkMesher.TileSize;
verts.Add(px); verts.Add(py); verts.Add(pz);
verts.Add(u); verts.Add(v);
verts.Add(face.Shade);
}
}
}