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.
100 lines
3.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|