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:
@@ -0,0 +1,33 @@
|
|||||||
|
# Simple unit cube
|
||||||
|
v -0.5 -0.5 -0.5
|
||||||
|
v 0.5 -0.5 -0.5
|
||||||
|
v 0.5 0.5 -0.5
|
||||||
|
v -0.5 0.5 -0.5
|
||||||
|
v -0.5 -0.5 0.5
|
||||||
|
v 0.5 -0.5 0.5
|
||||||
|
v 0.5 0.5 0.5
|
||||||
|
v -0.5 0.5 0.5
|
||||||
|
|
||||||
|
# Back face
|
||||||
|
f 1 2 3
|
||||||
|
f 1 3 4
|
||||||
|
|
||||||
|
# Front face
|
||||||
|
f 5 7 6
|
||||||
|
f 5 8 7
|
||||||
|
|
||||||
|
# Left face
|
||||||
|
f 1 5 6
|
||||||
|
f 1 6 2
|
||||||
|
|
||||||
|
# Right face
|
||||||
|
f 3 7 8
|
||||||
|
f 3 8 4
|
||||||
|
|
||||||
|
# Bottom face
|
||||||
|
f 1 4 8
|
||||||
|
f 1 8 5
|
||||||
|
|
||||||
|
# Top face
|
||||||
|
f 2 6 7
|
||||||
|
f 2 7 3
|
||||||
@@ -24,4 +24,11 @@
|
|||||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\..\Content\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<Link>Content\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Numerics;
|
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 Flecs.NET.Core;
|
using Flecs.NET.Core;
|
||||||
|
|
||||||
namespace CortexEngine.App;
|
namespace CortexEngine.App;
|
||||||
@@ -11,20 +13,27 @@ class Program
|
|||||||
{
|
{
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Cortex Engine Step 3 — Starting up...");
|
Console.WriteLine("Cortex Engine Step 4 — Starting up...");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var world = World.Create();
|
using var world = World.Create();
|
||||||
using var window = new Sdl3Window("Cortex Engine — Step 3", 1280, 720);
|
using var window = new Sdl3Window("Cortex Engine — Step 4", 1280, 720);
|
||||||
var timing = new Timing();
|
var timing = new Timing();
|
||||||
var input = new InputMapping();
|
var input = new InputMapping();
|
||||||
using var vulkan = new VulkanContext(window, enableValidation: false);
|
using var vulkan = new VulkanContext(window, enableValidation: false);
|
||||||
using var swapchain = new Swapchain(vulkan);
|
using var swapchain = new Swapchain(vulkan);
|
||||||
using var renderer = new TriangleRenderer(vulkan, swapchain);
|
using var renderer = new MeshRenderer(vulkan, swapchain);
|
||||||
|
|
||||||
var triangle = world.Entity("Triangle")
|
var modelPath = FindModelPath(args);
|
||||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f)));
|
var mesh = modelPath.EndsWith(".gltf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| modelPath.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? GltfLoader.Load(modelPath, new Vector3(0.7f, 0.6f, 0.5f))
|
||||||
|
: ObjLoader.Load(modelPath, new Vector3(0.7f, 0.6f, 0.5f));
|
||||||
|
|
||||||
|
var model = world.Entity("Model")
|
||||||
|
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, new Vector3(0.5f)))
|
||||||
|
.Set(mesh);
|
||||||
|
|
||||||
var frames = 0;
|
var frames = 0;
|
||||||
var lastFpsTime = 0.0;
|
var lastFpsTime = 0.0;
|
||||||
@@ -46,14 +55,10 @@ class Program
|
|||||||
swapchain.Recreate(lastWidth, lastHeight);
|
swapchain.Recreate(lastWidth, lastHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate the entity transform.
|
// Slowly rotate the model so we can see it in 3D.
|
||||||
ref var transform = ref triangle.Ensure<Transform>();
|
ref var transform = ref model.Ensure<Transform>();
|
||||||
transform.Position = new Vector3(
|
transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)timing.TotalTime * 0.5f)
|
||||||
MathF.Sin((float)timing.TotalTime) * 0.5f,
|
* Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)timing.TotalTime * 0.25f);
|
||||||
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);
|
renderer.RenderWorld(world);
|
||||||
|
|
||||||
@@ -74,4 +79,25 @@ class Program
|
|||||||
Environment.Exit(1);
|
Environment.Exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FindModelPath(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length > 0 && File.Exists(args[0]))
|
||||||
|
return args[0];
|
||||||
|
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
"Content/cube.obj",
|
||||||
|
"Models/cube.obj",
|
||||||
|
"cube.obj"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var candidate in candidates)
|
||||||
|
{
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FileNotFoundException("No model file found. Pass a .obj/.gltf/.glb path as argument or place Content/cube.obj next to the executable.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Engine.Core;
|
||||||
|
|
||||||
|
namespace Engine.Core.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A mesh component for the ECS.
|
||||||
|
/// Keeps CPU-side vertex/index data that the renderer uploads to GPU buffers.
|
||||||
|
/// </summary>
|
||||||
|
public record struct Mesh
|
||||||
|
{
|
||||||
|
public Vertex[] Vertices;
|
||||||
|
public uint[] Indices;
|
||||||
|
|
||||||
|
public Mesh(Vertex[] vertices, uint[] indices)
|
||||||
|
{
|
||||||
|
Vertices = vertices;
|
||||||
|
Indices = indices;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Engine.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A simple 3D vertex with position and color.
|
||||||
|
/// Layout matches the Vulkan vertex input description.
|
||||||
|
/// </summary>
|
||||||
|
public struct Vertex
|
||||||
|
{
|
||||||
|
public Vector3 Position;
|
||||||
|
public Vector3 Color;
|
||||||
|
|
||||||
|
public Vertex(Vector3 position, Vector3 color)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
Color = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
|
||||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release'" />
|
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release'" />
|
||||||
|
<PackageReference Include="SharpGLTF.Core" Version="1.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System;
|
||||||
|
using Silk.NET.Core;
|
||||||
|
using Silk.NET.Vulkan;
|
||||||
|
|
||||||
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GPU index buffer for indexed draws.
|
||||||
|
/// Uses 32-bit indices.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class IndexBuffer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly VulkanContext _context;
|
||||||
|
public Silk.NET.Vulkan.Buffer Buffer { get; }
|
||||||
|
public DeviceMemory Memory { get; }
|
||||||
|
public ulong Size { get; }
|
||||||
|
public uint Count { get; }
|
||||||
|
|
||||||
|
public IndexBuffer(VulkanContext context, ReadOnlySpan<byte> data, uint count)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
Size = (ulong)data.Length;
|
||||||
|
Count = count;
|
||||||
|
|
||||||
|
Buffer = CreateBuffer(Size, BufferUsageFlags.IndexBufferBit);
|
||||||
|
var memoryRequirements = GetMemoryRequirements(Buffer);
|
||||||
|
Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit);
|
||||||
|
|
||||||
|
var result = _context.Vk.BindBufferMemory(_context.Device, Buffer, Memory, 0);
|
||||||
|
if (result != Result.Success)
|
||||||
|
throw new InvalidOperationException($"vkBindBufferMemory failed: {result}");
|
||||||
|
|
||||||
|
CopyData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage)
|
||||||
|
{
|
||||||
|
var createInfo = new BufferCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.BufferCreateInfo,
|
||||||
|
Size = size,
|
||||||
|
Usage = usage,
|
||||||
|
SharingMode = SharingMode.Exclusive
|
||||||
|
};
|
||||||
|
|
||||||
|
Silk.NET.Vulkan.Buffer buffer;
|
||||||
|
var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer);
|
||||||
|
if (result != Result.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateBuffer failed: {result}");
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer)
|
||||||
|
{
|
||||||
|
MemoryRequirements requirements;
|
||||||
|
_context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements);
|
||||||
|
return requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties)
|
||||||
|
{
|
||||||
|
var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties);
|
||||||
|
var allocateInfo = new MemoryAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryAllocateInfo,
|
||||||
|
AllocationSize = requirements.Size,
|
||||||
|
MemoryTypeIndex = memoryTypeIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceMemory memory;
|
||||||
|
var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory);
|
||||||
|
if (result != Result.Success)
|
||||||
|
throw new InvalidOperationException($"vkAllocateMemory failed: {result}");
|
||||||
|
return memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties)
|
||||||
|
{
|
||||||
|
PhysicalDeviceMemoryProperties memoryProperties;
|
||||||
|
_context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties);
|
||||||
|
for (var i = 0; i < memoryProperties.MemoryTypeCount; i++)
|
||||||
|
{
|
||||||
|
if ((typeFilter & (1u << i)) != 0 &&
|
||||||
|
(memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties)
|
||||||
|
{
|
||||||
|
return (uint)i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Failed to find suitable memory type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopyData(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
void* mappedData;
|
||||||
|
var result = _context.Vk.MapMemory(_context.Device, Memory, 0, Size, MemoryMapFlags.None, &mappedData);
|
||||||
|
if (result != Result.Success)
|
||||||
|
throw new InvalidOperationException($"vkMapMemory failed: {result}");
|
||||||
|
|
||||||
|
fixed (byte* src = data)
|
||||||
|
{
|
||||||
|
global::System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Vk.UnmapMemory(_context.Device, Memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.Vk.DeviceWaitIdle(_context.Device);
|
||||||
|
_context.Vk.DestroyBuffer(_context.Device, Buffer, null);
|
||||||
|
_context.Vk.FreeMemory(_context.Device, Memory, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using Engine.Core;
|
||||||
|
using Engine.Core.Components;
|
||||||
|
using SharpGLTF.Schema2;
|
||||||
|
|
||||||
|
namespace Engine.Graphics.Loaders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// glTF/glTF binary loader using SharpGLTF.Core.
|
||||||
|
/// Loads the first primitive of the first mesh and converts it to a colored Mesh component.
|
||||||
|
/// </summary>
|
||||||
|
public static class GltfLoader
|
||||||
|
{
|
||||||
|
public static Engine.Core.Components.Mesh Load(string path, Vector3? defaultColor = null)
|
||||||
|
{
|
||||||
|
var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f);
|
||||||
|
|
||||||
|
var model = ModelRoot.Load(path);
|
||||||
|
if (model.LogicalMeshes.Count == 0)
|
||||||
|
throw new InvalidOperationException($"glTF file has no meshes: {path}");
|
||||||
|
|
||||||
|
var mesh = model.LogicalMeshes[0];
|
||||||
|
if (mesh.Primitives.Count == 0)
|
||||||
|
throw new InvalidOperationException($"glTF mesh has no primitives: {path}");
|
||||||
|
|
||||||
|
var primitive = mesh.Primitives[0];
|
||||||
|
|
||||||
|
if (!primitive.VertexAccessors.TryGetValue("POSITION", out var positionAccessor))
|
||||||
|
throw new InvalidOperationException($"glTF primitive has no POSITION accessor: {path}");
|
||||||
|
|
||||||
|
var positions = positionAccessor.AsVector3Array();
|
||||||
|
var vertices = new Vertex[positions.Count];
|
||||||
|
for (var i = 0; i < positions.Count; i++)
|
||||||
|
{
|
||||||
|
vertices[i] = new Vertex(positions[i], color);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint[] indices;
|
||||||
|
if (primitive.IndexAccessor != null)
|
||||||
|
{
|
||||||
|
var idx = primitive.IndexAccessor.AsIndexArray();
|
||||||
|
indices = new uint[idx.Count];
|
||||||
|
for (var i = 0; i < idx.Count; i++)
|
||||||
|
indices[i] = idx[i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non-indexed primitive
|
||||||
|
indices = new uint[positions.Count];
|
||||||
|
for (var i = 0; i < positions.Count; i++)
|
||||||
|
indices[i] = (uint)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Engine.Core.Components.Mesh(vertices, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Numerics;
|
||||||
|
using Engine.Core;
|
||||||
|
using Engine.Core.Components;
|
||||||
|
|
||||||
|
namespace Engine.Graphics.Loaders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal .obj loader.
|
||||||
|
/// Supports vertices (v) and faces (f). Ignores normals/UVs for now.
|
||||||
|
/// Produces a colored Mesh component.
|
||||||
|
/// </summary>
|
||||||
|
public static class ObjLoader
|
||||||
|
{
|
||||||
|
public static Mesh Load(string path, Vector3? defaultColor = null)
|
||||||
|
{
|
||||||
|
var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f);
|
||||||
|
|
||||||
|
var positions = new List<Vector3>();
|
||||||
|
var indices = new List<uint>();
|
||||||
|
|
||||||
|
foreach (var rawLine in File.ReadLines(path))
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
switch (parts[0])
|
||||||
|
{
|
||||||
|
case "v" when parts.Length >= 4:
|
||||||
|
positions.Add(new Vector3(
|
||||||
|
float.Parse(parts[1]),
|
||||||
|
float.Parse(parts[2]),
|
||||||
|
float.Parse(parts[3])));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "f" when parts.Length >= 4:
|
||||||
|
// Triangulate the face as a fan. Only the position index is used.
|
||||||
|
var baseIndex = ParseFaceIndex(parts[1]);
|
||||||
|
for (var i = 2; i < parts.Length - 1; i++)
|
||||||
|
{
|
||||||
|
indices.Add(baseIndex);
|
||||||
|
indices.Add(ParseFaceIndex(parts[i]));
|
||||||
|
indices.Add(ParseFaceIndex(parts[i + 1]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positions.Count == 0)
|
||||||
|
throw new InvalidOperationException($"OBJ file has no vertices: {path}");
|
||||||
|
|
||||||
|
var vertices = new Vertex[positions.Count];
|
||||||
|
for (var i = 0; i < positions.Count; i++)
|
||||||
|
{
|
||||||
|
vertices[i] = new Vertex(positions[i], color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Mesh(vertices, indices.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint ParseFaceIndex(string part)
|
||||||
|
{
|
||||||
|
// Formats: v, v/vt, v/vt/vn, v//vn
|
||||||
|
var slashIndex = part.IndexOf('/');
|
||||||
|
var indexStr = slashIndex == -1 ? part : part.Substring(0, slashIndex);
|
||||||
|
var index = int.Parse(indexStr);
|
||||||
|
return (uint)(index - 1); // OBJ indices are 1-based
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Flecs.NET.Core;
|
using Flecs.NET.Core;
|
||||||
using Silk.NET.Core;
|
using Silk.NET.Core;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
|
using Engine.Core;
|
||||||
using Engine.Core.Components;
|
using Engine.Core.Components;
|
||||||
|
|
||||||
namespace Engine.Graphics;
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline.
|
/// Renders indexed meshes attached to ECS entities.
|
||||||
/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world.
|
/// Uses Silk.NET.Vulkan and reads Mesh + Transform components from the ECS world.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class TriangleRenderer : IDisposable
|
public sealed unsafe class MeshRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly VulkanContext _context;
|
private readonly VulkanContext _context;
|
||||||
private readonly Swapchain _swapchain;
|
private readonly Swapchain _swapchain;
|
||||||
private readonly VulkanPipeline _pipeline;
|
private readonly VulkanPipeline _pipeline;
|
||||||
private readonly VertexBuffer _vertexBuffer;
|
private readonly Dictionary<Entity, MeshBuffers> _buffers = new();
|
||||||
private CommandPool _commandPool;
|
private CommandPool _commandPool;
|
||||||
private CommandBuffer[] _commandBuffers = null!;
|
private CommandBuffer[] _commandBuffers = null!;
|
||||||
private Silk.NET.Vulkan.Semaphore[] _imageAvailableSemaphores = null!;
|
private Silk.NET.Vulkan.Semaphore[] _imageAvailableSemaphores = null!;
|
||||||
@@ -24,37 +26,35 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
|||||||
private Silk.NET.Vulkan.Fence[] _inFlightFences = null!;
|
private Silk.NET.Vulkan.Fence[] _inFlightFences = null!;
|
||||||
private int _currentFrame;
|
private int _currentFrame;
|
||||||
|
|
||||||
public TriangleRenderer(VulkanContext context, Swapchain swapchain)
|
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;
|
_context = context;
|
||||||
_swapchain = swapchain;
|
_swapchain = swapchain;
|
||||||
|
|
||||||
_pipeline = new VulkanPipeline(context, swapchain);
|
_pipeline = new VulkanPipeline(context, swapchain);
|
||||||
_vertexBuffer = CreateTriangleBuffer();
|
|
||||||
CreateCommandPool();
|
CreateCommandPool();
|
||||||
CreateCommandBuffers();
|
CreateCommandBuffers();
|
||||||
CreateSyncObjects();
|
CreateSyncObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
private VertexBuffer CreateTriangleBuffer()
|
|
||||||
{
|
|
||||||
var vertices = new[]
|
|
||||||
{
|
|
||||||
0.0f, -0.5f, 1.0f, 0.0f, 0.0f,
|
|
||||||
0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
|
|
||||||
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
|
|
||||||
};
|
|
||||||
|
|
||||||
var bytes = new byte[vertices.Length * sizeof(float)];
|
|
||||||
fixed (byte* p = bytes)
|
|
||||||
fixed (float* v = vertices)
|
|
||||||
{
|
|
||||||
global::System.Buffer.MemoryCopy(v, p, bytes.Length, vertices.Length * sizeof(float));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VertexBuffer(_context, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateCommandPool()
|
private void CreateCommandPool()
|
||||||
{
|
{
|
||||||
var createInfo = new CommandPoolCreateInfo
|
var createInfo = new CommandPoolCreateInfo
|
||||||
@@ -160,16 +160,23 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
|||||||
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
|
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
|
||||||
_context.Vk.CmdSetScissor(cmd, 0, 1, &scissor);
|
_context.Vk.CmdSetScissor(cmd, 0, 1, &scissor);
|
||||||
|
|
||||||
var vertexBuffer = _vertexBuffer.Buffer;
|
|
||||||
var offset = 0ul;
|
|
||||||
_context.Vk.CmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
|
|
||||||
|
|
||||||
var drawCmd = cmd;
|
var drawCmd = cmd;
|
||||||
world.Each((Entity e, ref Transform transform) =>
|
world.Each((Entity e, ref Mesh mesh, ref Transform transform) =>
|
||||||
{
|
{
|
||||||
var bytes = BuildTriangleVertices(transform);
|
if (!_buffers.TryGetValue(e, out var buffers))
|
||||||
_vertexBuffer.Update(bytes);
|
{
|
||||||
_context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0);
|
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.CmdEndRenderPass(cmd);
|
||||||
@@ -207,34 +214,52 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
|||||||
_currentFrame++;
|
_currentFrame++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] BuildTriangleVertices(Transform transform)
|
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 matrix = transform.GetMatrix();
|
||||||
var positions = new Vector3[]
|
var bytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
|
||||||
{
|
|
||||||
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)
|
fixed (byte* p = bytes)
|
||||||
{
|
{
|
||||||
var dst = (float*)p;
|
var dst = (float*)p;
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < mesh.Vertices.Length; i++)
|
||||||
{
|
{
|
||||||
var transformed = Vector3.Transform(positions[i], matrix);
|
var transformed = Vector3.Transform(mesh.Vertices[i].Position, matrix);
|
||||||
dst[i * 5 + 0] = transformed.X;
|
dst[i * 6 + 0] = transformed.X;
|
||||||
dst[i * 5 + 1] = transformed.Y;
|
dst[i * 6 + 1] = transformed.Y;
|
||||||
dst[i * 5 + 2] = colors[i].X;
|
dst[i * 6 + 2] = transformed.Z;
|
||||||
dst[i * 5 + 3] = colors[i].Y;
|
dst[i * 6 + 3] = mesh.Vertices[i].Color.X;
|
||||||
dst[i * 5 + 4] = colors[i].Z;
|
dst[i * 6 + 4] = mesh.Vertices[i].Color.Y;
|
||||||
|
dst[i * 6 + 5] = mesh.Vertices[i].Color.Z;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
@@ -244,6 +269,10 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
|||||||
{
|
{
|
||||||
_context.Vk.DeviceWaitIdle(_context.Device);
|
_context.Vk.DeviceWaitIdle(_context.Device);
|
||||||
|
|
||||||
|
foreach (var buffers in _buffers.Values)
|
||||||
|
buffers.Dispose();
|
||||||
|
_buffers.Clear();
|
||||||
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
_context.Vk.DestroySemaphore(_context.Device, _renderFinishedSemaphores[i], null);
|
_context.Vk.DestroySemaphore(_context.Device, _renderFinishedSemaphores[i], null);
|
||||||
@@ -253,7 +282,6 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
|||||||
|
|
||||||
_context.Vk.DestroyCommandPool(_context.Device, _commandPool, null);
|
_context.Vk.DestroyCommandPool(_context.Device, _commandPool, null);
|
||||||
|
|
||||||
_vertexBuffer.Dispose();
|
|
||||||
_pipeline.Dispose();
|
_pipeline.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 fragColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 outColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
outColor = vec4(fragColor, 1.0);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec3 inPosition;
|
||||||
|
layout(location = 1) in vec3 inColor;
|
||||||
|
|
||||||
|
layout(location = 0) out vec3 fragColor;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
gl_Position = vec4(inPosition, 1.0);
|
||||||
|
fragColor = inColor;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using Silk.NET.Vulkan;
|
|||||||
namespace Engine.Graphics;
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Simple graphics pipeline for a triangle with vec2 position + vec3 color.
|
/// Simple graphics pipeline for indexed meshes with vec3 position + vec3 color.
|
||||||
/// Uses Silk.NET.Vulkan.
|
/// Uses Silk.NET.Vulkan.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe class VulkanPipeline : IDisposable
|
public sealed unsafe class VulkanPipeline : IDisposable
|
||||||
@@ -93,7 +93,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
|||||||
var bindingDescription = new VertexInputBindingDescription
|
var bindingDescription = new VertexInputBindingDescription
|
||||||
{
|
{
|
||||||
Binding = 0,
|
Binding = 0,
|
||||||
Stride = (uint)(5 * sizeof(float)),
|
Stride = (uint)(6 * sizeof(float)),
|
||||||
InputRate = VertexInputRate.Vertex
|
InputRate = VertexInputRate.Vertex
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
|||||||
{
|
{
|
||||||
Binding = 0,
|
Binding = 0,
|
||||||
Location = 0,
|
Location = 0,
|
||||||
Format = Format.R32G32Sfloat,
|
Format = Format.R32G32B32Sfloat,
|
||||||
Offset = 0
|
Offset = 0
|
||||||
},
|
},
|
||||||
new VertexInputAttributeDescription
|
new VertexInputAttributeDescription
|
||||||
@@ -111,7 +111,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
|||||||
Binding = 0,
|
Binding = 0,
|
||||||
Location = 1,
|
Location = 1,
|
||||||
Format = Format.R32G32B32Sfloat,
|
Format = Format.R32G32B32Sfloat,
|
||||||
Offset = (uint)(2 * sizeof(float))
|
Offset = (uint)(3 * sizeof(float))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user