diff --git a/CORTEX_ENGINE_ARCHITECTURE.md b/CORTEX_ENGINE_ARCHITECTURE.md index 45268ea..cd41212 100644 --- a/CORTEX_ENGINE_ARCHITECTURE.md +++ b/CORTEX_ENGINE_ARCHITECTURE.md @@ -15,7 +15,7 @@ Cortex Engine is a 3D game engine built from scratch to provide a Unity-like dev - **Read** the complete ECS world state through native JSON serialization. - **Modify** the running engine via declarative JSON commands and, in Development Mode, via hot-reloaded C# scripts. -The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Vortice.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend. +The architecture prioritizes **production maturity** over experimental technologies: Vulkan (via Silk.NET.Vulkan), Flecs.NET (C# bindings for the C-based Flecs ECS), SDL3-cs (ppy.SDL3-CS), and ImGui.NET/Hexa.NET.ImGui with a native Vulkan backend. --- @@ -137,7 +137,7 @@ All Roslyn and `AssemblyLoadContext` code is wrapped in `#if DEV_MODE`. **Flecs.NET** -- NuGet: `Flecs.NET.Release` 4.0.3 +- NuGet: `Flecs.NET.Debug` (Debug) / `Flecs.NET.Release` (Release) 4.0.4-build.546 - High-level C# wrapper over the C-based Flecs ECS - Supports .NET Standard 2.1, .NET 5/6/7/8 - NativeAOT-compatible via static linking: `true` diff --git a/src/CortexEngine.App/CortexEngine.App.csproj b/src/CortexEngine.App/CortexEngine.App.csproj index 65c8399..ad83476 100644 --- a/src/CortexEngine.App/CortexEngine.App.csproj +++ b/src/CortexEngine.App/CortexEngine.App.csproj @@ -11,6 +11,7 @@ DEV_MODE + linux-x64 diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 3efa5f1..760689b 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -1,6 +1,9 @@ using System; +using System.Numerics; using Engine.Core; +using Engine.Core.Components; using Engine.Graphics; +using Flecs.NET.Core; namespace CortexEngine.App; @@ -8,17 +11,21 @@ class Program { static void Main(string[] args) { - Console.WriteLine("Cortex Engine Step 2 — Starting up..."); + Console.WriteLine("Cortex Engine Step 3 — Starting up..."); try { - using var window = new Sdl3Window("Cortex Engine — Step 2", 1280, 720); + using var world = World.Create(); + using var window = new Sdl3Window("Cortex Engine — Step 3", 1280, 720); var timing = new Timing(); var input = new InputMapping(); using var vulkan = new VulkanContext(window, enableValidation: false); using var swapchain = new Swapchain(vulkan); using var renderer = new TriangleRenderer(vulkan, swapchain); + var triangle = world.Entity("Triangle") + .Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f))); + var frames = 0; var lastFpsTime = 0.0; var lastWidth = window.Width; @@ -39,7 +46,16 @@ class Program swapchain.Recreate(lastWidth, lastHeight); } - renderer.RenderFrame(); + // Animate the entity transform. + ref var transform = ref triangle.Ensure(); + transform.Position = new Vector3( + MathF.Sin((float)timing.TotalTime) * 0.5f, + 0.0f, + 0.0f); + transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, (float)timing.TotalTime); + transform.Scale = Vector3.One * (1.0f + MathF.Sin((float)timing.TotalTime * 2.0f) * 0.2f); + + renderer.RenderWorld(world); frames++; if (timing.TotalTime - lastFpsTime >= 1.0) diff --git a/src/Engine.Core/Components/Transform.cs b/src/Engine.Core/Components/Transform.cs new file mode 100644 index 0000000..5e8d5d9 --- /dev/null +++ b/src/Engine.Core/Components/Transform.cs @@ -0,0 +1,30 @@ +using System.Numerics; + +namespace Engine.Core.Components; + +/// +/// Basic 3D transform component for the ECS. +/// Uses SIMD-friendly System.Numerics types. +/// +public record struct Transform +{ + public Vector3 Position; + public Quaternion Rotation; + public Vector3 Scale; + + public Transform(Vector3? position = null, Quaternion? rotation = null, Vector3? scale = null) + { + Position = position ?? Vector3.Zero; + Rotation = rotation ?? Quaternion.Identity; + Scale = scale ?? Vector3.One; + } + + public static Transform Identity => new(Vector3.Zero, Quaternion.Identity, Vector3.One); + + public Matrix4x4 GetMatrix() + { + return Matrix4x4.CreateScale(Scale) + * Matrix4x4.CreateFromQuaternion(Rotation) + * Matrix4x4.CreateTranslation(Position); + } +} diff --git a/src/Engine.Core/Engine.Core.csproj b/src/Engine.Core/Engine.Core.csproj index 839b615..e76c22d 100644 --- a/src/Engine.Core/Engine.Core.csproj +++ b/src/Engine.Core/Engine.Core.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/Engine.Core/InputMapping.cs b/src/Engine.Core/InputMapping.cs index eaaef52..70d0a21 100644 --- a/src/Engine.Core/InputMapping.cs +++ b/src/Engine.Core/InputMapping.cs @@ -27,7 +27,7 @@ public sealed class InputMapping public void ProcessEvent(SDL_Event evt) { - switch ((SDL_EventType)evt.Type) + switch ((SDL_EventType)evt.type) { case SDL_EventType.SDL_EVENT_KEY_DOWN: if (!_keysDown.Contains((SDL_Keycode)evt.key.key)) diff --git a/src/Engine.Core/Sdl3Window.cs b/src/Engine.Core/Sdl3Window.cs index 5ecf90a..78c271e 100644 --- a/src/Engine.Core/Sdl3Window.cs +++ b/src/Engine.Core/Sdl3Window.cs @@ -50,7 +50,7 @@ public sealed unsafe class Sdl3Window : IDisposable SDL_Event evt; while (SDL3.SDL_PollEvent(&evt)) { - switch ((SDL_EventType)evt.Type) + switch ((SDL_EventType)evt.type) { case SDL_EventType.SDL_EVENT_QUIT: ShouldClose = true; diff --git a/src/Engine.Graphics/Engine.Graphics.csproj b/src/Engine.Graphics/Engine.Graphics.csproj index 3f59fa0..53ce07f 100644 --- a/src/Engine.Graphics/Engine.Graphics.csproj +++ b/src/Engine.Graphics/Engine.Graphics.csproj @@ -20,6 +20,8 @@ + + diff --git a/src/Engine.Graphics/TriangleRenderer.cs b/src/Engine.Graphics/TriangleRenderer.cs index 1437dae..6cbcd4b 100644 --- a/src/Engine.Graphics/TriangleRenderer.cs +++ b/src/Engine.Graphics/TriangleRenderer.cs @@ -1,12 +1,15 @@ using System; +using System.Numerics; +using Flecs.NET.Core; using Silk.NET.Core; using Silk.NET.Vulkan; +using Engine.Core.Components; namespace Engine.Graphics; /// /// Renders a colored triangle using a vertex buffer and a simple graphics pipeline. -/// Uses Silk.NET.Vulkan. +/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world. /// public sealed unsafe class TriangleRenderer : IDisposable { @@ -115,7 +118,7 @@ public sealed unsafe class TriangleRenderer : IDisposable } } - public void RenderFrame() + public void RenderWorld(World world) { var frame = _currentFrame % 2; @@ -160,7 +163,14 @@ public sealed unsafe class TriangleRenderer : IDisposable var vertexBuffer = _vertexBuffer.Buffer; var offset = 0ul; _context.Vk.CmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset); - _context.Vk.CmdDraw(cmd, 3, 1, 0, 0); + + var drawCmd = cmd; + world.Each((Entity e, ref Transform transform) => + { + var bytes = BuildTriangleVertices(transform); + _vertexBuffer.Update(bytes); + _context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0); + }); _context.Vk.CmdEndRenderPass(cmd); _context.Vk.EndCommandBuffer(cmd); @@ -197,6 +207,39 @@ public sealed unsafe class TriangleRenderer : IDisposable _currentFrame++; } + private byte[] BuildTriangleVertices(Transform transform) + { + var matrix = transform.GetMatrix(); + var positions = new Vector3[] + { + new Vector3(0.0f, -0.5f, 0.0f), + new Vector3(0.5f, 0.5f, 0.0f), + new Vector3(-0.5f, 0.5f, 0.0f) + }; + var colors = new[] + { + new Vector3(1.0f, 0.0f, 0.0f), + new Vector3(0.0f, 1.0f, 0.0f), + new Vector3(0.0f, 0.0f, 1.0f) + }; + + var bytes = new byte[3 * 5 * sizeof(float)]; + fixed (byte* p = bytes) + { + var dst = (float*)p; + for (var i = 0; i < 3; i++) + { + var transformed = Vector3.Transform(positions[i], matrix); + dst[i * 5 + 0] = transformed.X; + dst[i * 5 + 1] = transformed.Y; + dst[i * 5 + 2] = colors[i].X; + dst[i * 5 + 3] = colors[i].Y; + dst[i * 5 + 4] = colors[i].Z; + } + } + return bytes; + } + public void Dispose() { _context.Vk.DeviceWaitIdle(_context.Device); diff --git a/src/Engine.Graphics/VertexBuffer.cs b/src/Engine.Graphics/VertexBuffer.cs index a840cef..b58d353 100644 --- a/src/Engine.Graphics/VertexBuffer.cs +++ b/src/Engine.Graphics/VertexBuffer.cs @@ -102,6 +102,14 @@ public sealed unsafe class VertexBuffer : IDisposable _context.Vk.UnmapMemory(_context.Device, Memory); } + public void Update(ReadOnlySpan data) + { + if ((ulong)data.Length != Size) + throw new ArgumentException($"Vertex buffer update size mismatch: {data.Length} != {Size}"); + + CopyData(data); + } + public void Dispose() { _context.Vk.DeviceWaitIdle(_context.Device);