From 5229da40634728f14d1701ff3624d44f3421bee6 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 16:08:42 +0300 Subject: [PATCH 1/2] #134 Minecraft terrain logic --- Examples/MinecraftClone/Inventory.cs | 21 +- Examples/MinecraftClone/Minecraft.cs | 182 ++----- .../{ => Terrain}/Block/BlockBase.cs | 9 +- .../{ => Terrain/Block}/BlockFactory.cs | 28 +- .../MinecraftClone/Terrain/Block/BlockId.cs | 26 + .../{ => Terrain}/Block/Dirt.cs | 9 +- .../MinecraftClone/Terrain/Block/Grass.cs | 21 + Examples/MinecraftClone/Terrain/Block/Iron.cs | 17 + Examples/MinecraftClone/Terrain/Block/Leaf.cs | 17 + .../{ => Terrain}/Block/Stone.cs | 15 +- Examples/MinecraftClone/Terrain/Block/Wood.cs | 17 + Examples/MinecraftClone/Terrain/ChunkData.cs | 57 +++ .../Terrain/ChunkGenerationContext.cs | 27 + .../Terrain/IChunkGenerationLayer.cs | 6 + .../Terrain/Layers/BaseTerrainPass.cs | 59 +++ .../MinecraftClone/Terrain/Layers/CavePass.cs | 54 ++ .../Terrain/Layers/HeightMapPass.cs | 26 + .../Terrain/TerrainGenerator.cs | 158 ++++++ .../Terrain/TerrainGenerator_Old.cs | 145 ++++++ .../Terrain/terrain_layering_pipeline.md | 478 ++++++++++++++++++ 20 files changed, 1196 insertions(+), 176 deletions(-) rename Examples/MinecraftClone/{ => Terrain}/Block/BlockBase.cs (92%) rename Examples/MinecraftClone/{ => Terrain/Block}/BlockFactory.cs (54%) create mode 100644 Examples/MinecraftClone/Terrain/Block/BlockId.cs rename Examples/MinecraftClone/{ => Terrain}/Block/Dirt.cs (83%) create mode 100644 Examples/MinecraftClone/Terrain/Block/Grass.cs create mode 100644 Examples/MinecraftClone/Terrain/Block/Iron.cs create mode 100644 Examples/MinecraftClone/Terrain/Block/Leaf.cs rename Examples/MinecraftClone/{ => Terrain}/Block/Stone.cs (50%) create mode 100644 Examples/MinecraftClone/Terrain/Block/Wood.cs create mode 100644 Examples/MinecraftClone/Terrain/ChunkData.cs create mode 100644 Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs create mode 100644 Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/CavePass.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs create mode 100644 Examples/MinecraftClone/Terrain/TerrainGenerator.cs create mode 100644 Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs create mode 100644 Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md diff --git a/Examples/MinecraftClone/Inventory.cs b/Examples/MinecraftClone/Inventory.cs index ed8b7eb..b195560 100644 --- a/Examples/MinecraftClone/Inventory.cs +++ b/Examples/MinecraftClone/Inventory.cs @@ -1,4 +1,5 @@ -using System; +using Minecraft.Terrain.Block; +using System; using System.Linq; namespace Minecraft; @@ -31,7 +32,7 @@ public class Inventory public InventoryItem GetActiveSlotItems(int slotNumber) => Toolbar[slotNumber].Items; - /*public void AddItem(BlockType type) + /*public void AddItem(BlockId type) { var item = Items.FirstOrDefault(x => x.Type == type); if (item is not null) @@ -43,10 +44,10 @@ public InventoryItem GetActiveSlotItems(int slotNumber) /// /// Adds an item to the toolbar. /// - /// The type of the block to add to the toolbar. - public void AddToolbarItem(BlockType blockType) + /// The type of the block to add to the toolbar. + public void AddToolbarItem(BlockId BlockId) { - var slot = Toolbar.FirstOrDefault(i => i.Items.Type == blockType); + var slot = Toolbar.FirstOrDefault(i => i.Items.Type == BlockId); if (slot is not null) { slot.Items.Amount += 1; @@ -57,18 +58,18 @@ public void AddToolbarItem(BlockType blockType) return; } - if (SelectedSlot.Items.Type == BlockType.None) + if (SelectedSlot.Items.Type == BlockId.Air) { - SelectedSlot.Items = new InventoryItem { Type = blockType, Amount = 1 }; + SelectedSlot.Items = new InventoryItem { Type = BlockId, Amount = 1 }; Toolbar[SelectedSlotIndex] = SelectedSlot; return; } for (int i = 0; i < Toolbar.Length; i++) { - if (Toolbar[i].Items.Type == BlockType.None) + if (Toolbar[i].Items.Type == BlockId.Air) { - Toolbar[i].Items = new InventoryItem { Type = blockType, Amount = 1 }; + Toolbar[i].Items = new InventoryItem { Type = BlockId, Amount = 1 }; return; } } @@ -130,7 +131,7 @@ public class InventoryItem /// /// Gets or sets the type of the block in the inventory. /// - public BlockType Type { get; set; } = BlockType.None; + public BlockId Type { get; set; } = BlockId.Air; /// Gets or sets the amount of the block. public int Amount { get; set; } diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index 1b347e2..a7c4252 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -1,24 +1,17 @@ using ImGuiNET; - -using Minecraft.Block; using Microsoft.Extensions.Logging; - +using Minecraft.Terrain.Block; +using Minecraft.Terrain.Layers; using SharpEngine.Core; using SharpEngine.Core.Entities; -using SharpEngine.Core.Entities.Lights; -using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.UI; -using SharpEngine.Core.Entities.UI.Layouts; using SharpEngine.Core.Enums; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Windowing; -using SharpEngine.Core.ObjLoader.Loaders.ObjLoader; - using Silk.NET.Input; - using System; -using System.Numerics; namespace Minecraft; @@ -32,14 +25,14 @@ public class Minecraft : Game private readonly Scene _scene; private readonly ILogger _logger; - private SceneNode _lightsNode; - private SceneNode _blocksNode; - private Input _input; private readonly Inventory _inventory; private UIElement _uiElem; + private readonly Terrain.TerrainGenerator_New _terrain; + private readonly SceneNode _blocksNode; + /// /// Gets the main window. /// @@ -60,6 +53,17 @@ public Minecraft(Scene scene, ISettings settings, ILogger logger) _inventory = new Inventory(); + _blocksNode = _scene.AddNode("Blocks"); + _terrain = new Terrain.TerrainGenerator_New(_scene, _blocksNode, + [ + new HeightMapPass(), + new BaseTerrainPass(), + new CavePass(), + // new OrePass(), + // new WaterPass(), + // new SurfaceDecorationPass(), + // new FoliagePass() + ]); } /// @@ -72,26 +76,9 @@ public override void Initialize() _input = new Input(Camera); _inventory.Initialize(); - _lightsNode = _scene.Root.AddChild("lights"); - _blocksNode = _scene.Root.AddChild("blocks"); + _terrain.InitializeWorld(); - var gridLayout = new GridLayout(); - - // TODO: #89 Fix UI renderer - _uiElem = new UIElement("uiElement"); - _scene.UIElements.Add(_uiElem); - - var uiElem2 = new UIElement("uiElement"); - uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); - uiElem2.Transform.Position = new SharpEngine.Core.Numerics.Vector2(30, 0); - - gridLayout.AddChild(_uiElem, uiElem2); - _scene.UIElements.Add(_uiElem); - _scene.UIElements.Add(uiElem2); - - _scene.UIElements.Add(gridLayout); - - InitializeWorld(); + InitializeUI(); } catch (Exception ex) { @@ -99,6 +86,25 @@ public override void Initialize() } } + private void InitializeUI() + { + // var gridLayout = new GridLayout(); + + // TODO: #89 Fix UI renderer + _uiElem = new UIElement(Window.GetGL(), "uiElement"); + _scene.UIElements.Add(_uiElem); + + var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); + uiElem2.Transform.Scale = new Vector2(0.2f, 0.2f); + uiElem2.Transform.Position = new Vector2(30, 0); + + // gridLayout.AddChild(_uiElem, uiElem2); + _scene.UIElements.Add(_uiElem); + _scene.UIElements.Add(uiElem2); + + // _scene.UIElements.Add(gridLayout); + } + /// /// Handles rendering after the frame is drawn. /// @@ -133,10 +139,10 @@ public void OnAfterRender(Frame frame) ImGui.SliderFloat("rotation", ref rotation, 0, 360); ImGui.Text($"UI Element width: {_uiElem.Width}"); - ImGui.SliderFloat("width", ref width, 0, 100); + ImGui.SliderFloat("width", ref width, 0, Window.Width); ImGui.Text($"UI Element height: {_uiElem.Height}"); - ImGui.SliderFloat("height", ref height, 0, 100); + ImGui.SliderFloat("height", ref height, 0, Window.Height); ImGui.End(); @@ -148,82 +154,6 @@ public void OnAfterRender(Frame frame) _uiElem.Width = width; } - private void InitializeWorld() - { - InitializeLights(); - InitializeChunks(); - - // TODO: #2 Does not work yet. - // var torus = MeshService.Instance.LoadMesh("torus", @"C:\Users\antti\Documents\Untitled2.obj"); - - var model = ObjLoaderFactory.Load(Window.GetGL(), @"C:\Users\antti\Documents\Untitled2.obj"); - var go = new GameObject(model); - var go2 = new GameObject(model) - { - Transform = new Transform(new SharpEngine.Core.Numerics.Vector3(0, 0, 30)) - }; - - _scene.Root.AddChild(go); - _scene.Root.AddChild(go2); - } - - private void InitializeLights() - { - _lightsNode.AddChild(new DirectionalLight()); - - _lightsNode.AddChild( - new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), - new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), - new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), - new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) - ); - - _lightsNode.AddChild(new SpotLight() - { - Ambient = new Vector3(0.0f, 0.0f, 0.0f), - Diffuse = new Vector3(1.0f, 1.0f, 1.0f), - Specular = new Vector3(1.0f, 1.0f, 1.0f), - }); - } - - private void InitializeChunks() - { - // TODO: #88 Generate chunks when player moves - - // TODO: #87 Generate chunks using 3d Perlin noise - - const int chunkSize = 16; - const int numChunks = 1; - // const int numChunks = 3; - - for (int i = 0; i < numChunks; i++) - { - var chunkPos = new Vector3(i * chunkSize, 0, 0); - GenerateChunk(chunkSize, chunkPos); - } - } - - private void GenerateChunk(int chunkSize, Vector3 chunkPos) - { - for (int x = 0; x < chunkSize; x++) - { - for (int z = 0; z < chunkSize; z++) - { - var blockPos = chunkPos + new Vector3(x, 0, z); - - var dirt = new Dirt(blockPos, $"Dirt ({x}{z})"); - _blocksNode.AddChild(dirt); - - for (int y = 1; y < chunkSize; y++) - { - blockPos.Y = -y; - var stone = new Stone(blockPos, $"Dirt ({x}{z}.{y})"); - _blocksNode.AddChild(stone); - } - } - } - } - /// public override void Update(double deltaTime, IInputContext input) => _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); @@ -232,7 +162,7 @@ public override void Update(double deltaTime, IInputContext input) /// public override void HandleKeyboard(IKeyboard input, double deltaTime) { - for (int i = 0; i <= 9; i++) + for (int i = 1; i <= 9; i++) { if (input.IsKeyPressed(Key.Number0 + i)) { @@ -262,13 +192,13 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) { if (button == MouseButton.Right) { - if (_inventory.SelectedSlot.Items.Type != BlockType.None && _inventory.SelectedSlot.Items.Amount > 0) + if (_inventory.SelectedSlot.Items.Type != BlockId.Air && _inventory.SelectedSlot.Items.Amount > 0) { PlaceBlock(); _inventory.SelectedSlot.Items.Amount -= 1; if (_inventory.SelectedSlot.Items.Amount < 0) - _inventory.SelectedSlot.Items.Type = BlockType.None; + _inventory.SelectedSlot.Items.Type = BlockId.Air; } else { @@ -278,38 +208,38 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) if (button == MouseButton.Left) { - var destroyedBlockType = DestroyBlock(); - if (destroyedBlockType != BlockType.None) + var destroyedBlockId = DestroyBlock(); + if (destroyedBlockId != BlockId.Air) { // TODO: #86 The block should be added to the slot so that 0 is the last slot instead of 9. // TODO: #86 The first block destroyed doesn't seem to be added to the inventory. - _logger.LogInformation("Block destroyed: {DestroyedBlockType}.", destroyedBlockType); - _inventory.AddToolbarItem(destroyedBlockType); + _logger.LogInformation("Block destroyed: {DestroyedBlockId}.", destroyedBlockId); + _inventory.AddToolbarItem(destroyedBlockId); } } } - private BlockType DestroyBlock() + private BlockId DestroyBlock() { - if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 _, allowedTypes: typeof(BlockBase))) - return BlockType.None; + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out _, allowedTypes: typeof(BlockBase))) + return BlockId.Air; var block = (BlockBase)intersectingObject!; _blocksNode.RemoveChild(intersectingObject!); - return block.BlockType; + return block.BlockId; } private void PlaceBlock() { - if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 hitPosition, allowedTypes: typeof(BlockBase))) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out System.Numerics.Vector3 hitPosition, allowedTypes: typeof(BlockBase))) return; - var newBlockPosition = GetNewBlockPosition(hitPosition, intersectingObject!); + var newBlockPosition = (System.Numerics.Vector3)GetNewBlockPosition((Vector3)hitPosition, intersectingObject!); if (newBlockPosition == Camera.Position || newBlockPosition == hitPosition) return; - var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); + var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, (Vector3)newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); _blocksNode.AddChild(newBlock); _logger.LogInformation("New block created: {Pos}, block in view location: {IntersectingPos}", newBlock.Transform.Position, intersectingObject!.Transform.Position); @@ -317,8 +247,8 @@ private void PlaceBlock() private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) { - Vector3 normal = Ray.GetClosestFaceNormal(hitPosition, intersectingObject); - return (System.Numerics.Vector3)intersectingObject.Transform.Position + (normal * (System.Numerics.Vector3)intersectingObject.Transform.Scale); + Vector3 normal = (Vector3)Ray.GetClosestFaceNormal(hitPosition, intersectingObject); + return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); } /// diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Terrain/Block/BlockBase.cs similarity index 92% rename from Examples/MinecraftClone/Block/BlockBase.cs rename to Examples/MinecraftClone/Terrain/Block/BlockBase.cs index 1c62d38..8ada33a 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Examples/MinecraftClone/Terrain/Block/BlockBase.cs @@ -1,9 +1,8 @@ using SharpEngine.Core.Entities; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Primitives; -using System.Numerics; - -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; /// /// Represents a block in the game. @@ -13,10 +12,10 @@ public class BlockBase : GameObject /// /// Gets the block type. /// - public virtual BlockType BlockType { get; } + public virtual BlockId BlockId { get; } /// Gets a value indicating whether the block is solid. - public virtual bool IsSolid { get; } + public virtual bool IsSolid { get; } = true; /// Gets or sets the size of the block in the inventory. public int SizeInInventory { get; set; } = 1; diff --git a/Examples/MinecraftClone/BlockFactory.cs b/Examples/MinecraftClone/Terrain/Block/BlockFactory.cs similarity index 54% rename from Examples/MinecraftClone/BlockFactory.cs rename to Examples/MinecraftClone/Terrain/Block/BlockFactory.cs index 7991855..44d1deb 100644 --- a/Examples/MinecraftClone/BlockFactory.cs +++ b/Examples/MinecraftClone/Terrain/Block/BlockFactory.cs @@ -1,9 +1,7 @@ -using Minecraft.Block; - +using SharpEngine.Core.Numerics; using System; -using System.Numerics; -namespace Minecraft +namespace Minecraft.Terrain.Block { /// /// Used to create a block objects. @@ -18,27 +16,13 @@ public static class BlockFactory /// The name of the block to be created. /// The newly created block. /// - public static BlockBase CreateBlock(BlockType type, Vector3 position, string name) + public static BlockBase CreateBlock(BlockId type, Vector3 position, string name) => type switch { - BlockType.Dirt => new Dirt(position, name), - BlockType.Stone => new Stone(position, name), + BlockId.Dirt => new Dirt(position, name), + BlockId.Stone => new Stone(position, name), + BlockId.Grass => new Grass(position, name), _ => throw new ArgumentException("Invalid block type", nameof(type)), }; } - - /// - /// Represents the type of block. - /// - public enum BlockType - { - /// Represents no block type. - None, - - /// Represents a dirt block. - Dirt, - - /// Represents a stone block. - Stone - } } diff --git a/Examples/MinecraftClone/Terrain/Block/BlockId.cs b/Examples/MinecraftClone/Terrain/Block/BlockId.cs new file mode 100644 index 0000000..34b428a --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/BlockId.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Terrain.Block; + +/// +/// Represents the type of block. +/// +public enum BlockId : ushort +{ + Air = 0, + + Grass = 1, + Dirt = 2, + Stone = 3, + + Water = 10, + + CoalOre = 20, + IronOre = 21, + CopperOre = 22, + + Wood = 50, + + TallGrass = 100, + Flower = 101, + Log = 102, + Leaves = 103 +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Block/Dirt.cs b/Examples/MinecraftClone/Terrain/Block/Dirt.cs similarity index 83% rename from Examples/MinecraftClone/Block/Dirt.cs rename to Examples/MinecraftClone/Terrain/Block/Dirt.cs index 183d5d8..d76622e 100644 --- a/Examples/MinecraftClone/Block/Dirt.cs +++ b/Examples/MinecraftClone/Terrain/Block/Dirt.cs @@ -1,8 +1,8 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Extensions; -using System.Numerics; +using SharpEngine.Core.Numerics; -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; /// /// Represents a dirt block. @@ -20,8 +20,5 @@ public Dirt(Vector3 position, string name) : base(position, name, PathExtensions Default.FragmentShader) { } /// - public override BlockType BlockType => BlockType.Dirt; - - /// - public override bool IsSolid => true; + public override BlockId BlockId => BlockId.Dirt; } diff --git a/Examples/MinecraftClone/Terrain/Block/Grass.cs b/Examples/MinecraftClone/Terrain/Block/Grass.cs new file mode 100644 index 0000000..8fb3faa --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Grass.cs @@ -0,0 +1,21 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Grass : BlockBase +{ + /// + /// Initializes a new block. + /// + /// The position where the block should be initialized. + /// The name of the block to initialize. + public Grass(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\grass.jpg"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + Default.VertexShader, + Default.FragmentShader) { } + + /// + public override BlockId BlockId => BlockId.Grass; +} diff --git a/Examples/MinecraftClone/Terrain/Block/Iron.cs b/Examples/MinecraftClone/Terrain/Block/Iron.cs new file mode 100644 index 0000000..431bf46 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Iron.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Iron : BlockBase +{ + public Iron(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.IronOre; +} diff --git a/Examples/MinecraftClone/Terrain/Block/Leaf.cs b/Examples/MinecraftClone/Terrain/Block/Leaf.cs new file mode 100644 index 0000000..b047da2 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Leaf.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Leaf : BlockBase +{ + public Leaf(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.Leaves; +} diff --git a/Examples/MinecraftClone/Block/Stone.cs b/Examples/MinecraftClone/Terrain/Block/Stone.cs similarity index 50% rename from Examples/MinecraftClone/Block/Stone.cs rename to Examples/MinecraftClone/Terrain/Block/Stone.cs index 13352d3..dcd3b3f 100644 --- a/Examples/MinecraftClone/Block/Stone.cs +++ b/Examples/MinecraftClone/Terrain/Block/Stone.cs @@ -1,8 +1,8 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Extensions; -using System.Numerics; +using SharpEngine.Core.Numerics; -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; internal class Stone : BlockBase { @@ -14,13 +14,14 @@ internal class Stone : BlockBase /// /// The position where the block is created. /// The name of the object in the scene. - public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\container2.png"), - PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), - Default.VertexShader, - Default.FragmentShader) { } + public Stone(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); /// - public override BlockType BlockType => BlockType.Stone; + public override BlockId BlockId => BlockId.Stone; /// public override bool IsSolid => true; diff --git a/Examples/MinecraftClone/Terrain/Block/Wood.cs b/Examples/MinecraftClone/Terrain/Block/Wood.cs new file mode 100644 index 0000000..f58ff58 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Wood.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Wood : BlockBase +{ + public Wood(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.Wood; +} diff --git a/Examples/MinecraftClone/Terrain/ChunkData.cs b/Examples/MinecraftClone/Terrain/ChunkData.cs new file mode 100644 index 0000000..63d27c6 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/ChunkData.cs @@ -0,0 +1,57 @@ +using Minecraft.Terrain.Block; + +namespace Minecraft.Terrain; + +public sealed class ChunkData +{ + private readonly BlockId[,,] _blocks; + private readonly int[,] _heightMap; + + public ChunkData(int size, int height) + { + Size = size; + Height = height; + + _blocks = new BlockId[size, height, size]; + _heightMap = new int[size, size]; + } + + public int Size { get; } + + public int Height { get; } + + public BlockId GetBlock(int x, int y, int z) + { + return _blocks[x, y, z]; + } + + public void SetBlock(int x, int y, int z, BlockId block) + { + _blocks[x, y, z] = block; + } + + public int GetHeight(int x, int z) + { + return _heightMap[x, z]; + } + + public void SetHeight(int x, int z, int height) + { + _heightMap[x, z] = height; + } + + public bool IsInside(int x, int y, int z) + { + return x >= 0 && + y >= 0 && + z >= 0 && + x < Size && + y < Height && + z < Size; + } + + public bool IsAir(int x, int y, int z) + { + return GetBlock(x, y, z) == BlockId.Air; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs b/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs new file mode 100644 index 0000000..6f19da9 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs @@ -0,0 +1,27 @@ +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; + +namespace Minecraft.Terrain; + +public sealed class ChunkGenerationContext +{ + public required int Seed { get; init; } + + public required Vector3 ChunkPosition { get; init; } + + public required int ChunkSize { get; init; } + + public required int MinY { get; init; } + + public required int MaxY { get; init; } + + public required int WaterLevel { get; init; } + + public required INoiseGenerator TerrainNoise { get; init; } + + public required INoiseGenerator CaveNoise { get; init; } + + public required INoiseGenerator OreNoise { get; init; } + + public required INoiseGenerator FoliageNoise { get; init; } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs b/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs new file mode 100644 index 0000000..5980bbd --- /dev/null +++ b/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs @@ -0,0 +1,6 @@ +namespace Minecraft.Terrain; + +public interface IChunkGenerationLayer +{ + void Generate(ChunkGenerationContext context, ChunkData chunk); +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs b/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs new file mode 100644 index 0000000..4ad29f1 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs @@ -0,0 +1,59 @@ +using Minecraft.Terrain; +using Minecraft.Terrain.Block; +using System; + +namespace Minecraft.Terrain.Layers; + +public sealed class BaseTerrainPass : IChunkGenerationLayer +{ + private const int DirtDepth = 3; + + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int z = 0; z < chunk.Size; z++) + { + int surfaceWorldY = chunk.GetHeight(x, z); + int surfaceLocalY = WorldYToLocalY(context, surfaceWorldY); + + GenerateColumn(chunk, x, z, surfaceLocalY); + } + } + } + + private static void GenerateColumn(ChunkData chunk, int x, int z, int surfaceLocalY) + { + for (int localY = 0; localY < chunk.Height; localY++) + { + BlockId block = GetBlockForLayer(localY, surfaceLocalY); + + chunk.SetBlock(x, localY, z, block); + } + } + + private static BlockId GetBlockForLayer(int localY, int surfaceLocalY) + { + if (localY > surfaceLocalY) + return BlockId.Air; + + if (localY == 0) + return BlockId.Stone; + + int depthFromSurface = surfaceLocalY - localY; + + return depthFromSurface switch + { + 0 => BlockId.Grass, + + > 0 and <= DirtDepth => BlockId.Dirt, + + _ => BlockId.Stone + }; + } + + private static int WorldYToLocalY(ChunkGenerationContext context, int worldY) + { + return worldY - context.MinY; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/CavePass.cs b/Examples/MinecraftClone/Terrain/Layers/CavePass.cs new file mode 100644 index 0000000..6239107 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/CavePass.cs @@ -0,0 +1,54 @@ +using Minecraft.Terrain.Block; + +namespace Minecraft.Terrain.Layers; + +public sealed class CavePass : IChunkGenerationLayer +{ + private const float CaveThreshold = 0.68f; + + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int y = 0; y < chunk.Height; y++) + { + for (int z = 0; z < chunk.Size; z++) + { + TryCarveBlock(context, chunk, x, y, z); + } + } + } + } + + private static void TryCarveBlock( + ChunkGenerationContext context, + ChunkData chunk, + int localX, + int localY, + int localZ) + { + if (localY == 0) + return; + + BlockId block = chunk.GetBlock(localX, localY, localZ); + + if (block != BlockId.Stone) + return; + + float worldX = context.ChunkPosition.X + localX; + float worldY = LocalYToWorldY(context, localY); + float worldZ = context.ChunkPosition.Z + localZ; + + float caveNoise = context.CaveNoise.Sample3D(worldX, worldY, worldZ); + + if (caveNoise <= CaveThreshold) + return; + + chunk.SetBlock(localX, localY, localZ, BlockId.Air); + } + + private static int LocalYToWorldY(ChunkGenerationContext context, int localY) + { + return context.MinY + localY; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs b/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs new file mode 100644 index 0000000..cffdedc --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Terrain.Layers; + +internal class HeightMapPass : IChunkGenerationLayer +{ + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int z = 0; z < chunk.Size; z++) + { + float worldX = context.ChunkPosition.X + x; + float worldZ = context.ChunkPosition.Z + z; + + float noise = context.TerrainNoise.Sample2D(worldX, worldZ); + + int minSurfaceY = 0; + int maxSurfaceY = 16; + + int surfaceY = minSurfaceY + + (int)(noise * (maxSurfaceY - minSurfaceY)); + + chunk.SetHeight(x, z, surfaceY); + } + } + } +} diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs new file mode 100644 index 0000000..b71e3e9 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs @@ -0,0 +1,158 @@ +using Minecraft.Terrain.Block; +using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; +using SharpEngine.Core.Scenes; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Minecraft.Terrain; + +public sealed class TerrainGenerator_New +{ + private readonly INoiseGenerator _terrainNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + private readonly INoiseGenerator _caveNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + // TODO: Tweak + private readonly INoiseGenerator _oreNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + // TODO: Tweak + private readonly INoiseGenerator _foliageNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + private readonly IReadOnlyList _layers; + + private readonly Scene _scene; + + private SceneNode _lightsNode; + private SceneNode _blocksNode; + + internal TerrainGenerator_New(Scene scene, SceneNode blocksNode, IEnumerable? layers = null) + { + _scene = scene; + _lightsNode = _scene.AddNode("Lights"); + _blocksNode = blocksNode; + _layers = layers?.ToArray() ?? Array.Empty(); + } + + internal void InitializeWorld() + { + InitializeLights(); + InitializeChunks(); + } + + private void InitializeLights() + { + _lightsNode.AddChild(new DirectionalLight()); + + _lightsNode.AddChild( + new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + ); + + _lightsNode.AddChild(new SpotLight() + { + Ambient = new Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new Vector3(1.0f, 1.0f, 1.0f), + Specular = new Vector3(1.0f, 1.0f, 1.0f), + }); + } + + private void InitializeChunks() + { + const int chunkSize = 16; + const int numChunks = 5; + + for (int chunkZ = 0; chunkZ < numChunks; chunkZ++) + { + for (int chunkX = 0; chunkX < numChunks; chunkX++) + { + var chunkPos = new Vector3(chunkX * chunkSize, 0, chunkZ * chunkSize); + + var context = new ChunkGenerationContext + { + Seed = 1234, + ChunkPosition = chunkPos, + ChunkSize = chunkSize, + MinY = -16, + MaxY = 16, + WaterLevel = 0, + + TerrainNoise = _terrainNoise, + CaveNoise = _caveNoise, + OreNoise = _oreNoise, + FoliageNoise = _foliageNoise + }; + + ChunkData chunk = GenerateChunk(context); + + CreateBlocksFromChunkData(chunk, chunkPos); + } + } + } + + public ChunkData GenerateChunk(ChunkGenerationContext context) + { + int height = context.MaxY - context.MinY + 1; + + var chunk = new ChunkData(context.ChunkSize, height); + + foreach (IChunkGenerationLayer layer in _layers) + { + layer.Generate(context, chunk); + } + + return chunk; + } + + private void CreateBlocksFromChunkData(ChunkData chunk, Vector3 chunkPos) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int y = 0; y < chunk.Height; y++) + { + for (int z = 0; z < chunk.Size; z++) + { + BlockId block = chunk.GetBlock(x, y, z); + + if (block == BlockId.Air) + continue; + + var blockPos = chunkPos + new Vector3(x, y, z); + + var blockObject = BlockFactory.CreateBlock(block, blockPos, $"Block_{blockPos.X}_{blockPos.Y}_{blockPos.Z}"); + + _blocksNode.AddChild(blockObject); + } + } + } + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs new file mode 100644 index 0000000..84ee4f0 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs @@ -0,0 +1,145 @@ +using Minecraft.Terrain.Block; +using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; +using SharpEngine.Core.Scenes; + +namespace Minecraft.Terrain; + +internal class TerrainGenerator_Old +{ + private readonly INoiseGenerator _terrainNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + private readonly INoiseGenerator _caveNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + private readonly Scene _scene; + + private SceneNode _lightsNode; + private SceneNode _blocksNode; + + internal TerrainGenerator_Old(Scene scene, SceneNode blocksNode) + { + _scene = scene; + _lightsNode = _scene.AddNode("Lights"); + _blocksNode = blocksNode; + } + + internal void InitializeWorld() + { + InitializeLights(); + InitializeChunks(); + } + + private void InitializeLights() + { + _lightsNode.AddChild(new DirectionalLight()); + + _lightsNode.AddChild( + new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + ); + + _lightsNode.AddChild(new SpotLight() + { + Ambient = new Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new Vector3(1.0f, 1.0f, 1.0f), + Specular = new Vector3(1.0f, 1.0f, 1.0f), + }); + } + + private void InitializeChunks() + { + // TODO: #88 Generate chunks when player moves + + // TODO: #87 Generate chunks using 3d Perlin noise + + const int chunkSize = 16; + const int numChunks = 1; + // const int numChunks = 3; + + for (int i = 0; i < numChunks; i++) + { + var chunkPos = new Vector3(i * chunkSize, 0, 0); + GenerateChunk(chunkSize, chunkPos); + } + } + + private void GenerateChunk(int chunkSize, Vector3 chunkPos) + { + const int minTerrainHeight = 4; + const int maxTerrainHeight = 16; + + const int dirtDepth = 3; + + const int lowestY = -16; + + const float caveThreshold = 0.68f; + + for (int x = 0; x < chunkSize; x++) + { + for (int z = 0; z < chunkSize; z++) + { + float worldX = chunkPos.X + x; + float worldZ = chunkPos.Z + z; + + float terrainNoise = _terrainNoise.Sample2D(worldX, worldZ); + + int columnHeight = minTerrainHeight + + (int)(terrainNoise * (maxTerrainHeight - minTerrainHeight)); + + int surfaceY = (int)chunkPos.Y + columnHeight - 1; + int bottomY = (int)chunkPos.Y + lowestY; + + for (int worldY = surfaceY; worldY >= bottomY; worldY--) + { + var blockPos = new Vector3(worldX, worldY, worldZ); + + int depthFromSurface = surfaceY - worldY; + + if (depthFromSurface == 0) + { + var grass = new Grass(blockPos, $"Grass ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(grass); + + continue; + } + + if (depthFromSurface <= dirtDepth) + { + var dirt = new Dirt(blockPos, $"Dirt ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(dirt); + + continue; + } + + bool isLowestLevel = worldY == bottomY; + + if (!isLowestLevel) + { + float caveNoise = _caveNoise.Sample3D(worldX, worldY, worldZ); + + if (caveNoise > caveThreshold) + continue; + } + + var stone = new Stone(blockPos, $"Stone ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(stone); + } + } + } + } +} diff --git a/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md b/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md new file mode 100644 index 0000000..a971e31 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md @@ -0,0 +1,478 @@ +# Terrain Layering and Generation Pipeline + +This document describes a scalable terrain layering approach for chunk-based world generation. + +The goal is to avoid placing every terrain rule directly inside one large block loop. Instead, terrain generation should be treated as a sequence of independent generation passes. + +Each pass has one responsibility and modifies lightweight chunk/block data before the final renderable blocks or meshes are created. + +--- + +## Core Idea + +Terrain generation should be split into two main concerns: + +```text +Terrain shape + Determines where terrain exists. + +Terrain materials + Determines what each block should be. +``` + +For example: + +```text +Shape: + - Column height + - Cave positions + - Terrain slope + - Sea level + - Mountain regions + +Materials: + - Grass + - Dirt + - Stone + - Ore + - Water + - Foliage +``` + +This makes terrain easier to extend later with features such as ores, trees, flowers, water, snow, sand, or biome-specific surfaces. + +--- + +## Recommended Generation Pipeline + +```mermaid +flowchart TD + A[Chunk Coordinates] --> B[Height Map Pass] + + B --> C[Base Terrain Pass] + C --> C1[Grass Surface Layer] + C --> C2[Dirt Subsurface Layer] + C --> C3[Stone Underground Layer] + C --> C4[Bottom Stone / Bedrock Layer] + + C1 --> D[Chunk Block Data] + C2 --> D + C3 --> D + C4 --> D + + D --> E[Cave Pass] + E --> E1[Carve Only Stone] + E1 --> F[Ore Pass] + + F --> F1[Replace Stone With Ore] + F1 --> G[Water Pass] + + G --> G1[Fill Empty Blocks Below Water Level] + G1 --> H[Surface Decoration Pass] + + H --> H1[Grass Tufts] + H --> H2[Flowers] + H --> H3[Rocks] + + H1 --> I[Foliage Pass] + H2 --> I + H3 --> I + + I --> I1[Trees] + I --> I2[Bushes] + I --> I3[Large Plants] + + I1 --> J[Finalize Chunk] + I2 --> J + I3 --> J + + J --> K[Mesh / GameObject Creation] +``` + +--- + +## Layer Responsibilities + +### 1. Height Map Pass + +The height map pass decides the surface height for each `x/z` column. + +It should answer: + +```text +How tall is the terrain at this world position? +``` + +Typical inputs: + +- World seed +- World `x/z` position +- 2D noise +- Biome data +- Terrain settings + +Example output: + +```text +Column at x=10, z=4 has surfaceY = 12 +``` + +The height map pass should not decide whether a block is grass, dirt, stone, ore, or water. It only defines the terrain shape. + +--- + +### 2. Base Terrain Pass + +The base terrain pass creates the main solid terrain layers. + +Recommended rule: + +```text +Highest solid block = Grass +3 blocks below surface = Dirt +Everything below that = Stone +Lowest level = Stone or Bedrock +``` + +Conceptually: + +```text +surfaceY Grass +surfaceY - 1 Dirt +surfaceY - 2 Dirt +surfaceY - 3 Dirt +surfaceY - 4 Stone +surfaceY - 5 Stone +... +bottomY Stone / Bedrock +``` + +This pass should create predictable base terrain before caves, ores, water, or decorations are added. + +--- + +### 3. Cave Pass + +The cave pass removes underground blocks based on 3D noise. + +Important rule: + +```text +Caves should only carve stone. +``` + +This prevents caves from cutting holes through grass and dirt surface layers unless that is intentionally desired. + +Recommended cave rules: + +```text +If block is Stone +And block is not the lowest layer +And cave noise is above threshold +Then replace block with Air +``` + +The lowest layer should usually remain solid to prevent chunks from having holes through the bottom. + +--- + +### 4. Ore Pass + +The ore pass replaces some stone blocks with ore blocks. + +Important rule: + +```text +Ores should replace stone, not grass or dirt. +``` + +Example rules: + +```text +Coal: + Allowed in stone + Common + Wide Y range + +Iron: + Allowed in stone + Medium rarity + Mid/deep Y range + +Rare ore: + Allowed in stone + Low rarity + Deep Y range +``` + +Ore generation can use: + +- 3D noise +- Random chance +- Y-level ranges +- Vein size rules +- Biome-specific modifiers + +--- + +### 5. Water Pass + +The water pass fills empty spaces below a configured water level. + +Recommended rule: + +```text +If block is Air +And y is below or equal to waterLevel +Then place Water +``` + +This should usually happen after caves are carved, because caves below sea level may become flooded. + +Water generation may also consider: + +- Oceans +- Lakes +- Rivers +- Underground water +- Biome rules + +--- + +### 6. Surface Decoration Pass + +The surface decoration pass places small objects on top of terrain. + +Examples: + +- Grass tufts +- Flowers +- Pebbles +- Mushrooms +- Snow layers +- Fallen sticks + +Recommended rule: + +```text +If block is Grass +And space above is Air +And decoration noise/random allows it +Then place decoration above the grass block +``` + +Surface decorations should usually not replace the terrain block itself. They usually occupy the air block above the surface. + +--- + +### 7. Foliage Pass + +The foliage pass handles larger vegetation. + +Examples: + +- Trees +- Bushes +- Large plants +- Cacti +- Reeds + +Tree placement usually needs more checks than small decorations: + +```text +If block is Grass +And enough vertical space exists +And enough horizontal space exists +And tree noise/random allows it +Then place tree +``` + +Large foliage should be generated late because it may occupy several blocks. + +--- + +## Recommended Pass Order + +A practical order is: + +```text +1. Height Map Pass +2. Base Terrain Pass +3. Cave Pass +4. Bottom Layer Pass +5. Ore Pass +6. Water Pass +7. Surface Decoration Pass +8. Foliage Pass +9. Final Mesh / GameObject Creation +``` + +A more abstract version: + +```text +Shape +Material +Carving +Replacement +Fluid +Decoration +Finalization +``` + +--- + +## Why Use Passes? + +Using generation passes keeps each system focused. + +Instead of one huge method containing every rule: + +```text +GenerateChunk() + handles grass + handles dirt + handles stone + handles caves + handles ores + handles water + handles trees + handles flowers +``` + +Prefer: + +```text +HeightMapPass +BaseTerrainPass +CavePass +OrePass +WaterPass +DecorationPass +FoliagePass +``` + +This makes the world generator easier to extend, debug, test, and tune. + +--- + +## Recommended Data Flow + +```mermaid +flowchart LR + A[World Seed] --> B[Generation Settings] + B --> C[Chunk Data] + C --> D[Generation Passes] + D --> E[Block IDs] + E --> F[Chunk Mesh] + E --> G[Optional GameObjects] +``` + +The generator should ideally operate on lightweight block data first. + +For example: + +```text +BlockId.Grass +BlockId.Dirt +BlockId.Stone +BlockId.Air +BlockId.Water +BlockId.IronOre +``` + +After generation is complete, the engine can convert the chunk data into: + +- A mesh +- Renderable chunk objects +- Physics colliders +- Optional `GameObject` instances for interactive blocks + +This is usually better than creating one full `GameObject` for every block during terrain generation. + +--- + +## Mental Model: Masks + +A useful way to think about terrain features is through masks. + +A mask defines where something is allowed to exist. + +Examples: + +```text +Grass mask: + Only the highest solid block in a column + +Dirt mask: + Only the first 3 blocks below grass + +Stone mask: + Everything below dirt + +Cave mask: + Only stone blocks, excluding the bottom layer + +Ore mask: + Only stone blocks within allowed Y ranges + +Water mask: + Empty space below water level + +Tree mask: + Grass surface, enough empty space above, suitable random/noise value +``` + +This avoids messy conditionals and makes each terrain feature easier to reason about. + +--- + +## Suggested Future Interfaces + +Eventually, the terrain generator could be modeled around generation passes: + +```csharp +public interface IChunkGenerationPass +{ + void Generate(ChunkGenerationContext context, ChunkData chunk); +} +``` + +Example passes: + +```csharp +public sealed class HeightMapPass : IChunkGenerationPass; +public sealed class BaseTerrainPass : IChunkGenerationPass; +public sealed class CavePass : IChunkGenerationPass; +public sealed class OrePass : IChunkGenerationPass; +public sealed class WaterPass : IChunkGenerationPass; +public sealed class FoliagePass : IChunkGenerationPass; +``` + +The chunk generator then becomes a pipeline: + +```csharp +foreach (IChunkGenerationPass pass in _passes) +{ + pass.Generate(context, chunk); +} +``` + +--- + +## Summary + +Use a terrain generation pipeline where each layer or feature is handled by a dedicated pass. + +Recommended structure: + +```text +Height map decides shape. +Base terrain creates grass, dirt and stone. +Caves carve only stone. +Ores replace stone. +Water fills empty air below water level. +Decorations and foliage are placed last. +Final chunk data is converted into renderable meshes or objects. +``` + +This keeps the system flexible enough to support future terrain features such as foliage, water, ores, biomes, snow, rivers, and structures. From 84fe2c680cd040f6ad31563c3f8434264aa3ce7c Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 19:48:56 +0300 Subject: [PATCH 2/2] #87 Use SharpEngine Numerics --- Examples/MinecraftClone/Minecraft.cs | 12 ++++++------ SharpEngine.Core/Abstractions/IGame.cs | 6 ++++-- SharpEngine.Core/Entities/Views/CameraView.cs | 6 ++++-- SharpEngine.Core/Entities/Views/View.cs | 3 ++- SharpEngine.Core/Ray.cs | 2 +- SharpEngine.Core/Windowing/Window.cs | 4 ++-- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index bb4dd7b..cb637fd 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -146,8 +146,8 @@ public void OnAfterRender(Frame frame) ImGui.End(); - _uiElem.Transform.Position = new SharpEngine.Core.Numerics.Vector2(x, y); - _uiElem.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(sx, sy); + _uiElem.Transform.Position = new Vector2(x, y); + _uiElem.Transform.Scale = new Vector2(sx, sy); _uiElem.Transform.Rotation.Angle = rotation; _uiElem.Height = height; @@ -231,14 +231,14 @@ private BlockId DestroyBlock() private void PlaceBlock() { - if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out System.Numerics.Vector3 hitPosition, allowedTypes: typeof(BlockBase))) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 hitPosition, allowedTypes: typeof(BlockBase))) return; - var newBlockPosition = (System.Numerics.Vector3)GetNewBlockPosition((Vector3)hitPosition, intersectingObject!); + var newBlockPosition = GetNewBlockPosition(hitPosition, intersectingObject!); if (newBlockPosition == Camera.Position || newBlockPosition == hitPosition) return; - var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, (Vector3)newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); + var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); _blocksNode.AddChild(newBlock); _logger.LogInformation("New block created: {Pos}, block in view location: {IntersectingPos}", newBlock.Transform.Position, intersectingObject!.Transform.Position); @@ -246,7 +246,7 @@ private void PlaceBlock() private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) { - Vector3 normal = (Vector3)Ray.GetClosestFaceNormal(hitPosition, intersectingObject); + Vector3 normal = Ray.GetClosestFaceNormal(hitPosition, intersectingObject); return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); } diff --git a/SharpEngine.Core/Abstractions/IGame.cs b/SharpEngine.Core/Abstractions/IGame.cs index 0fad3ee..73662b9 100644 --- a/SharpEngine.Core/Abstractions/IGame.cs +++ b/SharpEngine.Core/Abstractions/IGame.cs @@ -1,7 +1,9 @@ using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; + using Silk.NET.Input; namespace SharpEngine.Core.Interfaces; @@ -21,7 +23,7 @@ public abstract class Game public Game() { Scene = new Scene(); - Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + Camera = new(new Vector3(0), new DefaultViewSettings()); } /// @@ -33,7 +35,7 @@ public Game() public Game(Scene scene) { Scene = scene; - Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + Camera = new(new Vector3(0), new DefaultViewSettings()); } /// diff --git a/SharpEngine.Core/Entities/Views/CameraView.cs b/SharpEngine.Core/Entities/Views/CameraView.cs index 16ea0fb..df06475 100644 --- a/SharpEngine.Core/Entities/Views/CameraView.cs +++ b/SharpEngine.Core/Entities/Views/CameraView.cs @@ -1,7 +1,9 @@ using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; -using System.Numerics; + using Plane = System.Numerics.Plane; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Views; @@ -103,7 +105,7 @@ public float Fov /// /// The view matrix. public override Matrix4x4 GetViewMatrix() - => Matrix4x4.CreateLookAt(Position, Position + _front, _up); + => Matrix4x4.CreateLookAt((System.Numerics.Vector3)Position, (System.Numerics.Vector3)(Position + _front), (System.Numerics.Vector3)_up); /// /// Gets the projection matrix of the camera. diff --git a/SharpEngine.Core/Entities/Views/View.cs b/SharpEngine.Core/Entities/Views/View.cs index b037f4e..aac388b 100644 --- a/SharpEngine.Core/Entities/Views/View.cs +++ b/SharpEngine.Core/Entities/Views/View.cs @@ -1,9 +1,10 @@ using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using System; -using System.Numerics; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Views; diff --git a/SharpEngine.Core/Ray.cs b/SharpEngine.Core/Ray.cs index 942346b..1084cfd 100644 --- a/SharpEngine.Core/Ray.cs +++ b/SharpEngine.Core/Ray.cs @@ -1,10 +1,10 @@ using SharpEngine.Core.Entities; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; namespace SharpEngine.Core; diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index fca661d..5b9e734 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -22,7 +22,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; +using SharpEngine.Core.Numerics; namespace SharpEngine.Core.Windowing; @@ -364,7 +364,7 @@ protected void OnUpdateFrame(Frame frame) var mouse = Input?.Mice[0]; if (mouse is not null) { - Camera.UpdateMousePosition(mouse.Position); + Camera.UpdateMousePosition((Vector2)mouse.Position); OnHandleMouse?.Invoke(mouse); }