diff --git a/atlas.png b/Assets/atlas.png similarity index 100% rename from atlas.png rename to Assets/atlas.png diff --git a/BlockData.cs b/BlockData.cs deleted file mode 100644 index 69b4f4a..0000000 --- a/BlockData.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Voxel -{ - public class BlockData - { - } -} diff --git a/Blocks.cs b/Blocks.cs deleted file mode 100644 index 7013935..0000000 --- a/Blocks.cs +++ /dev/null @@ -1,106 +0,0 @@ -namespace Voxel -{ - 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 Voxel.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 - { - public static readonly Dictionary Blocks; - - static BlockDefinitions() - { - Blocks = new Dictionary - { - {Voxel.Blocks.Stone, new BlockDefinition( - Voxel.Blocks.Stone, Textures.Stone - )}, - - {Voxel.Blocks.Dirt, new BlockDefinition( - Voxel.Blocks.Dirt, Textures.Dirt - )}, - - {Voxel.Blocks.OakPlanks, new BlockDefinition( - Voxel.Blocks.OakPlanks, Textures.OakPlanks - )}, - - {Voxel.Blocks.Bedrock, new BlockDefinition( - Voxel.Blocks.Bedrock, Textures.Bedrock - )}, - - {Voxel.Blocks.Sand, new BlockDefinition( - Voxel.Blocks.Sand, Textures.Sand - )}, - - { Voxel.Blocks.Grass, new BlockDefinition( - Voxel.Blocks.Grass, - Voxel.Textures.GrassSide, // West - Voxel.Textures.GrassSide, // East - Voxel.Textures.GrassTop, // Top - Voxel.Textures.Dirt, // Bottom - Voxel.Textures.GrassSide, // North - Voxel.Textures.GrassSide // South - )}, - - - { Voxel.Blocks.TNT, new BlockDefinition( - Voxel.Blocks.TNT, - Voxel.Textures.TntSide, // West - Voxel.Textures.TntSide, // East - Voxel.Textures.TntTop, // Top - Voxel.Textures.TntBottom, // Bottom - Voxel.Textures.TntSide, // North - Voxel.Textures.TntSide // South - )}, - }; - } - } -} diff --git a/Chunk.cs b/Chunk.cs deleted file mode 100644 index 5be78f4..0000000 --- a/Chunk.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Voxel -{ - 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; - - private Dictionary _blockData; - private Blocks[] _blocks; - public Dictionary Neighbors = new(); - - private static readonly (int dx, int dy, int dz)[] Offsets = new (int, int, int)[6] - { - ( 1, 0, 0), // +X - (-1, 0, 0), // -X - ( 0, 1, 0), // +Y - ( 0, -1, 0), // -Y - ( 0, 0, 1), // +Z - ( 0, 0, -1) // -Z - }; - - private static readonly Dictionary FaceToNeighborMap = new() - { - { 0, Orientation.West }, // +X face - { 1, Orientation.East }, // -X face - { 4, Orientation.North }, // +Z face - { 5, Orientation.South } // -Z face - }; - - public Chunk(int x, int y) - { - X = x; - Y = y; - - _blockData = new Dictionary(); - _blocks = new Blocks[TotalBlocks]; - _chunkMesh = new ChunkMesh(X, Y); - - Initialize(); - } - - public void SetBlock(int x, int y, int z, Blocks block, bool updateMesh = true) - { - int i = GetBlockIndex(x, y, z); - if (i == -1) return; - _blocks[i] = block; - - if (updateMesh) - { - UpdateChunkMesh(); - Renderer.MarkBuffersDirty(); - } - } - - public void SetBlockIndex(int i, Blocks block) - { - if (i < 0 || i >= TotalBlocks) return; - _blocks[i] = block; - } - - public Blocks GetBlock(int x, int y, int z) - { - int i = GetBlockIndex(x, y, z); - if (i == -1) return Blocks.Air; - return _blocks[i]; - } - - public BlockData GetBlockData(int x, int y, int z) - { - return new BlockData(); // TODO: Implement - } - - private void Initialize() - { - for (int x = 0; x < Size; x++) - { - for (int z = 0; z < Size; z++) - { - var position = GetWorldCoordinates(x, 0, z); - int height = Worldgen.GetHeight(position.x, position.z); - - for (int y = 0; y < Height; y++) - { - Blocks block = Worldgen.GetBlock(y, height); - SetBlock(x, y, z, block, false); - } - } - } - UpdateChunkMesh(); - } - - public (int x, int y, int z) IndexToPosition(int i) - { - if (i < 0 || i >= TotalBlocks) - return (0, 0, 0); - - int y = i >> 8; // y * 256 - int remainder = i & 0xFF; // Lower 8 bits: x + z * 16 - int z = remainder >> 4; // z * 16 - int x = remainder & 0xF; // x - - return (x, y, z); - } - - public (int x, int y, int z) GetWorldCoordinates(int localX, int localY, int localZ) - { - return (localX + (X * Size), localY, localZ + (Y * Size)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetBlockIndex(int x, int y, int z) - { - // bit shifting - if ((uint)x >= Size || (uint)y >= Height || (uint)z >= Size) - return -1; - - return x + (z << 4) + (y << 8); // x * 1, z * 16, y * 256 - } - - public void UpdateChunkMesh() - { - List faces = new List(TotalBlocks / 2); // Approximate capacity - GenerateFaces(faces); - _chunkMesh.SetFaces(faces); - } - - private void GenerateFaces(List faces) - { - for (int x = 0; x < Size; x++) - { - for (int z = 0; z < Size; z++) - { - for (int y = 0; y < Height; y++) - { - ProcessBlockFaces(x, y, z, faces); - } - } - } - } - - private void ProcessBlockFaces(int x, int y, int z, List faces) - { - int blockIndex = GetBlockIndex(x, y, z); - if (blockIndex == -1) return; - - Blocks block = _blocks[blockIndex]; - if (block == Blocks.Air) return; - - var blockDef = BlockDefinitions.Blocks[block]; - - for (int face = 0; face < 6; face++) - { - if (ShouldAddFace(x, y, z, face)) - { - AddFace(x, y, z, face, blockDef.FaceTextures[face], faces); - } - } - } - - private bool ShouldAddFace(int x, int y, int z, int face) - { - var offset = Offsets[face]; - int nx = x + offset.dx; - int ny = y + offset.dy; - int nz = z + offset.dz; - - int neighborIndex = GetBlockIndex(nx, ny, nz); - if (neighborIndex != -1) - { - return _blocks[neighborIndex] == Blocks.Air; - } - - return IsFaceVisibleAtChunkBoundary(x, y, z, face); - } - - private bool IsFaceVisibleAtChunkBoundary(int x, int y, int z, int face) - { - if (!FaceToNeighborMap.TryGetValue(face, out Orientation neighborOrientation)) - { - // top bottom faces always visible at chunk bounds - return true; - } - - if (!Neighbors.TryGetValue(neighborOrientation, out Chunk neighbor) || neighbor == null) - { - return true; // no neighbor, face is visible - } - - // Calculate coordinates in neighbor chunk - var offset = Offsets[face]; - int localX = x; - int localZ = z; - - if (offset.dx != 0) // east, west face - { - localX = WrapCoordinate(x + offset.dx, Size); - } - else if (offset.dz != 0) // north, south face - { - localZ = WrapCoordinate(z + offset.dz, Size); - } - - return neighbor.GetBlock(localX, y, localZ) == Blocks.Air; - } - - private int WrapCoordinate(int coord, int size) - { - if (coord < 0) return coord + size; - if (coord >= size) return coord - size; - return coord; - } - - private void AddFace(int x, int y, int z, int face, Textures texture, List faces) - { - byte lightLevel = 15; - - faces.Add(new FaceData( - (byte)x, - (byte)y, - (byte)z, - (Orientation)face, - texture, - lightLevel - )); - } - - public ChunkMesh GetChunkMesh() - { - return _chunkMesh; - } - } -} diff --git a/Input.cs b/Core/Input.cs similarity index 98% rename from Input.cs rename to Core/Input.cs index 6054e56..797d1f2 100644 --- a/Input.cs +++ b/Core/Input.cs @@ -1,7 +1,7 @@ using OpenTK.Windowing.Common; using OpenTK.Windowing.GraphicsLibraryFramework; -namespace Voxel +namespace Voxel.Core { public static class Input { diff --git a/Window.cs b/Core/Window.cs similarity index 98% rename from Window.cs rename to Core/Window.cs index 67cd9c6..8472b99 100644 --- a/Window.cs +++ b/Core/Window.cs @@ -3,8 +3,9 @@ using OpenTK.Windowing.Common; using OpenTK.Windowing.Common.Input; using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; +using Voxel.Graphics; -namespace Voxel +namespace Voxel.Core { public class Window(int width, int height, string title) : GameWindow(GameWindowSettings.Default, new NativeWindowSettings() { ClientSize = (width, height), Title = title }) { diff --git a/Entity.cs b/Entities/Entity.cs similarity index 97% rename from Entity.cs rename to Entities/Entity.cs index f25472c..b7111de 100644 --- a/Entity.cs +++ b/Entities/Entity.cs @@ -5,8 +5,10 @@ using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; +using Voxel.Core; +using Voxel.Physics; -namespace Voxel +namespace Voxel.Entities { public class Entity { diff --git a/Player.cs b/Entities/Player.cs similarity index 89% rename from Player.cs rename to Entities/Player.cs index 05ae595..1bb49db 100644 --- a/Player.cs +++ b/Entities/Player.cs @@ -1,8 +1,10 @@ using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.GraphicsLibraryFramework; +using Voxel.Core; +using Voxel.Graphics; -namespace Voxel +namespace Voxel.Entities { public class Player : Entity { @@ -152,23 +154,12 @@ namespace Voxel public void SwitchBlock(MouseWheelEventArgs e) { - var keys = BlockDefinitions.Blocks.Keys.ToList(); + int count = BlockDefinitions.CreativeInventory.Length; + int direction = e.OffsetY < 0 ? -1 : 1; - bool inverted = false; - - if (e.OffsetY < 0) - inverted = true; + _blockIndex = (_blockIndex + direction + count) % count; + _selectedBlock = BlockDefinitions.CreativeInventory[_blockIndex]; - if (inverted) - if (_blockIndex == 0) - _blockIndex = keys.Count -1; - else - _blockIndex -= 1; - else - _blockIndex += 1; - - _blockIndex = _blockIndex % keys.Count; - _selectedBlock = keys[_blockIndex]; Console.WriteLine(_selectedBlock); } } diff --git a/Camera.cs b/Graphics/Camera.cs similarity index 98% rename from Camera.cs rename to Graphics/Camera.cs index dccc0c6..24c7401 100644 --- a/Camera.cs +++ b/Graphics/Camera.cs @@ -1,6 +1,6 @@ using OpenTK.Mathematics; -namespace Voxel +namespace Voxel.Graphics { static class Camera { diff --git a/ChunkMesh.cs b/Graphics/ChunkMesh.cs similarity index 97% rename from ChunkMesh.cs rename to Graphics/ChunkMesh.cs index 9606a65..43cb958 100644 --- a/ChunkMesh.cs +++ b/Graphics/ChunkMesh.cs @@ -1,4 +1,4 @@ -namespace Voxel +namespace Voxel.Graphics { public class ChunkMesh { diff --git a/FaceData.cs b/Graphics/FaceData.cs similarity index 97% rename from FaceData.cs rename to Graphics/FaceData.cs index 379acf0..db1aab1 100644 --- a/FaceData.cs +++ b/Graphics/FaceData.cs @@ -1,6 +1,7 @@ using System.Runtime.InteropServices; +using Voxel.Core; -namespace Voxel +namespace Voxel.Graphics { [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct FaceData diff --git a/Renderer.cs b/Graphics/Renderer.cs similarity index 76% rename from Renderer.cs rename to Graphics/Renderer.cs index 0e68fb0..0421d1d 100644 --- a/Renderer.cs +++ b/Graphics/Renderer.cs @@ -1,8 +1,9 @@ using OpenTK.Graphics.OpenGL4; using OpenTK.Mathematics; +using Voxel.Core; using static System.Runtime.InteropServices.JavaScript.JSType; -namespace Voxel +namespace Voxel.Graphics { static class Renderer { @@ -19,7 +20,7 @@ namespace Voxel { string vertexPath = "Shaders/shader.vert"; string fragmentPath = "Shaders/shader.frag"; - string texturePath = "atlas.png"; + string texturePath = "Assets/atlas.png"; _shader = new Shader(vertexPath, fragmentPath); _texture = new Texture(texturePath); @@ -56,6 +57,34 @@ namespace Voxel RenderUi(); } + public static void UpdateChunkBuffer(Chunk chunk) + { + if (_world == null || !_chunkBufferSizes.TryGetValue((chunk.X, chunk.Y), out int faceOffset)) + return; + + ChunkMesh chunkMesh = chunk.GetChunkMesh(); + byte[] data = chunkMesh.GetPackedData(); + + int newByteSize = chunkMesh.Size * 4; + int currentAllocatedBytes = GetAllocatedSizeForChunk(chunk.X, chunk.Y); + + if (newByteSize <= currentAllocatedBytes) + { + GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _ssbo); + GL.BufferSubData(BufferTarget.ShaderStorageBuffer, (IntPtr)(faceOffset * 4), newByteSize, data); + } + else + { + MarkBuffersDirty(); + } + } + + private static int GetAllocatedSizeForChunk(int x, int y) + { + // todo, memory manager + return 0; + } + private static void UpdateAllChunksBuffer() { if (_world == null) return; diff --git a/Shader.cs b/Graphics/Shader.cs similarity index 96% rename from Shader.cs rename to Graphics/Shader.cs index 1a88320..c1023f2 100644 --- a/Shader.cs +++ b/Graphics/Shader.cs @@ -1,7 +1,7 @@ using OpenTK.Graphics.OpenGL4; using OpenTK.Mathematics; -namespace Voxel +namespace Voxel.Graphics { public class Shader { @@ -27,7 +27,7 @@ namespace Voxel Console.WriteLine(infoLog); } - GL.CompileShader(fragmentShader); + GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out success); if (success == 0) { string infoLog = GL.GetShaderInfoLog(fragmentShader); diff --git a/Texture.cs b/Graphics/Texture.cs similarity index 68% rename from Texture.cs rename to Graphics/Texture.cs index 8cf1417..141c85a 100644 --- a/Texture.cs +++ b/Graphics/Texture.cs @@ -19,14 +19,18 @@ namespace Voxel private void LoadFromFile() { StbImage.stbi_set_flip_vertically_on_load(1); - ImageResult image = ImageResult.FromStream(File.OpenRead(_path), ColorComponents.RedGreenBlueAlpha); - GL.BindTexture(TextureTarget.Texture2D, _handle); - GL.TexImage2D( - TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, - image.Width, image.Height, 0, - PixelFormat.Rgba, PixelType.UnsignedByte, image.Data - ); + using (var stream = File.OpenRead(_path)) + { + ImageResult image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + + GL.BindTexture(TextureTarget.Texture2D, _handle); + GL.TexImage2D( + TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, + image.Width, image.Height, 0, + PixelFormat.Rgba, PixelType.UnsignedByte, image.Data + ); + } GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); diff --git a/Textures.cs b/Graphics/Textures.cs similarity index 100% rename from Textures.cs rename to Graphics/Textures.cs diff --git a/AABB.cs b/Physics/AABB.cs similarity index 99% rename from AABB.cs rename to Physics/AABB.cs index 4d0c249..995071d 100644 --- a/AABB.cs +++ b/Physics/AABB.cs @@ -1,6 +1,6 @@ using OpenTK.Mathematics; -namespace Voxel +namespace Voxel.Physics { public struct AABB { diff --git a/Program.cs b/Program.cs index 92c218d..4801ca5 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,7 @@ using OpenTK.Mathematics; -using Voxel; +using Voxel.Core; +using Voxel.Entities; +using Voxel.Graphics; internal class Program { diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/UIElement.cs b/UI/UIElement.cs similarity index 100% rename from UIElement.cs rename to UI/UIElement.cs diff --git a/Voxel.csproj b/Voxel.csproj index d1caf0c..3b66508 100644 --- a/Voxel.csproj +++ b/Voxel.csproj @@ -13,7 +13,7 @@ - + Always diff --git a/World/Blocks.cs b/World/Blocks.cs new file mode 100644 index 0000000..32406f8 --- /dev/null +++ b/World/Blocks.cs @@ -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); + } +} diff --git a/World/Chunk.cs b/World/Chunk.cs new file mode 100644 index 0000000..428b267 --- /dev/null +++ b/World/Chunk.cs @@ -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 _faceBuffer = new List(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 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; + } + } +} \ No newline at end of file diff --git a/World.cs b/World/World.cs similarity index 83% rename from World.cs rename to World/World.cs index 491c6f5..2e297e5 100644 --- a/World.cs +++ b/World/World.cs @@ -1,6 +1,8 @@ using OpenTK.Mathematics; +using Voxel.Graphics; +using Voxel.Physics; -namespace Voxel +namespace Voxel.Core { public class World { @@ -43,13 +45,14 @@ namespace Voxel 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.Neighbors[opposite] = null; + neighbor.SetNeighbor(opposite, null); neighbor.UpdateChunkMesh(); } } @@ -111,16 +114,18 @@ namespace Voxel public void UpdateNeighboringChunks(Chunk chunk) { - chunk.Neighbors[Orientation.West] = null; - chunk.Neighbors[Orientation.East] = null; - chunk.Neighbors[Orientation.North] = null; - chunk.Neighbors[Orientation.South] = null; + for (int i = 0; i < 6; i++) + { + chunk.SetNeighbor((Orientation)i, null); + } foreach (var (orientation, neighbor) in GetChunkNeighbors(chunk)) { Orientation opposite = GetOppositeOrientation(orientation); - neighbor.Neighbors[opposite] = chunk; - chunk.Neighbors[orientation] = neighbor; + + neighbor.SetNeighbor(opposite, chunk); + chunk.SetNeighbor(orientation, neighbor); + neighbor.UpdateChunkMesh(); } } @@ -162,25 +167,24 @@ namespace Voxel chunk.SetBlock(localX, worldY, localZ, block, updateMesh: updateMesh); // temporary tnt functionality - if (block == Blocks.TNT) { - int radius = 4; Explode(worldX, worldY, worldZ, 4); + return; } - if (updateMesh == false) return; + if (!updateMesh) return; - if (block == Blocks.Air && IsOnChunkBorder(worldX, worldZ)) - { - foreach (var orientation in GetEdgeOrientations(localX, localZ, Chunk.Size)) - { - if (chunk.Neighbors.TryGetValue(orientation, out var neighbor) && neighbor != null) - { - neighbor.UpdateChunkMesh(); - } - } - } + 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) @@ -198,51 +202,46 @@ namespace Voxel // bounding box int minX = centerX - radius; int maxX = centerX + radius; - int minY = centerY - radius; - int maxY = centerY + 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 y = minY; y <= maxY; y++) + for (int z = minZ; z <= maxZ; z++) { - 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) { - // no update - SetBlock(x, y, z, Blocks.Air, updateMesh: false); - - // add chunk to update list - var (chunkX, chunkZ, localX, localZ) = WorldToChunkCoords(x, z); - Chunk chunk = GetChunk(chunkX, chunkZ); - - if (chunk != null) - affectedChunks.Add(chunk); - - if (IsOnChunkBorder(x, z)) - { - foreach (var orientation in GetEdgeOrientations(localX, localZ, Chunk.Size)) - { - if (chunk.Neighbors.TryGetValue(orientation, out var neighbor) && neighbor != null) - { - affectedChunks.Add(neighbor); - } - } - } + chunk.SetBlock(localX, y, localZ, Blocks.Air, updateMesh: true); } } } } - // Now rebuild meshes for all affected chunks - foreach (var chunk in affectedChunks) + RebuildDirtyChunks(); + } + + private void RebuildDirtyChunks() + { + foreach (var chunk in _chunks.Values) { - chunk.UpdateChunkMesh(); + if (chunk.IsDirty) + { + chunk.UpdateChunkMesh(); + Renderer.UpdateChunkBuffer(chunk); + chunk.IsDirty = false; + } } } diff --git a/Noise/FastNoiseLite.cs b/Worldgen/Noise/FastNoiseLite.cs similarity index 100% rename from Noise/FastNoiseLite.cs rename to Worldgen/Noise/FastNoiseLite.cs diff --git a/Worldgen.cs b/Worldgen/Worldgen.cs similarity index 99% rename from Worldgen.cs rename to Worldgen/Worldgen.cs index 7b8b898..1db77cb 100644 --- a/Worldgen.cs +++ b/Worldgen/Worldgen.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Voxel.Core; namespace Voxel {