Files
VoxelEngine/World/Chunk/ChunkMesher.cs
MDietermann 49d35cab5b feat: add multi-chunk world with cross-chunk face culling
Add a World that stores chunks in a Dictionary keyed by chunk
coordinate, with floor-division world<->chunk coordinate mapping
(correct for negative coords). The mesher takes an optional neighbor
sampler so faces at chunk borders are culled against adjacent chunks
instead of always meshing the shell. Render one ChunkRenderer per
chunk, placed via the model matrix. Reorganize the World layer into
Blocks/ and Chunk/ sub-namespaces.
2026-05-28 15:32:45 +02:00

100 lines
3.7 KiB
C#

using VoxelEngine.World.Blocks;
namespace VoxelEngine.World.Chunk;
public sealed class ChunkMesher : IChunkMesher
{
private const int AtlasTiles = 4; // horizontal strip of 4 tiles
// 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.GetTextureIndex(face.Kind));
}
}
return verts.ToArray();
}
private static void AddFace(List<float> verts, int x, int y, int z, Face face, int texIndex)
{
float tileWidth = 1f / ChunkMesher.AtlasTiles;
float uBase = texIndex * tileWidth;
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 * tileWidth;
verts.Add(px); verts.Add(py); verts.Add(pz);
verts.Add(u); verts.Add(cv);
verts.Add(face.Shade);
}
}
}