feat: Step 4 load .obj and glTF models into ECS

- Add Vertex struct and Mesh ECS component (vertices + indices).
- Add Vulkan IndexBuffer for indexed draws.
- Add MeshRenderer that draws Mesh + Transform entities with vkCmdDrawIndexed.
- Add minimal .obj loader (positions, faces) and glTF/glTF-binary loader via SharpGLTF.Core.
- Rewrite shaders to 3D position + color; recompile to SPIR-V.
- Update VulkanPipeline for new vertex format.
- Ship a sample Content/cube.obj and wire Program.cs to load it.
- Remove TriangleRenderer (replaced by MeshRenderer).
This commit is contained in:
emil28092005
2026-06-16 19:15:20 +03:00
parent 608c3c24a8
commit 05703f820f
14 changed files with 475 additions and 73 deletions
+287
View File
@@ -0,0 +1,287 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Flecs.NET.Core;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Engine.Core;
using Engine.Core.Components;
namespace Engine.Graphics;
/// <summary>
/// Renders indexed meshes attached to ECS entities.
/// Uses Silk.NET.Vulkan and reads Mesh + Transform components from the ECS world.
/// </summary>
public sealed unsafe class MeshRenderer : IDisposable
{
private readonly VulkanContext _context;
private readonly Swapchain _swapchain;
private readonly VulkanPipeline _pipeline;
private readonly Dictionary<Entity, MeshBuffers> _buffers = new();
private CommandPool _commandPool;
private CommandBuffer[] _commandBuffers = null!;
private Silk.NET.Vulkan.Semaphore[] _imageAvailableSemaphores = null!;
private Silk.NET.Vulkan.Semaphore[] _renderFinishedSemaphores = null!;
private Silk.NET.Vulkan.Fence[] _inFlightFences = null!;
private int _currentFrame;
private sealed class MeshBuffers : IDisposable
{
public VertexBuffer VertexBuffer;
public IndexBuffer IndexBuffer;
public MeshBuffers(VertexBuffer vertexBuffer, IndexBuffer indexBuffer)
{
VertexBuffer = vertexBuffer;
IndexBuffer = indexBuffer;
}
public void Dispose()
{
VertexBuffer.Dispose();
IndexBuffer.Dispose();
}
}
public MeshRenderer(VulkanContext context, Swapchain swapchain)
{
_context = context;
_swapchain = swapchain;
_pipeline = new VulkanPipeline(context, swapchain);
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
}
private void CreateCommandPool()
{
var createInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
QueueFamilyIndex = _context.GraphicsFamilyIndex,
Flags = CommandPoolCreateFlags.ResetCommandBufferBit
};
CommandPool commandPool;
var result = _context.Vk.CreateCommandPool(_context.Device, &createInfo, null, &commandPool);
if (result != Result.Success)
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
_commandPool = commandPool;
}
private void CreateCommandBuffers()
{
_commandBuffers = new CommandBuffer[2];
for (var i = 0; i < _commandBuffers.Length; i++)
{
var allocInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
Level = CommandBufferLevel.Primary,
CommandBufferCount = 1
};
CommandBuffer cmd;
var result = _context.Vk.AllocateCommandBuffers(_context.Device, &allocInfo, &cmd);
if (result != Result.Success)
throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}");
_commandBuffers[i] = cmd;
}
}
private void CreateSyncObjects()
{
_imageAvailableSemaphores = new Silk.NET.Vulkan.Semaphore[2];
_renderFinishedSemaphores = new Silk.NET.Vulkan.Semaphore[2];
_inFlightFences = new Silk.NET.Vulkan.Fence[2];
var semaphoreInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo };
var fenceInfo = new FenceCreateInfo
{
SType = StructureType.FenceCreateInfo,
Flags = FenceCreateFlags.SignaledBit
};
for (var i = 0; i < 2; i++)
{
Silk.NET.Vulkan.Semaphore imageAvailable, renderFinished;
Silk.NET.Vulkan.Fence fence;
_context.Vk.CreateSemaphore(_context.Device, &semaphoreInfo, null, &imageAvailable);
_context.Vk.CreateSemaphore(_context.Device, &semaphoreInfo, null, &renderFinished);
_context.Vk.CreateFence(_context.Device, &fenceInfo, null, &fence);
_imageAvailableSemaphores[i] = imageAvailable;
_renderFinishedSemaphores[i] = renderFinished;
_inFlightFences[i] = fence;
}
}
public void RenderWorld(World world)
{
var frame = _currentFrame % 2;
var fence = _inFlightFences[frame];
_context.Vk.WaitForFences(_context.Device, 1, &fence, true, ulong.MaxValue);
_context.Vk.ResetFences(_context.Device, 1, &fence);
uint imageIndex;
var result = _context.KhrSwapchain!.AcquireNextImage(_context.Device, _swapchain.Handle, ulong.MaxValue, _imageAvailableSemaphores[frame], new Silk.NET.Vulkan.Fence(), &imageIndex);
if (result == Result.ErrorOutOfDateKhr)
return;
var cmd = _commandBuffers[frame];
_context.Vk.ResetCommandBuffer(cmd, CommandBufferResetFlags.None);
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.OneTimeSubmitBit
};
_context.Vk.BeginCommandBuffer(cmd, &beginInfo);
var clearColor = new ClearValue(new ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f));
var renderPassInfo = new RenderPassBeginInfo
{
SType = StructureType.RenderPassBeginInfo,
RenderPass = _swapchain.RenderPass,
Framebuffer = _swapchain.Framebuffers[imageIndex],
RenderArea = new Rect2D(new Offset2D(0, 0), _swapchain.Extent),
ClearValueCount = 1,
PClearValues = &clearColor
};
_context.Vk.CmdBeginRenderPass(cmd, &renderPassInfo, SubpassContents.Inline);
_context.Vk.CmdBindPipeline(cmd, PipelineBindPoint.Graphics, _pipeline.Handle);
var viewport = new Viewport(0, 0, _swapchain.Extent.Width, _swapchain.Extent.Height, 0, 1);
var scissor = new Rect2D(new Offset2D(0, 0), _swapchain.Extent);
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
_context.Vk.CmdSetScissor(cmd, 0, 1, &scissor);
var drawCmd = cmd;
world.Each((Entity e, ref Mesh mesh, ref Transform transform) =>
{
if (!_buffers.TryGetValue(e, out var buffers))
{
buffers = CreateMeshBuffers(mesh);
_buffers[e] = buffers;
}
var bytes = BuildMeshVertices(mesh, transform);
buffers.VertexBuffer.Update(bytes);
var vertexBuffer = buffers.VertexBuffer.Buffer;
var offset = 0ul;
_context.Vk.CmdBindVertexBuffers(drawCmd, 0, 1, &vertexBuffer, &offset);
_context.Vk.CmdBindIndexBuffer(drawCmd, buffers.IndexBuffer.Buffer, 0, IndexType.Uint32);
_context.Vk.CmdDrawIndexed(drawCmd, buffers.IndexBuffer.Count, 1, 0, 0, 0);
});
_context.Vk.CmdEndRenderPass(cmd);
_context.Vk.EndCommandBuffer(cmd);
var waitSemaphore = _imageAvailableSemaphores[frame];
var signalSemaphore = _renderFinishedSemaphores[frame];
var stageMask = PipelineStageFlags.ColorAttachmentOutputBit;
var submitInfo = new SubmitInfo
{
SType = StructureType.SubmitInfo,
WaitSemaphoreCount = 1,
PWaitSemaphores = &waitSemaphore,
PWaitDstStageMask = &stageMask,
CommandBufferCount = 1,
PCommandBuffers = &cmd,
SignalSemaphoreCount = 1,
PSignalSemaphores = &signalSemaphore
};
_context.Vk.QueueSubmit(_context.GraphicsQueue, 1, &submitInfo, _inFlightFences[frame]);
var swapchain = _swapchain.Handle;
var presentInfo = new PresentInfoKHR
{
SType = StructureType.PresentInfoKhr,
WaitSemaphoreCount = 1,
PWaitSemaphores = &signalSemaphore,
SwapchainCount = 1,
PSwapchains = &swapchain,
PImageIndices = &imageIndex
};
_context.KhrSwapchain!.QueuePresent(_context.PresentQueue, &presentInfo);
_currentFrame++;
}
private MeshBuffers CreateMeshBuffers(Mesh mesh)
{
var vertexBytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
fixed (byte* p = vertexBytes)
{
var dst = (float*)p;
for (var i = 0; i < mesh.Vertices.Length; i++)
{
var v = mesh.Vertices[i];
dst[i * 6 + 0] = v.Position.X;
dst[i * 6 + 1] = v.Position.Y;
dst[i * 6 + 2] = v.Position.Z;
dst[i * 6 + 3] = v.Color.X;
dst[i * 6 + 4] = v.Color.Y;
dst[i * 6 + 5] = v.Color.Z;
}
}
var indexBytes = new byte[mesh.Indices.Length * sizeof(uint)];
fixed (byte* p = indexBytes)
fixed (uint* src = mesh.Indices)
{
global::System.Buffer.MemoryCopy(src, p, indexBytes.Length, mesh.Indices.Length * sizeof(uint));
}
return new MeshBuffers(
new VertexBuffer(_context, vertexBytes),
new IndexBuffer(_context, indexBytes, (uint)mesh.Indices.Length));
}
private byte[] BuildMeshVertices(Mesh mesh, Transform transform)
{
var matrix = transform.GetMatrix();
var bytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
fixed (byte* p = bytes)
{
var dst = (float*)p;
for (var i = 0; i < mesh.Vertices.Length; i++)
{
var transformed = Vector3.Transform(mesh.Vertices[i].Position, matrix);
dst[i * 6 + 0] = transformed.X;
dst[i * 6 + 1] = transformed.Y;
dst[i * 6 + 2] = transformed.Z;
dst[i * 6 + 3] = mesh.Vertices[i].Color.X;
dst[i * 6 + 4] = mesh.Vertices[i].Color.Y;
dst[i * 6 + 5] = mesh.Vertices[i].Color.Z;
}
}
return bytes;
}
public void Dispose()
{
_context.Vk.DeviceWaitIdle(_context.Device);
foreach (var buffers in _buffers.Values)
buffers.Dispose();
_buffers.Clear();
for (var i = 0; i < 2; i++)
{
_context.Vk.DestroySemaphore(_context.Device, _renderFinishedSemaphores[i], null);
_context.Vk.DestroySemaphore(_context.Device, _imageAvailableSemaphores[i], null);
_context.Vk.DestroyFence(_context.Device, _inFlightFences[i], null);
}
_context.Vk.DestroyCommandPool(_context.Device, _commandPool, null);
_pipeline.Dispose();
}
}