feat: ECS scene with 7 objects — torus knot + 6 rotating cubes
- 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
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Numerics;
|
|||||||
using Engine.Core;
|
using Engine.Core;
|
||||||
using Engine.Core.Components;
|
using Engine.Core.Components;
|
||||||
using Engine.Graphics;
|
using Engine.Graphics;
|
||||||
|
using Engine.Graphics.Loaders;
|
||||||
using Engine.Graphics.Vulkan;
|
using Engine.Graphics.Vulkan;
|
||||||
using Flecs.NET.Core;
|
using Flecs.NET.Core;
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class Program
|
|||||||
{
|
{
|
||||||
static async Task Main(string[] args)
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -25,7 +26,7 @@ class Program
|
|||||||
|
|
||||||
var cameraEntity = world.Entity("Camera")
|
var cameraEntity = world.Entity("Camera")
|
||||||
.Set(new Camera(
|
.Set(new Camera(
|
||||||
new Vector3(0, 0, -6),
|
new Vector3(0, 2, -8),
|
||||||
new Vector3(0, 0, 0),
|
new Vector3(0, 0, 0),
|
||||||
Vector3.UnitY,
|
Vector3.UnitY,
|
||||||
MathF.PI / 4f,
|
MathF.PI / 4f,
|
||||||
@@ -36,11 +37,38 @@ class Program
|
|||||||
var cameraController = new FreeFlyCameraController(cameraEntity);
|
var cameraController = new FreeFlyCameraController(cameraEntity);
|
||||||
Console.WriteLine("Camera: FreeFly (WASD + right-click mouse look, Q/E up/down, Shift boost)");
|
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 lastWidth = window.Width;
|
||||||
var lastHeight = window.Height;
|
var lastHeight = window.Height;
|
||||||
var frames = 0;
|
var frames = 0;
|
||||||
var lastFpsTime = 0.0;
|
var lastFpsTime = 0.0;
|
||||||
var timing = new Timing();
|
var timing = new Timing();
|
||||||
|
var totalTime = 0.0f;
|
||||||
|
|
||||||
while (!window.ShouldClose)
|
while (!window.ShouldClose)
|
||||||
{
|
{
|
||||||
@@ -60,6 +88,28 @@ class Program
|
|||||||
|
|
||||||
cameraController.Update(input, (float)timing.DeltaTime);
|
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<Transform>();
|
||||||
|
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<Transform>();
|
||||||
|
t.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle * (1f + i * 0.2f));
|
||||||
|
cubeEntity.Set(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderer.RenderWorld(world);
|
renderer.RenderWorld(world);
|
||||||
|
|
||||||
frames++;
|
frames++;
|
||||||
@@ -81,4 +131,20 @@ class Program
|
|||||||
|
|
||||||
await Task.CompletedTask;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
|
|||||||
private readonly VulkanSwapchain _swapchain;
|
private readonly VulkanSwapchain _swapchain;
|
||||||
private readonly VulkanPipeline _pipeline;
|
private readonly VulkanPipeline _pipeline;
|
||||||
private readonly VulkanFrameResources _frameResources;
|
private readonly VulkanFrameResources _frameResources;
|
||||||
private readonly VulkanVertexBuffer _vertexBuffer;
|
|
||||||
private readonly VulkanIndexBuffer _indexBuffer;
|
private readonly Dictionary<ulong, (VulkanVertexBuffer vb, VulkanIndexBuffer ib, uint indexCount)> _meshCache = new();
|
||||||
private readonly uint _indexCount;
|
|
||||||
|
|
||||||
private int _frameIndex;
|
private int _frameIndex;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
@@ -38,15 +37,6 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
|
|||||||
|
|
||||||
_frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex,
|
_frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex,
|
||||||
swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout);
|
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)
|
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Render(Matrix4x4 vp)
|
drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix()));
|
||||||
|
});
|
||||||
|
|
||||||
|
Render(vp, drawCalls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls)
|
||||||
{
|
{
|
||||||
_frameResources.WaitFrame(_frameIndex);
|
_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,
|
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
|
||||||
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
|
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
|
||||||
Render(vp);
|
Render(vp, drawCalls);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,17 +193,17 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
|
|||||||
Vk.vkCmdBindDescriptorSets(cmd, VkPipelineBindPoint.Graphics, _pipeline.PipelineLayout,
|
Vk.vkCmdBindDescriptorSets(cmd, VkPipelineBindPoint.Graphics, _pipeline.PipelineLayout,
|
||||||
0, 1, &descSet, 0, null);
|
0, 1, &descSet, 0, null);
|
||||||
|
|
||||||
var bufferHandle = _vertexBuffer.Buffer;
|
foreach (var dc in drawCalls)
|
||||||
|
{
|
||||||
|
var vertexBuf = dc.vertexBuf;
|
||||||
ulong offset = 0;
|
ulong offset = 0;
|
||||||
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &bufferHandle, &offset);
|
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset);
|
||||||
|
Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1);
|
||||||
|
|
||||||
Vk.vkCmdBindIndexBuffer(cmd, _indexBuffer.Buffer, 0, 1);
|
var model = dc.model;
|
||||||
|
|
||||||
var angle = _totalTime * 0.2f;
|
|
||||||
var rot = Matrix4x4.CreateRotationY(angle);
|
|
||||||
var model = rot;
|
|
||||||
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model);
|
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model);
|
||||||
Vk.vkCmdDrawIndexed(cmd, _indexCount, 1, 0, 0, 0);
|
Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
Vk.vkCmdEndRendering(cmd);
|
Vk.vkCmdEndRendering(cmd);
|
||||||
|
|
||||||
@@ -332,22 +340,6 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
|
|||||||
Vk.vkCmdPipelineBarrier2(cmd, &depInfo);
|
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)
|
private static byte[] LoadShader(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
@@ -390,8 +382,13 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
|
|||||||
|
|
||||||
Vk.vkDeviceWaitIdle(_ctx.Device);
|
Vk.vkDeviceWaitIdle(_ctx.Device);
|
||||||
|
|
||||||
_vertexBuffer?.Dispose();
|
foreach (var (vb, ib, _) in _meshCache.Values)
|
||||||
_indexBuffer?.Dispose();
|
{
|
||||||
|
vb.Dispose();
|
||||||
|
ib.Dispose();
|
||||||
|
}
|
||||||
|
_meshCache.Clear();
|
||||||
|
|
||||||
_frameResources?.Dispose();
|
_frameResources?.Dispose();
|
||||||
_pipeline?.Dispose();
|
_pipeline?.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user