feat: add face-culled chunk mesher behind IChunkMesher
This commit is contained in:
55
VoxelEngine.Tests/ChunkMesherTests.cs
Normal file
55
VoxelEngine.Tests/ChunkMesherTests.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using VoxelEngine.World;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace VoxelEngine.Tests;
|
||||||
|
|
||||||
|
public class ChunkMesherTests
|
||||||
|
{
|
||||||
|
private const int FloatsPerFace = 36; // 6 verts * 6 floats
|
||||||
|
|
||||||
|
private static int FaceCount(float[] mesh) => mesh.Length / FloatsPerFace;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyChunk_ProducesNoFaces()
|
||||||
|
{
|
||||||
|
var mesh = new ChunkMesher().Build(new Chunk(), new BlockRegistry());
|
||||||
|
Assert.Empty(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleBlockSurroundedByAir_Produces6Faces()
|
||||||
|
{
|
||||||
|
var chunk = new Chunk();
|
||||||
|
chunk[8, 8, 8] = 1; // stone
|
||||||
|
|
||||||
|
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||||
|
|
||||||
|
Assert.Equal(6, FaceCount(mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FullySolidChunk_ProducesOnlyOuterShell()
|
||||||
|
{
|
||||||
|
var chunk = new Chunk();
|
||||||
|
for (int x = 0; x < Chunk.Size; x++)
|
||||||
|
for (int y = 0; y < Chunk.Size; y++)
|
||||||
|
for (int z = 0; z < Chunk.Size; z++)
|
||||||
|
chunk[x, y, z] = 1; // stone
|
||||||
|
|
||||||
|
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||||
|
|
||||||
|
// 6 sides * 16 * 16 visible faces
|
||||||
|
Assert.Equal(6 * Chunk.Size * Chunk.Size, FaceCount(mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EveryVertexHasSixFloats()
|
||||||
|
{
|
||||||
|
var chunk = new Chunk();
|
||||||
|
chunk[0, 0, 0] = 1;
|
||||||
|
|
||||||
|
var mesh = new ChunkMesher().Build(chunk, new BlockRegistry());
|
||||||
|
|
||||||
|
Assert.Equal(0, mesh.Length % 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
World/ChunkMesher.cs
Normal file
93
World/ChunkMesher.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace VoxelEngine.World;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Kind = kind; Normal = normal; Base = @base; U = u; V = v; 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)
|
||||||
|
{
|
||||||
|
var verts = new List<float>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (neighborOpaque) continue;
|
||||||
|
|
||||||
|
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 / AtlasTiles;
|
||||||
|
float uBase = texIndex * tileWidth;
|
||||||
|
|
||||||
|
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 * tileWidth;
|
||||||
|
float v = cv;
|
||||||
|
|
||||||
|
verts.Add(px); verts.Add(py); verts.Add(pz);
|
||||||
|
verts.Add(u); verts.Add(v);
|
||||||
|
verts.Add(face.Shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
World/IChunkMesher.cs
Normal file
7
World/IChunkMesher.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace VoxelEngine.World;
|
||||||
|
|
||||||
|
public interface IChunkMesher
|
||||||
|
{
|
||||||
|
// Returns interleaved vertices: [pos.x, pos.y, pos.z, u, v, shade] per vertex.
|
||||||
|
float[] Build(Chunk chunk, BlockRegistry registry);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user