mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
msh viewport initial
This commit is contained in:
@@ -2,7 +2,9 @@ using System.Numerics;
|
||||
using ImGuiNET;
|
||||
using NResUI.Abstractions;
|
||||
using NResUI.Rendering.Viewport;
|
||||
using NativeFileDialogSharp;
|
||||
using NResUI.Rendering.Viewport.Meshes;
|
||||
using NResUI.Rendering.Viewport.Msh;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
|
||||
@@ -15,6 +17,9 @@ public sealed class ViewportPanel : IImGuiPanel
|
||||
private readonly ViewportCamera _camera = new();
|
||||
private readonly ViewportInputController _inputController = new();
|
||||
|
||||
private string? _loadedModelPath;
|
||||
private string? _loadError;
|
||||
|
||||
public ViewportPanel(GL gl, IWindow window)
|
||||
{
|
||||
var cubeMesh = PrimitiveMeshes.CreateCube(gl);
|
||||
@@ -32,6 +37,7 @@ public sealed class ViewportPanel : IImGuiPanel
|
||||
return;
|
||||
}
|
||||
|
||||
DrawModelControls();
|
||||
DrawSelectionStatus();
|
||||
DrawViewportToolbar();
|
||||
DrawDebugControls();
|
||||
@@ -73,14 +79,73 @@ public sealed class ViewportPanel : IImGuiPanel
|
||||
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()
|
||||
{
|
||||
var selectedPiece = _scene.SelectedPiece;
|
||||
if (selectedPiece != null)
|
||||
{
|
||||
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
|
||||
{
|
||||
ImGui.TextDisabled("Selected: none");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawViewportToolbar()
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
|
||||
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
|
||||
<ProjectReference Include="..\MshLib\MshLib.csproj" />
|
||||
<ProjectReference Include="..\NResLib\NResLib.csproj" />
|
||||
<ProjectReference Include="..\ResTreeLib\ResTreeLib.csproj" />
|
||||
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
|
||||
|
||||
@@ -185,6 +185,15 @@ public static unsafe class PrimitiveMeshes
|
||||
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(
|
||||
GL gl,
|
||||
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);
|
||||
}
|
||||
@@ -14,13 +14,16 @@ public sealed class ViewportPiece
|
||||
public Vector3 BoundsMin { get; }
|
||||
public Vector3 BoundsMax { get; }
|
||||
|
||||
public ViewportPieceDebugInfo? DebugInfo { get; }
|
||||
|
||||
public ViewportPiece(
|
||||
int id,
|
||||
string name,
|
||||
GpuMesh mesh,
|
||||
Matrix4x4 localTransform,
|
||||
Vector3 boundsMin,
|
||||
Vector3 boundsMax)
|
||||
Vector3 boundsMax,
|
||||
ViewportPieceDebugInfo? debugInfo = null)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
@@ -28,6 +31,7 @@ public sealed class ViewportPiece
|
||||
LocalTransform = localTransform;
|
||||
BoundsMin = boundsMin;
|
||||
BoundsMax = boundsMax;
|
||||
DebugInfo = debugInfo;
|
||||
}
|
||||
|
||||
public static ViewportPiece CreateUnitCube(int id, string name, GpuMesh mesh)
|
||||
@@ -38,6 +42,16 @@ public sealed class ViewportPiece
|
||||
mesh,
|
||||
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 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 MsaaFramebuffer _framebuffer;
|
||||
|
||||
public GL Gl => _gl;
|
||||
|
||||
private ShaderProgram? _meshShader;
|
||||
private ShaderProgram? _outlineShader;
|
||||
|
||||
|
||||
@@ -37,6 +37,19 @@ public sealed class ViewportScene
|
||||
_pieces.Add(piece);
|
||||
}
|
||||
|
||||
public void ClearPieces()
|
||||
{
|
||||
_pieces.Clear();
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
public void ReplacePieces(IEnumerable<ViewportPiece> pieces)
|
||||
{
|
||||
_pieces.Clear();
|
||||
_pieces.AddRange(pieces);
|
||||
ClearSelection();
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
SelectedPieceId = -1;
|
||||
|
||||
Reference in New Issue
Block a user