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
@@ -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<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)
{
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.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<ViewportPiece>();
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<ViewportPiece> 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
@@ -1,5 +1,7 @@
namespace NResUI.Rendering.Inspection;
using NResUI.Rendering.Viewport.Msh;
/// <summary>
/// Состояние просмотра меша. State/LOD задаются только для конкретных pieces,
/// потому что 0x01 хранит таблицу slot indices отдельно для каждого узла.
@@ -15,6 +17,10 @@ public sealed class MeshRenderState
public HashSet<int> HiddenPieceIds { get; } = [];
public Dictionary<int, int> PieceStateOverrides { 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()
{
@@ -27,4 +27,13 @@ public sealed class MshModelGeometry
/// <summary>Render batches 0x0D: диапазоны индексов, material id и render flags.</summary>
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;
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;
}
}
@@ -13,7 +13,22 @@ public static class MshRestPoseBuilder
/// Собирает bind/rest pose для pieces из fallback keyframes 0x08.
/// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform.
/// </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 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<Msh0x01.Node> nodes,
List<Msh0x08.AnimationDescriptor> animationDescriptors,
IReadOnlyList<MshTransformKeyframe> animationDescriptors,
IReadOnlyList<MshAnimationMapEntry> 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<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)
{
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,
+7 -1
View File
@@ -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();
}
}
@@ -39,12 +39,14 @@ public sealed class ViewportScene
public void ClearPieces()
{
DisposePieces();
_pieces.Clear();
ClearSelection();
}
public void ReplacePieces(IEnumerable<ViewportPiece> 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,