Represents a sampled animation clip: stores a name, sample rate, native source rate, loop flag and a list of frames where each frame is an array of per-bone local XForm values. Provides constructors for empty clips and for wrapping an existing frame list.
using System;
using System.Collections.Generic;
using HumanoidRetargeter.Maths;
namespace HumanoidRetargeter.Skeleton;
/// <summary>
/// A sampled animation clip: a fixed-rate sequence of frames, each holding one
/// parent-relative local transform per bone (skeleton bone order). Clips are always
/// resampled at ingest — no key data is preserved.
/// </summary>
public sealed class Clip
{
/// <summary>Clip (sequence) name.</summary>
public string Name { get; }
/// <summary>Sample rate in frames per second.</summary>
public float Fps { get; }
/// <summary>
/// Native frame rate of the take in the SOURCE file (FBX GlobalSettings
/// TimeMode/CustomFrameRate, BVH 1/FrameTime). External frame ranges — Unity
/// <c>.fbx.meta</c> <c>clipAnimations</c> definitions — are expressed in THIS rate, so
/// they must be rescaled by <c>Fps / NativeFps</c> to index the resampled
/// <see cref="Frames"/>. Equals <see cref="Fps"/> when the importer records no native rate.
/// </summary>
public float NativeFps { get; }
/// <summary>Whether the clip is authored to loop.</summary>
public bool Looping { get; }
/// <summary>Frames in playback order; each entry is one local transform per bone.</summary>
public List<XForm[]> Frames { get; }
/// <summary>Number of frames currently in the clip.</summary>
public int FrameCount => Frames.Count;
/// <summary>
/// Clip duration in seconds at <see cref="Fps"/>: the time span between the first and the
/// last sample, <c>(FrameCount - 1) / Fps</c> (frames are fence posts, intervals are the
/// spans between them — matching the DMX timeFrame this clip serializes to). Zero for
/// empty and single-frame clips.
/// </summary>
public float Duration => FrameCount <= 1 ? 0f : (FrameCount - 1) / Fps;
/// <summary>Creates an empty clip.</summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="fps"/> is not positive.</exception>
public Clip(string name, float fps, bool looping)
: this(name, fps, looping, new List<XForm[]>())
{
}
/// <summary>Creates a clip wrapping an existing frame list (not copied).</summary>
/// <param name="nativeFps">Source-file native frame rate (<see cref="NativeFps"/>);
/// null = same as <paramref name="fps"/>.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="fps"/> (or a
/// provided <paramref name="nativeFps"/>) is not positive.</exception>
public Clip(string name, float fps, bool looping, List<XForm[]> frames, float? nativeFps = null)
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(frames);
if (!(fps > 0f) || !float.IsFinite(fps))
throw new ArgumentOutOfRangeException(nameof(fps), fps, "Fps must be a positive finite number.");
if (nativeFps is { } native && (!(native > 0f) || !float.IsFinite(native)))
throw new ArgumentOutOfRangeException(nameof(nativeFps), native, "NativeFps must be a positive finite number.");
Name = name;
Fps = fps;
NativeFps = nativeFps ?? fps;
Looping = looping;
Frames = frames;
}
}