diff --git a/Assets/textures/atlas.png b/Assets/textures/atlas.png index 943844a..6d50d5e 100644 Binary files a/Assets/textures/atlas.png and b/Assets/textures/atlas.png differ diff --git a/Core/Game.cs b/Core/Game.cs index fd9dcd9..64047f5 100644 --- a/Core/Game.cs +++ b/Core/Game.cs @@ -4,7 +4,8 @@ using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; using VoxelEngine.Rendering; -using VoxelEngine.World; +using VoxelEngine.World.Blocks; +using VoxelEngine.World.Chunk; namespace VoxelEngine.Core; @@ -16,9 +17,11 @@ public sealed class Game : GameWindow private readonly BlockRegistry _registry = new(); private Shader _shader = null!; private Texture _texture = null!; - private ChunkRenderer _renderer = null!; + private readonly Dictionary _renderer = new(); private Camera _camera = null!; + private World.World _world; + public Game(GameWindowSettings gameSettings, NativeWindowSettings 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._texture = new Texture("Assets/textures/atlas.png"); - Chunk chunk = this.BuildChunk(); - float[] mesh = new ChunkMesher().Build(chunk, this._registry); - this._renderer = new ChunkRenderer(mesh); + this._world = new World.World(this._registry); + + 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); @@ -65,15 +87,20 @@ public sealed class Game : GameWindow if (!this.IsFocused) return; 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.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.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.LeftShift)) this._camera.Move(-Vector3.UnitY * distance); + if (this.KeyboardState.IsKeyDown(Keys.LeftControl)) this._camera.Move(-Vector3.UnitY * distance); Vector2 delta = this.MouseState.Delta; 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); this._shader.Use(); - this._shader.SetMatrix4("model", Matrix4.Identity); this._shader.SetMatrix4("view", this._camera.GetViewMatrix()); this._shader.SetMatrix4("projection", this._camera.GetProjectionMatrix()); this._shader.SetInt("atlas", 0); 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(); } @@ -106,7 +141,10 @@ public sealed class Game : GameWindow protected override void OnUnload() { - this._renderer.Dispose(); + foreach (ChunkRenderer rendererValue in this._renderer.Values) + { + rendererValue.Dispose(); + } this._texture.Dispose(); this._shader.Dispose(); base.OnUnload(); diff --git a/VoxelEngine.Tests/ChunkMesherTests.cs b/VoxelEngine.Tests/ChunkMesherTests.cs index eefbfde..88b3619 100644 --- a/VoxelEngine.Tests/ChunkMesherTests.cs +++ b/VoxelEngine.Tests/ChunkMesherTests.cs @@ -1,4 +1,6 @@ using VoxelEngine.World; +using VoxelEngine.World.Blocks; +using VoxelEngine.World.Chunk; using Xunit; namespace VoxelEngine.Tests; diff --git a/VoxelEngine.Tests/ChunkTests.cs b/VoxelEngine.Tests/ChunkTests.cs index 646a569..d548786 100644 --- a/VoxelEngine.Tests/ChunkTests.cs +++ b/VoxelEngine.Tests/ChunkTests.cs @@ -1,4 +1,5 @@ using VoxelEngine.World; +using VoxelEngine.World.Chunk; using Xunit; namespace VoxelEngine.Tests; diff --git a/World/BlockFace.cs b/World/Blocks/BlockFace.cs similarity index 78% rename from World/BlockFace.cs rename to World/Blocks/BlockFace.cs index 4193936..4e81fd9 100644 --- a/World/BlockFace.cs +++ b/World/Blocks/BlockFace.cs @@ -1,4 +1,4 @@ -namespace VoxelEngine.World; +namespace VoxelEngine.World.Blocks; public enum BlockFace { diff --git a/World/BlockRegistry.cs b/World/Blocks/BlockRegistry.cs similarity index 91% rename from World/BlockRegistry.cs rename to World/Blocks/BlockRegistry.cs index c08c86c..69e9905 100644 --- a/World/BlockRegistry.cs +++ b/World/Blocks/BlockRegistry.cs @@ -1,6 +1,4 @@ -using VoxelEngine.World.Blocks; - -namespace VoxelEngine.World; +namespace VoxelEngine.World.Blocks; public sealed class BlockRegistry { diff --git a/World/IBlock.cs b/World/Blocks/IBlock.cs similarity index 93% rename from World/IBlock.cs rename to World/Blocks/IBlock.cs index d181611..6f5de68 100644 --- a/World/IBlock.cs +++ b/World/Blocks/IBlock.cs @@ -1,4 +1,4 @@ -namespace VoxelEngine.World; +namespace VoxelEngine.World.Blocks; public interface IBlock { diff --git a/World/Chunk.cs b/World/Chunk/Chunk.cs similarity index 94% rename from World/Chunk.cs rename to World/Chunk/Chunk.cs index b680dd1..4bc4f89 100644 --- a/World/Chunk.cs +++ b/World/Chunk/Chunk.cs @@ -1,4 +1,4 @@ -namespace VoxelEngine.World; +namespace VoxelEngine.World.Chunk; public sealed class Chunk { diff --git a/World/ChunkMesher.cs b/World/Chunk/ChunkMesher.cs similarity index 89% rename from World/ChunkMesher.cs rename to World/Chunk/ChunkMesher.cs index 4cdb53c..ac333c7 100644 --- a/World/ChunkMesher.cs +++ b/World/Chunk/ChunkMesher.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using VoxelEngine.World.Blocks; -namespace VoxelEngine.World; +namespace VoxelEngine.World.Chunk; public sealed class ChunkMesher : IChunkMesher { @@ -45,7 +45,7 @@ public sealed class ChunkMesher : IChunkMesher { (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) + public float[] Build(Chunk chunk, BlockRegistry registry, Func? neighborOpaqueOutside = null) { List verts = new(); @@ -64,7 +64,9 @@ public sealed class ChunkMesher : IChunkMesher // 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; + 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)); diff --git a/World/Chunk/IChunkMesher.cs b/World/Chunk/IChunkMesher.cs new file mode 100644 index 0000000..4799123 --- /dev/null +++ b/World/Chunk/IChunkMesher.cs @@ -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? neighborOpaqueOutside = null); +} diff --git a/World/IChunkMesher.cs b/World/IChunkMesher.cs deleted file mode 100644 index dcee99e..0000000 --- a/World/IChunkMesher.cs +++ /dev/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); -} diff --git a/World/IWorld.cs b/World/IWorld.cs new file mode 100644 index 0000000..f286a70 --- /dev/null +++ b/World/IWorld.cs @@ -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); +} \ No newline at end of file diff --git a/World/World.cs b/World/World.cs new file mode 100644 index 0000000..cc44063 --- /dev/null +++ b/World/World.cs @@ -0,0 +1,63 @@ +using System.Collections; +using OpenTK.Mathematics; +using VoxelEngine.World.Blocks; + +namespace VoxelEngine.World; + +public sealed class World : IWorld, IEnumerable> +{ + private readonly Dictionary _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> GetEnumerator() + => this._chunks.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + +} \ No newline at end of file