using System.Collections; using System.Numerics; using Common; using MshLib; using Quaternion = System.Numerics.Quaternion; using Vector3 = System.Numerics.Vector3; namespace NResUI.Rendering.Viewport.Msh; public static class MshRestPoseBuilder { /// /// Собирает bind/rest pose для pieces из fallback keyframes 0x08. /// Циклы и битые parent-ссылки не должны ломать viewport, поэтому такие узлы остаются с identity transform. /// 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; var poses = new MshPieceRestPose[nodeList.Count]; var state = new byte[nodeList.Count]; for (var nodeIndex = 0; nodeIndex < nodeList.Count; nodeIndex++) BuildNodePose(nodeIndex, nodeList, animationList, animationMap, poses, state, poseMode, sampleTime); return poses; } private static Matrix4x4 BuildNodePose( int nodeIndex, List nodes, IReadOnlyList animationDescriptors, IReadOnlyList animationMap, MshPieceRestPose[] poses, byte[] state, MshAnimationPoseMode poseMode, float sampleTime) { if (state[nodeIndex] == 2) return poses[nodeIndex].MeshSpaceTransform; if (state[nodeIndex] == 1) { // Broken/cyclic parent chain. Keep this node local so the viewer remains usable. return Matrix4x4.Identity; } state[nodeIndex] = 1; var node = nodes[nodeIndex]!; var parentIndex = GetParentIndex(node); var fallbackKeyframeIndex = GetFallbackKeyframeIndex(node); var hasFallbackPose = fallbackKeyframeIndex >= 0 && fallbackKeyframeIndex < animationDescriptors.Count; Matrix4x4 localTransform; if (hasFallbackPose) { localTransform = BuildLocalTransform(node, animationDescriptors, animationMap, poseMode, sampleTime); } else { localTransform = Matrix4x4.Identity; } if (parentIndex == -1) { // Root nodes describe object placement in game space; the viewer keeps the model centered. localTransform.Translation = Vector3.Zero; } var meshSpaceTransform = localTransform; if (parentIndex >= 0 && parentIndex < nodes.Count && parentIndex != nodeIndex) { var parentTransform = BuildNodePose(parentIndex, nodes, animationDescriptors, animationMap, poses, state, poseMode, sampleTime); meshSpaceTransform = localTransform * parentTransform; } poses[nodeIndex] = new MshPieceRestPose( NodeIndex: nodeIndex, ParentIndex: parentIndex, FallbackKeyframeIndex: fallbackKeyframeIndex, HasFallbackPose: hasFallbackPose, LocalTransform: localTransform, MeshSpaceTransform: meshSpaceTransform); state[nodeIndex] = 2; 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; } private static int GetFallbackKeyframeIndex(Msh0x01.Node node) { try { var value = Convert.ToUInt16(node.FallbackKey0x08); return value == ushort.MaxValue ? -1 : value; } catch { return -1; } } private static Matrix4x4 BuildTransformFromAnimationDescriptor(MshTransformKeyframe descriptor) { return MshAnimationSampler.ToMatrix(MshAnimationSampler.FromKeyframe(descriptor)); } private static Vector3 ToSystemVector3(dynamic vector) { return new Vector3( Convert.ToSingle(vector.X), Convert.ToSingle(vector.Y), Convert.ToSingle(vector.Z)); } private static Quaternion ToSystemQuaternion(UShortQuaternion packedQuaternion) { // MSH 0x08 stores quaternion as W, X, Y, Z. // System.Numerics.Quaternion constructor expects X, Y, Z, W. 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/game convention is opposite to our System.Numerics/OpenGL transform convention. // Conjugating converts the stored piece rotation into the viewport convention. // The MSH keyframe rotation is stored as mesh-to-parent instead of parent-to-mesh, so the viewer needs the inverse. return Quaternion.Conjugate(q); } } public enum MshAnimationPoseMode { DefaultKeyframe, AnimationSample } public readonly record struct MshPieceRestPose( int NodeIndex, int ParentIndex, int FallbackKeyframeIndex, bool HasFallbackPose, Matrix4x4 LocalTransform, Matrix4x4 MeshSpaceTransform);