Code/HumanoidRetargeter/Core/Solve/PlanarSolver.cs

The 2D planar retargeting solver. Projects a 3D source clip through an orthographic camera and rotates mapped target bones to match the projected source directions. Builds per-clip link chains, handles depth order and foreshortening, and grounds the feet once per clip.

#nullable enable annotations

using System;
using System.Collections.Generic;
using System.Numerics;
using HumanoidRetargeter.Core.Mapping;
using HumanoidRetargeter.Core.Maths;
using HumanoidRetargeter.Core.Skeleton;
using HumanoidRetargeter.Core.Target;
using SkeletonModel = HumanoidRetargeter.Core.Skeleton.Skeleton;

namespace HumanoidRetargeter.Core.Solve;

using Vector3 = System.Numerics.Vector3; // s&box compat: shadow engine's global-namespace Vector3 (see Code/HumanoidRetargeter/Assembly.cs)

/// <summary>
/// Retargets a 3D clip onto a 2D plane of the target armature: an orthographic camera
/// (<see cref="PlanarView"/>) projects the source joints, and each mapped target bone takes
/// the DIRECTION of its projected source segment, so the target keeps its own proportions
/// and only rotates inside the plane.
/// </summary>
/// <remarks>
/// <para>The target's plane comes from its rest geometry. An armature drawn flat (all mapped
/// joints in one coordinate plane) stays in its own plane whatever the camera; any other
/// armature uses the camera turned around its own character frame.</para>
/// <para>A rigid transform has no scale, so foreshortening moves the next joint closer: the
/// length a segment loses to the projection is lost by the bone. Depth order is real depth:
/// shoulders and hips sit at the source's depth from the trunk, scaled to the target, which
/// puts the near limbs in front of the far ones; a flat armature adds this to its own layers.
/// Limbs below them stay parallel to the plane.</para>
/// <para>A segment that rests further than 45 degrees out of the plane has no 2D angle. Arms
/// are laid into the plane (a T-pose seen from the side hangs its arms down first), anything
/// else keeps its real direction (a foot seen from the front). Fingers and unmapped bones ride
/// their parent.</para>
/// </remarks>
public sealed class PlanarSolver : IRetargetSolver
{
    /// <summary>Below this projected / 3D length a segment holds its previous angle.</summary>
    private const float HoldRatio = 0.2f;

    /// <summary>The fastest a bone turns in the plane, in radians per second (1800 degrees,
    /// past real limb speed). A limb crossing the view axis flips its projected angle by up
    /// to 180 degrees; followed at once, that swung a hand 50 cm in one frame of real mocap.</summary>
    private const float MaxTurnSpeed = 10f * MathF.PI;

    /// <summary>Below this planar / full length a segment rests out of the plane (45 degrees):
    /// forced into it, a steep s&amp;box foot seen from the front would point at the floor.</summary>
    private const float RestInPlaneRatio = 0.70710678f;

    /// <summary>The shortest a foreshortened segment gets, as a share of its length.</summary>
    private const float MinForeshorten = 0.1f;

    /// <summary>An armature is flat when its thinnest extent is below this share of its largest.</summary>
    private const float FlatRatio = 0.02f;

    /// <summary>Attachment scale limits, as a factor of the rig's overall scale: a stub over
    /// a tiny source segment must not fling the shoulders off the body.</summary>
    private const float MinAttachScale = 0.5f, MaxAttachScale = 2f;

    /// <summary>Joints closer than this share of the rig's height are the same pivot.</summary>
    private const float SamePivotRatio = 1e-3f;

    private const float Epsilon = 1e-6f;

    private static readonly BoneRole[] Trunk =
    {
        BoneRole.Hips, BoneRole.Spine0, BoneRole.Spine1, BoneRole.Spine2, BoneRole.Spine3,
        BoneRole.Spine4, BoneRole.Neck, BoneRole.Head,
    };

    private static readonly (BoneRole Girdle, BoneRole[] Chain)[] Arms =
    {
        (BoneRole.ClavicleL, new[] { BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL }),
        (BoneRole.ClavicleR, new[] { BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR }),
    };

    private static readonly BoneRole[][] Legs =
    {
        new[] { BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL },
        new[] { BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR },
    };

    /// <inheritdoc />
    /// <exception cref="ArgumentException">Thrown when the source or target lacks the bones
    /// the camera needs (hips, a trunk, and on any non-flat rig the character frame).</exception>
    public Clip Solve(SourceScene source, MappingResult sourceMap, TargetRig target, SolveOptions options)
    {
        ArgumentNullException.ThrowIfNull(source);
        ArgumentNullException.ThrowIfNull(sourceMap);
        ArgumentNullException.ThrowIfNull(target);
        options ??= new SolveOptions();

        if (options.ClipIndex < 0 || options.ClipIndex >= source.Clips.Count)
            throw new ArgumentOutOfRangeException(nameof(options), options.ClipIndex,
                $"ClipIndex out of range; the source has {source.Clips.Count} clip(s).");
        var clip = source.Clips[options.ClipIndex];

        var output = new Clip(options.ClipName ?? clip.Name, clip.Fps, clip.Looping);
        if (clip.Frames.Count == 0)
            return output;
        var plan = new Plan(source, clip, sourceMap, target, options);
        plan.GroundFeet(clip);
        foreach (var frame in clip.Frames)
            output.Frames.Add(plan.SolveFrame(frame));
        return output;
    }

    /// <summary>
    /// The camera that sees a <see cref="SolverKind.Planar"/> clip of <paramref name="target"/>
    /// square-on, in rig space: its up, and the direction from the character toward it.
    /// </summary>
    /// <exception cref="ArgumentException">Thrown when a non-flat rig has no character frame,
    /// or a flat one no hips and trunk.</exception>
    public static (Vector3 Up, Vector3 Toward) CameraAxes(TargetRig target, PlanarView view)
    {
        ArgumentNullException.ThrowIfNull(target);
        var basis = TargetBasis(target, view);
        return (basis.Up, basis.Toward);
    }

    /// <summary>
    /// The view an armature drawn flat was drawn from: <see cref="PlanarView.Side"/> when its
    /// toes reach further across the plane than its hips, else <see cref="PlanarView.Front"/>.
    /// Null for an armature that is not flat, which any view can look at.
    /// </summary>
    public static PlanarView? DrawnView(TargetRig target)
    {
        ArgumentNullException.ThrowIfNull(target);
        if (FlatAxis(target) is not { } axis)
            return null;
        var rest = target.Skeleton.RestWorld;
        float Across(BoneRole from, BoneRole to)
        {
            if (target.BoneForRole(from) is not { } a || target.BoneForRole(to) is not { } b)
                return 0f;
            var span = rest[b].Pos - rest[a].Pos;
            return (span - axis * Vector3.Dot(span, axis)).Length();
        }
        var hipSpan = Across(BoneRole.UpperLegR, BoneRole.UpperLegL);
        var toeSpan = MathF.Max(Across(BoneRole.FootL, BoneRole.ToeL), Across(BoneRole.FootR, BoneRole.ToeR));
        return toeSpan > hipSpan ? PlanarView.Side : PlanarView.Front;
    }

    /// <summary>
    /// The camera a <see cref="SolverKind.Planar"/> solve looks at a source through, in the
    /// source's own space, for showing that source the way it is projected.
    /// <paramref name="firstFrame"/> is the clip's first frame in world space.
    /// </summary>
    /// <exception cref="ArgumentException">Thrown when the mapping lacks the bones a character
    /// frame needs.</exception>
    public static (Vector3 Right, Vector3 Up, Vector3 Toward) SourceCamera(
        SkeletonModel skeleton, MappingResult map, XForm[] firstFrame, PlanarView view)
    {
        ArgumentNullException.ThrowIfNull(skeleton);
        ArgumentNullException.ThrowIfNull(map);
        ArgumentNullException.ThrowIfNull(firstFrame);
        var anatomical = CharacterFrame.Compute(skeleton, map, skeleton.RestWorld).Up;
        var basis = ViewBasis.Of(FacingFrame(skeleton, map, firstFrame, NearestAxis(anatomical)), view).Rolled(view.RollDegrees);
        return (basis.Right, basis.Up, basis.Toward);
    }

    /// <summary>A character frame around <paramref name="up"/>, facing the way the clip starts.</summary>
    private static CharacterFrame FacingFrame(SkeletonModel skeleton, MappingResult map, XForm[] firstFrame, Vector3 up)
    {
        try
        {
            return CharacterFrame.Compute(skeleton, map, firstFrame, up);
        }
        catch (ArgumentException)
        {
            // A first frame lying on its side has no hip line to face along.
            return CharacterFrame.Compute(skeleton, map, skeleton.RestWorld, up);
        }
    }

    private static ViewBasis TargetBasis(TargetRig rig, PlanarView view)
        => FlatAxis(rig) is { } axis ? FlatBasis(rig, axis) : ViewBasis.Of(TargetFrame(rig), view);

    /// <summary>The coordinate axis nearest <paramref name="direction"/>, keeping its sign.
    /// Hips to shoulders is not vertical: a Mixamo rig leans 7 degrees, which tilts the whole
    /// view and sinks the feet as the character travels.</summary>
    private static Vector3 NearestAxis(Vector3 direction)
    {
        var size = Vector3.Abs(direction);
        if (size.X >= size.Y && size.X >= size.Z)
            return new Vector3(MathF.Sign(direction.X), 0f, 0f);
        return size.Y >= size.Z
            ? new Vector3(0f, MathF.Sign(direction.Y), 0f)
            : new Vector3(0f, 0f, MathF.Sign(direction.Z));
    }

    private static CharacterFrame TargetFrame(TargetRig rig)
    {
        try
        {
            var map = rig.ToMappingResult();
            var rest = rig.Skeleton.RestWorld;
            return CharacterFrame.Compute(rig.Skeleton, map, rest,
                NearestAxis(CharacterFrame.Compute(rig.Skeleton, map, rest).Up));
        }
        catch (ArgumentException e)
        {
            throw new ArgumentException($"2D retarget: target character frame not computable ({e.Message}).", e);
        }
    }

    /// <summary>The coordinate axis the mapped joints have no extent along, or null.</summary>
    // shortcut: only sees armatures flat along a coordinate axis, a rotated flat rig reads as 3D; fit a plane if one turns up.
    private static Vector3? FlatAxis(TargetRig rig)
    {
        var min = new Vector3(float.MaxValue);
        var max = new Vector3(float.MinValue);
        var mapped = false;
        for (var i = 0; i < rig.Skeleton.Count; i++)
        {
            if (rig.RoleOf(i) is null)
                continue;
            mapped = true;
            min = Vector3.Min(min, rig.Skeleton.RestWorld[i].Pos);
            max = Vector3.Max(max, rig.Skeleton.RestWorld[i].Pos);
        }
        if (!mapped)
            return null;
        var size = max - min;
        var limit = MathF.Max(size.X, MathF.Max(size.Y, size.Z)) * FlatRatio;
        if (size.X <= limit)
            return Vector3.UnitX;
        if (size.Y <= limit)
            return Vector3.UnitY;
        return size.Z <= limit ? Vector3.UnitZ : null;
    }

    /// <summary>The basis of a flat armature. Screen right is the way its toes point
    /// when drawn in profile, its left when drawn from the front; the source's facing
    /// then lands on the rig's own, whichever way it was drawn.</summary>
    private static ViewBasis FlatBasis(TargetRig rig, Vector3 axis)
    {
        var rest = rig.Skeleton.RestWorld;
        var trunk = new List<int>(Trunk.Length);
        foreach (var role in Trunk)
        {
            if (rig.BoneForRole(role) is { } bone)
                trunk.Add(bone);
        }
        if (trunk.Count < 2 || rig.BoneForRole(BoneRole.Hips) is null)
            throw new ArgumentException("2D retarget: a flat armature needs its hips and a spine, neck or head mapped.");

        var up = rest[trunk[^1]].Pos - rest[trunk[0]].Pos;
        up = NearestAxis(up - axis * Vector3.Dot(up, axis));
        var right = Vector3.Cross(up, axis);

        float Along(BoneRole from, BoneRole to)
            => rig.BoneForRole(from) is { } a && rig.BoneForRole(to) is { } b
                ? Vector3.Dot(rest[b].Pos - rest[a].Pos, right)
                : 0f;

        var toes = Along(BoneRole.FootL, BoneRole.ToeL) + Along(BoneRole.FootR, BoneRole.ToeR);
        var hips = Along(BoneRole.UpperLegR, BoneRole.UpperLegL);
        return (MathF.Abs(hips) > MathF.Abs(toes) ? hips : toes) < 0f
            ? new ViewBasis(-right, up, -axis)
            : new ViewBasis(right, up, axis);
    }

    /// <summary>Right, up and toward-viewer axes of a view, in rig space.</summary>
    private readonly record struct ViewBasis(Vector3 Right, Vector3 Up, Vector3 Toward)
    {
        public static ViewBasis Of(CharacterFrame frame, PlanarView view)
        {
            var yaw = view.YawDegrees * MathF.PI / 180f;
            var pitch = view.PitchDegrees * MathF.PI / 180f;
            var right = MathF.Cos(yaw) * frame.Lateral + MathF.Sin(yaw) * frame.Forward;
            var toward = MathF.Cos(yaw) * frame.Forward - MathF.Sin(yaw) * frame.Lateral;
            return new ViewBasis(right,
                MathF.Cos(pitch) * frame.Up - MathF.Sin(pitch) * toward,
                MathF.Cos(pitch) * toward + MathF.Sin(pitch) * frame.Up);
        }

        /// <summary>The same camera turned in the picture's own plane.</summary>
        public ViewBasis Rolled(float degrees)
        {
            var roll = degrees * MathF.PI / 180f;
            return new ViewBasis(
                MathF.Cos(roll) * Right + MathF.Sin(roll) * Up,
                MathF.Cos(roll) * Up - MathF.Sin(roll) * Right,
                Toward);
        }

        /// <summary>A rig-space vector as (right, up, toward).</summary>
        public Vector3 ToView(Vector3 world)
            => new(Vector3.Dot(world, Right), Vector3.Dot(world, Up), Vector3.Dot(world, Toward));

        public Vector3 ToWorld(Vector3 view) => view.X * Right + view.Y * Up + view.Z * Toward;
    }

    /// <summary>Places a joint from another one by the source's offset between them, depth
    /// included: what a shoulder or a thigh does hanging off the trunk.</summary>
    private sealed record Attachment(int From, int SrcFrom, int Joint, int SrcJoint, float RestDepth)
    {
        public float Scale { get; set; } = 1f;
    }

    /// <summary>One target bone driven from one source bone.</summary>
    private sealed class Link
    {
        public required int Bone { get; init; }
        public required int SrcBone { get; init; }

        /// <summary>The next joint along the chain; -1 on a leaf, which carries its parent's line on.</summary>
        public int Next { get; init; } = -1;
        public int SrcNext { get; init; } = -1;

        /// <summary>The rest parent line in the source bone's frame (leaves only).</summary>
        public Vector3 SrcLeafAxis { get; init; }

        public float Length { get; init; }
        public float SrcLength { get; init; }
        public required Vector3 RestDirection { get; init; }
        public required Quaternion RestRotation { get; init; }

        /// <summary>The rest rotation laid into the plane, and the angle it then has there.</summary>
        public Quaternion LaidRotation { get; init; }
        public float RestAngle { get; init; }

        /// <summary>Rests along the view axis: keeps its real direction, not a 2D angle.</summary>
        public bool KeepsDepth { get; init; }

        /// <summary>Where this bone goes, when not at the tip of the link before it.</summary>
        public Attachment? Root { get; set; }

        /// <summary>A clavicle: aims at the joint this places instead of taking an angle.</summary>
        public Attachment? Tip { get; set; }

        public float? HeldAngle { get; set; }
    }

    /// <summary>Everything built once per solve. One instance per clip: held angles carry
    /// from frame to frame, so frames must be solved in order.</summary>
    private sealed class Plan
    {
        private readonly SkeletonModel _src;
        private readonly SkeletonModel _tgt;
        private readonly MappingResult _srcMap;
        private readonly TargetRig _rig;
        private readonly ViewBasis _srcView;
        private readonly ViewBasis _tgtView;
        private readonly bool _tgtIsFlat;
        private readonly bool _foreshorten;
        private readonly bool _inPlace;
        private readonly List<Link> _links = new();
        private readonly bool[] _isDriven;
        private readonly int _srcHips;
        private readonly int _tgtHips;
        private readonly Vector3 _srcUp;
        private readonly Vector3 _firstHips;
        private readonly Vector3 _hipsReference;
        private readonly float _scaleH;
        private readonly float _scaleV;
        private readonly float _maxTurn;
        private float _groundLift;
        private readonly Vector3[] _position;
        private readonly Quaternion[] _rotation;
        private readonly XForm[] _world;

        public Plan(SourceScene source, Clip clip, MappingResult srcMap, TargetRig rig, SolveOptions options)
        {
            _src = source.Skeleton;
            _tgt = rig.Skeleton;
            _srcMap = srcMap;
            _rig = rig;
            _foreshorten = options.Foreshorten;
            _inPlace = options.PlanarInPlace;
            _maxTurn = MaxTurnSpeed / clip.Fps;
            _isDriven = new bool[_tgt.Count];
            _position = new Vector3[_tgt.Count];
            _rotation = new Quaternion[_tgt.Count];
            _world = new XForm[_tgt.Count];

            var trunk = Joints(Trunk);
            if (trunk.Count < 2 || trunk[0].Tgt != rig.BoneForRole(BoneRole.Hips))
                throw new ArgumentException(
                    "2D retarget needs the hips and a spine, neck or head mapped on both the source and the target.");
            (_srcHips, _tgtHips) = trunk[0];

            var fileUp = source.UpAxis switch
            {
                0 => new Vector3(source.UpAxisSign, 0f, 0f),
                2 => new Vector3(0f, 0f, source.UpAxisSign),
                _ => new Vector3(0f, source.UpAxisSign, 0f),
            };
            var firstWorld = new Pose(clip.Frames[0]).ToWorld(_src);
            _srcView = ViewBasis.Of(SourceFrame(fileUp, firstWorld), options.View).Rolled(options.View.RollDegrees);
            _srcUp = fileUp;

            _tgtIsFlat = FlatAxis(rig) is not null;
            _tgtView = TargetBasis(rig, options.View);

            AddChain(trunk, laysIntoPlane: false, root: null);
            foreach (var (girdle, chain) in Arms)
                AddLimb(girdle, chain, laysIntoPlane: true);
            foreach (var chain in Legs)
                AddLimb(null, chain, laysIntoPlane: false);

            var scale = RigScale(_ => true);
            foreach (var link in _links)
            {
                ScaleAttachment(link.Root, scale);
                ScaleAttachment(link.Tip, scale);
            }
            // The hips move with the legs that carry them, or a dip lifts or sinks the feet.
            var legScale = RigScale(link => _rig.RoleOf(link.Bone) is BoneRole.UpperLegL or BoneRole.UpperLegR
                or BoneRole.LowerLegL or BoneRole.LowerLegR);
            _scaleH = options.HipScaleHorizontal ?? legScale;
            _scaleV = options.HipScaleVertical ?? legScale;

            _firstHips = firstWorld[_srcHips].Pos;
            _hipsReference = HipsReference(source, clip);
        }

        /// <summary>The joints of <paramref name="roles"/> present on both rigs, in order. A
        /// target joint on the same pivot as the previous one rides it: no direction to take.</summary>
        private List<(int Src, int Tgt)> Joints(BoneRole[] roles)
        {
            var samePivot = RigHeight() * SamePivotRatio;
            var joints = new List<(int Src, int Tgt)>(roles.Length);
            foreach (var role in roles)
            {
                if (_rig.BoneForRole(role) is not { } tgt || !_srcMap.RoleToBone.TryGetValue(role, out var src))
                    continue;
                if (joints.Count > 0 && Vector3.Distance(
                        _tgt.RestWorld[tgt].Pos, _tgt.RestWorld[joints[^1].Tgt].Pos) < samePivot)
                    continue;
                joints.Add((src, tgt));
            }
            return joints;
        }

        private float RigHeight()
        {
            var min = new Vector3(float.MaxValue);
            var max = new Vector3(float.MinValue);
            foreach (var bone in _tgt.RestWorld)
            {
                min = Vector3.Min(min, bone.Pos);
                max = Vector3.Max(max, bone.Pos);
            }
            var size = max - min;
            return MathF.Max(size.X, MathF.Max(size.Y, size.Z));
        }

        /// <summary>The source's character frame, facing the way the clip starts: mocap
        /// rarely faces the way its T-pose does, and the view is relative to the performer.
        /// Up is the axis nearest the anatomical one, or the file's vertical when the rest leans too far.</summary>
        private CharacterFrame SourceFrame(Vector3 fileUp, XForm[] firstWorld)
        {
            try
            {
                var anatomical = CharacterFrame.Compute(_src, _srcMap, _src.RestWorld).Up;
                var up = Vector3.Dot(anatomical, fileUp) < 0.70710678f ? fileUp : NearestAxis(anatomical);
                return FacingFrame(_src, _srcMap, firstWorld, up);
            }
            catch (ArgumentException e)
            {
                throw new ArgumentException($"2D retarget: source character frame not computable ({e.Message}).", e);
            }
        }

        private void AddLimb(BoneRole? girdle, BoneRole[] roles, bool laysIntoPlane)
        {
            var joints = Joints(roles);
            if (joints.Count < 2)
                return;

            var first = joints[0];
            var clavicle = girdle is { } role ? Joints(new[] { role }) : null;
            if (clavicle is { Count: 1 })
            {
                var (srcBone, bone) = clavicle[0];
                var rest = _tgt.RestWorld;
                var direction = rest[first.Tgt].Pos - rest[bone].Pos;
                var link = new Link
                {
                    Bone = bone, SrcBone = srcBone, Next = first.Tgt, SrcNext = first.Src,
                    RestDirection = direction, RestRotation = rest[bone].Rot,
                    Root = AttachToTrunk(bone, srcBone),
                    Tip = NewAttachment(bone, srcBone, first.Tgt, first.Src),
                };
                _links.Add(link);
                _isDriven[bone] = true;
                AddChain(joints, laysIntoPlane, root: null);
                return;
            }
            AddChain(joints, laysIntoPlane, AttachToTrunk(first.Tgt, first.Src));
        }

        private Attachment AttachToTrunk(int bone, int srcBone)
        {
            var anchor = _links[0];
            for (var p = _tgt[bone].ParentIndex; p >= 0; p = _tgt[p].ParentIndex)
            {
                if (_links.Find(l => l.Bone == p) is { } link)
                {
                    anchor = link;
                    break;
                }
            }
            return NewAttachment(anchor.Bone, anchor.SrcBone, bone, srcBone);
        }

        private Attachment NewAttachment(int from, int srcFrom, int joint, int srcJoint)
        {
            var restOffset = _tgt.RestWorld[joint].Pos - _tgt.RestWorld[from].Pos;
            return new Attachment(from, srcFrom, joint, srcJoint,
                _tgtIsFlat ? Vector3.Dot(restOffset, _tgtView.Toward) : 0f);
        }

        private void AddChain(List<(int Src, int Tgt)> joints, bool laysIntoPlane, Attachment? root)
        {
            var srcRest = _src.RestWorld;
            var rest = _tgt.RestWorld;
            for (var k = 0; k < joints.Count; k++)
            {
                var (srcBone, bone) = joints[k];
                var isLeaf = k == joints.Count - 1;
                var next = isLeaf ? (Src: -1, Tgt: -1) : joints[k + 1];
                var line = isLeaf
                    ? rest[bone].Pos - rest[joints[k - 1].Tgt].Pos
                    : rest[next.Tgt].Pos - rest[bone].Pos;
                var view = _tgtView.ToView(line);
                var planarLength = MathF.Sqrt(view.X * view.X + view.Y * view.Y);

                var isInPlane = planarLength >= RestInPlaneRatio * line.Length();
                var laid = isInPlane ? _tgtView.ToWorld(new Vector3(view.X, view.Y, 0f)) : -_tgtView.Up;
                var keepsDepth = !isInPlane && !laysIntoPlane;
                _links.Add(new Link
                {
                    Bone = bone, SrcBone = srcBone, Next = next.Tgt, SrcNext = next.Src,
                    SrcLeafAxis = isLeaf
                        ? Vector3.Transform(srcRest[srcBone].Pos - srcRest[joints[k - 1].Src].Pos,
                            Quaternion.Conjugate(srcRest[srcBone].Rot))
                        : Vector3.Zero,
                    Length = isLeaf ? 0f : line.Length(),
                    SrcLength = isLeaf ? 0f : Vector3.Distance(srcRest[next.Src].Pos, srcRest[srcBone].Pos),
                    RestDirection = line,
                    RestRotation = rest[bone].Rot,
                    LaidRotation = keepsDepth
                        ? rest[bone].Rot
                        : MathQ.Normalize(MathQ.FromTo(line, laid) * rest[bone].Rot),
                    RestAngle = isInPlane ? MathF.Atan2(view.Y, view.X) : -MathF.PI / 2f,
                    KeepsDepth = keepsDepth,
                    Root = k == 0 ? root : null,
                });
                _isDriven[bone] = true;
            }
        }

        /// <summary>How much bigger the target is than the source, over the chosen segments.</summary>
        private float RigScale(Func<Link, bool> counts)
        {
            float length = 0f, srcLength = 0f;
            foreach (var link in _links)
            {
                if (!counts(link))
                    continue;
                length += link.Length;
                srcLength += link.SrcLength;
            }
            var scale = srcLength > Epsilon ? length / srcLength : 1f;
            return float.IsFinite(scale) && scale > 0f ? scale : 1f;
        }

        private void ScaleAttachment(Attachment? attachment, float rigScale)
        {
            if (attachment is null)
                return;
            var length = Vector3.Distance(_tgt.RestWorld[attachment.Joint].Pos, _tgt.RestWorld[attachment.From].Pos);
            var srcLength = Vector3.Distance(_src.RestWorld[attachment.SrcJoint].Pos, _src.RestWorld[attachment.SrcFrom].Pos);
            attachment.Scale = srcLength > Epsilon
                ? Math.Clamp(length / srcLength, MinAttachScale * rigScale, MaxAttachScale * rigScale)
                : rigScale;
        }

        /// <summary>The source hips position the target's rest hips stand for: over where
        /// the clip starts, and for a rest with no authored placement (BVH), at the rest's
        /// height above the clip's own ground.</summary>
        private Vector3 HipsReference(SourceScene source, Clip clip)
        {
            var rest = _src.RestWorld[_srcHips].Pos;
            var travel = _firstHips - rest;
            var reference = rest + travel - _srcUp * Vector3.Dot(travel, _srcUp);
            if (source.RestPlacementAuthored)
                return reference;

            var motionGround = float.MaxValue;
            foreach (var frame in clip.Frames)
            {
                foreach (var bone in new Pose(frame).ToWorld(_src))
                    motionGround = MathF.Min(motionGround, Vector3.Dot(bone.Pos, _srcUp));
            }
            var restGround = float.MaxValue;
            foreach (var bone in _src.RestWorld)
                restGround = MathF.Min(restGround, Vector3.Dot(bone.Pos, _srcUp));
            return reference + _srcUp * (motionGround - restGround);
        }

        /// <summary>One height correction for the whole clip, so the lowest a foot gets is
        /// where it rests. Legs shorter than the source's cannot reach as far down as the
        /// hips drop; correcting frame by frame would flatten a jump.</summary>
        public void GroundFeet(Clip clip)
        {
            var feet = new List<int>();
            foreach (var role in new[] { BoneRole.FootL, BoneRole.FootR, BoneRole.ToeL, BoneRole.ToeR })
            {
                if (_rig.BoneForRole(role) is { } bone && _isDriven[bone])
                    feet.Add(bone);
            }
            if (feet.Count == 0)
                return;

            var rest = float.MaxValue;
            foreach (var foot in feet)
                rest = MathF.Min(rest, Vector3.Dot(_tgt.RestWorld[foot].Pos, _tgtView.Up));

            var lowest = float.MaxValue;
            foreach (var frame in clip.Frames)
            {
                SolveFrame(frame);
                foreach (var foot in feet)
                    lowest = MathF.Min(lowest, Vector3.Dot(_position[foot], _tgtView.Up));
            }
            foreach (var link in _links)
                link.HeldAngle = null;
            _groundLift = rest - lowest;
        }

        public XForm[] SolveFrame(XForm[] sourceLocals)
        {
            var source = new Pose(sourceLocals).ToWorld(_src);
            _position[_tgtHips] = HipsPosition(source);
            foreach (var link in _links)
                SolveLink(link, source);

            var locals = new XForm[_tgt.Count];
            for (var i = 0; i < locals.Length; i++)
            {
                var parent = _tgt[i].ParentIndex;
                var parentWorld = parent < 0 ? XForm.Identity : _world[parent];
                if (_isDriven[i])
                {
                    _world[i] = new XForm(_position[i], _rotation[i]);
                    locals[i] = XForm.ToLocal(parentWorld, _world[i]);
                }
                else
                {
                    locals[i] = _tgt[i].RestLocal;
                    _world[i] = XForm.Compose(parentWorld, locals[i]);
                }
            }
            return locals;
        }

        private Vector3 HipsPosition(XForm[] source)
        {
            var hips = source[_srcHips].Pos;
            if (_inPlace)
                hips = _firstHips + _srcUp * Vector3.Dot(hips - _firstHips, _srcUp);
            var travel = _srcView.ToView(hips - _hipsReference);
            return _tgt.RestWorld[_tgtHips].Pos + _tgtView.ToWorld(
                new Vector3(travel.X * _scaleH, travel.Y * _scaleV + _groundLift, 0f));
        }

        private void SolveLink(Link link, XForm[] source)
        {
            if (link.Root is { } root)
                _position[link.Bone] = Attach(root, source);
            var position = _position[link.Bone];

            if (link.Tip is { } tip)
            {
                _position[link.Next] = Attach(tip, source);
                var aim = _position[link.Next] - position;
                if (_tgtIsFlat)
                    aim -= _tgtView.Toward * Vector3.Dot(aim, _tgtView.Toward);
                _rotation[link.Bone] = MathQ.Normalize(MathQ.FromTo(link.RestDirection, aim) * link.RestRotation);
                return;
            }

            var segment = _srcView.ToView(SourceLine(link, source));
            if (link.KeepsDepth)
            {
                var direction = _tgtView.ToWorld(segment);
                _rotation[link.Bone] = MathQ.Normalize(MathQ.FromTo(link.RestDirection, direction) * link.RestRotation);
                if (link.Next >= 0 && direction.LengthSquared() > Epsilon)
                    _position[link.Next] = position + Vector3.Normalize(direction) * link.Length;
                return;
            }

            var planarLength = MathF.Sqrt(segment.X * segment.X + segment.Y * segment.Y);
            var length = segment.Length();
            var ratio = length > Epsilon ? MathF.Min(1f, planarLength / length) : 1f;
            var angle = planarLength > Epsilon
                ? MathF.Atan2(segment.Y, segment.X)
                : link.HeldAngle ?? link.RestAngle;
            if (link.HeldAngle is { } held)
            {
                var step = ratio < HoldRatio ? 0f : MathF.IEEERemainder(angle - held, 2f * MathF.PI);
                angle = held + Math.Clamp(step, -_maxTurn, _maxTurn);
            }
            link.HeldAngle = angle;

            var turn = Quaternion.CreateFromAxisAngle(_tgtView.Toward, angle - link.RestAngle);
            _rotation[link.Bone] = MathQ.Normalize(turn * link.LaidRotation);
            if (link.Next >= 0)
            {
                var foreshortened = _foreshorten ? MathF.Max(MinForeshorten, ratio) : 1f;
                var along = _tgtView.ToWorld(new Vector3(MathF.Cos(angle), MathF.Sin(angle), 0f));
                _position[link.Next] = position + along * link.Length * foreshortened;
            }
        }

        private Vector3 SourceLine(Link link, XForm[] source)
            => link.Next >= 0
                ? source[link.SrcNext].Pos - source[link.SrcBone].Pos
                : Vector3.Transform(link.SrcLeafAxis, source[link.SrcBone].Rot);

        private Vector3 Attach(Attachment attachment, XForm[] source)
        {
            var offset = _srcView.ToView(source[attachment.SrcJoint].Pos - source[attachment.SrcFrom].Pos)
                * attachment.Scale;
            offset.Z += attachment.RestDepth;
            return _position[attachment.From] + _tgtView.ToWorld(offset);
        }
    }
}