feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)
- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs) - Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering), synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2) - Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs - Staging buffer → device-local vertex buffer pattern - Correct swapchain semaphore indexing (per-image, not per-frame-in-flight) - VK_EXT_debug_utils debug messenger with validation layer fallback - Dynamic viewport/scissor (no pipeline recreation on resize) - Simplified Program.cs to triangle-only rendering - Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code - Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
This commit is contained in:
@@ -17,15 +17,6 @@
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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' OR '$(Configuration)' == 'ReleaseAOT'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -2,6 +2,9 @@ using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Render context created by a backend. Owns the window and can create a renderer.
|
||||
/// </summary>
|
||||
public interface IRenderContext : IDisposable
|
||||
{
|
||||
IWindow Window { get; }
|
||||
|
||||
@@ -3,10 +3,14 @@ using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Backend-agnostic renderer interface. Minimal version for triangle rendering.
|
||||
/// </summary>
|
||||
public interface IRenderer : IDisposable
|
||||
{
|
||||
void RenderWorld(World world);
|
||||
void RequestScreenshot(string outputPath);
|
||||
|
||||
void RequestScreenshot(string path);
|
||||
bool IsScreenshotRequested { get; }
|
||||
IScreenshotProvider ScreenshotProvider { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to the latest captured screenshot bytes.
|
||||
/// </summary>
|
||||
public interface IScreenshotProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the path of the screenshot file if a screenshot is available; otherwise null.
|
||||
/// </summary>
|
||||
string? TryTakeScreenshotPath();
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
public static class GltfLoader
|
||||
{
|
||||
public static Mesh Load(string path, Vector3? color = null)
|
||||
{
|
||||
var tint = color ?? new Vector3(0.7f, 0.6f, 0.5f);
|
||||
|
||||
var modelRoot = SharpGLTF.Schema2.ModelRoot.Load(path);
|
||||
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
foreach (var scene in modelRoot.LogicalScenes)
|
||||
{
|
||||
foreach (var node in scene.VisualChildren)
|
||||
{
|
||||
var mesh = node.Mesh;
|
||||
if (mesh == null) continue;
|
||||
|
||||
foreach (var primitive in mesh.Primitives)
|
||||
{
|
||||
var posAccess = primitive.GetVertexAccessor("POSITION");
|
||||
var normAccess = primitive.GetVertexAccessor("NORMAL");
|
||||
if (posAccess == null) continue;
|
||||
|
||||
var indexAccess = primitive.IndexAccessor;
|
||||
var baseVertex = (uint)vertices.Count;
|
||||
|
||||
for (var i = 0; i < posAccess.Count; i++)
|
||||
{
|
||||
var pos = posAccess.AsVector3Array()[i];
|
||||
var normal = normAccess != null
|
||||
? normAccess.AsVector3Array()[i]
|
||||
: Vector3.UnitY;
|
||||
|
||||
vertices.Add(new Vertex(pos, tint, normal));
|
||||
}
|
||||
|
||||
if (indexAccess != null)
|
||||
{
|
||||
var indexArray = indexAccess.AsIndicesArray();
|
||||
foreach (var idx in indexArray)
|
||||
{
|
||||
indices.Add((uint)idx + baseVertex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint i = 0; i < posAccess.Count; i++)
|
||||
{
|
||||
indices.Add(baseVertex + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.Count == 0)
|
||||
throw new InvalidOperationException($"GLTF file '{path}' contains no meshes.");
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,54 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal OBJ loader.
|
||||
/// </summary>
|
||||
public static class ObjLoader
|
||||
{
|
||||
private static readonly Vector3 DefaultColor = new(0.7f, 0.6f, 0.5f);
|
||||
|
||||
public static Mesh Load(string path, Vector3? color = null)
|
||||
public static Mesh Load(string path, Vector3? defaultColor = null)
|
||||
{
|
||||
var tint = color ?? DefaultColor;
|
||||
var lines = File.ReadAllLines(path);
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"OBJ file not found: {path}", path);
|
||||
|
||||
var color = defaultColor ?? new Vector3(0.7f, 0.6f, 0.5f);
|
||||
var positions = new List<Vector3>();
|
||||
var normals = new List<Vector3>();
|
||||
|
||||
var texcoords = new List<Vector2>();
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
var faceNormals = new List<Vector3>();
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
var trimmed = line.Trim();
|
||||
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith('#')) continue;
|
||||
|
||||
if (line.Length == 0 || line.StartsWith('#'))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
continue;
|
||||
var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0) continue;
|
||||
|
||||
switch (parts[0])
|
||||
{
|
||||
case "v":
|
||||
positions.Add(new Vector3(
|
||||
float.Parse(parts[1], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[2], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[3], CultureInfo.InvariantCulture)));
|
||||
positions.Add(ParseVector3(parts));
|
||||
break;
|
||||
|
||||
case "vn":
|
||||
normals.Add(new Vector3(
|
||||
float.Parse(parts[1], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[2], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[3], CultureInfo.InvariantCulture)));
|
||||
normals.Add(ParseVector3(parts));
|
||||
break;
|
||||
case "vt":
|
||||
texcoords.Add(ParseVector2(parts));
|
||||
break;
|
||||
|
||||
case "f":
|
||||
ParseFace(parts, positions, normals, vertices, indices, tint);
|
||||
ParseFace(parts, positions, normals, texcoords, color, vertices, indices, faceNormals);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.Count == 0)
|
||||
throw new InvalidOperationException($"OBJ file '{path}' contains no faces.");
|
||||
throw new InvalidOperationException($"OBJ file contains no geometry: {path}");
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
@@ -63,47 +57,52 @@ public static class ObjLoader
|
||||
string[] parts,
|
||||
List<Vector3> positions,
|
||||
List<Vector3> normals,
|
||||
List<Vector2> texcoords,
|
||||
Vector3 color,
|
||||
List<Vertex> vertices,
|
||||
List<uint> indices,
|
||||
Vector3 tint)
|
||||
List<Vector3> faceNormals)
|
||||
{
|
||||
var faceData = new List<(int posIdx, int normIdx)>();
|
||||
var faceIndices = new List<uint>();
|
||||
faceNormals.Clear();
|
||||
|
||||
for (var i = 1; i < parts.Length; i++)
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var vertexData = parts[i].Split('/');
|
||||
var posIdx = int.Parse(vertexData[0]) - 1;
|
||||
var normIdx = vertexData.Length > 2 && !string.IsNullOrEmpty(vertexData[2])
|
||||
? int.Parse(vertexData[2]) - 1
|
||||
: -1;
|
||||
var sub = parts[i].Split('/');
|
||||
var posIndex = int.Parse(sub[0]) - 1;
|
||||
var pos = positions[posIndex];
|
||||
|
||||
faceData.Add((posIdx, normIdx));
|
||||
Vector3 normal = Vector3.UnitY;
|
||||
if (sub.Length > 2 && !string.IsNullOrEmpty(sub[2]))
|
||||
{
|
||||
normal = normals[int.Parse(sub[2]) - 1];
|
||||
}
|
||||
|
||||
vertices.Add(new Vertex(pos, color, normal));
|
||||
faceIndices.Add((uint)(vertices.Count - 1));
|
||||
}
|
||||
|
||||
if (faceData.Count < 3) return;
|
||||
|
||||
for (var i = 1; i < faceData.Count - 1; i++)
|
||||
// Triangulate as a fan.
|
||||
for (int i = 2; i < faceIndices.Count; i++)
|
||||
{
|
||||
var d0 = faceData[0];
|
||||
var d1 = faceData[i];
|
||||
var d2 = faceData[i + 1];
|
||||
|
||||
var p0 = positions[d0.posIdx];
|
||||
var p1 = positions[d1.posIdx];
|
||||
var p2 = positions[d2.posIdx];
|
||||
|
||||
var normal = d0.normIdx >= 0 && d0.normIdx < normals.Count
|
||||
? normals[d0.normIdx]
|
||||
: MeshMath.ComputeFaceNormal(p0, p1, p2);
|
||||
|
||||
var i0 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p0, tint, normal));
|
||||
var i1 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p1, tint, normal));
|
||||
var i2 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p2, tint, normal));
|
||||
|
||||
indices.Add(i0); indices.Add(i1); indices.Add(i2);
|
||||
indices.Add(faceIndices[0]);
|
||||
indices.Add(faceIndices[i - 1]);
|
||||
indices.Add(faceIndices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 ParseVector3(string[] parts)
|
||||
{
|
||||
return new Vector3(
|
||||
float.Parse(parts[1]),
|
||||
float.Parse(parts[2]),
|
||||
float.Parse(parts[3]));
|
||||
}
|
||||
|
||||
private static Vector2 ParseVector2(string[] parts)
|
||||
{
|
||||
return new Vector2(
|
||||
float.Parse(parts[1]),
|
||||
parts.Length > 2 ? float.Parse(parts[2]) : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@ using System.Numerics;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Basic mesh math utilities.
|
||||
/// </summary>
|
||||
public static class MeshMath
|
||||
{
|
||||
public static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var edge1 = b - a;
|
||||
var edge2 = c - a;
|
||||
var normal = Vector3.Cross(edge2, edge1);
|
||||
|
||||
if (normal.LengthSquared() < 1e-12f)
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var cross = Vector3.Cross(ab, ac);
|
||||
if (cross.LengthSquared() < 0.0000001f)
|
||||
return Vector3.UnitY;
|
||||
|
||||
return Vector3.Normalize(normal);
|
||||
return Vector3.Normalize(cross);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,85 +4,129 @@ using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Procedural mesh generators.
|
||||
/// </summary>
|
||||
public static class ProceduralMesh
|
||||
{
|
||||
public static Mesh CreateSphere(float radius, int slices, int stacks, Vector3 color)
|
||||
public static Mesh CreateCube(float size, Vector3 color)
|
||||
{
|
||||
var vertices = new Vertex[(stacks + 1) * (slices + 1)];
|
||||
var indices = new uint[stacks * slices * 6];
|
||||
|
||||
var vi = 0;
|
||||
for (var i = 0; i <= stacks; i++)
|
||||
var s = size * 0.5f;
|
||||
var vertices = new[]
|
||||
{
|
||||
var phi = MathF.PI * i / stacks;
|
||||
var y = radius * MathF.Cos(phi);
|
||||
var r = radius * MathF.Sin(phi);
|
||||
// Front
|
||||
new Vertex(new Vector3(-s, -s, s), color, new Vector3(0, 0, 1)),
|
||||
new Vertex(new Vector3( s, -s, s), color, new Vector3(0, 0, 1)),
|
||||
new Vertex(new Vector3( s, s, s), color, new Vector3(0, 0, 1)),
|
||||
new Vertex(new Vector3(-s, s, s), color, new Vector3(0, 0, 1)),
|
||||
// Back
|
||||
new Vertex(new Vector3( s, -s, -s), color, new Vector3(0, 0, -1)),
|
||||
new Vertex(new Vector3(-s, -s, -s), color, new Vector3(0, 0, -1)),
|
||||
new Vertex(new Vector3(-s, s, -s), color, new Vector3(0, 0, -1)),
|
||||
new Vertex(new Vector3( s, s, -s), color, new Vector3(0, 0, -1)),
|
||||
// Top
|
||||
new Vertex(new Vector3(-s, s, s), color, new Vector3(0, 1, 0)),
|
||||
new Vertex(new Vector3( s, s, s), color, new Vector3(0, 1, 0)),
|
||||
new Vertex(new Vector3( s, s, -s), color, new Vector3(0, 1, 0)),
|
||||
new Vertex(new Vector3(-s, s, -s), color, new Vector3(0, 1, 0)),
|
||||
// Bottom
|
||||
new Vertex(new Vector3(-s, -s, -s), color, new Vector3(0, -1, 0)),
|
||||
new Vertex(new Vector3( s, -s, -s), color, new Vector3(0, -1, 0)),
|
||||
new Vertex(new Vector3( s, -s, s), color, new Vector3(0, -1, 0)),
|
||||
new Vertex(new Vector3(-s, -s, s), color, new Vector3(0, -1, 0)),
|
||||
// Right
|
||||
new Vertex(new Vector3( s, -s, s), color, new Vector3(1, 0, 0)),
|
||||
new Vertex(new Vector3( s, -s, -s), color, new Vector3(1, 0, 0)),
|
||||
new Vertex(new Vector3( s, s, -s), color, new Vector3(1, 0, 0)),
|
||||
new Vertex(new Vector3( s, s, s), color, new Vector3(1, 0, 0)),
|
||||
// Left
|
||||
new Vertex(new Vector3(-s, -s, -s), color, new Vector3(-1, 0, 0)),
|
||||
new Vertex(new Vector3(-s, -s, s), color, new Vector3(-1, 0, 0)),
|
||||
new Vertex(new Vector3(-s, s, s), color, new Vector3(-1, 0, 0)),
|
||||
new Vertex(new Vector3(-s, s, -s), color, new Vector3(-1, 0, 0)),
|
||||
};
|
||||
|
||||
for (var j = 0; j <= slices; j++)
|
||||
{
|
||||
var theta = 2.0f * MathF.PI * j / slices;
|
||||
var x = r * MathF.Cos(theta);
|
||||
var z = r * MathF.Sin(theta);
|
||||
|
||||
var pos = new Vector3(x, y, z);
|
||||
var normal = Vector3.Normalize(pos);
|
||||
|
||||
vertices[vi++] = new Vertex(pos, color, normal);
|
||||
}
|
||||
}
|
||||
|
||||
var ii = 0;
|
||||
for (var i = 0; i < stacks; i++)
|
||||
var indices = new uint[]
|
||||
{
|
||||
for (var j = 0; j < slices; j++)
|
||||
{
|
||||
var a = (uint)(i * (slices + 1) + j);
|
||||
var b = a + 1;
|
||||
var c = a + (uint)(slices + 1);
|
||||
var d = c + 1;
|
||||
|
||||
indices[ii++] = a; indices[ii++] = c; indices[ii++] = b;
|
||||
indices[ii++] = b; indices[ii++] = c; indices[ii++] = d;
|
||||
}
|
||||
}
|
||||
0, 1, 2, 0, 2, 3,
|
||||
4, 5, 6, 4, 6, 7,
|
||||
8, 9, 10, 8, 10, 11,
|
||||
12, 13, 14, 12, 14, 15,
|
||||
16, 17, 18, 16, 18, 19,
|
||||
20, 21, 22, 20, 22, 23,
|
||||
};
|
||||
|
||||
return new Mesh(vertices, indices);
|
||||
}
|
||||
|
||||
public static Mesh CreateGrid(int halfSize, float spacing, Vector3 color)
|
||||
public static Mesh CreateSphere(float radius, int sectors, int stacks, Vector3 color)
|
||||
{
|
||||
var lines = 2 * halfSize + 1;
|
||||
var vertices = new List<Vertex>(lines * 4 * 2);
|
||||
var indices = new List<uint>(lines * 4 * 2);
|
||||
var extent = halfSize * spacing;
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
for (var i = -halfSize; i <= halfSize; i++)
|
||||
for (int i = 0; i <= stacks; i++)
|
||||
{
|
||||
var stackAngle = MathF.PI / 2 - i * MathF.PI / stacks;
|
||||
var xy = radius * MathF.Cos(stackAngle);
|
||||
var z = radius * MathF.Sin(stackAngle);
|
||||
|
||||
for (int j = 0; j <= sectors; j++)
|
||||
{
|
||||
var sectorAngle = j * 2 * MathF.PI / sectors;
|
||||
var x = xy * MathF.Cos(sectorAngle);
|
||||
var y = xy * MathF.Sin(sectorAngle);
|
||||
var pos = new Vector3(x, y, z);
|
||||
var normal = Vector3.Normalize(pos);
|
||||
vertices.Add(new Vertex(pos, color, normal));
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < stacks; i++)
|
||||
{
|
||||
var k1 = (uint)(i * (sectors + 1));
|
||||
var k2 = (uint)(k1 + sectors + 1);
|
||||
|
||||
for (int j = 0; j < sectors; j++, k1++, k2++)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
indices.Add(k1);
|
||||
indices.Add(k2);
|
||||
indices.Add(k1 + 1);
|
||||
}
|
||||
|
||||
if (i != stacks - 1)
|
||||
{
|
||||
indices.Add(k1 + 1);
|
||||
indices.Add(k2);
|
||||
indices.Add(k2 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
|
||||
public static Mesh CreateGrid(int lines, float spacing, Vector3 color)
|
||||
{
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
var max = lines * spacing;
|
||||
var normal = Vector3.UnitY;
|
||||
|
||||
for (int i = -lines; i <= lines; i++)
|
||||
{
|
||||
var pos = i * spacing;
|
||||
|
||||
var i0 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, -extent), color, Vector3.UnitY));
|
||||
var i1 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, extent), color, Vector3.UnitY));
|
||||
indices.Add(i0); indices.Add(i1);
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, -max), color, normal));
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, max), color, normal));
|
||||
indices.Add((uint)(vertices.Count - 2));
|
||||
indices.Add((uint)(vertices.Count - 1));
|
||||
|
||||
var i2 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, -extent), color, Vector3.UnitY));
|
||||
var i3 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, extent), color, Vector3.UnitY));
|
||||
indices.Add(i2); indices.Add(i3);
|
||||
|
||||
var i4 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(-extent, 0, pos), color, Vector3.UnitY));
|
||||
var i5 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(extent, 0, pos), color, Vector3.UnitY));
|
||||
indices.Add(i4); indices.Add(i5);
|
||||
|
||||
var i6 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(-extent, 0, pos), color, Vector3.UnitY));
|
||||
var i7 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(extent, 0, pos), color, Vector3.UnitY));
|
||||
indices.Add(i6); indices.Add(i7);
|
||||
vertices.Add(new Vertex(new Vector3(-max, 0, pos), color, normal));
|
||||
vertices.Add(new Vertex(new Vector3( max, 0, pos), color, normal));
|
||||
indices.Add((uint)(vertices.Count - 2));
|
||||
indices.Add((uint)(vertices.Count - 1));
|
||||
}
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating render backends by name.
|
||||
/// </summary>
|
||||
public static class RenderBackendFactory
|
||||
{
|
||||
private static readonly Dictionary<string, Func<int, int, bool, IRenderContext>> _backends =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, Func<int, int, bool, IRenderContext>> _backends = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Register a backend factory. Case-insensitive lookup.
|
||||
/// </summary>
|
||||
public static void Register(string name, Func<int, int, bool, IRenderContext> factory)
|
||||
{
|
||||
_backends[name] = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a render context for the given backend.
|
||||
/// </summary>
|
||||
public static IRenderContext Create(string name, int width, int height, bool enableValidation)
|
||||
{
|
||||
if (_backends.TryGetValue(name, out var factory))
|
||||
return factory(width, height, enableValidation);
|
||||
if (!_backends.TryGetValue(name, out var factory))
|
||||
throw new NotSupportedException($"Render backend '{name}' is not registered.");
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"Unknown render backend '{name}'. Available: {string.Join(", ", _backends.Keys)}");
|
||||
return factory(width, height, enableValidation);
|
||||
}
|
||||
|
||||
public static bool IsRegistered(string name) => _backends.ContainsKey(name);
|
||||
}
|
||||
|
||||
@@ -1,86 +1,45 @@
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Engine.Core.Components;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes and deserializes entity scenes to JSON.
|
||||
/// Minimal version: handles Transform, Material, Light, Camera, Mesh.
|
||||
/// </summary>
|
||||
public static class SceneSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
WriteIndented = true,
|
||||
IncludeFields = true
|
||||
};
|
||||
|
||||
public static string SaveToString(World world)
|
||||
{
|
||||
var entities = new List<SceneEntityData>();
|
||||
|
||||
world.Each((Entity e, ref Transform _) =>
|
||||
var entities = new List<SceneEntity>();
|
||||
world.Each((Entity e, ref Transform t) =>
|
||||
{
|
||||
var name = e.Name();
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
var entity = new SceneEntity { Name = name };
|
||||
|
||||
var data = new SceneEntityData { Name = name };
|
||||
|
||||
if (e.Has<Transform>())
|
||||
{
|
||||
var t = e.Get<Transform>();
|
||||
data.Transform = new TransformData
|
||||
{
|
||||
Position = new float[] { t.Position.X, t.Position.Y, t.Position.Z },
|
||||
Rotation = new float[] { t.Rotation.X, t.Rotation.Y, t.Rotation.Z, t.Rotation.W },
|
||||
Scale = new float[] { t.Scale.X, t.Scale.Y, t.Scale.Z }
|
||||
};
|
||||
}
|
||||
entity.Transform = t;
|
||||
|
||||
if (e.Has<Material>())
|
||||
{
|
||||
var m = e.Get<Material>();
|
||||
data.Material = new MaterialData
|
||||
{
|
||||
Albedo = new float[] { m.Albedo.X, m.Albedo.Y, m.Albedo.Z },
|
||||
Roughness = m.Roughness,
|
||||
Metallic = m.Metallic,
|
||||
TexturePath = m.TexturePath
|
||||
};
|
||||
}
|
||||
entity.Material = e.Get<Material>();
|
||||
|
||||
if (e.Has<Light>())
|
||||
{
|
||||
var l = e.Get<Light>();
|
||||
data.Light = new LightData
|
||||
{
|
||||
Type = l.Type.ToString(),
|
||||
Direction = new float[] { l.Direction.X, l.Direction.Y, l.Direction.Z },
|
||||
Position = new float[] { l.Position.X, l.Position.Y, l.Position.Z },
|
||||
Color = new float[] { l.Color.X, l.Color.Y, l.Color.Z },
|
||||
Intensity = l.Intensity,
|
||||
Range = l.Range
|
||||
};
|
||||
}
|
||||
entity.Light = e.Get<Light>();
|
||||
|
||||
if (e.Has<Camera>())
|
||||
{
|
||||
var c = e.Get<Camera>();
|
||||
data.Camera = new CameraData
|
||||
{
|
||||
Position = new float[] { c.Position.X, c.Position.Y, c.Position.Z },
|
||||
Target = new float[] { c.Target.X, c.Target.Y, c.Target.Z },
|
||||
Up = new float[] { c.Up.X, c.Up.Y, c.Up.Z },
|
||||
FieldOfView = c.FieldOfView,
|
||||
AspectRatio = c.AspectRatio,
|
||||
NearPlane = c.NearPlane,
|
||||
FarPlane = c.FarPlane
|
||||
};
|
||||
}
|
||||
entity.Camera = e.Get<Camera>();
|
||||
|
||||
entities.Add(data);
|
||||
entities.Add(entity);
|
||||
});
|
||||
|
||||
return JsonSerializer.Serialize(entities, JsonOptions);
|
||||
return JsonSerializer.Serialize(entities, Options);
|
||||
}
|
||||
|
||||
public static void SaveToFile(World world, string path)
|
||||
@@ -91,114 +50,42 @@ public static class SceneSerializer
|
||||
|
||||
public static int LoadFromString(World world, string json)
|
||||
{
|
||||
var entities = JsonSerializer.Deserialize<List<SceneEntityData>>(json, JsonOptions);
|
||||
var entities = JsonSerializer.Deserialize<List<SceneEntity>>(json, Options);
|
||||
if (entities == null) return 0;
|
||||
|
||||
foreach (var data in entities)
|
||||
foreach (var e in entities)
|
||||
{
|
||||
var entity = world.Entity(data.Name);
|
||||
var entity = world.Entity(e.Name);
|
||||
entity.Set(e.Transform);
|
||||
|
||||
if (data.Transform != null)
|
||||
{
|
||||
var t = data.Transform;
|
||||
entity.Set(new Transform(
|
||||
new Vector3(t.Position[0], t.Position[1], t.Position[2]),
|
||||
new Quaternion(t.Rotation[0], t.Rotation[1], t.Rotation[2], t.Rotation[3]),
|
||||
new Vector3(t.Scale[0], t.Scale[1], t.Scale[2])));
|
||||
}
|
||||
if (e.Material != null)
|
||||
entity.Set(e.Material.Value);
|
||||
|
||||
if (data.Material != null)
|
||||
{
|
||||
var m = data.Material;
|
||||
entity.Set(new Material(
|
||||
new Vector3(m.Albedo[0], m.Albedo[1], m.Albedo[2]),
|
||||
m.Roughness, m.Metallic, m.TexturePath));
|
||||
}
|
||||
if (e.Light != null)
|
||||
entity.Set(e.Light.Value);
|
||||
|
||||
if (data.Light != null)
|
||||
{
|
||||
var l = data.Light;
|
||||
if (l.Type == "Directional")
|
||||
{
|
||||
entity.Set(Light.Directional(
|
||||
new Vector3(l.Direction[0], l.Direction[1], l.Direction[2]),
|
||||
new Vector3(l.Color[0], l.Color[1], l.Color[2]),
|
||||
l.Intensity));
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Set(Light.Point(
|
||||
new Vector3(l.Position[0], l.Position[1], l.Position[2]),
|
||||
new Vector3(l.Color[0], l.Color[1], l.Color[2]),
|
||||
l.Intensity, l.Range));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Camera != null)
|
||||
{
|
||||
var c = data.Camera;
|
||||
entity.Set(new Camera(
|
||||
new Vector3(c.Position[0], c.Position[1], c.Position[2]),
|
||||
new Vector3(c.Target[0], c.Target[1], c.Target[2]),
|
||||
new Vector3(c.Up[0], c.Up[1], c.Up[2]),
|
||||
c.FieldOfView, c.AspectRatio, c.NearPlane, c.FarPlane));
|
||||
}
|
||||
if (e.Camera != null)
|
||||
entity.Set(e.Camera.Value);
|
||||
}
|
||||
|
||||
return entities.Count;
|
||||
}
|
||||
|
||||
public static int LoadFromFile(World world, string path)
|
||||
public static void LoadFromFile(World world, string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"Scene file not found: {path}", path);
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
return LoadFromString(world, json);
|
||||
LoadFromString(world, json);
|
||||
}
|
||||
|
||||
private class SceneEntityData
|
||||
private class SceneEntity
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public TransformData? Transform { get; set; }
|
||||
public MaterialData? Material { get; set; }
|
||||
public LightData? Light { get; set; }
|
||||
public CameraData? Camera { get; set; }
|
||||
}
|
||||
|
||||
private class TransformData
|
||||
{
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Rotation { get; set; } = Array.Empty<float>();
|
||||
public float[] Scale { get; set; } = Array.Empty<float>();
|
||||
}
|
||||
|
||||
private class MaterialData
|
||||
{
|
||||
public float[] Albedo { get; set; } = Array.Empty<float>();
|
||||
public float Roughness { get; set; }
|
||||
public float Metallic { get; set; }
|
||||
public string? TexturePath { get; set; }
|
||||
}
|
||||
|
||||
private class LightData
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public float[] Direction { get; set; } = Array.Empty<float>();
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Color { get; set; } = Array.Empty<float>();
|
||||
public float Intensity { get; set; }
|
||||
public float Range { get; set; }
|
||||
}
|
||||
|
||||
private class CameraData
|
||||
{
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Target { get; set; } = Array.Empty<float>();
|
||||
public float[] Up { get; set; } = Array.Empty<float>();
|
||||
public float FieldOfView { get; set; }
|
||||
public float AspectRatio { get; set; }
|
||||
public float NearPlane { get; set; }
|
||||
public float FarPlane { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public Transform Transform { get; set; }
|
||||
public Material? Material { get; set; }
|
||||
public Light? Light { get; set; }
|
||||
public Camera? Camera { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user