From a4914f28c56ecbddb675a5e0646a8e42829137d5 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 17:00:14 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20ECS=20scene=20with=207=20objects=20?= =?UTF-8?q?=E2=80=94=20torus=20knot=20+=206=20rotating=20cubes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VulkanRenderer: mesh cache by entity ID, lazy buffer creation - RenderWorld iterates entities with Transform+Mesh, draws each with per-entity model matrix via push constants - Program.cs: creates scene with 1 torus knot (center, scale 1.5) + 6 cubes at different positions, all rotating around Y at different speeds - Camera at (0, 2, -8) looking at origin, FreeFly controls - 0 validation errors, ~2100 FPS with 7 objects --- src/CortexEngine.App/Program.cs | 70 ++++++++++++++++- src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 83 ++++++++++---------- 2 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index d4c794e..94a7f25 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -2,6 +2,7 @@ using System.Numerics; using Engine.Core; using Engine.Core.Components; using Engine.Graphics; +using Engine.Graphics.Loaders; using Engine.Graphics.Vulkan; using Flecs.NET.Core; @@ -11,7 +12,7 @@ class Program { static async Task Main(string[] args) { - Console.WriteLine("Cortex Engine — Vulkan (pure P/Invoke)..."); + Console.WriteLine("Cortex Engine — Vulkan ECS Scene (pure P/Invoke)..."); try { @@ -25,7 +26,7 @@ class Program var cameraEntity = world.Entity("Camera") .Set(new Camera( - new Vector3(0, 0, -6), + new Vector3(0, 2, -8), new Vector3(0, 0, 0), Vector3.UnitY, MathF.PI / 4f, @@ -36,11 +37,38 @@ class Program var cameraController = new FreeFlyCameraController(cameraEntity); Console.WriteLine("Camera: FreeFly (WASD + right-click mouse look, Q/E up/down, Shift boost)"); + var torusKnot = LoadMesh("Content/torusknot.obj", new Vector3(0.8f, 0.6f, 0.3f)); + var cube = LoadMesh("Content/cube.obj", new Vector3(0.5f, 0.7f, 0.9f)); + + world.Entity("TorusKnot") + .Set(new Transform(Vector3.Zero, Quaternion.Identity, new Vector3(1.5f))) + .Set(torusKnot); + + var cubePositions = new Vector3[] + { + new(-4, 0, 0), + new(4, 0, 0), + new(0, -3, 0), + new(0, 3, 0), + new(-3, 2, 3), + new(3, -2, -3), + }; + + for (var i = 0; i < cubePositions.Length; i++) + { + world.Entity($"Cube{i}") + .Set(new Transform(cubePositions[i], Quaternion.Identity, new Vector3(1.5f))) + .Set(cube); + } + + Console.WriteLine($"[Scene] 1 torus knot + {cubePositions.Length} cubes"); + var lastWidth = window.Width; var lastHeight = window.Height; var frames = 0; var lastFpsTime = 0.0; var timing = new Timing(); + var totalTime = 0.0f; while (!window.ShouldClose) { @@ -60,6 +88,28 @@ class Program cameraController.Update(input, (float)timing.DeltaTime); + totalTime += (float)timing.DeltaTime; + var angle = totalTime * 0.3f; + + var torusEntity = world.Lookup("TorusKnot"); + if ((ulong)torusEntity.Id != 0) + { + var t = torusEntity.Get(); + t.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle); + torusEntity.Set(t); + } + + for (var i = 0; i < cubePositions.Length; i++) + { + var cubeEntity = world.Lookup($"Cube{i}"); + if ((ulong)cubeEntity.Id != 0) + { + var t = cubeEntity.Get(); + t.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle * (1f + i * 0.2f)); + cubeEntity.Set(t); + } + } + renderer.RenderWorld(world); frames++; @@ -81,4 +131,20 @@ class Program await Task.CompletedTask; } + + static Mesh LoadMesh(string path, Vector3 color) + { + if (!File.Exists(path)) + { + var altPath = Path.Combine(AppContext.BaseDirectory, path); + if (!File.Exists(altPath)) + { + altPath = Path.Combine(AppContext.BaseDirectory, "Content", Path.GetFileName(path)); + if (!File.Exists(altPath)) + throw new FileNotFoundException($"Mesh file not found: {path}"); + } + return ObjLoader.Load(altPath, color); + } + return ObjLoader.Load(path, color); + } } diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index 2092819..1eb8c77 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -13,9 +13,8 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen private readonly VulkanSwapchain _swapchain; private readonly VulkanPipeline _pipeline; private readonly VulkanFrameResources _frameResources; - private readonly VulkanVertexBuffer _vertexBuffer; - private readonly VulkanIndexBuffer _indexBuffer; - private readonly uint _indexCount; + + private readonly Dictionary _meshCache = new(); private int _frameIndex; private bool _disposed; @@ -38,15 +37,6 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout); - - var mesh = LoadMesh("Content/torusknot.obj"); - _indexCount = (uint)mesh.Indices.Length; - - _vertexBuffer = new VulkanVertexBuffer(ctx.Device, ctx.PhysicalDevice, - _frameResources.CommandPool, ctx.GraphicsQueue, ctx, mesh.Vertices); - - _indexBuffer = new VulkanIndexBuffer(ctx.Device, _frameResources.CommandPool, - ctx.GraphicsQueue, ctx, mesh.Indices); } public void RenderWorld(World world) @@ -71,10 +61,28 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen vp = Matrix4x4.CreateLookAt(new Vector3(0, 0, -6), Vector3.Zero, Vector3.UnitY) * proj; } - Render(vp); + var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)>(); + + world.Each((Entity e, ref Transform t, ref Mesh m) => + { + var eid = (ulong)e.Id; + if (!_meshCache.TryGetValue(eid, out var entry)) + { + var vb = new VulkanVertexBuffer(_ctx.Device, _ctx.PhysicalDevice, + _frameResources.CommandPool, _ctx.GraphicsQueue, _ctx, m.Vertices); + var ib = new VulkanIndexBuffer(_ctx.Device, _frameResources.CommandPool, + _ctx.GraphicsQueue, _ctx, m.Indices); + entry = (vb, ib, (uint)m.Indices.Length); + _meshCache[eid] = entry; + } + + drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix())); + }); + + Render(vp, drawCalls); } - private void Render(Matrix4x4 vp) + private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls) { _frameResources.WaitFrame(_frameIndex); @@ -86,7 +94,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen { _swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width, _ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height); - Render(vp); + Render(vp, drawCalls); return; } @@ -185,17 +193,17 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen Vk.vkCmdBindDescriptorSets(cmd, VkPipelineBindPoint.Graphics, _pipeline.PipelineLayout, 0, 1, &descSet, 0, null); - var bufferHandle = _vertexBuffer.Buffer; - ulong offset = 0; - Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &bufferHandle, &offset); + foreach (var dc in drawCalls) + { + var vertexBuf = dc.vertexBuf; + ulong offset = 0; + Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset); + Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1); - Vk.vkCmdBindIndexBuffer(cmd, _indexBuffer.Buffer, 0, 1); - - var angle = _totalTime * 0.2f; - var rot = Matrix4x4.CreateRotationY(angle); - var model = rot; - Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model); - Vk.vkCmdDrawIndexed(cmd, _indexCount, 1, 0, 0, 0); + var model = dc.model; + Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model); + Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); + } Vk.vkCmdEndRendering(cmd); @@ -332,22 +340,6 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen Vk.vkCmdPipelineBarrier2(cmd, &depInfo); } - private static Mesh LoadMesh(string path) - { - if (!File.Exists(path)) - { - var altPath = Path.Combine(AppContext.BaseDirectory, path); - if (!File.Exists(altPath)) - { - altPath = Path.Combine(AppContext.BaseDirectory, "Content", Path.GetFileName(path)); - if (!File.Exists(altPath)) - throw new FileNotFoundException($"Mesh file not found: {path}"); - } - return ObjLoader.Load(altPath, new Vector3(0.8f, 0.6f, 0.3f)); - } - return ObjLoader.Load(path, new Vector3(0.8f, 0.6f, 0.3f)); - } - private static byte[] LoadShader(string path) { if (!File.Exists(path)) @@ -390,8 +382,13 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen Vk.vkDeviceWaitIdle(_ctx.Device); - _vertexBuffer?.Dispose(); - _indexBuffer?.Dispose(); + foreach (var (vb, ib, _) in _meshCache.Values) + { + vb.Dispose(); + ib.Dispose(); + } + _meshCache.Clear(); + _frameResources?.Dispose(); _pipeline?.Dispose(); }