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
+39 -13
View File
@@ -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.");
}
}