msh viewport initial

This commit is contained in:
bird_egop
2026-06-05 21:12:25 +03:00
parent 614787789e
commit 49d9e23738
9 changed files with 381 additions and 2 deletions
+65
View File
@@ -2,7 +2,9 @@ using System.Numerics;
using ImGuiNET; using ImGuiNET;
using NResUI.Abstractions; using NResUI.Abstractions;
using NResUI.Rendering.Viewport; using NResUI.Rendering.Viewport;
using NativeFileDialogSharp;
using NResUI.Rendering.Viewport.Meshes; using NResUI.Rendering.Viewport.Meshes;
using NResUI.Rendering.Viewport.Msh;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
using Silk.NET.Windowing; using Silk.NET.Windowing;
@@ -15,6 +17,9 @@ public sealed class ViewportPanel : IImGuiPanel
private readonly ViewportCamera _camera = new(); private readonly ViewportCamera _camera = new();
private readonly ViewportInputController _inputController = new(); private readonly ViewportInputController _inputController = new();
private string? _loadedModelPath;
private string? _loadError;
public ViewportPanel(GL gl, IWindow window) public ViewportPanel(GL gl, IWindow window)
{ {
var cubeMesh = PrimitiveMeshes.CreateCube(gl); var cubeMesh = PrimitiveMeshes.CreateCube(gl);
@@ -32,6 +37,7 @@ public sealed class ViewportPanel : IImGuiPanel
return; return;
} }
DrawModelControls();
DrawSelectionStatus(); DrawSelectionStatus();
DrawViewportToolbar(); DrawViewportToolbar();
DrawDebugControls(); DrawDebugControls();
@@ -73,14 +79,73 @@ public sealed class ViewportPanel : IImGuiPanel
ImGui.End(); ImGui.End();
} }
private void DrawModelControls()
{
if (ImGui.Button("Open MSH in viewport"))
{
var result = Dialog.FileOpen("msh");
if (result.IsOk)
LoadMsh(result.Path);
}
ImGui.SameLine();
if (ImGui.Button("Reset cube"))
{
var cubeMesh = PrimitiveMeshes.CreateCube(_renderer.Gl);
_scene.ReplacePieces(new[] { ViewportPiece.CreateUnitCube(0, "Cube", cubeMesh) });
_loadedModelPath = null;
_loadError = null;
_camera.Reset();
}
if (_loadedModelPath != null)
ImGui.TextDisabled($"Model: {Path.GetFileName(_loadedModelPath)}");
if (_loadError != null)
ImGui.TextColored(new Vector4(1.0f, 0.35f, 0.25f, 1.0f), $"MSH load failed: {_loadError}");
}
private void LoadMsh(string path)
{
var loadResult = MshViewportLoader.LoadFromFile(_renderer.Gl, path);
if (!loadResult.IsSuccess)
{
_loadError = loadResult.Error ?? "Unknown error.";
return;
}
_scene.ReplacePieces(loadResult.Pieces);
_loadedModelPath = loadResult.SourcePath;
_loadError = null;
if (_scene.TryGetSceneWorldBounds(out var bounds))
_camera.FrameBounds(bounds.Min, bounds.Max);
else
_camera.Reset();
}
private void DrawSelectionStatus() private void DrawSelectionStatus()
{ {
var selectedPiece = _scene.SelectedPiece; var selectedPiece = _scene.SelectedPiece;
if (selectedPiece != null) if (selectedPiece != null)
{
ImGui.Text($"Selected: {selectedPiece.Name}"); ImGui.Text($"Selected: {selectedPiece.Name}");
var debugInfo = selectedPiece.DebugInfo;
if (debugInfo != null)
{
ImGui.TextDisabled(
$"{debugInfo.SourceKind} | parent {debugInfo.SourceParentIndex} | slot {debugInfo.GeometrySlotIndex} | " +
$"flags 0x{debugInfo.Msh01Flags:X4} | batches {debugInfo.BatchCount} | tris {debugInfo.TriangleCount}");
}
}
else else
{
ImGui.TextDisabled("Selected: none"); ImGui.TextDisabled("Selected: none");
} }
}
private void DrawViewportToolbar() private void DrawViewportToolbar()
{ {
+1
View File
@@ -20,6 +20,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" /> <ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" /> <ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\MshLib\MshLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" /> <ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" /> <ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" /> <ProjectReference Include="..\ScrLib\ScrLib.csproj" />
@@ -185,6 +185,15 @@ public static unsafe class PrimitiveMeshes
return CreateIndexedMesh(gl, vertices, indices, PrimitiveType.Lines); return CreateIndexedMesh(gl, vertices, indices, PrimitiveType.Lines);
} }
public static GpuMesh CreateColoredIndexedMesh(
GL gl,
IReadOnlyList<float> vertices,
IReadOnlyList<uint> indices,
PrimitiveType primitiveType = PrimitiveType.Triangles)
{
return CreateIndexedMesh(gl, vertices.ToArray(), indices.ToArray(), primitiveType);
}
private static GpuMesh CreateIndexedMesh( private static GpuMesh CreateIndexedMesh(
GL gl, GL gl,
float[] vertices, float[] vertices,
@@ -0,0 +1,31 @@
namespace NResUI.Rendering.Viewport.Msh;
public sealed class MshViewportLoadResult
{
public bool IsSuccess { get; }
public string? Error { get; }
public string? SourcePath { get; }
public IReadOnlyList<ViewportPiece> Pieces { get; }
private MshViewportLoadResult(
bool isSuccess,
string? error,
string? sourcePath,
IReadOnlyList<ViewportPiece> pieces)
{
IsSuccess = isSuccess;
Error = error;
SourcePath = sourcePath;
Pieces = pieces;
}
public static MshViewportLoadResult Success(string sourcePath, IReadOnlyList<ViewportPiece> pieces)
{
return new MshViewportLoadResult(true, null, sourcePath, pieces);
}
public static MshViewportLoadResult Failure(string error, string? sourcePath = null)
{
return new MshViewportLoadResult(false, error, sourcePath, Array.Empty<ViewportPiece>());
}
}
@@ -0,0 +1,231 @@
using System.Numerics;
using MshLib;
using NResLib;
using NResUI.Rendering.Viewport.Meshes;
using Silk.NET.OpenGL;
namespace NResUI.Rendering.Viewport.Msh;
public static class MshViewportLoader
{
private const int DefaultModelState = 0;
private const int DefaultLod = 0;
public static MshViewportLoadResult LoadFromFile(GL gl, string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
var parseResult = NResParser.ReadFile(fs);
if (parseResult.Archive == null)
return MshViewportLoadResult.Failure(parseResult.Error ?? "Failed to parse MSH/NRes file.", path);
var archive = parseResult.Archive;
var meshType = MshConverter.DetectMeshType(archive);
if (meshType != MshType.Model)
{
return MshViewportLoadResult.Failure(
$"Only normal MSH models are supported for this viewport pass. Detected: {meshType}.",
path);
}
fs.Seek(0, SeekOrigin.Begin);
var pieces = LoadModelPieces(gl, fs, archive);
if (pieces.Count == 0)
return MshViewportLoadResult.Failure("MSH was parsed, but no renderable geometry pieces were found.", path);
return MshViewportLoadResult.Success(path, pieces);
}
catch (Exception ex)
{
return MshViewportLoadResult.Failure(ex.Message, path);
}
}
private static List<ViewportPiece> LoadModelPieces(GL gl, FileStream fs, NResArchive archive)
{
var nodes = Msh0x01.ReadComponent(fs, archive);
var geometry = Msh0x02.ReadComponent(fs, archive);
var positions = Msh0x03.ReadComponent(fs, archive);
var indices = Msh0x06.ReadComponent(fs, archive);
var batches = Msh0x0D.ReadComponent(fs, archive);
var names = TryReadNames(fs, archive);
var pieces = new List<ViewportPiece>();
for (var nodeIndex = 0; nodeIndex < nodes.Nodes.Count; nodeIndex++)
{
var node = nodes.Nodes[nodeIndex];
var slotIndex = node.ResolveSlotIndex(DefaultModelState, DefaultLod);
if (slotIndex == ushort.MaxValue)
continue;
if (slotIndex >= geometry.Slots.Count)
continue;
var slot = geometry.Slots[slotIndex];
if (!slot.HasBatches)
continue;
var meshBuildResult = BuildPieceMesh(gl, nodeIndex, slot, positions, indices, batches);
if (meshBuildResult == null)
continue;
var name = ResolvePieceName(names, nodeIndex);
var parentIndex = node.ParentIndexOrMinusOne;
pieces.Add(new ViewportPiece(
id: nodeIndex,
name: name,
mesh: meshBuildResult.Mesh,
localTransform: Matrix4x4.Identity,
boundsMin: meshBuildResult.BoundsMin,
boundsMax: meshBuildResult.BoundsMax,
debugInfo: new ViewportPieceDebugInfo
{
SourceKind = "MSH 0x01 piece",
SourcePieceIndex = nodeIndex,
SourceParentIndex = parentIndex,
GeometrySlotIndex = slotIndex,
Msh01Flags = (uint)node.Flags,
BatchCount = meshBuildResult.BatchCount,
TriangleCount = meshBuildResult.TriangleCount
}));
}
return pieces;
}
private static PieceMeshBuildResult? BuildPieceMesh(
GL gl,
int nodeIndex,
Msh0x02.GeometrySlot slot,
IReadOnlyList<Common.Vector3> positions,
IReadOnlyList<ushort> indices,
IReadOnlyList<Msh0x0D.Batch> batches)
{
var vertices = new List<float>();
var outIndices = new List<uint>();
var color = PickDebugColor(nodeIndex);
var batchCount = 0;
var triangleCount = 0;
var boundsMin = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
var boundsMax = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
for (var batchIndex = slot.BatchStart0x0D; batchIndex < slot.BatchEndExclusive0x0D; batchIndex++)
{
if (batchIndex < 0 || batchIndex >= batches.Count)
continue;
var batch = batches[batchIndex];
if (batch.IndexStart0x06 + batch.IndexCount0x06 > indices.Count)
continue;
batchCount++;
for (var i = 0; i + 2 < batch.IndexCount0x06; i += 3)
{
var indexBase = (int)batch.IndexStart0x06 + i;
var vertexIndex0 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 0]);
var vertexIndex1 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 1]);
var vertexIndex2 = checked((int)batch.BaseVertex0x03 + indices[indexBase + 2]);
if (!IsValidTriangle(positions.Count, vertexIndex0, vertexIndex1, vertexIndex2))
continue;
AddTriangleVertex(positions[vertexIndex0], color, vertices, outIndices, ref boundsMin, ref boundsMax);
AddTriangleVertex(positions[vertexIndex1], color, vertices, outIndices, ref boundsMin, ref boundsMax);
AddTriangleVertex(positions[vertexIndex2], color, vertices, outIndices, ref boundsMin, ref boundsMax);
triangleCount++;
}
}
if (triangleCount == 0)
return null;
var mesh = PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles);
return new PieceMeshBuildResult(mesh, boundsMin, boundsMax, batchCount, triangleCount);
}
private static void AddTriangleVertex(
Common.Vector3 position,
Vector3 color,
List<float> vertices,
List<uint> indices,
ref Vector3 boundsMin,
ref Vector3 boundsMax)
{
var vertexIndex = (uint)(vertices.Count / 6);
vertices.Add(position.X);
vertices.Add(position.Y);
vertices.Add(position.Z);
vertices.Add(color.X);
vertices.Add(color.Y);
vertices.Add(color.Z);
indices.Add(vertexIndex);
var p = new Vector3(position.X, position.Y, position.Z);
boundsMin = Vector3.Min(boundsMin, p);
boundsMax = Vector3.Max(boundsMax, p);
}
private static bool IsValidTriangle(int vertexCount, int vertexIndex0, int vertexIndex1, int vertexIndex2)
{
return IsValidVertexIndex(vertexCount, vertexIndex0) &&
IsValidVertexIndex(vertexCount, vertexIndex1) &&
IsValidVertexIndex(vertexCount, vertexIndex2);
}
private static bool IsValidVertexIndex(int vertexCount, int vertexIndex)
{
return vertexIndex >= 0 && vertexIndex < vertexCount;
}
private static List<string> TryReadNames(FileStream fs, NResArchive archive)
{
try
{
return Msh0x0A.ReadComponent(fs, archive);
}
catch
{
return new List<string>();
}
}
private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
{
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
return $"{nodeIndex}: {names[nodeIndex]}";
return $"{nodeIndex}: piece_{nodeIndex:D3}";
}
private static Vector3 PickDebugColor(int index)
{
ReadOnlySpan<Vector3> palette =
[
new Vector3(0.90f, 0.36f, 0.30f),
new Vector3(0.30f, 0.68f, 0.95f),
new Vector3(0.42f, 0.82f, 0.42f),
new Vector3(0.95f, 0.78f, 0.32f),
new Vector3(0.78f, 0.48f, 0.95f),
new Vector3(0.36f, 0.86f, 0.78f),
new Vector3(0.95f, 0.55f, 0.78f),
new Vector3(0.72f, 0.72f, 0.72f),
];
return palette[index % palette.Length];
}
private sealed record PieceMeshBuildResult(
GpuMesh Mesh,
Vector3 BoundsMin,
Vector3 BoundsMax,
int BatchCount,
int TriangleCount);
}
+16 -2
View File
@@ -14,13 +14,16 @@ public sealed class ViewportPiece
public Vector3 BoundsMin { get; } public Vector3 BoundsMin { get; }
public Vector3 BoundsMax { get; } public Vector3 BoundsMax { get; }
public ViewportPieceDebugInfo? DebugInfo { get; }
public ViewportPiece( public ViewportPiece(
int id, int id,
string name, string name,
GpuMesh mesh, GpuMesh mesh,
Matrix4x4 localTransform, Matrix4x4 localTransform,
Vector3 boundsMin, Vector3 boundsMin,
Vector3 boundsMax) Vector3 boundsMax,
ViewportPieceDebugInfo? debugInfo = null)
{ {
Id = id; Id = id;
Name = name; Name = name;
@@ -28,6 +31,7 @@ public sealed class ViewportPiece
LocalTransform = localTransform; LocalTransform = localTransform;
BoundsMin = boundsMin; BoundsMin = boundsMin;
BoundsMax = boundsMax; BoundsMax = boundsMax;
DebugInfo = debugInfo;
} }
public static ViewportPiece CreateUnitCube(int id, string name, GpuMesh mesh) public static ViewportPiece CreateUnitCube(int id, string name, GpuMesh mesh)
@@ -38,6 +42,16 @@ public sealed class ViewportPiece
mesh, mesh,
Matrix4x4.Identity, Matrix4x4.Identity,
new Vector3(-1.0f, -1.0f, -1.0f), new Vector3(-1.0f, -1.0f, -1.0f),
new Vector3(1.0f, 1.0f, 1.0f)); new Vector3(1.0f, 1.0f, 1.0f),
new ViewportPieceDebugInfo
{
SourceKind = "Debug primitive",
SourcePieceIndex = id,
SourceParentIndex = -1,
GeometrySlotIndex = 0,
Msh01Flags = 0,
BatchCount = 1,
TriangleCount = 12
});
} }
} }
@@ -0,0 +1,13 @@
namespace NResUI.Rendering.Viewport;
public sealed class ViewportPieceDebugInfo
{
public int SourcePieceIndex { get; init; }
public int SourceParentIndex { get; init; }
public int GeometrySlotIndex { get; init; }
public uint Msh01Flags { get; init; }
public int BatchCount { get; init; }
public int TriangleCount { get; init; }
public string SourceKind { get; init; } = "MSH";
}
@@ -12,6 +12,8 @@ public sealed class ViewportRenderer
private readonly IWindow _window; private readonly IWindow _window;
private readonly MsaaFramebuffer _framebuffer; private readonly MsaaFramebuffer _framebuffer;
public GL Gl => _gl;
private ShaderProgram? _meshShader; private ShaderProgram? _meshShader;
private ShaderProgram? _outlineShader; private ShaderProgram? _outlineShader;
@@ -37,6 +37,19 @@ public sealed class ViewportScene
_pieces.Add(piece); _pieces.Add(piece);
} }
public void ClearPieces()
{
_pieces.Clear();
ClearSelection();
}
public void ReplacePieces(IEnumerable<ViewportPiece> pieces)
{
_pieces.Clear();
_pieces.AddRange(pieces);
ClearSelection();
}
public void ClearSelection() public void ClearSelection()
{ {
SelectedPieceId = -1; SelectedPieceId = -1;