2026-06-05 21:12:25 +03:00
|
|
|
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);
|
2026-06-06 21:26:56 +03:00
|
|
|
var pieces = LoadModelPieces(gl, fs, archive, path);
|
2026-06-05 21:12:25 +03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
private static List<ViewportPiece> LoadModelPieces(GL gl, FileStream fs, NResArchive archive, string path)
|
2026-06-05 21:12:25 +03:00
|
|
|
{
|
|
|
|
|
var nodes = Msh0x01.ReadComponent(fs, archive);
|
|
|
|
|
var geometry = Msh0x02.ReadComponent(fs, archive);
|
|
|
|
|
var positions = Msh0x03.ReadComponent(fs, archive);
|
2026-06-06 21:26:56 +03:00
|
|
|
var uvs = TryReadUvs(fs, archive);
|
2026-06-05 21:12:25 +03:00
|
|
|
var indices = Msh0x06.ReadComponent(fs, archive);
|
|
|
|
|
var batches = Msh0x0D.ReadComponent(fs, archive);
|
2026-06-06 00:42:07 +03:00
|
|
|
var animationDescriptors = Msh0x08.ReadComponent(fs, archive);
|
|
|
|
|
var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
|
|
|
|
|
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
|
2026-06-05 21:12:25 +03:00
|
|
|
var names = TryReadNames(fs, archive);
|
2026-06-06 21:26:56 +03:00
|
|
|
var materialLibrary = WeaMaterialLibrary.TryLoadForMsh(gl, path);
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
var pieces = new List<ViewportPiece>();
|
|
|
|
|
|
|
|
|
|
for (var nodeIndex = 0; nodeIndex < nodes.Nodes.Count; nodeIndex++)
|
|
|
|
|
{
|
|
|
|
|
var node = nodes.Nodes[nodeIndex];
|
2026-06-06 00:42:07 +03:00
|
|
|
var restPose = restPoses[nodeIndex];
|
2026-06-05 21:12:25 +03:00
|
|
|
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;
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
var meshBuildResult = BuildPieceMeshes(gl, nodeIndex, slot, positions, uvs, indices, batches, materialLibrary);
|
2026-06-05 21:12:25 +03:00
|
|
|
if (meshBuildResult == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var name = ResolvePieceName(names, nodeIndex);
|
|
|
|
|
pieces.Add(new ViewportPiece(
|
|
|
|
|
id: nodeIndex,
|
|
|
|
|
name: name,
|
2026-06-06 21:26:56 +03:00
|
|
|
meshes: meshBuildResult.Meshes,
|
2026-06-06 00:42:07 +03:00
|
|
|
localTransform: restPose.MeshSpaceTransform * mshToViewportTransform,
|
2026-06-05 21:12:25 +03:00
|
|
|
boundsMin: meshBuildResult.BoundsMin,
|
|
|
|
|
boundsMax: meshBuildResult.BoundsMax,
|
|
|
|
|
debugInfo: new ViewportPieceDebugInfo
|
|
|
|
|
{
|
|
|
|
|
SourceKind = "MSH 0x01 piece",
|
|
|
|
|
SourcePieceIndex = nodeIndex,
|
2026-06-06 00:42:07 +03:00
|
|
|
SourceParentIndex = restPose.ParentIndex,
|
2026-06-05 21:12:25 +03:00
|
|
|
GeometrySlotIndex = slotIndex,
|
|
|
|
|
Msh01Flags = (uint)node.Flags,
|
|
|
|
|
BatchCount = meshBuildResult.BatchCount,
|
2026-06-06 00:42:07 +03:00
|
|
|
TriangleCount = meshBuildResult.TriangleCount,
|
|
|
|
|
FallbackKeyframeIndex = restPose.FallbackKeyframeIndex,
|
|
|
|
|
HasRestPose = restPose.HasFallbackPose
|
2026-06-05 21:12:25 +03:00
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pieces;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
private static PieceMeshBuildResult? BuildPieceMeshes(
|
2026-06-05 21:12:25 +03:00
|
|
|
GL gl,
|
|
|
|
|
int nodeIndex,
|
|
|
|
|
Msh0x02.GeometrySlot slot,
|
|
|
|
|
IReadOnlyList<Common.Vector3> positions,
|
2026-06-06 21:26:56 +03:00
|
|
|
IReadOnlyList<Msh05Uv> uvs,
|
2026-06-05 21:12:25 +03:00
|
|
|
IReadOnlyList<ushort> indices,
|
2026-06-06 21:26:56 +03:00
|
|
|
IReadOnlyList<Msh0x0D.Batch> batches,
|
|
|
|
|
WeaMaterialLibrary materialLibrary)
|
2026-06-05 21:12:25 +03:00
|
|
|
{
|
2026-06-06 21:26:56 +03:00
|
|
|
var meshes = new List<GpuMesh>();
|
|
|
|
|
var debugColor = PickDebugColor(nodeIndex);
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
var vertices = new List<float>();
|
|
|
|
|
var outIndices = new List<uint>();
|
|
|
|
|
var batchTriangleCount = 0;
|
|
|
|
|
|
|
|
|
|
var material = materialLibrary.FindMaterial(batch.MaterialIndex) ?? ViewportMaterial.Untextured;
|
|
|
|
|
var vertexColor = material.HasTexture ? Vector3.One : debugColor;
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-06-06 00:42:07 +03:00
|
|
|
var p0 = ToNumericsVector3(positions[vertexIndex0]);
|
|
|
|
|
var p1 = ToNumericsVector3(positions[vertexIndex1]);
|
|
|
|
|
var p2 = ToNumericsVector3(positions[vertexIndex2]);
|
|
|
|
|
|
|
|
|
|
var normal = Vector3.Cross(p1 - p0, p2 - p0);
|
|
|
|
|
if (normal.LengthSquared() < 1e-8f)
|
|
|
|
|
normal = Vector3.UnitY;
|
|
|
|
|
else
|
|
|
|
|
normal = Vector3.Normalize(normal);
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
AddTriangleVertex(p0, vertexColor, normal, GetUv(uvs, vertexIndex0), vertices, outIndices, ref boundsMin, ref boundsMax);
|
|
|
|
|
AddTriangleVertex(p1, vertexColor, normal, GetUv(uvs, vertexIndex1), vertices, outIndices, ref boundsMin, ref boundsMax);
|
|
|
|
|
AddTriangleVertex(p2, vertexColor, normal, GetUv(uvs, vertexIndex2), vertices, outIndices, ref boundsMin, ref boundsMax);
|
|
|
|
|
batchTriangleCount++;
|
2026-06-05 21:12:25 +03:00
|
|
|
}
|
2026-06-06 21:26:56 +03:00
|
|
|
|
|
|
|
|
if (batchTriangleCount == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
batchCount++;
|
|
|
|
|
triangleCount += batchTriangleCount;
|
|
|
|
|
meshes.Add(PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles, material));
|
2026-06-05 21:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
if (triangleCount == 0 || meshes.Count == 0)
|
2026-06-05 21:12:25 +03:00
|
|
|
return null;
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
return new PieceMeshBuildResult(meshes, boundsMin, boundsMax, batchCount, triangleCount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Vector2 GetUv(IReadOnlyList<Msh05Uv> uvs, int vertexIndex)
|
|
|
|
|
{
|
|
|
|
|
if (vertexIndex < 0 || vertexIndex >= uvs.Count)
|
|
|
|
|
return Vector2.Zero;
|
|
|
|
|
|
|
|
|
|
var uv = uvs[vertexIndex];
|
|
|
|
|
return new Vector2(uv.U / 1024.0f, 1.0f - uv.V / 1024.0f);
|
2026-06-05 21:12:25 +03:00
|
|
|
}
|
2026-06-06 21:26:56 +03:00
|
|
|
|
2026-06-06 00:42:07 +03:00
|
|
|
private static Vector3 ToNumericsVector3(Common.Vector3 position)
|
|
|
|
|
{
|
|
|
|
|
return new Vector3(position.X, position.Y, position.Z);
|
|
|
|
|
}
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
private static void AddTriangleVertex(
|
2026-06-06 00:42:07 +03:00
|
|
|
Vector3 position,
|
2026-06-05 21:12:25 +03:00
|
|
|
Vector3 color,
|
2026-06-06 00:42:07 +03:00
|
|
|
Vector3 normal,
|
2026-06-06 21:26:56 +03:00
|
|
|
Vector2 uv,
|
2026-06-05 21:12:25 +03:00
|
|
|
List<float> vertices,
|
|
|
|
|
List<uint> indices,
|
|
|
|
|
ref Vector3 boundsMin,
|
2026-06-06 21:26:56 +03:00
|
|
|
ref Vector3 boundsMax)
|
2026-06-05 21:12:25 +03:00
|
|
|
{
|
2026-06-06 21:26:56 +03:00
|
|
|
var vertexIndex = (uint)(vertices.Count / PrimitiveMeshes.FloatsPerVertex);
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
vertices.Add(position.X);
|
|
|
|
|
vertices.Add(position.Y);
|
|
|
|
|
vertices.Add(position.Z);
|
|
|
|
|
vertices.Add(color.X);
|
|
|
|
|
vertices.Add(color.Y);
|
|
|
|
|
vertices.Add(color.Z);
|
2026-06-06 00:42:07 +03:00
|
|
|
vertices.Add(normal.X);
|
|
|
|
|
vertices.Add(normal.Y);
|
|
|
|
|
vertices.Add(normal.Z);
|
2026-06-06 21:26:56 +03:00
|
|
|
vertices.Add(uv.X);
|
|
|
|
|
vertices.Add(uv.Y);
|
2026-06-05 21:12:25 +03:00
|
|
|
|
|
|
|
|
indices.Add(vertexIndex);
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
boundsMin = Vector3.Min(boundsMin, position);
|
|
|
|
|
boundsMax = Vector3.Max(boundsMax, position);
|
2026-06-05 21:12:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 21:26:56 +03:00
|
|
|
private static List<Msh05Uv> TryReadUvs(FileStream fs, NResArchive archive)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return Msh0x05.ReadComponent(fs, archive);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return new List<Msh05Uv>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-05 21:12:25 +03:00
|
|
|
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(
|
2026-06-06 21:26:56 +03:00
|
|
|
IReadOnlyList<GpuMesh> Meshes,
|
2026-06-05 21:12:25 +03:00
|
|
|
Vector3 BoundsMin,
|
|
|
|
|
Vector3 BoundsMax,
|
|
|
|
|
int BatchCount,
|
|
|
|
|
int TriangleCount);
|
|
|
|
|
}
|