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:
@@ -23,8 +23,6 @@
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics.Vulkan\Engine.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Engine.AI\Engine.AI.csproj" />
|
||||
<ProjectReference Include="..\Engine.Physics\Engine.Physics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Engine.AI;
|
||||
#if !RELEASE_AOT
|
||||
using Engine.AI.Mcp;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
#endif
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using Engine.Graphics;
|
||||
using Engine.Graphics.Loaders;
|
||||
using Engine.Graphics.Vulkan;
|
||||
using Engine.Physics;
|
||||
using Flecs.NET.Core;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace CortexEngine.App;
|
||||
|
||||
@@ -22,280 +9,40 @@ class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Cortex Engine — Vulkan Backend, Pure P/Invoke...");
|
||||
Console.WriteLine("Cortex Engine — Vulkan Triangle (pure P/Invoke)...");
|
||||
|
||||
try
|
||||
{
|
||||
if (args.Contains("--mcp-stdio"))
|
||||
{
|
||||
RunMcpStdioServer();
|
||||
return;
|
||||
}
|
||||
|
||||
var cameraTour = args.Contains("--camera-tour");
|
||||
var testScene = args.Contains("--test-scene");
|
||||
if (testScene)
|
||||
cameraTour = true;
|
||||
|
||||
using var world = World.Create();
|
||||
var timing = new Timing();
|
||||
using var physicsWorld = new PhysicsWorld();
|
||||
|
||||
VulkanBackendRegistrar.EnsureRegistered();
|
||||
using var renderContext = RenderBackendFactory.Create("vulkan", 1280, 720, enableValidation: true);
|
||||
var window = renderContext.Window;
|
||||
var input = window.Input;
|
||||
using var renderer = renderContext.CreateRenderer();
|
||||
|
||||
VulkanImGui? imGuiLayer = null;
|
||||
if (!cameraTour && renderer is VulkanRenderer vkRenderer)
|
||||
{
|
||||
ImGui.CreateContext();
|
||||
imGuiLayer = new VulkanImGui(vkRenderer._ctx, vkRenderer._swapchain);
|
||||
vkRenderer.ImGuiLayer = imGuiLayer;
|
||||
ImGui.GetIO().DisplaySize = new System.Numerics.Vector2(window.Width, window.Height);
|
||||
Console.WriteLine("[App] ImGui initialized.");
|
||||
}
|
||||
using var world = World.Create();
|
||||
|
||||
var (modelPath, mcpPort) = ParseArgs(args);
|
||||
var mesh = LoadModel(modelPath);
|
||||
|
||||
var processor = new AiCommandProcessor(world, LoadModel, path => renderer.RequestScreenshot(path));
|
||||
var queue = new AiCommandQueue(processor, renderer.ScreenshotProvider);
|
||||
|
||||
var cameraEntity = world.Entity("Camera")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.75f, -30.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Camera(
|
||||
new Vector3(0.0f, 0.75f, -30.0f),
|
||||
new Vector3(0.0f, 0.5f, 0.0f),
|
||||
Vector3.UnitY,
|
||||
MathF.PI / 12.0f,
|
||||
1280.0f / 720.0f,
|
||||
0.1f,
|
||||
100.0f));
|
||||
|
||||
world.Entity("MainLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(Light.Directional(new Vector3(0.4f, -1.0f, -0.3f), new Vector3(1.0f, 0.95f, 0.85f), 2.0f));
|
||||
|
||||
ICameraController[] cameraControllers =
|
||||
{
|
||||
new FreeFlyCameraController(cameraEntity),
|
||||
new OrbitCameraController(cameraEntity, new Vector3(0.0f, 0.5f, 0.0f))
|
||||
};
|
||||
var activeControllerIndex = 0;
|
||||
var cameraController = cameraControllers[activeControllerIndex];
|
||||
Console.WriteLine($"Active camera controller: {cameraController.Name} (press F to toggle)");
|
||||
|
||||
if (testScene)
|
||||
{
|
||||
Console.WriteLine("Calibration test scene enabled.");
|
||||
CreateCalibrationScene(world, mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateDemoScene(world, mesh);
|
||||
}
|
||||
|
||||
#if !RELEASE_AOT
|
||||
WebApplication? mcpApp = null;
|
||||
Task? mcpTask = null;
|
||||
|
||||
if (mcpPort > 0)
|
||||
{
|
||||
mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
|
||||
mcpTask = mcpApp.RunAsync();
|
||||
_ = mcpTask.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Console.WriteLine($"MCP server error: {t.Exception?.GetBaseException().Message}");
|
||||
else if (t.IsCanceled)
|
||||
Console.WriteLine("MCP server canceled.");
|
||||
else
|
||||
Console.WriteLine("MCP server stopped.");
|
||||
}, TaskScheduler.Default);
|
||||
|
||||
Console.WriteLine($"MCP HTTP server listening on http://localhost:{mcpPort}/ (SSE)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("MCP server disabled (--mcp-port 0).");
|
||||
}
|
||||
#endif
|
||||
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var lastWidth = window.Width;
|
||||
var lastHeight = window.Height;
|
||||
var demoScreenshotRequested = false;
|
||||
var currentFps = 0;
|
||||
|
||||
var tourPoses = testScene
|
||||
? new CameraPose[]
|
||||
{
|
||||
new("test_front", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_back", new Vector3(0.0f, 0.75f, 30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_left", new Vector3(-30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_right", new Vector3(30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_top", new Vector3(0.0f, 30.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), -Vector3.UnitZ),
|
||||
new("test_shifted", new Vector3(15.0f, 0.75f, -22.5f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_rotated", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(2.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_yaw_15", new Vector3(7.76f, 0.75f, -28.98f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_yaw_30", new Vector3(15.0f, 0.75f, -25.98f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_yaw_45", new Vector3(21.21f, 0.75f, -21.21f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_yaw_90", new Vector3(30.0f, 0.75f, 0.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_pitch_45", new Vector3(0.0f, 21.96f, -21.21f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_close", new Vector3(0.0f, 0.75f, -15.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_far", new Vector3(0.0f, 0.75f, -60.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_farther", new Vector3(0.0f, 0.75f, -120.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("test_toward", new Vector3(0.0f, 0.75f, -20.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY)
|
||||
}
|
||||
: new CameraPose[]
|
||||
{
|
||||
new("front", new Vector3(0.0f, 0.75f, -30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("top", new Vector3(0.0f, 30.0f, 0.0f), new Vector3(0.0f, 0.0f, 0.0f), -Vector3.UnitZ),
|
||||
new("side", new Vector3(30.0f, 0.75f, 4.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("close", new Vector3(1.0f, 0.75f, -5.0f), new Vector3(0.5f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("low", new Vector3(0.0f, 0.25f, -6.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY),
|
||||
new("back", new Vector3(0.0f, 0.75f, 30.0f), new Vector3(0.0f, 0.5f, 0.0f), Vector3.UnitY)
|
||||
};
|
||||
var tourIndex = -1;
|
||||
var tourSettleFrames = 0;
|
||||
var tourScreenshotPending = false;
|
||||
var tourDone = false;
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var timing = new Timing();
|
||||
|
||||
while (!window.ShouldClose)
|
||||
{
|
||||
timing.Tick();
|
||||
window.PumpEvents();
|
||||
input.BeginFrame();
|
||||
|
||||
var processed = queue.ProcessPending();
|
||||
if (processed > 0)
|
||||
Console.WriteLine($"Processed {processed} AI command(s)");
|
||||
|
||||
if (window.Width != lastWidth || window.Height != lastHeight)
|
||||
{
|
||||
lastWidth = window.Width;
|
||||
lastHeight = window.Height;
|
||||
renderContext.Resize(lastWidth, lastHeight);
|
||||
ref var camera = ref cameraEntity.Ensure<Camera>();
|
||||
camera.AspectRatio = (float)lastWidth / lastHeight;
|
||||
}
|
||||
|
||||
if (input.IsKeyPressed(Key.F))
|
||||
{
|
||||
activeControllerIndex = (activeControllerIndex + 1) % cameraControllers.Length;
|
||||
cameraController = cameraControllers[activeControllerIndex];
|
||||
Console.WriteLine($"Active camera controller: {cameraController.Name}");
|
||||
}
|
||||
|
||||
if (!cameraTour)
|
||||
cameraController.Update(input, (float)timing.DeltaTime);
|
||||
|
||||
if (cameraTour && !tourDone)
|
||||
{
|
||||
if (tourIndex < 0)
|
||||
{
|
||||
tourIndex = 0;
|
||||
SetCameraPose(cameraEntity, tourPoses[tourIndex]);
|
||||
tourSettleFrames = 0;
|
||||
tourScreenshotPending = true;
|
||||
}
|
||||
|
||||
if (tourScreenshotPending)
|
||||
{
|
||||
tourSettleFrames++;
|
||||
if (tourSettleFrames >= 5)
|
||||
{
|
||||
var path = $"Screenshots/tour_{tourPoses[tourIndex].Name}.png";
|
||||
renderer.RequestScreenshot(path);
|
||||
Console.WriteLine($"Tour screenshot: {path}");
|
||||
tourScreenshotPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tourScreenshotPending && !renderer.IsScreenshotRequested)
|
||||
{
|
||||
tourIndex++;
|
||||
if (tourIndex >= tourPoses.Length)
|
||||
{
|
||||
tourDone = true;
|
||||
Console.WriteLine("Camera tour complete.");
|
||||
window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCameraPose(cameraEntity, tourPoses[tourIndex]);
|
||||
tourSettleFrames = 0;
|
||||
tourScreenshotPending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!cameraTour)
|
||||
{
|
||||
var toInit = new List<(Entity, RigidBody, Transform)>();
|
||||
world.Each((Entity e, ref RigidBody rb, ref Transform t) =>
|
||||
{
|
||||
if (!rb.IsInitialized)
|
||||
toInit.Add((e, rb, t));
|
||||
});
|
||||
|
||||
foreach (var (e, rbData, t) in toInit)
|
||||
{
|
||||
physicsWorld.CreateBody(e, rbData, t);
|
||||
var rb = rbData;
|
||||
rb.IsInitialized = true;
|
||||
e.Set(rb);
|
||||
}
|
||||
|
||||
physicsWorld.Update((float)timing.DeltaTime);
|
||||
physicsWorld.SyncTransforms(world, null);
|
||||
}
|
||||
|
||||
if (!demoScreenshotRequested && !cameraTour && frames >= 15)
|
||||
{
|
||||
renderer.RequestScreenshot("Screenshots/demo.png");
|
||||
demoScreenshotRequested = true;
|
||||
}
|
||||
|
||||
if (imGuiLayer != null)
|
||||
{
|
||||
ImGui.NewFrame();
|
||||
ImGui.Begin("Cortex Engine Debug");
|
||||
ImGui.Text($"FPS: {currentFps}");
|
||||
ImGui.Text($"Delta: {timing.DeltaTime * 1000.0:F2} ms");
|
||||
ImGui.Text($"Camera: {cameraController.Name}");
|
||||
ImGui.Separator();
|
||||
var cam = cameraEntity.Get<Camera>();
|
||||
ImGui.Text($"Pos: ({cam.Position.X:F2}, {cam.Position.Y:F2}, {cam.Position.Z:F2})");
|
||||
ImGui.Text($"Target: ({cam.Target.X:F2}, {cam.Target.Y:F2}, {cam.Target.Z:F2})");
|
||||
ImGui.Separator();
|
||||
|
||||
var entityCount = 0;
|
||||
world.Each((Entity e, ref Transform _) => entityCount++);
|
||||
ImGui.Text($"Entities: {entityCount}");
|
||||
ImGui.Text($"Press F to toggle camera");
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.Text("Lights:");
|
||||
world.Each((Entity e, ref Light light) =>
|
||||
{
|
||||
ImGui.Text($" {e.Name()}: {light.Type} I={light.Intensity:F1}");
|
||||
});
|
||||
|
||||
ImGui.End();
|
||||
ImGui.Render();
|
||||
}
|
||||
|
||||
renderer.RenderWorld(world);
|
||||
queue.CompletePendingScreenshots();
|
||||
|
||||
frames++;
|
||||
if (timing.TotalTime - lastFpsTime >= 1.0)
|
||||
{
|
||||
currentFps = frames;
|
||||
Console.WriteLine($"FPS: {frames}, Delta: {timing.DeltaTime * 1000.0:F2} ms");
|
||||
frames = 0;
|
||||
lastFpsTime = timing.TotalTime;
|
||||
@@ -303,181 +50,13 @@ class Program
|
||||
}
|
||||
|
||||
Console.WriteLine("Shutting down...");
|
||||
imGuiLayer?.Dispose();
|
||||
#if !RELEASE_AOT
|
||||
if (mcpApp != null)
|
||||
await mcpApp.StopAsync();
|
||||
if (mcpTask != null)
|
||||
await mcpTask;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fatal error: {ex}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateDemoScene(World world, Mesh mesh)
|
||||
{
|
||||
var sphere = ProceduralMesh.CreateSphere(0.5f, 32, 16, new Vector3(0.8f, 0.8f, 0.8f));
|
||||
var torusKnot = ObjLoader.Load("Content/torusknot.obj", new Vector3(0.8f, 0.8f, 0.8f));
|
||||
|
||||
var cubes = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[]
|
||||
{
|
||||
("CubeCenter", new Vector3(0, 5f, 0), new Vector3(0.9f, 0.6f, 0.3f), 0.5f, 0.3f, 0.1f),
|
||||
("CubeRed", new Vector3(0.3f, 7f, 0.3f), new Vector3(0.85f, 0.15f, 0.15f), 0.5f, 0.4f, 0.2f),
|
||||
("CubeGreen", new Vector3(-0.3f, 9f, -0.3f), new Vector3(0.2f, 0.8f, 0.3f), 0.5f, 0.5f, 0.0f),
|
||||
("CubeBlue", new Vector3(0.1f, 11f, 0.1f), new Vector3(0.2f, 0.4f, 0.9f), 0.6f, 0.2f, 0.3f),
|
||||
("CubeYellow", new Vector3(-0.2f, 13f, 0.2f), new Vector3(0.95f, 0.85f, 0.2f), 0.5f, 0.6f, 0.0f),
|
||||
("CubeOrange", new Vector3(0.15f, 15f, -0.1f), new Vector3(0.95f, 0.5f, 0.1f), 0.45f, 0.5f, 0.1f),
|
||||
};
|
||||
|
||||
foreach (var (name, pos, color, scale, rough, metal) in cubes)
|
||||
{
|
||||
world.Entity(name)
|
||||
.Set(new Transform(pos, Quaternion.Identity, new Vector3(scale)))
|
||||
.Set(mesh)
|
||||
.Set(new Material(color, roughness: rough, metallic: metal))
|
||||
.Set(RigidBody.DynamicBox(new Vector3(scale * 0.5f), mass: scale * 2f));
|
||||
}
|
||||
|
||||
var spheres = new (string name, Vector3 pos, Vector3 color, float scale, float rough, float metal)[]
|
||||
{
|
||||
("SphereGold", new Vector3(3, 6f, -2), new Vector3(1.0f, 0.85f, 0.4f), 1.0f, 0.1f, 1.0f),
|
||||
("SphereChrome", new Vector3(-3, 8f, 0), new Vector3(0.9f, 0.9f, 0.95f), 1.0f, 0.05f, 1.0f),
|
||||
("SphereRed", new Vector3(3, 10f, 2), new Vector3(0.9f, 0.1f, 0.1f), 1.0f, 0.4f, 0.0f),
|
||||
};
|
||||
|
||||
foreach (var (name, pos, color, scale, rough, metal) in spheres)
|
||||
{
|
||||
world.Entity(name)
|
||||
.Set(new Transform(pos, Quaternion.Identity, new Vector3(scale)))
|
||||
.Set(sphere)
|
||||
.Set(new Material(color, roughness: rough, metallic: metal))
|
||||
.Set(RigidBody.DynamicSphere(scale * 0.5f, mass: scale));
|
||||
}
|
||||
|
||||
world.Entity("TorusKnot")
|
||||
.Set(new Transform(new Vector3(0, 0.5f, -6), Quaternion.Identity, new Vector3(1.5f)))
|
||||
.Set(torusKnot)
|
||||
.Set(new Material(new Vector3(0.9f, 0.9f, 0.9f), roughness: 0.25f, metallic: 0.6f));
|
||||
|
||||
world.Entity("Floor")
|
||||
.Set(new Transform(new Vector3(0, -0.5f, 0), Quaternion.Identity, new Vector3(20, 0.5f, 20)))
|
||||
.Set(mesh)
|
||||
.Set(new Material(new Vector3(0.45f, 0.45f, 0.5f), roughness: 0.8f, metallic: 0.0f))
|
||||
.Set(RigidBody.StaticPlane(20f));
|
||||
|
||||
world.Entity("Grid")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
|
||||
.Set(new Material(new Vector3(0.5f, 0.5f, 0.55f), roughness: 0.9f, metallic: 0.0f));
|
||||
}
|
||||
|
||||
private static void CreateCalibrationScene(World world, Mesh mesh)
|
||||
{
|
||||
var positions = new (string name, Vector3 pos, Vector3 color)[]
|
||||
{
|
||||
("CubeOrigin", new Vector3(0.0f, 0.5f, 0.0f), new Vector3(1.0f, 1.0f, 1.0f)),
|
||||
("CubeRight", new Vector3(2.0f, 0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f)),
|
||||
("CubeLeft", new Vector3(-2.0f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f)),
|
||||
("CubeFront", new Vector3(0.0f, 0.5f, 2.0f), new Vector3(0.0f, 0.0f, 1.0f)),
|
||||
("CubeBack", new Vector3(0.0f, 0.5f, -2.0f), new Vector3(1.0f, 1.0f, 0.0f)),
|
||||
("CubeUp", new Vector3(0.0f, 2.5f, 0.0f), new Vector3(1.0f, 0.0f, 1.0f)),
|
||||
("CubeFar", new Vector3(0.0f, 0.5f, 8.0f), new Vector3(0.0f, 1.0f, 1.0f)),
|
||||
("CubeFarLeft", new Vector3(-5.0f, 0.5f, 5.0f), new Vector3(0.5f, 0.5f, 1.0f))
|
||||
};
|
||||
|
||||
foreach (var (name, pos, color) in positions)
|
||||
{
|
||||
world.Entity(name)
|
||||
.Set(new Transform(pos, Quaternion.Identity, new Vector3(0.5f)))
|
||||
.Set(mesh)
|
||||
.Set(new Material(color, roughness: 0.5f, metallic: 0.1f));
|
||||
}
|
||||
|
||||
world.Entity("Grid")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
|
||||
.Set(new Material(new Vector3(0.5f, 0.5f, 0.55f), roughness: 0.9f, metallic: 0.0f));
|
||||
}
|
||||
|
||||
private static Mesh LoadModel(string path)
|
||||
{
|
||||
return path.EndsWith(".gltf", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)
|
||||
? GltfLoader.Load(path, new Vector3(0.7f, 0.6f, 0.5f))
|
||||
: ObjLoader.Load(path, new Vector3(0.7f, 0.6f, 0.5f));
|
||||
}
|
||||
|
||||
private static (string modelPath, int mcpPort) ParseArgs(string[] args)
|
||||
{
|
||||
var modelPath = FindModelPath(args);
|
||||
var mcpPort = 5000;
|
||||
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--mcp-port" && i + 1 < args.Length && int.TryParse(args[i + 1], out var port))
|
||||
{
|
||||
mcpPort = port;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (modelPath, mcpPort);
|
||||
}
|
||||
|
||||
private static string FindModelPath(string[] args)
|
||||
{
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
if (arg == "--mcp-port")
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(arg))
|
||||
return arg;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
private static void RunMcpStdioServer()
|
||||
{
|
||||
Console.WriteLine("Starting headless stdio MCP server...");
|
||||
using var world = World.Create();
|
||||
var processor = new AiCommandProcessor(world, LoadModel, _ => { });
|
||||
var server = new Engine.AI.Stdio.McpStdioServer(processor);
|
||||
server.Run();
|
||||
}
|
||||
|
||||
private readonly record struct CameraPose(string Name, Vector3 Position, Vector3 Target, Vector3 Up, float Fov = MathF.PI / 12.0f);
|
||||
|
||||
private static void SetCameraPose(Entity cameraEntity, CameraPose pose)
|
||||
{
|
||||
ref var camera = ref cameraEntity.Ensure<Camera>();
|
||||
camera.Position = pose.Position;
|
||||
camera.Target = pose.Target;
|
||||
camera.Up = pose.Up;
|
||||
camera.FieldOfView = pose.Fov;
|
||||
cameraEntity.Set(camera);
|
||||
Console.WriteLine($"Camera pose '{pose.Name}': pos={pose.Position}, target={pose.Target}, up={pose.Up}, fov={pose.Fov * 180f / MathF.PI:F0}°");
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user