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.
This commit is contained in:
19
World/Chunk/Chunk.cs
Normal file
19
World/Chunk/Chunk.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace VoxelEngine.World.Chunk;
|
||||
|
||||
public sealed class Chunk
|
||||
{
|
||||
public const int Size = 16;
|
||||
|
||||
private readonly ushort[] _ids = new ushort[Chunk.Size * Chunk.Size * Chunk.Size];
|
||||
|
||||
public ushort this[int x, int y, int z]
|
||||
{
|
||||
get => this._ids[Chunk.Index(x, y, z)];
|
||||
set => this._ids[Chunk.Index(x, y, z)] = value;
|
||||
}
|
||||
|
||||
public static bool InBounds(int x, int y, int z) =>
|
||||
x is >= 0 and < Chunk.Size && y is >= 0 and < Chunk.Size && z is >= 0 and < Chunk.Size;
|
||||
|
||||
private static int Index(int x, int y, int z) => (x * Chunk.Size + y) * Chunk.Size + z;
|
||||
}
|
||||
99
World/Chunk/ChunkMesher.cs
Normal file
99
World/Chunk/ChunkMesher.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
World/Chunk/IChunkMesher.cs
Normal file
9
World/Chunk/IChunkMesher.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using VoxelEngine.World.Blocks;
|
||||
|
||||
namespace VoxelEngine.World.Chunk;
|
||||
|
||||
public interface IChunkMesher
|
||||
{
|
||||
// Returns interleaved vertices: [pos.x, pos.y, pos.z, u, v, shade] per vertex.
|
||||
float[] Build(Chunk chunk, BlockRegistry registry, Func<int, int, int, bool>? neighborOpaqueOutside = null);
|
||||
}
|
||||
Reference in New Issue
Block a user