Code/HumanoidRetargeter/Skeleton/SourceScene.cs

Data container representing an imported source animation file: stores a Skeleton (rest pose in centimeters), resampled Clips, unit scale applied, FBX/global axis metadata, optional authored mapping and an alternate mid-pose bind skeleton, plus import notes and a flag about rest placement.

File Access
#nullable enable annotations

using System;
using System.Collections.Generic;
using HumanoidRetargeter.Mapping;

namespace HumanoidRetargeter.Skeleton;

/// <summary>
/// The result of ingesting a source animation file: a skeleton with its rest pose plus the
/// file's clips resampled on a fixed fps grid.
/// </summary>
/// <remarks>
/// Unit policy: all translations are converted to <b>centimeters</b> at import time
/// (<see cref="UnitScaleCm"/> records the applied source-unit→cm factor for diagnostics).
/// Axis policy: the file's native axes are <b>preserved</b> — no axis conversion is performed.
/// The original FBX <c>GlobalSettings</c> axes are recorded so later pipeline stages can
/// interpret directions (axis indices: 0 = X, 1 = Y, 2 = Z).
/// </remarks>
public sealed class SourceScene
{
    /// <summary>Source skeleton with rest pose, in centimeters, native axes.</summary>
    public Skeleton Skeleton { get; }

    /// <summary>Clips resampled at a fixed fps; locals indexed in skeleton bone order.</summary>
    public IReadOnlyList<Clip> Clips { get; }

    /// <summary>Source-unit → centimeter factor that was applied to all translations at import.</summary>
    public float UnitScaleCm { get; }

    /// <summary>Up axis index from GlobalSettings (0 = X, 1 = Y, 2 = Z; FBX default 1).</summary>
    public int UpAxis { get; }

    /// <summary>Sign of the up axis (+1 or -1).</summary>
    public int UpAxisSign { get; }

    /// <summary>Front axis index from GlobalSettings (FBX default 2 = Z).</summary>
    public int FrontAxis { get; }

    /// <summary>Sign of the front axis (+1 or -1).</summary>
    public int FrontAxisSign { get; }

    /// <summary>Coordinate (right) axis index from GlobalSettings (FBX default 0 = X).</summary>
    public int CoordAxis { get; }

    /// <summary>Sign of the coordinate axis (+1 or -1).</summary>
    public int CoordAxisSign { get; }

    /// <summary>OriginalUpAxis from GlobalSettings (-1 when the exporter did not record one).</summary>
    public int OriginalUpAxis { get; }

    /// <summary>
    /// Human-readable import diagnostics (e.g. cross-stack static-translation disagreements).
    /// Empty when the import was unambiguous.
    /// </summary>
    public IReadOnlyList<string> Notes { get; }

    /// <summary>
    /// True (the default) when the format anchors the rest skeleton at an authored world
    /// placement shared with its motion data — FBX/glTF bind poses live in the same scene
    /// space as their animation curves, so absolute root translations are meaningful as-is.
    /// The BVH importer sets this FALSE: a BVH rest skeleton is OFFSETs only (root at the
    /// file origin, ground implicitly at the rest feet) while its MOTION root positions are
    /// absolute capture-volume coordinates (ground at the capture floor, subject anywhere on
    /// the stage) — the two spaces share no origin or ground level, so the solver normalizes
    /// each clip's placement against the rest skeleton before measuring pelvis travel
    /// (see <c>GeometricSolver</c> remarks).
    /// </summary>
    public bool RestPlacementAuthored { get; init; } = true;

    /// <summary>
    /// A role mapping AUTHORED inside the source file itself, when the format carries one —
    /// a VRM's <c>humanoid.humanBones</c> block (set by the glTF importer,
    /// <see cref="MappingSource.Authored"/>, confidence 1.0). Null for formats/files without
    /// authored role data. <see cref="Retargeter.ResolveMapping"/> consults this before user
    /// presets and shipped-profile detection — it is ground truth from the file.
    /// </summary>
    public MappingResult? AuthoredMapping { get; set; }

    /// <summary>
    /// Alternate skeleton whose rest is the file's Pose/BindPose, offered by the FBX
    /// importer when the node transforms are GROSSLY posed away from it (a mid-pose
    /// export — the primary <see cref="Skeleton"/> then rests in an animation snapshot:
    /// a foot at hip height, IK'd hands). Same bones in the same order, so
    /// <see cref="Clips"/> and mappings apply to either. The retargeter adopts it after
    /// mapping when it makes the character stand straighter along the file's up axis
    /// (solving on a posed rest was measured 48° mean off ground truth on a real rig);
    /// null for consistent exports and non-FBX sources.
    /// </summary>
    public Skeleton? MidPoseBindSkeleton { get; set; }

    /// <summary>Creates a source scene container.</summary>
    public SourceScene(
        Skeleton skeleton,
        IReadOnlyList<Clip> clips,
        float unitScaleCm,
        int upAxis = 1, int upAxisSign = 1,
        int frontAxis = 2, int frontAxisSign = 1,
        int coordAxis = 0, int coordAxisSign = 1,
        int originalUpAxis = -1,
        IReadOnlyList<string>? notes = null)
    {
        Skeleton = skeleton ?? throw new ArgumentNullException(nameof(skeleton));
        Clips = clips ?? throw new ArgumentNullException(nameof(clips));
        UnitScaleCm = unitScaleCm;
        UpAxis = upAxis;
        UpAxisSign = upAxisSign;
        FrontAxis = frontAxis;
        FrontAxisSign = frontAxisSign;
        CoordAxis = coordAxis;
        CoordAxisSign = coordAxisSign;
        OriginalUpAxis = originalUpAxis;
        Notes = notes ?? Array.Empty<string>();
    }
}