animations and QOL

This commit is contained in:
bird_egop
2026-06-12 02:06:32 +03:00
parent 4d0d1aca12
commit 1cb3940cce
14 changed files with 444 additions and 32 deletions
+2
View File
@@ -105,6 +105,8 @@ public static class Msh0x01
ushort FallbackKey0x08, ushort FallbackKey0x08,
ushort[] Msh02SlotIndicesByStateAndLOD) ushort[] Msh02SlotIndicesByStateAndLOD)
{ {
public ushort DefaultKeyframeIndex0x08 => FallbackKey0x08;
public ushort ResolveSlotIndex(int state, int lod = 0) public ushort ResolveSlotIndex(int state, int lod = 0)
{ {
// State и LOD выбираются для конкретного узла/piece, не для всего файла сразу. // State и LOD выбираются для конкретного узла/piece, не для всего файла сразу.
+9 -9
View File
@@ -5,7 +5,7 @@ namespace MshLib;
public static class Msh0x08 public static class Msh0x08
{ {
public static List<AnimationDescriptor> ReadComponent(FileStream mshFs, NResArchive archive) public static List<MshTransformKeyframe> ReadComponent(FileStream mshFs, NResArchive archive)
{ {
var entry = archive.Files.FirstOrDefault(x => x.FileType == "08 00 00 00"); 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); mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
var descriptors = new List<AnimationDescriptor>(); var descriptors = new List<MshTransformKeyframe>();
for (var i = 0; i < entry.ElementCount; i++) for (var i = 0; i < entry.ElementCount; i++)
{ {
descriptors.Add(new AnimationDescriptor( descriptors.Add(new MshTransformKeyframe(
new Vector3(mshFs.ReadFloatLittleEndian(), new Vector3(mshFs.ReadFloatLittleEndian(),
mshFs.ReadFloatLittleEndian(), mshFs.ReadFloatLittleEndian(),
mshFs.ReadFloatLittleEndian()), mshFs.ReadFloatLittleEndian()),
@@ -36,10 +36,10 @@ public static class Msh0x08
return descriptors; return descriptors;
} }
public record AnimationDescriptor(
Vector3 Position,
float Time,
UShortQuaternion Rotation
);
} }
public record MshTransformKeyframe(
Vector3 Position,
float Time,
UShortQuaternion Rotation
);
+53
View File
@@ -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<MshAnimationMapEntry>(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<MshAnimationMapEntry> Entries
)
{
public int MaxAnimationTime { get; init; }
}
+92
View File
@@ -0,0 +1,92 @@
using System.Numerics;
namespace MshLib;
public static class MshAnimationSampler
{
public static MshSampledPieceTransform SamplePieceAnimationAtTime(
Msh0x01.Node node,
IReadOnlyList<MshTransformKeyframe> keyframes,
IReadOnlyList<MshAnimationMapEntry> 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);
}
+12
View File
@@ -26,6 +26,7 @@ public class App
public ImFontPtr OpenSansFont; public ImFontPtr OpenSansFont;
private List<IImGuiPanel> _imGuiPanels = []; private List<IImGuiPanel> _imGuiPanels = [];
private List<IUpdateReceiver> _updateReceivers = [];
private Later _later = new(); private Later _later = new();
public App() public App()
@@ -59,6 +60,11 @@ public class App
serviceCollection.AddSingleton(type); serviceCollection.AddSingleton(type);
} }
foreach (var type in Utils.GetAssignableTypes<IUpdateReceiver>())
{
serviceCollection.AddSingleton(type);
}
serviceCollection.AddSingleton(openGl); serviceCollection.AddSingleton(openGl);
serviceCollection.AddSingleton(window); serviceCollection.AddSingleton(window);
@@ -79,6 +85,10 @@ public class App
.Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!) .Select(t => (serviceProvider.GetService(t) as IImGuiPanel)!)
.ToList(); .ToList();
_updateReceivers = Utils.GetAssignableTypes<IUpdateReceiver>()
.Select(t => (serviceProvider.GetService(t) as IUpdateReceiver)!)
.ToList();
foreach (var type in Utils.GetAssignableTypes<ILaunchReceiver>()) foreach (var type in Utils.GetAssignableTypes<ILaunchReceiver>())
{ {
var launchReceiver = serviceProvider.GetService(type) as ILaunchReceiver; var launchReceiver = serviceProvider.GetService(type) as ILaunchReceiver;
@@ -159,6 +169,8 @@ public class App
public void Update(double delta) public void Update(double delta)
{ {
foreach (var updateReceiver in _updateReceivers)
updateReceiver.OnUpdate((float)delta);
} }
public void OnKeyPressed(Key key) public void OnKeyPressed(Key key)
+119 -1
View File
@@ -2,17 +2,19 @@ using System.Numerics;
using ImGuiNET; using ImGuiNET;
using NResUI.Abstractions; using NResUI.Abstractions;
using NResUI.Models; using NResUI.Models;
using NResUI.Rendering.Inspection;
using NResUI.Rendering.Import.Msh; using NResUI.Rendering.Import.Msh;
using NResUI.Rendering.Materials; using NResUI.Rendering.Materials;
using NResUI.Rendering.Viewport; using NResUI.Rendering.Viewport;
using NativeFileDialogSharp; 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;
namespace NResUI.ImGuiUI; namespace NResUI.ImGuiUI;
public sealed class ViewportPanel : IImGuiPanel public sealed class ViewportPanel : IImGuiPanel, IUpdateReceiver
{ {
private readonly ViewportRenderer _renderer; private readonly ViewportRenderer _renderer;
private readonly ViewportScene _scene; private readonly ViewportScene _scene;
@@ -46,6 +48,7 @@ public sealed class ViewportPanel : IImGuiPanel
DrawSelectionStatus(); DrawSelectionStatus();
DrawViewportToolbar(); DrawViewportToolbar();
DrawDebugControls(); DrawDebugControls();
DrawAnimationControls();
RebuildSceneIfNeeded(); RebuildSceneIfNeeded();
SyncSelectionFromViewModel(); SyncSelectionFromViewModel();
@@ -90,6 +93,27 @@ public sealed class ViewportPanel : IImGuiPanel
ImGui.End(); 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() private void DrawModelControls()
{ {
@@ -183,9 +207,34 @@ public sealed class ViewportPanel : IImGuiPanel
_scene.ReplacePieces(pieces); _scene.ReplacePieces(pieces);
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId; _scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
ApplyCurrentAnimationPose(document);
_viewModel.MarkSceneRebuilt(); _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() private void SyncSelectionFromViewModel()
{ {
if (_viewModel.Document == null) if (_viewModel.Document == null)
@@ -260,6 +309,75 @@ public sealed class ViewportPanel : IImGuiPanel
_scene.Debug.Wireframe = wireframe; _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() private static void DrawViewportHelpOverlay()
{ {
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
@@ -60,6 +60,9 @@ public static class MshMeshDocumentImporter
var indices = Msh0x06.ReadComponent(fs, archive); var indices = Msh0x06.ReadComponent(fs, archive);
var batches = Msh0x0D.ReadComponent(fs, archive); var batches = Msh0x0D.ReadComponent(fs, archive);
var animationDescriptors = Msh0x08.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 restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
var names = TryReadNames(fs, archive, warnings); var names = TryReadNames(fs, archive, warnings);
@@ -121,7 +124,10 @@ public static class MshMeshDocumentImporter
Normals = normals, Normals = normals,
Uvs = uvs, Uvs = uvs,
Indices = indices, 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<string> 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<MshTransformKeyframe> 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<string> names, int nodeIndex) private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
{ {
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex])) if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
@@ -4,6 +4,7 @@ using NResUI.Rendering.Inspection;
using NResUI.Rendering.Materials; using NResUI.Rendering.Materials;
using NResUI.Rendering.Viewport; using NResUI.Rendering.Viewport;
using NResUI.Rendering.Viewport.Meshes; using NResUI.Rendering.Viewport.Meshes;
using NResUI.Rendering.Viewport.Msh;
using Silk.NET.OpenGL; using Silk.NET.OpenGL;
namespace NResUI.Rendering.Import.Msh; namespace NResUI.Rendering.Import.Msh;
@@ -26,18 +27,25 @@ public static class MshViewportMeshFactory
var result = new List<ViewportPiece>(); var result = new List<ViewportPiece>();
var modelGeometry = document.MshModelGeometry; var modelGeometry = document.MshModelGeometry;
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f); 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) foreach (var pieceInfo in document.Pieces)
{ {
if (!renderState.IsPieceVisible(pieceInfo.Id)) if (!renderState.IsPieceVisible(pieceInfo.Id))
continue; continue;
var pieceTransform = poses[pieceInfo.Id].MeshSpaceTransform;
var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState); var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState);
var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod); var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod);
if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count) if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count)
{ {
if (renderState.ShowEmptyPieces) 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; continue;
} }
@@ -45,7 +53,7 @@ public static class MshViewportMeshFactory
if (!slot.HasBatches) if (!slot.HasBatches)
{ {
if (renderState.ShowEmptyPieces) 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; continue;
} }
@@ -60,7 +68,7 @@ public static class MshViewportMeshFactory
if (meshBuildResult == null) if (meshBuildResult == null)
{ {
if (renderState.ShowEmptyPieces) 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; continue;
} }
@@ -68,7 +76,7 @@ public static class MshViewportMeshFactory
id: pieceInfo.Id, id: pieceInfo.Id,
name: pieceInfo.Name, name: pieceInfo.Name,
meshes: meshBuildResult.Meshes, meshes: meshBuildResult.Meshes,
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform, localTransform: pieceTransform * mshToViewportTransform,
boundsMin: meshBuildResult.BoundsMin, boundsMin: meshBuildResult.BoundsMin,
boundsMax: meshBuildResult.BoundsMax, boundsMax: meshBuildResult.BoundsMax,
debugInfo: new ViewportPieceDebugInfo debugInfo: new ViewportPieceDebugInfo
@@ -121,6 +129,7 @@ public static class MshViewportMeshFactory
GL gl, GL gl,
List<ViewportPiece> pieces, List<ViewportPiece> pieces,
MeshPieceInfo pieceInfo, MeshPieceInfo pieceInfo,
Matrix4x4 pieceTransform,
Matrix4x4 mshToViewportTransform, Matrix4x4 mshToViewportTransform,
string reason, string reason,
int geometrySlotIndex = -1) int geometrySlotIndex = -1)
@@ -131,7 +140,7 @@ public static class MshViewportMeshFactory
id: pieceInfo.Id, id: pieceInfo.Id,
name: pieceInfo.Name, name: pieceInfo.Name,
mesh: marker, mesh: marker,
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform, localTransform: pieceTransform * mshToViewportTransform,
boundsMin: new Vector3(-0.35f, -0.35f, -0.35f), boundsMin: new Vector3(-0.35f, -0.35f, -0.35f),
boundsMax: new Vector3(0.35f, 0.35f, 0.35f), boundsMax: new Vector3(0.35f, 0.35f, 0.35f),
debugInfo: new ViewportPieceDebugInfo debugInfo: new ViewportPieceDebugInfo
@@ -1,5 +1,7 @@
namespace NResUI.Rendering.Inspection; namespace NResUI.Rendering.Inspection;
using NResUI.Rendering.Viewport.Msh;
/// <summary> /// <summary>
/// Состояние просмотра меша. State/LOD задаются только для конкретных pieces, /// Состояние просмотра меша. State/LOD задаются только для конкретных pieces,
/// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла. /// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла.
@@ -15,6 +17,10 @@ public sealed class MeshRenderState
public HashSet<int> HiddenPieceIds { get; } = []; public HashSet<int> HiddenPieceIds { get; } = [];
public Dictionary<int, int> PieceStateOverrides { get; } = []; public Dictionary<int, int> PieceStateOverrides { get; } = [];
public Dictionary<int, int> PieceLodOverrides { get; } = []; public Dictionary<int, int> 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() public void ClearSelection()
{ {
@@ -27,4 +27,13 @@ public sealed class MshModelGeometry
/// <summary>Render batches 0x0D: диапазоны индексов, material id и render flags.</summary> /// <summary>Render batches 0x0D: диапазоны индексов, material id и render flags.</summary>
public required IReadOnlyList<Msh0x0D.Batch> Batches { get; init; } public required IReadOnlyList<Msh0x0D.Batch> Batches { get; init; }
/// <summary>Piece transform keyframes from MSH 0x08.</summary>
public required IReadOnlyList<MshTransformKeyframe> TransformKeyframes { get; init; }
/// <summary>Animation frame to 0x08 keyframe map from MSH 0x13. Optional in some files.</summary>
public required IReadOnlyList<MshAnimationMapEntry> AnimationMap { get; init; }
/// <summary>Animation frame count/window from MSH 0x13 NRes metadata.</summary>
public required int MaxAnimationTime { get; init; }
} }
+16 -1
View File
@@ -3,9 +3,10 @@ using NResUI.Rendering.Viewport;
namespace NResUI.Rendering.Viewport.Meshes; namespace NResUI.Rendering.Viewport.Meshes;
public sealed class GpuMesh public sealed class GpuMesh : IDisposable
{ {
private readonly GL _gl; private readonly GL _gl;
private bool _disposed;
public uint VertexArrayObject { get; } public uint VertexArrayObject { get; }
public uint VertexBufferObject { get; } public uint VertexBufferObject { get; }
@@ -34,7 +35,21 @@ public sealed class GpuMesh
public unsafe void Draw() public unsafe void Draw()
{ {
if (_disposed)
return;
_gl.BindVertexArray(VertexArrayObject); _gl.BindVertexArray(VertexArrayObject);
_gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null); _gl.DrawElements(PrimitiveType, IndexCount, DrawElementsType.UnsignedInt, null);
} }
public void Dispose()
{
if (_disposed)
return;
_gl.DeleteBuffer(ElementBufferObject);
_gl.DeleteBuffer(VertexBufferObject);
_gl.DeleteVertexArray(VertexArrayObject);
_disposed = true;
}
} }
@@ -13,7 +13,22 @@ public static class MshRestPoseBuilder
/// Собирает bind/rest pose для pieces из fallback keyframes 0x08. /// Собирает bind/rest pose для pieces из fallback keyframes 0x08.
/// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform. /// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform.
/// </summary> /// </summary>
public static IReadOnlyList<MshPieceRestPose> BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, List<Msh0x08.AnimationDescriptor> animationDescriptors) public static IReadOnlyList<MshPieceRestPose> BuildRestPose(Msh0x01.Msh0x01Component nodesComponent, IReadOnlyList<MshTransformKeyframe> animationDescriptors)
{
return BuildPose(
nodesComponent,
animationDescriptors,
[],
MshAnimationPoseMode.DefaultKeyframe,
0.0f);
}
public static IReadOnlyList<MshPieceRestPose> BuildPose(
Msh0x01.Msh0x01Component nodesComponent,
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
IReadOnlyList<MshAnimationMapEntry> animationMap,
MshAnimationPoseMode poseMode,
float sampleTime)
{ {
var nodeList = nodesComponent.Nodes; var nodeList = nodesComponent.Nodes;
var animationList = animationDescriptors; var animationList = animationDescriptors;
@@ -22,7 +37,7 @@ public static class MshRestPoseBuilder
var state = new byte[nodeList.Count]; var state = new byte[nodeList.Count];
for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++) 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; return poses;
} }
@@ -30,9 +45,12 @@ public static class MshRestPoseBuilder
private static Matrix4x4 BuildNodePose( private static Matrix4x4 BuildNodePose(
int nodeIndex, int nodeIndex,
List<Msh0x01.Node> nodes, List<Msh0x01.Node> nodes,
List<Msh0x08.AnimationDescriptor> animationDescriptors, IReadOnlyList<MshTransformKeyframe> animationDescriptors,
IReadOnlyList<MshAnimationMapEntry> animationMap,
MshPieceRestPose[] poses, MshPieceRestPose[] poses,
byte[] state) byte[] state,
MshAnimationPoseMode poseMode,
float sampleTime)
{ {
if (state[nodeIndex] == 2) if (state[nodeIndex] == 2)
return poses[nodeIndex].MeshSpaceTransform; return poses[nodeIndex].MeshSpaceTransform;
@@ -49,11 +67,10 @@ public static class MshRestPoseBuilder
var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node); var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node);
var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count; var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count;
// var localTransform = Matrix4x4.Identity;
Matrix4x4 localTransform; Matrix4x4 localTransform;
if (hasFallbackPose) if (hasFallbackPose)
{ {
localTransform = BuildTransformFromAnimationDescriptor(animationDescriptors[fallbackKeyframeIndex]!); localTransform = BuildLocalTransform(node, animationDescriptors, animationMap, poseMode, sampleTime);
} }
else else
{ {
@@ -69,7 +86,7 @@ public static class MshRestPoseBuilder
var meshSpaceTransform = localTransform; var meshSpaceTransform = localTransform;
if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex) 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; meshSpaceTransform = localTransform * parentTransform;
} }
@@ -85,6 +102,43 @@ public static class MshRestPoseBuilder
return meshSpaceTransform; return meshSpaceTransform;
} }
private static Matrix4x4 BuildLocalTransform(
Msh0x01.Node node,
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
IReadOnlyList<MshAnimationMapEntry> 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<MshTransformKeyframe> animationDescriptors,
IReadOnlyList<MshAnimationMapEntry> 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) private static int GetParentIndex(Msh0x01.Node node)
{ {
return node.ParentIndexOrLink == ushort.MaxValue ? -1 : node.ParentIndexOrLink; 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); return MshAnimationSampler.ToMatrix(MshAnimationSampler.FromKeyframe(descriptor));
var rotation = ToSystemQuaternion(descriptor.Rotation);
var matrix = Matrix4x4.CreateFromQuaternion(rotation);
matrix.Translation = position;
return matrix;
} }
private static Vector3 ToSystemVector3(dynamic vector) private static Vector3 ToSystemVector3(dynamic vector)
@@ -143,6 +192,12 @@ public static class MshRestPoseBuilder
} }
} }
public enum MshAnimationPoseMode
{
DefaultKeyframe,
AnimationSample
}
public readonly record struct MshPieceRestPose( public readonly record struct MshPieceRestPose(
int NodeIndex, int NodeIndex,
int ParentIndex, int ParentIndex,
+7 -1
View File
@@ -3,7 +3,7 @@ using NResUI.Rendering.Viewport.Meshes;
namespace NResUI.Rendering.Viewport; namespace NResUI.Rendering.Viewport;
public sealed class ViewportPiece public sealed class ViewportPiece : IDisposable
{ {
public int Id { get; } public int Id { get; }
public string Name { get; } public string Name { get; }
@@ -75,4 +75,10 @@ public sealed class ViewportPiece
TriangleCount = 12 TriangleCount = 12
}); });
} }
public void Dispose()
{
foreach (var mesh in Meshes)
mesh.Dispose();
}
} }
@@ -39,12 +39,14 @@ public sealed class ViewportScene
public void ClearPieces() public void ClearPieces()
{ {
DisposePieces();
_pieces.Clear(); _pieces.Clear();
ClearSelection(); ClearSelection();
} }
public void ReplacePieces(IEnumerable<ViewportPiece> pieces) public void ReplacePieces(IEnumerable<ViewportPiece> pieces)
{ {
DisposePieces();
_pieces.Clear(); _pieces.Clear();
_pieces.AddRange(pieces); _pieces.AddRange(pieces);
ClearSelection(); ClearSelection();
@@ -104,6 +106,12 @@ public sealed class ViewportScene
return scene; return scene;
} }
private void DisposePieces()
{
foreach (var piece in _pieces)
piece.Dispose();
}
private static bool TryTransformBounds( private static bool TryTransformBounds(
Vector3 localMin, Vector3 localMin,
Vector3 localMax, Vector3 localMax,