Editor/Embedded/ValveResourceFormat/ResourceTypes/ModelAnimation/Animation.cs

Represents a model animation resource. Parses KV data to build frame blocks, segment decoders, movement, events, activities and sequence params, and provides frame decoding and root motion lookup.

Native InteropFile Access
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.Utils;
using System.Runtime.InteropServices;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.ResourceTypes.ModelAnimation.SegmentDecoders;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.ResourceTypes.ModelFlex;
using HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.Serialization.KeyValues;

#nullable disable

namespace HumanoidRetargeter.EditorTools.Embedded.ValveResourceFormat.ResourceTypes.ModelAnimation;

    /// <summary>
    /// Represents a model animation with frame data, events, and movement information.
    /// </summary>
    public class Animation
    {
        /// <summary>
        /// Gets the name of the animation.
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// Gets the frames per second of the animation.
        /// </summary>
        public float Fps { get; }

        /// <summary>
        /// Gets the total number of frames in the animation.
        /// </summary>
        public int FrameCount { get; }

        /// <summary>
        /// Gets a value indicating whether the animation loops.
        /// </summary>
        public bool IsLooping { get; }

        /// <summary>
        /// Gets or sets a value indicating whether the animation is hidden.
        /// </summary>
        public bool Hidden { get; init; }

        /// <summary>
        /// Gets or sets a value indicating whether this is a delta animation.
        /// </summary>
        public bool Delta { get; init; }

        /// <summary>
        /// Gets or sets a value indicating whether this animation is in world space.
        /// </summary>
        public bool Worldspace { get; init; }
        private AnimationFrameBlock[] FrameBlocks { get; }
        private AnimationSegmentDecoder[] SegmentArray { get; }

        /// <summary>
        /// Gets the movement data for this animation.
        /// </summary>
        public AnimationMovement[] Movements { get; }

        /// <summary>
        /// Gets the events defined in this animation.
        /// </summary>
        public AnimationEvent[] Events { get; }

        /// <summary>
        /// Gets the activities associated with this animation.
        /// </summary>
        public AnimationActivity[] Activities { get; }

        /// <summary>
        /// Gets the sequence parameters for this animation.
        /// </summary>
        public AnimationSequenceParams SequenceParams { get; }

        private Animation(KVObject animDesc, AnimationSegmentDecoder[] segmentArray)
        {
            // Get animation properties
            Name = animDesc.GetProperty<string>("m_name");
            Fps = animDesc.GetFloatProperty("fps");
            SegmentArray = segmentArray;

            var flags = animDesc.GetSubCollection("m_flags");
            IsLooping = flags.GetProperty<bool>("m_bLooping");
            Hidden = flags.GetProperty<bool>("m_bHidden");
            Delta = flags.GetProperty<bool>("m_bDelta");
            Worldspace = flags.GetProperty<bool>("m_bLegacyWorldspace");

            var pDataObject = animDesc.GetProperty<object>("m_pData");
            var pData = pDataObject as KVObject;
            FrameCount = pData.GetInt32Property("m_nFrames");

            var frameBlockArray = pData.GetArray("m_frameblockArray");
            FrameBlocks = new AnimationFrameBlock[frameBlockArray.Length];
            for (var i = 0; i < frameBlockArray.Length; i++)
            {
                FrameBlocks[i] = new AnimationFrameBlock(frameBlockArray[i]);
            }

            var movementArray = animDesc.GetArray("m_movementArray");
            Movements = new AnimationMovement[movementArray.Length];
            for (var i = 0; i < movementArray.Length; i++)
            {
                Movements[i] = new AnimationMovement(movementArray[i]);
            }

            Events = animDesc.GetArray("m_eventArray")
                                 .Select(x => new AnimationEvent(x))
                                 .ToArray();

            Activities = animDesc.GetArray("m_activityArray")
                                    .Select(x => new AnimationActivity(x))
                                    .ToArray();

            var sequenceParams = animDesc.GetSubCollection("m_sequenceParams");
            SequenceParams = new AnimationSequenceParams(sequenceParams);
        }

        /// <summary>
        /// Creates animation instances from the provided animation data and decode key.
        /// </summary>
        public static IEnumerable<Animation> FromData(KVObject animationData, KVObject decodeKey,
            Skeleton skeleton, FlexController[] flexControllers)
        {
            var animArray = animationData.GetArray<KVObject>("m_animArray");

            if (animArray.Length == 0)
            {
                return [];
            }

            var decoderArrayKV = animationData.GetArray("m_decoderArray");
            var decoderArray = new string[decoderArrayKV.Length];
            for (var i = 0; i < decoderArrayKV.Length; i++)
            {
                decoderArray[i] = decoderArrayKV[i].GetProperty<string>("m_szName");
            }

            //var channelElements = decodeKey.GetInt32Property("m_nChannelElements");
            var dataChannelArrayKV = decodeKey.GetArray("m_dataChannelArray");
            var dataChannelArray = new AnimationDataChannel[dataChannelArrayKV.Length];
            for (var i = 0; i < dataChannelArrayKV.Length; i++)
            {
                dataChannelArray[i] = new AnimationDataChannel(skeleton, flexControllers, dataChannelArrayKV[i]);
            }

            var segmentArrayKV = animationData.GetArray("m_segmentArray");
            var segmentArray = new AnimationSegmentDecoder[segmentArrayKV.Length];
            for (var i = 0; i < segmentArrayKV.Length; i++)
            {
                var segmentKV = segmentArrayKV[i];
                var container = segmentKV.GetArray<byte>("m_container");
                var containerSpan = container.AsSpan();
                var localChannel = dataChannelArray[segmentKV.GetInt32Property("m_nLocalChannel")];

                // Read header
                var decoder = decoderArray[BitConverter.ToInt16(containerSpan[0..2])];
                //var cardinality = BitConverter.ToInt16(containerSpan[2..4]);
                var numElements = BitConverter.ToInt16(containerSpan[4..6]);
                //var totalLength = BitConverter.ToInt16(containerSpan[6..8]);

                // Read bone list
                var end = 8 + numElements * 2;
                var elements = MemoryMarshal.Cast<byte, short>(containerSpan[8..end]);
                var remapTable = new int[localChannel.RemapTable.Length];

                for (var j = 0; j < remapTable.Length; j++)
                {
                    remapTable[j] = elements.IndexOf((short)localChannel.RemapTable[j]);
                }

                var wantedElements = remapTable.Where(boneID => boneID != -1).ToArray();
                remapTable = remapTable
                    .Select((boneID, i) => (boneID, i))
                    .Where(t => t.boneID != -1)
                    .Select(t => t.i)
                    .ToArray();

                if (localChannel.Attribute == AnimationChannelAttribute.Unknown)
                {
                    Console.Error.WriteLine($"Unknown channel attribute encountered with '{decoder}' decoder");
                    continue;
                }

                var containerSegment = new ArraySegment<byte>(container, end, container.Length - end);

                // Look at the decoder to see what to read
                segmentArray[i] = decoder switch
                {
                    nameof(CCompressedStaticFullVector3) => new CCompressedStaticFullVector3(),
                    nameof(CCompressedStaticVector3) => new CCompressedStaticVector3(),
                    nameof(CCompressedStaticQuaternion) => new CCompressedStaticQuaternion(),
                    nameof(CCompressedStaticFloat) => new CCompressedStaticFloat(),

                    nameof(CCompressedFullVector3) => new CCompressedFullVector3(),
                    nameof(CCompressedDeltaVector3) => new CCompressedDeltaVector3(),
                    nameof(CCompressedAnimVector3) => new CCompressedAnimVector3(),
                    nameof(CCompressedAnimQuaternion) => new CCompressedAnimQuaternion(),
                    nameof(CCompressedFullQuaternion) => new CCompressedFullQuaternion(),
                    nameof(CCompressedFullFloat) => new CCompressedFullFloat(),
                    _ => null,
                };

                if (segmentArray[i] != null)
                {
                    segmentArray[i].Initialize(containerSegment, wantedElements, remapTable, localChannel.Attribute, numElements);
                    continue;
                }

#if DEBUG
                Console.WriteLine($"Unhandled animation bone decoder type '{decoder}' for attribute '{localChannel.Attribute}'");
#endif
            }

            return animArray
                .Select(anim => new Animation(anim, segmentArray))
                .ToArray();
        }

        /// <summary>
        /// Creates animation instances from a resource file.
        /// </summary>
        public static IEnumerable<Animation> FromResource(Resource resource, KVObject decodeKey, Skeleton skeleton, FlexController[] flexControllers)
            => FromData(GetAnimationData(resource), decodeKey, skeleton, flexControllers);

        private static KVObject GetAnimationData(Resource resource)
            => resource.DataBlock.AsKeyValueCollection();

        private int GetMovementIndexForTime(float time)
        {
            var frame = (int)MathF.Floor(time * Fps);
            return GetMovementIndexForFrame(frame);
        }

        private int GetMovementIndexForFrame(int frame)
        {
            for (var i = 0; i < Movements.Length; i++)
            {
                var movement = Movements[i];
                if (movement.EndFrame > frame)
                {
                    return i;
                }
            }
            return Movements.Length - 1;
        }

        /// <summary>
        /// Determines whether this animation has movement data.
        /// </summary>
        public bool HasMovementData()
        {
            return Movements.Length > 0;
        }

        /// <summary>
        /// Returns interpolated root motion data at the specified time.
        /// </summary>
        public AnimationMovement.MovementData GetMovementOffsetData(float time)
        {
            if (!HasMovementData())
            {
                return new();
            }

            GetMovementForTime(time, out var movement, out var nextMovement, out var t);
            return AnimationMovement.Lerp(movement, nextMovement, time);
        }

        /// <summary>
        /// Returns interpolated root motion data at the specified frame.
        /// </summary>
        public AnimationMovement.MovementData GetMovementOffsetData(int frame)
        {
            if (!HasMovementData())
            {
                return new();
            }

            var movementIndex = GetMovementIndexForFrame(frame);
            var lastMovement = movementIndex == 0 ? null : Movements[movementIndex - 1];
            var movement = Movements[movementIndex];

            var movementTime = frame / (float)movement.EndFrame;
            return AnimationMovement.Lerp(lastMovement, movement, movementTime);
        }

        /// <summary>
        /// Returns root motion data at the specified animation time for interpolation.
        /// </summary>
        private void GetMovementForTime(float time, out AnimationMovement lastMovement, out AnimationMovement nextMovement, out float t)
        {
            time %= FrameCount / Fps;

            var nextMovementIndex = GetMovementIndexForTime(time);
            var lastMovementIndex = nextMovementIndex - 1;

            nextMovement = Movements[nextMovementIndex];
            if (nextMovementIndex == 0)
            {
                lastMovement = null;

                var movementTime = nextMovement.EndFrame / Fps;
                t = time / movementTime;
                return;
            }

            lastMovement = Movements[lastMovementIndex];

            var startTime = lastMovement.EndFrame / Fps;
            var endTime = nextMovement.EndFrame / Fps;

            var movementDuration = endTime - startTime;
            var elapsedTime = time - startTime;

            t = Math.Min(1f, elapsedTime / movementDuration);
        }

        /// <summary>
        /// Decodes animation data for the specified frame.
        /// </summary>
        public void DecodeFrame(Frame outFrame)
        {
            // Read all frame blocks
            foreach (var frameBlock in FrameBlocks)
            {
                // Only consider blocks that actual contain info for this frame
                if (outFrame.FrameIndex >= frameBlock.StartFrame && outFrame.FrameIndex <= frameBlock.EndFrame)
                {
                    foreach (var segmentIndex in frameBlock.SegmentIndexArray)
                    {
                        var segment = SegmentArray[segmentIndex];
                        // Segment could be null for unknown decoders
                        segment?.Read(outFrame.FrameIndex - frameBlock.StartFrame, outFrame);
                    }
                }
            }
        }

        /// <inheritdoc/>
        /// <remarks>
        /// Returns the animation name.
        /// </remarks>
        public override string ToString()
        {
            return Name;
        }
    }