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:
Binary file not shown.
|
Before Width: | Height: | Size: 236 B After Width: | Height: | Size: 2.4 KiB |
60
Core/Game.cs
60
Core/Game.cs
@@ -4,7 +4,8 @@ using OpenTK.Windowing.Common;
|
|||||||
using OpenTK.Windowing.Desktop;
|
using OpenTK.Windowing.Desktop;
|
||||||
using OpenTK.Windowing.GraphicsLibraryFramework;
|
using OpenTK.Windowing.GraphicsLibraryFramework;
|
||||||
using VoxelEngine.Rendering;
|
using VoxelEngine.Rendering;
|
||||||
using VoxelEngine.World;
|
using VoxelEngine.World.Blocks;
|
||||||
|
using VoxelEngine.World.Chunk;
|
||||||
|
|
||||||
namespace VoxelEngine.Core;
|
namespace VoxelEngine.Core;
|
||||||
|
|
||||||
@@ -16,9 +17,11 @@ public sealed class Game : GameWindow
|
|||||||
private readonly BlockRegistry _registry = new();
|
private readonly BlockRegistry _registry = new();
|
||||||
private Shader _shader = null!;
|
private Shader _shader = null!;
|
||||||
private Texture _texture = null!;
|
private Texture _texture = null!;
|
||||||
private ChunkRenderer _renderer = null!;
|
private readonly Dictionary<Vector3i, ChunkRenderer> _renderer = new();
|
||||||
private Camera _camera = null!;
|
private Camera _camera = null!;
|
||||||
|
|
||||||
|
private World.World _world;
|
||||||
|
|
||||||
public Game(GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
|
public Game(GameWindowSettings gameSettings, NativeWindowSettings nativeSettings)
|
||||||
: base(gameSettings, nativeSettings) { }
|
: base(gameSettings, nativeSettings) { }
|
||||||
|
|
||||||
@@ -33,9 +36,28 @@ public sealed class Game : GameWindow
|
|||||||
this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag");
|
this._shader = new Shader("Assets/shaders/chunk.vert", "Assets/shaders/chunk.frag");
|
||||||
this._texture = new Texture("Assets/textures/atlas.png");
|
this._texture = new Texture("Assets/textures/atlas.png");
|
||||||
|
|
||||||
Chunk chunk = this.BuildChunk();
|
this._world = new World.World(this._registry);
|
||||||
float[] mesh = new ChunkMesher().Build(chunk, this._registry);
|
|
||||||
this._renderer = new ChunkRenderer(mesh);
|
for (int x = -2; x < 2; x++)
|
||||||
|
for (int z = -2; z < 2; z++)
|
||||||
|
{
|
||||||
|
this._world.SetChunk(new Vector3i(x, 0, z), this.BuildChunk());
|
||||||
|
}
|
||||||
|
|
||||||
|
ChunkMesher mesher = new();
|
||||||
|
foreach ((Vector3i pos, Chunk chunk) in this._world)
|
||||||
|
{
|
||||||
|
Vector3i origin = pos * Chunk.Size;
|
||||||
|
float[] mesh = mesher.Build(
|
||||||
|
chunk,
|
||||||
|
this._registry,
|
||||||
|
(nx, ny, nz) => this._world.GetBlock(
|
||||||
|
origin.X + nx,
|
||||||
|
origin.Y + ny,
|
||||||
|
origin.Z + nz).IsOpaque
|
||||||
|
);
|
||||||
|
this._renderer[pos] = new ChunkRenderer(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
this._camera = new Camera(new Vector3(8f, 20f, 40f), this.ClientSize.X / (float)this.ClientSize.Y);
|
this._camera = new Camera(new Vector3(8f, 20f, 40f), this.ClientSize.X / (float)this.ClientSize.Y);
|
||||||
|
|
||||||
@@ -65,15 +87,20 @@ public sealed class Game : GameWindow
|
|||||||
if (!this.IsFocused) return;
|
if (!this.IsFocused) return;
|
||||||
|
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.Escape)) {
|
if (this.KeyboardState.IsKeyDown(Keys.Escape)) {
|
||||||
this.Close(); return; }
|
this.Close(); return;
|
||||||
|
}
|
||||||
|
|
||||||
float distance = Game.MoveSpeed * (float)args.Time;
|
float speedMulti = 1f;
|
||||||
|
|
||||||
|
if (this.KeyboardState.IsKeyDown(Keys.LeftShift)) speedMulti = 1.8f ;
|
||||||
|
|
||||||
|
float distance = Game.MoveSpeed * speedMulti * (float)args.Time;
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.W)) this._camera.Move(this._camera.Front * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.W)) this._camera.Move(this._camera.Front * distance);
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.S)) this._camera.Move(-this._camera.Front * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.S)) this._camera.Move(-this._camera.Front * distance);
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.A)) this._camera.Move(-this._camera.Right * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.A)) this._camera.Move(-this._camera.Right * distance);
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.D)) this._camera.Move(this._camera.Right * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.D)) this._camera.Move(this._camera.Right * distance);
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.Space)) this._camera.Move(Vector3.UnitY * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.Space)) this._camera.Move(Vector3.UnitY * distance);
|
||||||
if (this.KeyboardState.IsKeyDown(Keys.LeftShift)) this._camera.Move(-Vector3.UnitY * distance);
|
if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._camera.Move(-Vector3.UnitY * distance);
|
||||||
|
|
||||||
Vector2 delta = this.MouseState.Delta;
|
Vector2 delta = this.MouseState.Delta;
|
||||||
this._camera.AddRotation(delta.X * Game.Sensitivity, -delta.Y * Game.Sensitivity);
|
this._camera.AddRotation(delta.X * Game.Sensitivity, -delta.Y * Game.Sensitivity);
|
||||||
@@ -86,12 +113,20 @@ public sealed class Game : GameWindow
|
|||||||
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||||
|
|
||||||
this._shader.Use();
|
this._shader.Use();
|
||||||
this._shader.SetMatrix4("model", Matrix4.Identity);
|
|
||||||
this._shader.SetMatrix4("view", this._camera.GetViewMatrix());
|
this._shader.SetMatrix4("view", this._camera.GetViewMatrix());
|
||||||
this._shader.SetMatrix4("projection", this._camera.GetProjectionMatrix());
|
this._shader.SetMatrix4("projection", this._camera.GetProjectionMatrix());
|
||||||
this._shader.SetInt("atlas", 0);
|
this._shader.SetInt("atlas", 0);
|
||||||
this._texture.Use(TextureUnit.Texture0);
|
this._texture.Use(TextureUnit.Texture0);
|
||||||
this._renderer.Draw();
|
|
||||||
|
foreach ((Vector3i pos, ChunkRenderer renderer) in this._renderer)
|
||||||
|
{
|
||||||
|
this._shader.SetMatrix4("model", Matrix4.CreateTranslation(
|
||||||
|
pos.X * Chunk.Size,
|
||||||
|
pos.Y * Chunk.Size,
|
||||||
|
pos.Z * Chunk.Size
|
||||||
|
));
|
||||||
|
renderer.Draw();
|
||||||
|
}
|
||||||
|
|
||||||
this.SwapBuffers();
|
this.SwapBuffers();
|
||||||
}
|
}
|
||||||
@@ -106,7 +141,10 @@ public sealed class Game : GameWindow
|
|||||||
|
|
||||||
protected override void OnUnload()
|
protected override void OnUnload()
|
||||||
{
|
{
|
||||||
this._renderer.Dispose();
|
foreach (ChunkRenderer rendererValue in this._renderer.Values)
|
||||||
|
{
|
||||||
|
rendererValue.Dispose();
|
||||||
|
}
|
||||||
this._texture.Dispose();
|
this._texture.Dispose();
|
||||||
this._shader.Dispose();
|
this._shader.Dispose();
|
||||||
base.OnUnload();
|
base.OnUnload();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using VoxelEngine.World;
|
using VoxelEngine.World;
|
||||||
|
using VoxelEngine.World.Blocks;
|
||||||
|
using VoxelEngine.World.Chunk;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace VoxelEngine.Tests;
|
namespace VoxelEngine.Tests;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using VoxelEngine.World;
|
using VoxelEngine.World;
|
||||||
|
using VoxelEngine.World.Chunk;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace VoxelEngine.Tests;
|
namespace VoxelEngine.Tests;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace VoxelEngine.World;
|
namespace VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
public enum BlockFace
|
public enum BlockFace
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using VoxelEngine.World.Blocks;
|
namespace VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World;
|
|
||||||
|
|
||||||
public sealed class BlockRegistry
|
public sealed class BlockRegistry
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace VoxelEngine.World;
|
namespace VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
public interface IBlock
|
public interface IBlock
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace VoxelEngine.World;
|
namespace VoxelEngine.World.Chunk;
|
||||||
|
|
||||||
public sealed class Chunk
|
public sealed class Chunk
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
namespace VoxelEngine.World;
|
namespace VoxelEngine.World.Chunk;
|
||||||
|
|
||||||
public sealed class ChunkMesher : IChunkMesher
|
public sealed class ChunkMesher : IChunkMesher
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ public sealed class ChunkMesher : IChunkMesher
|
|||||||
{ (0, 0), (1, 0), (1, 1), (0, 1) };
|
{ (0, 0), (1, 0), (1, 1), (0, 1) };
|
||||||
private static readonly int[] Triangle = { 0, 1, 2, 0, 2, 3 };
|
private static readonly int[] Triangle = { 0, 1, 2, 0, 2, 3 };
|
||||||
|
|
||||||
public float[] Build(Chunk chunk, BlockRegistry registry)
|
public float[] Build(Chunk chunk, BlockRegistry registry, Func<int, int, int, bool>? neighborOpaqueOutside = null)
|
||||||
{
|
{
|
||||||
List<float> verts = new();
|
List<float> verts = new();
|
||||||
|
|
||||||
@@ -64,7 +64,9 @@ public sealed class ChunkMesher : IChunkMesher
|
|||||||
|
|
||||||
// Out-of-chunk neighbors are treated as non-opaque (single chunk).
|
// Out-of-chunk neighbors are treated as non-opaque (single chunk).
|
||||||
bool neighborOpaque =
|
bool neighborOpaque =
|
||||||
Chunk.InBounds(nx, ny, nz) && registry.Get(chunk[nx, ny, nz]).IsOpaque;
|
Chunk.InBounds(nx, ny, nz)
|
||||||
|
? registry.Get(chunk[nx, ny, nz]).IsOpaque
|
||||||
|
: (neighborOpaqueOutside?.Invoke(nx, ny, nz) ?? false);
|
||||||
if (neighborOpaque) continue;
|
if (neighborOpaque) continue;
|
||||||
|
|
||||||
ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureIndex(face.Kind));
|
ChunkMesher.AddFace(verts, x, y, z, face, block.GetTextureIndex(face.Kind));
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
11
World/IWorld.cs
Normal file
11
World/IWorld.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using OpenTK.Mathematics;
|
||||||
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
|
namespace VoxelEngine.World;
|
||||||
|
|
||||||
|
public interface IWorld
|
||||||
|
{
|
||||||
|
public void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk);
|
||||||
|
public Chunk.Chunk? GetChunk(Vector3i chunkPos);
|
||||||
|
public IBlock GetBlock(int worldX, int worldY, int worldZ);
|
||||||
|
}
|
||||||
63
World/World.cs
Normal file
63
World/World.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using OpenTK.Mathematics;
|
||||||
|
using VoxelEngine.World.Blocks;
|
||||||
|
|
||||||
|
namespace VoxelEngine.World;
|
||||||
|
|
||||||
|
public sealed class World : IWorld, IEnumerable<KeyValuePair<Vector3i, Chunk.Chunk>>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<Vector3i, Chunk.Chunk> _chunks = new();
|
||||||
|
private readonly BlockRegistry _registry;
|
||||||
|
|
||||||
|
public World(BlockRegistry registry)
|
||||||
|
{
|
||||||
|
this._registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetChunk(Vector3i chunkPos, Chunk.Chunk chunk)
|
||||||
|
{
|
||||||
|
this._chunks[chunkPos] = chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Chunk.Chunk? GetChunk(Vector3i chunkPos)
|
||||||
|
{
|
||||||
|
return this._chunks.GetValueOrDefault(chunkPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IBlock GetBlock(int worldX, int worldY, int worldZ)
|
||||||
|
{
|
||||||
|
Vector3i chunkPos = new(
|
||||||
|
World.FloorDiv(worldX, Chunk.Chunk.Size),
|
||||||
|
World.FloorDiv(worldY, Chunk.Chunk.Size),
|
||||||
|
World.FloorDiv(worldZ, Chunk.Chunk.Size)
|
||||||
|
);
|
||||||
|
|
||||||
|
Chunk.Chunk? chunk = this.GetChunk(chunkPos);
|
||||||
|
if (chunk == null)
|
||||||
|
{
|
||||||
|
return this._registry.Air;
|
||||||
|
}
|
||||||
|
|
||||||
|
int localX = worldX - chunkPos.X * Chunk.Chunk.Size;
|
||||||
|
int localY = worldY - chunkPos.Y * Chunk.Chunk.Size;
|
||||||
|
int localZ = worldZ - chunkPos.Z * Chunk.Chunk.Size;
|
||||||
|
|
||||||
|
return this._registry.Get(chunk[localX, localY, localZ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FloorDiv(int value, int size)
|
||||||
|
{
|
||||||
|
int quotient = value / size;
|
||||||
|
if (value % size != 0 && value < 0)
|
||||||
|
{
|
||||||
|
quotient--;
|
||||||
|
}
|
||||||
|
return quotient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<Vector3i, Chunk.Chunk>> GetEnumerator()
|
||||||
|
=> this._chunks.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user