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
+58
View File
@@ -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);
}
}
+76
View File
@@ -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
}
}