From 1cb3940cced9841f55a193cb061f3ba3e0e504dd Mon Sep 17 00:00:00 2001 From: bird_egop Date: Fri, 12 Jun 2026 02:06:32 +0300 Subject: [PATCH] animations and QOL --- MshLib/Msh0x01.cs | 2 + MshLib/Msh0x08.cs | 18 +-- MshLib/Msh0x13.cs | 53 ++++++++ MshLib/MshAnimationSampler.cs | 92 ++++++++++++++ NResUI/App.cs | 12 ++ NResUI/ImGuiUI/ViewportPanel.cs | 120 +++++++++++++++++- .../Import/Msh/MshMeshDocumentImporter.cs | 29 ++++- .../Import/Msh/MshViewportMeshFactory.cs | 19 ++- .../Rendering/Inspection/MeshRenderState.cs | 6 + .../Rendering/Inspection/MshModelGeometry.cs | 9 ++ NResUI/Rendering/Viewport/Meshes/GpuMesh.cs | 17 ++- .../Viewport/Msh/MshRestPoseBuilder.cs | 83 ++++++++++-- NResUI/Rendering/Viewport/ViewportPiece.cs | 8 +- NResUI/Rendering/Viewport/ViewportScene.cs | 8 ++ 14 files changed, 444 insertions(+), 32 deletions(-) create mode 100644 MshLib/Msh0x13.cs create mode 100644 MshLib/MshAnimationSampler.cs diff --git a/MshLib/Msh0x01.cs b/MshLib/Msh0x01.cs index 7cc58d0..b511941 100644 --- a/MshLib/Msh0x01.cs +++ b/MshLib/Msh0x01.cs @@ -105,6 +105,8 @@ public static class Msh0x01 ushort FallbackKey0x08, ushort[] Msh02SlotIndicesByStateAndLOD) { + public ushort DefaultKeyframeIndex0x08 => FallbackKey0x08; + public ushort ResolveSlotIndex(int state, int lod = 0) { // State и LOD выбираются для конкретного узла/piece, не для всего файла сразу. diff --git a/MshLib/Msh0x08.cs b/MshLib/Msh0x08.cs index 0ad2ad6..97d24e5 100644 --- a/MshLib/Msh0x08.cs +++ b/MshLib/Msh0x08.cs @@ -5,7 +5,7 @@ namespace MshLib; public static class Msh0x08 { - public static List ReadComponent(FileStream mshFs, NResArchive archive) + public static List ReadComponent(FileStream mshFs, NResArchive archive) { var entry = archive.Files.FirstOrDefault(x => x.FileType == "08 00 00 00"); @@ -16,11 +16,11 @@ public static class Msh0x08 mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); - var descriptors = new List(); + var descriptors = new List(); for (var i = 0; i < entry.ElementCount; i++) { - descriptors.Add(new AnimationDescriptor( + descriptors.Add(new MshTransformKeyframe( new Vector3(mshFs.ReadFloatLittleEndian(), mshFs.ReadFloatLittleEndian(), mshFs.ReadFloatLittleEndian()), @@ -36,10 +36,10 @@ public static class Msh0x08 return descriptors; } - - public record AnimationDescriptor( - Vector3 Position, - float Time, - UShortQuaternion Rotation - ); } + +public record MshTransformKeyframe( + Vector3 Position, + float Time, + UShortQuaternion Rotation +); diff --git a/MshLib/Msh0x13.cs b/MshLib/Msh0x13.cs new file mode 100644 index 0000000..d14184a --- /dev/null +++ b/MshLib/Msh0x13.cs @@ -0,0 +1,53 @@ +using System.Buffers.Binary; +using NResLib; + +namespace MshLib; + +public static class Msh0x13 +{ + public static Msh0x13Component ReadComponent(FileStream mshFs, NResArchive archive) + { + var entry = archive.Files.FirstOrDefault(x => x.FileType == "13 00 00 00"); + + if (entry is null) + { + throw new Exception("Archive doesn't contain animation map component (0x13)"); + } + + if (entry.ElementSize < 2) + { + throw new Exception("Animation map component (0x13) element size is too small"); + } + + var data = new byte[entry.FileLength]; + mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var elementCount = checked((int)entry.ElementCount); + var entries = new List(elementCount); + var span = data.AsSpan(); + for (var i = 0; i < elementCount; i++) + { + var elementOffset = i * entry.ElementSize; + entries.Add(new MshAnimationMapEntry( + BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(elementOffset, 2)))); + } + + return new Msh0x13Component(entry.ElementSize, entries) + { + MaxAnimationTime = entry.Magic1 + }; + } +} + +public readonly record struct MshAnimationMapEntry( + ushort KeyframeIndex +); + +public sealed record Msh0x13Component( + int ElementSize, + List Entries +) +{ + public int MaxAnimationTime { get; init; } +} diff --git a/MshLib/MshAnimationSampler.cs b/MshLib/MshAnimationSampler.cs new file mode 100644 index 0000000..750bfee --- /dev/null +++ b/MshLib/MshAnimationSampler.cs @@ -0,0 +1,92 @@ +using System.Numerics; + +namespace MshLib; + +public static class MshAnimationSampler +{ + public static MshSampledPieceTransform SamplePieceAnimationAtTime( + Msh0x01.Node node, + IReadOnlyList keyframes, + IReadOnlyList animationMap, + float sampleTime) + { + var defaultKeyframeIndex = node.DefaultKeyframeIndex0x08; + if (defaultKeyframeIndex == ushort.MaxValue || defaultKeyframeIndex >= keyframes.Count) + return MshSampledPieceTransform.Identity; + + var frameIndex = (int)MathF.Round(sampleTime - 0.5f, MidpointRounding.AwayFromZero); + if (frameIndex < 0 || + node.AnimMapStart0x13 == ushort.MaxValue || + node.AnimMapStart0x13 + frameIndex >= animationMap.Count) + { + return FromKeyframe(keyframes[defaultKeyframeIndex]); + } + + var keyframeIndex = animationMap[node.AnimMapStart0x13 + frameIndex].KeyframeIndex; + if (keyframeIndex >= defaultKeyframeIndex || + keyframeIndex >= keyframes.Count || + keyframeIndex + 1 >= keyframes.Count) + { + return FromKeyframe(keyframes[defaultKeyframeIndex]); + } + + var key0 = keyframes[keyframeIndex]; + var key1 = keyframes[keyframeIndex + 1]; + + if (sampleTime == key0.Time) + return FromKeyframe(key0); + + if (sampleTime == key1.Time) + return FromKeyframe(key1); + + var duration = key1.Time - key0.Time; + if (MathF.Abs(duration) < 1e-6f) + return FromKeyframe(key0); + + var t = (sampleTime - key0.Time) / duration; + return new MshSampledPieceTransform( + Vector3.Lerp(ToNumericsVector3(key0.Position), ToNumericsVector3(key1.Position), t), + Quaternion.Slerp(ToNumericsQuaternion(key0.Rotation), ToNumericsQuaternion(key1.Rotation), t)); + } + + public static Matrix4x4 ToMatrix(MshSampledPieceTransform transform) + { + var matrix = Matrix4x4.CreateFromQuaternion(transform.Rotation); + matrix.Translation = transform.Position; + return matrix; + } + + public static MshSampledPieceTransform FromKeyframe(MshTransformKeyframe keyframe) + { + return new MshSampledPieceTransform( + ToNumericsVector3(keyframe.Position), + ToNumericsQuaternion(keyframe.Rotation)); + } + + private static Vector3 ToNumericsVector3(Common.Vector3 vector) + { + return new Vector3(vector.X, vector.Y, vector.Z); + } + + private static Quaternion ToNumericsQuaternion(Common.UShortQuaternion packedQuaternion) + { + var q = new Quaternion( + packedQuaternion.X / 32767f, + packedQuaternion.Y / 32767f, + packedQuaternion.Z / 32767f, + packedQuaternion.W / 32767f); + + if (q.LengthSquared() < 1e-8f) + return Quaternion.Identity; + + // MSH stores mesh-to-parent rotation; viewport composition uses the inverse. + return Quaternion.Conjugate(Quaternion.Normalize(q)); + } +} + +public readonly record struct MshSampledPieceTransform( + Vector3 Position, + Quaternion Rotation) +{ + public static MshSampledPieceTransform Identity { get; } = new(Vector3.Zero, Quaternion.Identity); +} diff --git a/NResUI/App.cs b/NResUI/App.cs index 393b2df..74688e9 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -26,6 +26,7 @@ public class App public ImFontPtr OpenSansFont; private List _imGuiPanels = []; + private List _updateReceivers = []; private Later _later = new(); public App() @@ -59,6 +60,11 @@ public class App serviceCollection.AddSingleton(type); } + foreach (var type in Utils.GetAssignableTypes()) + { + serviceCollection.AddSingleton(type); + } + serviceCollection.AddSingleton(openGl); serviceCollection.AddSingleton(window); @@ -78,6 +84,10 @@ public class App _imGuiPanels = Utils.GetAssignableTypes() .Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!) .ToList(); + + _updateReceivers = Utils.GetAssignableTypes() + .Select(t => (serviceProvider.GetService(t) as IUpdateReceiver)!) + .ToList(); foreach (var type in Utils.GetAssignableTypes()) { @@ -159,6 +169,8 @@ public class App public void Update(double delta) { + foreach (var updateReceiver in _updateReceivers) + updateReceiver.OnUpdate((float)delta); } public void OnKeyPressed(Key key) diff --git a/NResUI/ImGuiUI/ViewportPanel.cs b/NResUI/ImGuiUI/ViewportPanel.cs index 07a26c3..9ef7563 100644 --- a/NResUI/ImGuiUI/ViewportPanel.cs +++ b/NResUI/ImGuiUI/ViewportPanel.cs @@ -2,17 +2,19 @@ using System.Numerics; using ImGuiNET; using NResUI.Abstractions; using NResUI.Models; +using NResUI.Rendering.Inspection; using NResUI.Rendering.Import.Msh; using NResUI.Rendering.Materials; using NResUI.Rendering.Viewport; using NativeFileDialogSharp; using NResUI.Rendering.Viewport.Meshes; +using NResUI.Rendering.Viewport.Msh; using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace NResUI.ImGuiUI; -public sealed class ViewportPanel : IImGuiPanel +public sealed class ViewportPanel : IImGuiPanel, IUpdateReceiver { private readonly ViewportRenderer _renderer; private readonly ViewportScene _scene; @@ -46,6 +48,7 @@ public sealed class ViewportPanel : IImGuiPanel DrawSelectionStatus(); DrawViewportToolbar(); DrawDebugControls(); + DrawAnimationControls(); RebuildSceneIfNeeded(); SyncSelectionFromViewModel(); @@ -90,6 +93,27 @@ public sealed class ViewportPanel : IImGuiPanel ImGui.End(); } + public void OnUpdate(float delta) + { + var document = _viewModel.Document; + if (document?.MshModelGeometry == null) + return; + + var state = _viewModel.RenderState; + if (!state.AnimationPlay) + return; + + var maxAnimationTime = GetMaxAnimationTime(document); + if (maxAnimationTime <= 0.0f) + return; + + state.AnimationSampleTime = WrapAnimationTime( + state.AnimationSampleTime + delta * state.AnimationSpeed, + maxAnimationTime); + state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample; + ApplyCurrentAnimationPose(document); + } + private void DrawModelControls() { @@ -183,9 +207,34 @@ public sealed class ViewportPanel : IImGuiPanel _scene.ReplacePieces(pieces); _scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId; + ApplyCurrentAnimationPose(document); _viewModel.MarkSceneRebuilt(); } + private void ApplyCurrentAnimationPose(MeshDocument document) + { + var modelGeometry = document.MshModelGeometry; + if (modelGeometry == null) + return; + + var renderState = _viewModel.RenderState; + var poses = MshRestPoseBuilder.BuildPose( + modelGeometry.Nodes, + modelGeometry.TransformKeyframes, + modelGeometry.AnimationMap, + renderState.AnimationPoseMode, + renderState.AnimationSampleTime); + + var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f); + foreach (var piece in _scene.Pieces) + { + if (piece.Id < 0 || piece.Id >= poses.Count) + continue; + + piece.LocalTransform = poses[piece.Id].MeshSpaceTransform * mshToViewportTransform; + } + } + private void SyncSelectionFromViewModel() { if (_viewModel.Document == null) @@ -260,6 +309,75 @@ public sealed class ViewportPanel : IImGuiPanel _scene.Debug.Wireframe = wireframe; } + private void DrawAnimationControls() + { + var document = _viewModel.Document; + var state = _viewModel.RenderState; + ImGui.SeparatorText("Animation"); + var hasAnimationDocument = document?.MshModelGeometry != null; + var maxAnimationTime = hasAnimationDocument ? GetMaxAnimationTime(document!) : 0.0f; + + if (!hasAnimationDocument) + ImGui.BeginDisabled(); + + if (ImGui.Button("Reset to default keyframe")) + { + state.AnimationPlay = false; + state.AnimationPoseMode = MshAnimationPoseMode.DefaultKeyframe; + if (document != null) + ApplyCurrentAnimationPose(document); + } + + var animationSampleTime = state.AnimationSampleTime; + if (ImGui.SliderFloat("Anim time", ref animationSampleTime, 0.0f, maxAnimationTime)) + { + state.AnimationSampleTime = animationSampleTime; + state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample; + if (document != null) + ApplyCurrentAnimationPose(document); + } + + var play = state.AnimationPlay; + if (ImGui.Checkbox("Play", ref play)) + { + state.AnimationPlay = play; + if (play) + state.AnimationPoseMode = MshAnimationPoseMode.AnimationSample; + else if (document != null) + ApplyCurrentAnimationPose(document); + } + + ImGui.SameLine(); + + var speed = state.AnimationSpeed; + if (ImGui.InputFloat("Speed", ref speed, 0.1f, 1.0f)) + state.AnimationSpeed = speed; + + if (!hasAnimationDocument) + ImGui.EndDisabled(); + } + + private static float GetMaxAnimationTime(MeshDocument document) + { + var maxAnimationTime = document.MshModelGeometry?.MaxAnimationTime ?? 0; + if (maxAnimationTime > 0) + return maxAnimationTime; + + var keyframes = document.MshModelGeometry?.TransformKeyframes; + return keyframes == null || keyframes.Count == 0 + ? 0.0f + : MathF.Max(0.0f, keyframes.Max(x => x.Time)); + } + + private static float WrapAnimationTime(float time, float maxAnimationTime) + { + if (maxAnimationTime <= 0.0f) + return 0.0f; + + var wrapped = time % maxAnimationTime; + return wrapped < 0.0f ? wrapped + maxAnimationTime : wrapped; + } + private static void DrawViewportHelpOverlay() { var drawList = ImGui.GetWindowDrawList(); diff --git a/NResUI/Rendering/Import/Msh/MshMeshDocumentImporter.cs b/NResUI/Rendering/Import/Msh/MshMeshDocumentImporter.cs index e0f928c..3bd5976 100644 --- a/NResUI/Rendering/Import/Msh/MshMeshDocumentImporter.cs +++ b/NResUI/Rendering/Import/Msh/MshMeshDocumentImporter.cs @@ -60,6 +60,9 @@ public static class MshMeshDocumentImporter var indices = Msh0x06.ReadComponent(fs, archive); var batches = Msh0x0D.ReadComponent(fs, archive); var animationDescriptors = Msh0x08.ReadComponent(fs, archive); + var animationMapComponent = TryReadAnimationMap(fs, archive, warnings); + var animationMap = animationMapComponent?.Entries ?? []; + var maxAnimationTime = animationMapComponent?.MaxAnimationTime ?? InferMaxAnimationTime(animationDescriptors); var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors); var names = TryReadNames(fs, archive, warnings); @@ -121,7 +124,10 @@ public static class MshMeshDocumentImporter Normals = normals, Uvs = uvs, Indices = indices, - Batches = batches + Batches = batches, + TransformKeyframes = animationDescriptors, + AnimationMap = animationMap, + MaxAnimationTime = maxAnimationTime } }; } @@ -228,6 +234,27 @@ public static class MshMeshDocumentImporter } } + private static Msh0x13Component? TryReadAnimationMap(FileStream fs, NResArchive archive, ICollection warnings) + { + try + { + return Msh0x13.ReadComponent(fs, archive); + } + catch (Exception ex) + { + warnings.Add($"MSH 0x13 animation map is unavailable: {ex.Message}"); + return null; + } + } + + private static int InferMaxAnimationTime(IReadOnlyList keyframes) + { + if (keyframes.Count == 0) + return 0; + + return (int)MathF.Ceiling(MathF.Max(0.0f, keyframes.Max(x => x.Time))); + } + private static string ResolvePieceName(IReadOnlyList names, int nodeIndex) { if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex])) diff --git a/NResUI/Rendering/Import/Msh/MshViewportMeshFactory.cs b/NResUI/Rendering/Import/Msh/MshViewportMeshFactory.cs index e9d9d3c..d58e407 100644 --- a/NResUI/Rendering/Import/Msh/MshViewportMeshFactory.cs +++ b/NResUI/Rendering/Import/Msh/MshViewportMeshFactory.cs @@ -4,6 +4,7 @@ using NResUI.Rendering.Inspection; using NResUI.Rendering.Materials; using NResUI.Rendering.Viewport; using NResUI.Rendering.Viewport.Meshes; +using NResUI.Rendering.Viewport.Msh; using Silk.NET.OpenGL; namespace NResUI.Rendering.Import.Msh; @@ -26,18 +27,25 @@ public static class MshViewportMeshFactory var result = new List(); var modelGeometry = document.MshModelGeometry; var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f); + var poses = MshRestPoseBuilder.BuildPose( + modelGeometry.Nodes, + modelGeometry.TransformKeyframes, + modelGeometry.AnimationMap, + renderState.AnimationPoseMode, + renderState.AnimationSampleTime); foreach (var pieceInfo in document.Pieces) { if (!renderState.IsPieceVisible(pieceInfo.Id)) continue; + var pieceTransform = poses[pieceInfo.Id].MeshSpaceTransform; var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState); var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod); if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count) { if (renderState.ShowEmptyPieces) - AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "No geometry slot for selected state/LOD"); + AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "No geometry slot for selected state/LOD"); continue; } @@ -45,7 +53,7 @@ public static class MshViewportMeshFactory if (!slot.HasBatches) { if (renderState.ShowEmptyPieces) - AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot has no batches", slotIndex); + AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "Geometry slot has no batches", slotIndex); continue; } @@ -60,7 +68,7 @@ public static class MshViewportMeshFactory if (meshBuildResult == null) { if (renderState.ShowEmptyPieces) - AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot produced no valid triangles", slotIndex); + AddEmptyPieceMarker(gl, result, pieceInfo, pieceTransform, mshToViewportTransform, "Geometry slot produced no valid triangles", slotIndex); continue; } @@ -68,7 +76,7 @@ public static class MshViewportMeshFactory id: pieceInfo.Id, name: pieceInfo.Name, meshes: meshBuildResult.Meshes, - localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform, + localTransform: pieceTransform * mshToViewportTransform, boundsMin: meshBuildResult.BoundsMin, boundsMax: meshBuildResult.BoundsMax, debugInfo: new ViewportPieceDebugInfo @@ -121,6 +129,7 @@ public static class MshViewportMeshFactory GL gl, List pieces, MeshPieceInfo pieceInfo, + Matrix4x4 pieceTransform, Matrix4x4 mshToViewportTransform, string reason, int geometrySlotIndex = -1) @@ -131,7 +140,7 @@ public static class MshViewportMeshFactory id: pieceInfo.Id, name: pieceInfo.Name, mesh: marker, - localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform, + localTransform: pieceTransform * mshToViewportTransform, boundsMin: new Vector3(-0.35f, -0.35f, -0.35f), boundsMax: new Vector3(0.35f, 0.35f, 0.35f), debugInfo: new ViewportPieceDebugInfo diff --git a/NResUI/Rendering/Inspection/MeshRenderState.cs b/NResUI/Rendering/Inspection/MeshRenderState.cs index ba7c7bb..5f9070e 100644 --- a/NResUI/Rendering/Inspection/MeshRenderState.cs +++ b/NResUI/Rendering/Inspection/MeshRenderState.cs @@ -1,5 +1,7 @@ namespace NResUI.Rendering.Inspection; +using NResUI.Rendering.Viewport.Msh; + /// /// Состояние просмотра меша. State/LOD задаются только для конкретных pieces, /// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла. @@ -15,6 +17,10 @@ public sealed class MeshRenderState public HashSet HiddenPieceIds { get; } = []; public Dictionary PieceStateOverrides { get; } = []; public Dictionary PieceLodOverrides { get; } = []; + public MshAnimationPoseMode AnimationPoseMode { get; set; } = MshAnimationPoseMode.DefaultKeyframe; + public float AnimationSampleTime { get; set; } + public float AnimationSpeed { get; set; } = 1.0f; + public bool AnimationPlay { get; set; } public void ClearSelection() { diff --git a/NResUI/Rendering/Inspection/MshModelGeometry.cs b/NResUI/Rendering/Inspection/MshModelGeometry.cs index 3019965..fa5235c 100644 --- a/NResUI/Rendering/Inspection/MshModelGeometry.cs +++ b/NResUI/Rendering/Inspection/MshModelGeometry.cs @@ -27,4 +27,13 @@ public sealed class MshModelGeometry /// Render batches 0x0D: диапазоны индексов, material id и render flags. public required IReadOnlyList Batches { get; init; } + + /// Piece transform keyframes from MSH 0x08. + public required IReadOnlyList TransformKeyframes { get; init; } + + /// Animation frame to 0x08 keyframe map from MSH 0x13. Optional in some files. + public required IReadOnlyList AnimationMap { get; init; } + + /// Animation frame count/window from MSH 0x13 NRes metadata. + public required int MaxAnimationTime { get; init; } } diff --git a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs index eeda762..21b34d3 100644 --- a/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs +++ b/NResUI/Rendering/Viewport/Meshes/GpuMesh.cs @@ -3,9 +3,10 @@ using NResUI.Rendering.Viewport; namespace NResUI.Rendering.Viewport.Meshes; -public sealed class GpuMesh +public sealed class GpuMesh : IDisposable { private readonly GL _gl; + private bool _disposed; public uint VertexArrayObject { get; } public uint VertexBufferObject { get; } @@ -34,7 +35,21 @@ public sealed class GpuMesh public unsafe void Draw() { + if (_disposed) + return; + _gl.BindVertexArray(VertexArrayObject); _gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null); } + + public void Dispose() + { + if (_disposed) + return; + + _gl.DeleteBuffer(ElementBufferObject); + _gl.DeleteBuffer(VertexBufferObject); + _gl.DeleteVertexArray(VertexArrayObject); + _disposed = true; + } } diff --git a/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs index 003c345..0892bef 100644 --- a/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs +++ b/NResUI/Rendering/Viewport/Msh/MshRestPoseBuilder.cs @@ -13,7 +13,22 @@ public static class MshRestPoseBuilder /// Собирает bind/rest pose для pieces из fallback keyframes 0x08. /// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform. /// - public static IReadOnlyList BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, List animationDescriptors) + public static IReadOnlyList BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, IReadOnlyList animationDescriptors) + { + return BuildPose( + nodesComponent, + animationDescriptors, + [], + MshAnimationPoseMode.DefaultKeyframe, + 0.0f); + } + + public static IReadOnlyList BuildPose( + Msh0x01.Msh0x01Component nodesComponent, + IReadOnlyList animationDescriptors, + IReadOnlyList animationMap, + MshAnimationPoseMode poseMode, + float sampleTime) { var nodeList = nodesComponent.Nodes; var animationList = animationDescriptors; @@ -22,7 +37,7 @@ public static class MshRestPoseBuilder var state = new byte[nodeList.Count]; for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++) - BuildNodePose(nodeIndex, nodeList, animationList, poses, state); + BuildNodePose(nodeIndex, nodeList, animationList, animationMap, poses, state, poseMode, sampleTime); return poses; } @@ -30,9 +45,12 @@ public static class MshRestPoseBuilder private static Matrix4x4 BuildNodePose( int nodeIndex, List nodes, - List animationDescriptors, + IReadOnlyList animationDescriptors, + IReadOnlyList animationMap, MshPieceRestPose[] poses, - byte[] state) + byte[] state, + MshAnimationPoseMode poseMode, + float sampleTime) { if (state[nodeIndex] == 2) return poses[nodeIndex].MeshSpaceTransform; @@ -49,11 +67,10 @@ public static class MshRestPoseBuilder var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node); var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count; - // var localTransform = Matrix4x4.Identity; Matrix4x4 localTransform; if (hasFallbackPose) { - localTransform = BuildTransformFromAnimationDescriptor(animationDescriptors[fallbackKeyframeIndex]!); + localTransform = BuildLocalTransform(node, animationDescriptors, animationMap, poseMode, sampleTime); } else { @@ -69,7 +86,7 @@ public static class MshRestPoseBuilder var meshSpaceTransform = localTransform; if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex) { - var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, poses, state); + var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, animationMap, poses, state, poseMode, sampleTime); meshSpaceTransform = localTransform * parentTransform; } @@ -85,6 +102,43 @@ public static class MshRestPoseBuilder return meshSpaceTransform; } + private static Matrix4x4 BuildLocalTransform( + Msh0x01.Node node, + IReadOnlyList animationDescriptors, + IReadOnlyList animationMap, + MshAnimationPoseMode poseMode, + float sampleTime) + { + if (poseMode == MshAnimationPoseMode.AnimationSample) + { + return MshAnimationSampler.ToMatrix(MshAnimationSampler.SamplePieceAnimationAtTime( + node, + animationDescriptors, + animationMap, + sampleTime)); + } + + return BuildTransformFromAnimationDescriptor(animationDescriptors[node.DefaultKeyframeIndex0x08]); + } + + private static bool IsValidSampleStream( + Msh0x01.Node node, + IReadOnlyList animationDescriptors, + IReadOnlyList animationMap, + float sampleTime) + { + if (animationMap.Count == 0 || node.AnimMapStart0x13 == ushort.MaxValue) + return false; + + var frameIndex = (int)MathF.Round(sampleTime - 0.5f, MidpointRounding.AwayFromZero); + if (frameIndex < 0 || node.AnimMapStart0x13 + frameIndex >= animationMap.Count) + return false; + + var keyframeIndex = animationMap[node.AnimMapStart0x13 + frameIndex].KeyframeIndex; + return keyframeIndex < node.DefaultKeyframeIndex0x08 && + keyframeIndex + 1 < animationDescriptors.Count; + } + private static int GetParentIndex(Msh0x01.Node node) { return node.ParentIndexOrLink == ushort.MaxValue ? -1 : node.ParentIndexOrLink; @@ -103,14 +157,9 @@ public static class MshRestPoseBuilder } } - private static Matrix4x4 BuildTransformFromAnimationDescriptor(Msh0x08.AnimationDescriptor descriptor) + private static Matrix4x4 BuildTransformFromAnimationDescriptor(MshTransformKeyframe descriptor) { - var position = ToSystemVector3(descriptor.Position); - var rotation = ToSystemQuaternion(descriptor.Rotation); - - var matrix = Matrix4x4.CreateFromQuaternion(rotation); - matrix.Translation = position; - return matrix; + return MshAnimationSampler.ToMatrix(MshAnimationSampler.FromKeyframe(descriptor)); } private static Vector3 ToSystemVector3(dynamic vector) @@ -143,6 +192,12 @@ public static class MshRestPoseBuilder } } +public enum MshAnimationPoseMode +{ + DefaultKeyframe, + AnimationSample +} + public readonly record struct MshPieceRestPose( int NodeIndex, int ParentIndex, diff --git a/NResUI/Rendering/Viewport/ViewportPiece.cs b/NResUI/Rendering/Viewport/ViewportPiece.cs index e6b2fbb..b63d0c3 100644 --- a/NResUI/Rendering/Viewport/ViewportPiece.cs +++ b/NResUI/Rendering/Viewport/ViewportPiece.cs @@ -3,7 +3,7 @@ using NResUI.Rendering.Viewport.Meshes; namespace NResUI.Rendering.Viewport; -public sealed class ViewportPiece +public sealed class ViewportPiece : IDisposable { public int Id { get; } public string Name { get; } @@ -75,4 +75,10 @@ public sealed class ViewportPiece TriangleCount = 12 }); } + + public void Dispose() + { + foreach (var mesh in Meshes) + mesh.Dispose(); + } } diff --git a/NResUI/Rendering/Viewport/ViewportScene.cs b/NResUI/Rendering/Viewport/ViewportScene.cs index 937b8d1..1bc5ef8 100644 --- a/NResUI/Rendering/Viewport/ViewportScene.cs +++ b/NResUI/Rendering/Viewport/ViewportScene.cs @@ -39,12 +39,14 @@ public sealed class ViewportScene public void ClearPieces() { + DisposePieces(); _pieces.Clear(); ClearSelection(); } public void ReplacePieces(IEnumerable pieces) { + DisposePieces(); _pieces.Clear(); _pieces.AddRange(pieces); ClearSelection(); @@ -104,6 +106,12 @@ public sealed class ViewportScene return scene; } + private void DisposePieces() + { + foreach (var piece in _pieces) + piece.Dispose(); + } + private static bool TryTransformBounds( Vector3 localMin, Vector3 localMax,