Major refactor and organization, optimizations to chunk, world and renderer

This commit is contained in:
max
2026-03-24 22:31:40 +01:00
parent dbc546fd0e
commit eb6294c09e
25 changed files with 410 additions and 441 deletions

120
World/Blocks.cs Normal file
View File

@@ -0,0 +1,120 @@
using System.Runtime.CompilerServices;
namespace Voxel.Core
{
public enum Blocks : byte
{
Air,
Stone,
Dirt,
OakPlanks,
Grass,
Bedrock,
Sand,
TNT
}
public enum Orientation : byte
{
West = 0, // + X
East = 1, // - X
Top = 2, // + Y
Bottom = 3,// - Y
North = 4, // + Z
South = 5, // - Z
}
public class BlockDefinition
{
public Blocks BlockType;
public Textures[] FaceTextures;
public BlockDefinition(Blocks type, Textures singleTexture)
{
BlockType = type;
FaceTextures = new Textures[6];
for (int i = 0; i < 6; i++) FaceTextures[i] = singleTexture;
}
public BlockDefinition(
Blocks type,
Textures west,
Textures east,
Textures top,
Textures bottom,
Textures north,
Textures south
)
{
BlockType = type;
FaceTextures = new Textures[]
{
west, east, top, bottom, north, south
};
}
}
public static class BlockDefinitions
{
private static readonly BlockDefinition[] _definitions = new BlockDefinition[256];
public static readonly Blocks[] CreativeInventory = new[]
{
Blocks.Stone,
Blocks.Dirt,
Blocks.OakPlanks,
Blocks.Bedrock,
Blocks.Sand,
Blocks.TNT
};
static BlockDefinitions()
{
// simple blocks
Define(Blocks.Stone, Textures.Stone);
Define(Blocks.Dirt, Textures.Dirt);
Define(Blocks.OakPlanks, Textures.OakPlanks);
Define(Blocks.Bedrock, Textures.Bedrock);
Define(Blocks.Sand, Textures.Sand);
// multi-texture blocks
// west (+X)
// east (-X)
// top
// bottom
// north (+Z)
// south (-Z)
Define(Blocks.Grass,
Textures.GrassSide,
Textures.GrassSide,
Textures.GrassTop,
Textures.Dirt,
Textures.GrassSide,
Textures.GrassSide
);
Define(Blocks.TNT,
Textures.TntSide,
Textures.TntSide,
Textures.TntTop,
Textures.TntBottom,
Textures.TntSide,
Textures.TntSide
);
}
/// highly optimized getter for the hot-path (Meshing/Raycasting)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static BlockDefinition Get(Blocks type)
{
return _definitions[(byte)type];
}
// internal helper to map the array
private static void Define(Blocks type, Textures tex)
=> _definitions[(byte)type] = new BlockDefinition(type, tex);
private static void Define(Blocks type, Textures w, Textures e, Textures t, Textures b, Textures n, Textures s)
=> _definitions[(byte)type] = new BlockDefinition(type, w, e, t, b, n, s);
}
}

176
World/Chunk.cs Normal file
View File

@@ -0,0 +1,176 @@
using System.Runtime.CompilerServices;
using Voxel.Graphics;
namespace Voxel.Core
{
public class Chunk
{
public const int Size = 16;
public const int Height = 256;
public const int TotalBlocks = Size * Size * Height;
public readonly int X;
public readonly int Y;
private ChunkMesh _chunkMesh;
public bool IsDirty;
private Blocks[] _blocks;
private readonly Chunk[] _neighbors = new Chunk[6];
private static readonly List<FaceData> _faceBuffer = new List<FaceData>(8192);
private static readonly (int dx, int dy, int dz)[] Offsets = new (int, int, int)[6]
{
( 1, 0, 0), // West (+X)
(-1, 0, 0), // East (-X)
( 0, 1, 0), // Top (+Y)
( 0, -1, 0), // Bottom(-Y)
( 0, 0, 1), // North (+Z)
( 0, 0, -1) // South (-Z)
};
public Chunk(int x, int y)
{
X = x;
Y = y;
_blocks = new Blocks[TotalBlocks];
_chunkMesh = new ChunkMesh(X, Y);
Initialize();
}
public void SetNeighbor(Orientation orientation, Chunk neighbor)
=> _neighbors[(int)orientation] = neighbor;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Chunk GetNeighbor(Orientation orientation)
{
return _neighbors[(int)orientation];
}
public void SetBlock(int x, int y, int z, Blocks block, bool updateMesh = true)
{
int i = GetBlockIndex(x, y, z);
if (i == -1 || _blocks[i] == block) return;
_blocks[i] = block;
if (updateMesh)
{
IsDirty = true;
MarkNeighborsDirtyIfEdge(x, y, z);
}
}
private void MarkNeighborsDirtyIfEdge(int x, int y, int z)
{
if (x == Size - 1) _neighbors[(int)Orientation.West]?.MarkDirty();
if (x == 0) _neighbors[(int)Orientation.East]?.MarkDirty();
if (z == Size - 1) _neighbors[(int)Orientation.North]?.MarkDirty();
if (z == 0) _neighbors[(int)Orientation.South]?.MarkDirty();
}
public void MarkDirty() => IsDirty = true;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Blocks GetBlock(int x, int y, int z)
{
int i = GetBlockIndex(x, y, z);
return i == -1 ? Blocks.Air : _blocks[i];
}
private void Initialize()
{
for (int x = 0; x < Size; x++)
{
for (int z = 0; z < Size; z++)
{
int worldX = x + (X * Size);
int worldZ = z + (Y * Size);
int height = Worldgen.GetHeight(worldX, worldZ);
for (int y = 0; y < Height; y++)
{
_blocks[GetBlockIndex(x, y, z)] = Worldgen.GetBlock(y, height);
}
}
}
IsDirty = true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetBlockIndex(int x, int y, int z)
{
if ((uint)x >= Size || (uint)y >= Height || (uint)z >= Size) return -1;
return x + (z << 4) + (y << 8); // x + z*16 + y*256
}
public void UpdateChunkMesh()
{
// Lock or use ThreadLocal for multi-threading later
lock (_faceBuffer)
{
_faceBuffer.Clear();
GenerateFaces(_faceBuffer);
_chunkMesh.SetFaces(_faceBuffer);
}
IsDirty = false;
}
private void GenerateFaces(List<FaceData> faces)
{
for (int y = 0; y < Height; y++)
{
int yOff = y << 8;
for (int z = 0; z < Size; z++)
{
int zyOff = yOff + (z << 4);
for (int x = 0; x < Size; x++)
{
int i = zyOff + x;
Blocks block = _blocks[i];
if (block == Blocks.Air) continue;
var blockDef = BlockDefinitions.Get(block);
for (int f = 0; f < 6; f++)
{
if (ShouldAddFace(x, y, z, f))
{
faces.Add(new FaceData((byte)x, (byte)y, (byte)z, (Orientation)f, blockDef.FaceTextures[f], 15));
}
}
}
}
}
}
private bool ShouldAddFace(int x, int y, int z, int faceIndex)
{
var offset = Offsets[faceIndex];
int nx = x + offset.dx;
int ny = y + offset.dy;
int nz = z + offset.dz;
if ((uint)nx < Size && (uint)ny < Height && (uint)nz < Size)
{
return _blocks[nx + (nz << 4) + (ny << 8)] == Blocks.Air;
}
// external boundary check
Chunk neighbor = _neighbors[faceIndex];
if (neighbor == null) return true; // At edge of loaded world
// wrap coordinates for the neighbor
int lx = (nx + Size) % Size;
int lz = (nz + Size) % Size;
return neighbor.GetBlock(lx, ny, lz) == Blocks.Air;
}
public ChunkMesh GetChunkMesh()
{
return _chunkMesh;
}
}
}

373
World/World.cs Normal file
View File

@@ -0,0 +1,373 @@
using OpenTK.Mathematics;
using Voxel.Graphics;
using Voxel.Physics;
namespace Voxel.Core
{
public class World
{
private Dictionary<(int, int), Chunk> _chunks;
private (int x, int z) _lastCenter = (0, 0);
private int _loadDistance = 8;
bool chunkLoadingInitialized = false;
private static readonly Dictionary<Orientation, (int x, int y)> _neighborOffsets = new()
{
{ Orientation.West, (1, 0) },
{ Orientation.East, (-1, 0) },
{ Orientation.North, (0, 1) },
{ Orientation.South, (0, -1) }
};
public World()
{
_chunks = new Dictionary<(int, int), Chunk>();
}
public Chunk GetChunk(int chunkX, int chunkZ)
{
_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk);
return chunk;
}
public void AddChunk(Chunk chunk)
{
_chunks[(chunk.X, chunk.Y)] = chunk;
UpdateNeighboringChunks(chunk);
chunk.UpdateChunkMesh();
Renderer.MarkBuffersDirty();
}
public void RemoveChunk(int chunkX, int chunkZ)
{
if (_chunks.TryGetValue((chunkX, chunkZ), out Chunk chunk))
{
var neighbors = GetChunkNeighbors(chunk).ToList();
_chunks.Remove((chunkX, chunkZ));
// update neighbor references to this chunk
foreach (var (orientation, neighbor) in neighbors)
{
var opposite = GetOppositeOrientation(orientation);
neighbor.SetNeighbor(opposite, null);
neighbor.UpdateChunkMesh();
}
}
}
public new void UpdateChunkLoading(Vector3 playerPosition)
{
int centerX = (int)Math.Floor(playerPosition.X / Chunk.Size);
int centerZ = (int)Math.Floor(playerPosition.Z / Chunk.Size);
if ((centerX == _lastCenter.x && centerZ == _lastCenter.z) && chunkLoadingInitialized)
return;
_lastCenter = (centerX, centerZ);
int minX = centerX - _loadDistance;
int maxX = centerX + _loadDistance;
int minZ = centerZ - _loadDistance;
int maxZ = centerZ + _loadDistance;
UnloadDistantChunks(minX, maxX, minZ, maxZ);
LoadChunksInRange(minX, maxX, minZ, maxZ);
chunkLoadingInitialized = true;
}
private void UnloadDistantChunks(int minX, int maxX, int minZ, int maxZ)
{
var chunksToRemove = new List<(int, int)>();
foreach (var (chunkX, chunkZ) in _chunks.Keys)
{
if (chunkX < minX || chunkX > maxX || chunkZ < minZ || chunkZ > maxZ)
{
chunksToRemove.Add((chunkX, chunkZ));
}
}
foreach (var chunkPos in chunksToRemove)
{
RemoveChunk(chunkPos.Item1, chunkPos.Item2);
}
}
private void LoadChunksInRange(int minX, int maxX, int minZ, int maxZ)
{
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
if (!_chunks.ContainsKey((x, z)))
{
Chunk chunk = new Chunk(x, z);
AddChunk(chunk);
}
}
}
}
public void UpdateNeighboringChunks(Chunk chunk)
{
for (int i = 0; i < 6; i++)
{
chunk.SetNeighbor((Orientation)i, null);
}
foreach (var (orientation, neighbor) in GetChunkNeighbors(chunk))
{
Orientation opposite = GetOppositeOrientation(orientation);
neighbor.SetNeighbor(opposite, chunk);
chunk.SetNeighbor(orientation, neighbor);
neighbor.UpdateChunkMesh();
}
}
private Orientation GetOppositeOrientation(Orientation orientation)
{
return orientation switch
{
Orientation.West => Orientation.East,
Orientation.East => Orientation.West,
Orientation.North => Orientation.South,
Orientation.South => Orientation.North,
_ => orientation
};
}
public IEnumerable<Chunk> GetAllChunks()
{
return _chunks.Values;
}
public Blocks GetBlock(int worldX, int worldY, int worldZ)
{
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) return 0; // air if chunk not loaded
return chunk.GetBlock(localX, worldY, localZ);
}
public void SetBlock(int worldX, int worldY, int worldZ, Blocks block, bool updateMesh = true)
{
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) return;
chunk.SetBlock(localX, worldY, localZ, block, updateMesh: updateMesh);
// temporary tnt functionality
if (block == Blocks.TNT)
{
Explode(worldX, worldY, worldZ, 4);
return;
}
if (!updateMesh) return;
UpdateNeighborsAtBoundary(chunk, localX, worldY, localZ);
RebuildDirtyChunks();
}
private void UpdateNeighborsAtBoundary(Chunk chunk, int lx, int ly, int lz)
{
if (lx == Chunk.Size - 1) chunk.GetNeighbor(Orientation.West)?.MarkDirty();
else if (lx == 0) chunk.GetNeighbor(Orientation.East)?.MarkDirty();
if (lz == Chunk.Size - 1) chunk.GetNeighbor(Orientation.North)?.MarkDirty();
else if (lz == 0) chunk.GetNeighbor(Orientation.South)?.MarkDirty();
}
public bool IsOnChunkBorder(int worldX, int worldZ)
{
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(worldX, worldZ);
return (localX == Chunk.Size - 1 || localX == 0) || (localZ == Chunk.Size - 1 || localZ == 0);
}
// temporary tnt functionality
public void Explode(int centerX, int centerY, int centerZ, int radius)
{
int radiusSq = radius * radius;
var affectedChunks = new HashSet<Chunk>(); // store chunks that will change
// bounding box
int minX = centerX - radius;
int maxX = centerX + radius;
int minY = Math.Max(0, centerY - radius);
int maxY = Math.Min(Chunk.Height - 1, centerY + radius);
int minZ = centerZ - radius;
int maxZ = centerZ + radius;
for (int x = minX; x <= maxX; x++)
{
for (int z = minZ; z <= maxZ; z++)
{
var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(x, z);
Chunk chunk = GetChunk(chunkX, chunkZ);
if (chunk == null) continue;
for (int y = minY; y <= maxY; y++)
{
int dx = x - centerX;
int dy = y - centerY;
int dz = z - centerZ;
if (dx * dx + dy * dy + dz * dz <= radiusSq)
{
chunk.SetBlock(localX, y, localZ, Blocks.Air, updateMesh: true);
}
}
}
}
RebuildDirtyChunks();
}
private void RebuildDirtyChunks()
{
foreach (var chunk in _chunks.Values)
{
if (chunk.IsDirty)
{
chunk.UpdateChunkMesh();
Renderer.UpdateChunkBuffer(chunk);
chunk.IsDirty = false;
}
}
}
public static (int chunkX, int chunkZ, int localX, int localZ) WorldToChunkCoords(int worldX, int worldZ)
{
int chunkX = worldX >= 0 ? worldX / Chunk.Size : (worldX + 1) / Chunk.Size - 1;
int chunkZ = worldZ >= 0 ? worldZ / Chunk.Size : (worldZ + 1) / Chunk.Size - 1;
int localX = ((worldX % Chunk.Size) + Chunk.Size) % Chunk.Size;
int localZ = ((worldZ % Chunk.Size) + Chunk.Size) % Chunk.Size;
return (chunkX, chunkZ, localX, localZ);
}
List<Orientation> GetEdgeOrientations(int localX, int localZ, int size)
{
var orientations = new List<Orientation>();
if (localX == size - 1) orientations.Add(Orientation.West);
if (localX == 0) orientations.Add(Orientation.East);
if (localZ == size - 1) orientations.Add(Orientation.North);
if (localZ == 0) orientations.Add(Orientation.South);
return orientations;
}
public IEnumerable<(Orientation orientation, Chunk neighbor)> GetChunkNeighbors(Chunk chunk)
{
int chunkX = chunk.X;
int chunkY = chunk.Y;
foreach (var kv in _neighborOffsets)
{
Chunk neighbor = GetChunk(chunkX + kv.Value.x, chunkY + kv.Value.y);
if (neighbor != null)
yield return (kv.Key, neighbor);
}
}
public (bool success, Blocks block, int x, int y, int z, Vector3i normal) Raycast(Vector3 origin, Vector3 direction, float maxDistance)
{
int x = (int)MathF.Floor(origin.X);
int y = (int)MathF.Floor(origin.Y);
int z = (int)MathF.Floor(origin.Z);
int stepX = direction.X > 0 ? 1 : -1;
int stepY = direction.Y > 0 ? 1 : -1;
int stepZ = direction.Z > 0 ? 1 : -1;
float tDeltaX = direction.X != 0 ? MathF.Abs(1 / direction.X) : float.MaxValue;
float tDeltaY = direction.Y != 0 ? MathF.Abs(1 / direction.Y) : float.MaxValue;
float tDeltaZ = direction.Z != 0 ? MathF.Abs(1 / direction.Z) : float.MaxValue;
float tMaxX = direction.X > 0 ? (MathF.Floor(origin.X) + 1 - origin.X) * tDeltaX
: (origin.X - MathF.Floor(origin.X)) * tDeltaX;
float tMaxY = direction.Y > 0 ? (MathF.Floor(origin.Y) + 1 - origin.Y) * tDeltaY
: (origin.Y - MathF.Floor(origin.Y)) * tDeltaY;
float tMaxZ = direction.Z > 0 ? (MathF.Floor(origin.Z) + 1 - origin.Z) * tDeltaZ
: (origin.Z - MathF.Floor(origin.Z)) * tDeltaZ;
float distance = 0f;
Vector3i normal = Vector3i.Zero;
while (distance <= maxDistance)
{
Blocks block = GetBlock(x, y, z);
if (block != Blocks.Air)
return (true, block, x, y, z, normal);
if (tMaxX < tMaxY && tMaxX < tMaxZ)
{
x += stepX;
normal = new Vector3i(-stepX, 0, 0);
distance = tMaxX;
tMaxX += tDeltaX;
}
else if (tMaxY < tMaxZ)
{
y += stepY;
normal = new Vector3i(0, -stepY, 0);
distance = tMaxY;
tMaxY += tDeltaY;
}
else
{
z += stepZ;
normal = new Vector3i(0, 0, -stepZ);
distance = tMaxZ;
tMaxZ += tDeltaZ;
}
}
return (false, Blocks.Air, 0, 0, 0, Vector3i.Zero);
}
public List<AABB> GetColliders(AABB body)
{
List<AABB> collisions = new List<AABB>();
int minX = (int)Math.Floor(body.Min.X);
int maxX = (int)Math.Ceiling(body.Max.X) - 1;
int minY = (int)Math.Floor(body.Min.Y);
int maxY = (int)Math.Ceiling(body.Max.Y) - 1;
int minZ = (int)Math.Floor(body.Min.Z);
int maxZ = (int)Math.Ceiling(body.Max.Z) - 1;
for (int x = minX; x <= maxX; x++)
{
for (int y = minY; y <= maxY; y++)
{
for (int z = minZ; z <= maxZ; z++)
{
if (GetBlock(x, y, z) != Blocks.Air)
{
collisions.Add(new AABB(
new Vector3(x, y, z),
new Vector3(x + 1, y + 1, z + 1)
));
}
}
}
}
return collisions;
}
}
}