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:
@@ -24,4 +24,11 @@
|
||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\Content\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Content\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using Engine.Graphics;
|
||||
using Engine.Graphics.Loaders;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace CortexEngine.App;
|
||||
@@ -11,20 +13,27 @@ class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Cortex Engine Step 3 — Starting up...");
|
||||
Console.WriteLine("Cortex Engine Step 4 — Starting up...");
|
||||
|
||||
try
|
||||
{
|
||||
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 input = new InputMapping();
|
||||
using var vulkan = new VulkanContext(window, enableValidation: false);
|
||||
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")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f)));
|
||||
var modelPath = FindModelPath(args);
|
||||
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 lastFpsTime = 0.0;
|
||||
@@ -46,14 +55,10 @@ class Program
|
||||
swapchain.Recreate(lastWidth, lastHeight);
|
||||
}
|
||||
|
||||
// Animate the entity transform.
|
||||
ref var transform = ref triangle.Ensure<Transform>();
|
||||
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);
|
||||
// Slowly rotate the model so we can see it in 3D.
|
||||
ref var transform = ref model.Ensure<Transform>();
|
||||
transform.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)timing.TotalTime * 0.5f)
|
||||
* Quaternion.CreateFromAxisAngle(Vector3.UnitX, (float)timing.TotalTime * 0.25f);
|
||||
|
||||
renderer.RenderWorld(world);
|
||||
|
||||
@@ -74,4 +79,25 @@ class Program
|
||||
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="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="SharpGLTF.Core" Version="1.0.6" />
|
||||
</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.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 a colored triangle using a vertex buffer and a simple graphics pipeline.
|
||||
/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world.
|
||||
/// 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 TriangleRenderer : IDisposable
|
||||
public sealed unsafe class MeshRenderer : IDisposable
|
||||
{
|
||||
private readonly VulkanContext _context;
|
||||
private readonly Swapchain _swapchain;
|
||||
private readonly VulkanPipeline _pipeline;
|
||||
private readonly VertexBuffer _vertexBuffer;
|
||||
private readonly Dictionary<Entity, MeshBuffers> _buffers = new();
|
||||
private CommandPool _commandPool;
|
||||
private CommandBuffer[] _commandBuffers = 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 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;
|
||||
_swapchain = swapchain;
|
||||
|
||||
_pipeline = new VulkanPipeline(context, swapchain);
|
||||
_vertexBuffer = CreateTriangleBuffer();
|
||||
CreateCommandPool();
|
||||
CreateCommandBuffers();
|
||||
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()
|
||||
{
|
||||
var createInfo = new CommandPoolCreateInfo
|
||||
@@ -160,16 +160,23 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
|
||||
_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;
|
||||
world.Each((Entity e, ref Transform transform) =>
|
||||
world.Each((Entity e, ref Mesh mesh, ref Transform transform) =>
|
||||
{
|
||||
var bytes = BuildTriangleVertices(transform);
|
||||
_vertexBuffer.Update(bytes);
|
||||
_context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0);
|
||||
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);
|
||||
@@ -207,34 +214,52 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
_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 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)];
|
||||
var bytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
|
||||
fixed (byte* p = bytes)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
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;
|
||||
@@ -244,6 +269,10 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
{
|
||||
_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);
|
||||
@@ -253,7 +282,6 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
|
||||
_context.Vk.DestroyCommandPool(_context.Device, _commandPool, null);
|
||||
|
||||
_vertexBuffer.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;
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public sealed unsafe class VulkanPipeline : IDisposable
|
||||
@@ -93,7 +93,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
var bindingDescription = new VertexInputBindingDescription
|
||||
{
|
||||
Binding = 0,
|
||||
Stride = (uint)(5 * sizeof(float)),
|
||||
Stride = (uint)(6 * sizeof(float)),
|
||||
InputRate = VertexInputRate.Vertex
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
{
|
||||
Binding = 0,
|
||||
Location = 0,
|
||||
Format = Format.R32G32Sfloat,
|
||||
Format = Format.R32G32B32Sfloat,
|
||||
Offset = 0
|
||||
},
|
||||
new VertexInputAttributeDescription
|
||||
@@ -111,7 +111,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
Binding = 0,
|
||||
Location = 1,
|
||||
Format = Format.R32G32B32Sfloat,
|
||||
Offset = (uint)(2 * sizeof(float))
|
||||
Offset = (uint)(3 * sizeof(float))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user