HumanoidMocap/Core/Motion/CaptureDespike.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidMocapX.Core.Cleanup;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Target;

namespace HumanoidMocapX.Core.Motion;
using Vector3 = System.Numerics.Vector3;

/// <summary>Removes snaps: frames where a joint leaves the smooth path through its neighbours. On an ordinary clip
/// FlowHMR stays within 1.5 cm of that path in 94% of frames; on the breakin clip's upside-down breakdance only 42%
/// do, with jumps of up to 39 cm. Each joint's rotation (and the root's position) is compared with a cubic fitted over
/// <see cref="Window"/> frames around it (Savitzky-Golay); a frame further off than <see cref="StartDegrees"/> (or
/// <see cref="StartCm"/>) is pulled onto the fit, entirely from <see cref="FullDegrees"/> (<see cref="FullCm"/>); the
/// first and last three frames are left as captured.
/// Steady fast motion, a spin included, follows a cubic and is left alone. It runs on the capture, before contacts are
/// applied, and again on the retargeted animation, so no cleanup step leaves a snap behind either.</summary>
public static class CaptureDespike
{
    public const int Window = 7, Passes = 2;
    public const float StartDegrees = 3, FullDegrees = 8, StartCm = 1, FullCm = 3;

    /// <summary>Despikes every bone's rotation and the root's position in place. Returns the number of corrected
    /// samples; 0 for fewer than <see cref="Window"/> frames.</summary>
    public static int Apply( MotionDocument document )
    {
        var frames = document.Frames.Count;
        if ( frames < Window ) return 0;
        var corrected = 0; var changed = new bool[frames];
        var rotations = new Quaternion[frames]; var positions = new Vector3[frames];
        for ( var pass = 0; pass < Passes; pass++ )
        {
            for ( var bone = 0; bone < document.Bones.Count; bone++ )
            {
                for ( var f = 0; f < frames; f++ ) rotations[f] = MotionDocument.Q( document.Frames[f].Rotations[bone] );
                corrected += Rotations( rotations, changed );
                for ( var f = 0; f < frames; f++ ) if ( changed[f] ) document.Frames[f].Rotations[bone] = new[] { rotations[f].X, rotations[f].Y, rotations[f].Z, rotations[f].W };
            }
            for ( var f = 0; f < frames; f++ ) positions[f] = MotionDocument.V( document.Frames[f].Positions[0] );
            corrected += Positions( positions, changed, 100 );
            for ( var f = 0; f < frames; f++ ) if ( changed[f] ) document.Frames[f].Positions[0] = new[] { positions[f].X, positions[f].Y, positions[f].Z };
        }
        return corrected;
    }

    /// <summary>A hand or foot moving less than this, in centimetres a frame, is held where it was by
    /// <see cref="Apply(List{XForm[]}, TargetRig, float)"/>; the hold eases in and out over <see cref="EaseFrames"/>.</summary>
    public const float StillCm = .5f;
    public const int EaseFrames = 3;

    /// <summary>Despikes a retargeted animation, then puts every hand and foot that was still before (planted, held or
    /// locked by the cleanup) back where it was: smoothing a spin's arm after its hand was planted slid the hand up to
    /// 9 cm a frame. Returns the number of corrected samples; 0 for fewer than <see cref="Window"/> frames.</summary>
    public static int Apply( List<XForm[]> frames, TargetRig target, float unitsPerCm )
    {
        if ( frames.Count < Window ) return 0;
        var rig = target.Skeleton;
        var chains = new[] { (BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL), (BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR),
                (BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL), (BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR) }
            .Select( c => target.BoneForRole( c.Item1 ) is int a && target.BoneForRole( c.Item2 ) is int b && target.BoneForRole( c.Item3 ) is int e ? (A: a, B: b, C: e) : ((int A, int B, int C)?)null )
            .Where( c => c is not null ).Select( c => c!.Value ).ToArray();
        // Where each hand and foot is held (still, as the cleanup planted or locked it), and how firmly: 1 while held,
        // easing out over EaseFrames on either side so a hold ends without a step.
        var world = new XForm[rig.Count];
        var held = chains.Select( _ => new XForm[frames.Count] ).ToArray();
        for ( var f = 0; f < frames.Count; f++ )
        {
            FkUtil.ToWorld( frames[f], rig, world );
            for ( var c = 0; c < chains.Length; c++ ) held[c][f] = world[chains[c].C];
        }
        var still = StillCm * unitsPerCm;
        var holding = held.Select( track => Holding( track.Select( x => x.Pos ).ToArray(), still ) ).ToArray();
        // Toes too: through toe-off the toe stays planted while the ankle lifts, so it is held on its own.
        var toes = new[] { BoneRole.ToeL, BoneRole.ToeR }.Select( target.BoneForRole ).Where( b => b is int t && rig[t].ParentIndex >= 0 ).Select( b => b!.Value ).ToArray();
        var toesHeld = toes.Select( _ => new Vector3[frames.Count] ).ToArray();
        for ( var f = 0; f < frames.Count; f++ )
        {
            FkUtil.ToWorld( frames[f], rig, world );
            for ( var t = 0; t < toes.Length; t++ ) toesHeld[t][f] = world[toes[t]].Pos;
        }
        var toesHolding = toesHeld.Select( track => Holding( track, still ) ).ToArray();
        // Three rounds: each world-space reach can leave a joint above it slightly off its path for the next round to take out.
        var corrected = 0;
        for ( var round = 0; round < 3; round++ ) corrected += Settle( frames, target, unitsPerCm, chains, held, holding, toes, toesHeld, toesHolding );
        return corrected;
    }

    /// <summary>How firmly a joint following <paramref name="path"/> is held per frame: 1 where it moves less than
    /// <paramref name="still"/> to either neighbour, easing out over EaseFrames on either side so a hold ends without a step.</summary>
    static float[] Holding( Vector3[] path, float still )
    {
        var weight = new float[path.Length];
        for ( var f = 1; f + 1 < path.Length; f++ )
        {
            if ( Vector3.Distance( path[f], path[f - 1] ) >= still || Vector3.Distance( path[f + 1], path[f] ) >= still ) continue;
            for ( var j = Math.Max( 0, f - EaseFrames ); j <= Math.Min( path.Length - 1, f + EaseFrames ); j++ )
            {
                var w = 1 - Math.Abs( j - f ) / (float)(EaseFrames + 1); w = w * w * (3 - 2 * w);
                weight[j] = Math.Max( weight[j], w );
            }
        }
        return weight;
    }

    static int Settle( List<XForm[]> frames, TargetRig target, float unitsPerCm, (int A, int B, int C)[] chains, XForm[][] before, float[][] holding,
        int[] toes, Vector3[][] toesHeld, float[][] toesHolding )
    {
        var rig = target.Skeleton; var world = new XForm[rig.Count];
        var corrected = Apply( frames, rig, unitsPerCm );
        if ( corrected == 0 ) return 0;
        // Each held hand or foot is put back where it was held.
        for ( var c = 0; c < chains.Length; c++ )
        {
            var (a, b, e) = chains[c];
            for ( var f = 0; f < frames.Count; f++ )
            {
                var w = holding[c][f]; if ( w <= 0 ) continue;
                FkUtil.ToWorld( frames[f], rig, world );
                var goal = Vector3.Lerp( world[e].Pos, before[c][f].Pos, w );
                var attitude = Quaternion.Normalize( Quaternion.Slerp( world[e].Rot, before[c][f].Rot, w ) );
                var bend = Vector3.Cross( world[b].Pos - world[a].Pos, world[e].Pos - world[b].Pos );
                if ( bend.LengthSquared() < 1e-8f ) bend = Vector3.Transform( Vector3.UnitX, world[a].Rot );
                var ik = TwoBoneIk.Solve( world[a].Pos, world[b].Pos, world[e].Pos, goal, soften: 0, stableBendAxis: bend );
                EffectorIk.ApplyWorldDeltas( frames[f], rig, a, b, e, ik.UpperWorldDelta, ik.LowerWorldDelta, world );
                FkUtil.ToWorld( frames[f], rig, world );
                var parent = rig[e].ParentIndex;
                frames[f][e].Rot = Quaternion.Normalize( (parent < 0 ? Quaternion.Identity : Quaternion.Inverse( world[parent].Rot )) * attitude );
            }
        }
        // Joint turns that each follow a smooth path can still add up to a flick at the hand or foot (a dancer's foot
        // doubled back 5 cm at the top of a swing): each limb's end is despiked too, its limb reaching for the fitted place
        // with the end's attitude kept.
        var path = new Vector3[frames.Count]; var changed = new bool[frames.Count];
        for ( var pass = 0; pass < Passes; pass++ )
            for ( var c = 0; c < chains.Length; c++ )
            {
                var (a, b, e) = chains[c];
                for ( var f = 0; f < frames.Count; f++ ) { FkUtil.ToWorld( frames[f], rig, world ); path[f] = world[e].Pos; }
                corrected += Positions( path, changed, 1 / unitsPerCm );
                for ( var f = 0; f < frames.Count; f++ )
                {
                    // A hand or foot that is held stays where its hold put it.
                    if ( !changed[f] || holding[c][f] >= 1 ) continue;
                    FkUtil.ToWorld( frames[f], rig, world );
                    var attitude = world[e].Rot;
                    var bend = Vector3.Cross( world[b].Pos - world[a].Pos, world[e].Pos - world[b].Pos );
                    if ( bend.LengthSquared() < 1e-8f ) bend = Vector3.Transform( Vector3.UnitX, world[a].Rot );
                    var ik = TwoBoneIk.Solve( world[a].Pos, world[b].Pos, world[e].Pos, path[f], soften: 0, stableBendAxis: bend );
                    EffectorIk.ApplyWorldDeltas( frames[f], rig, a, b, e, ik.UpperWorldDelta, ik.LowerWorldDelta, world );
                    FkUtil.ToWorld( frames[f], rig, world );
                    var parent = rig[e].ParentIndex;
                    frames[f][e].Rot = Quaternion.Normalize( (parent < 0 ? Quaternion.Identity : Quaternion.Inverse( world[parent].Rot )) * attitude );
                }
                // How far the elbow or knee bends: near full stretch a few millimetres of reach swing it tens of degrees, and a
                // held Citizen arm snapped straight and back as it was let go. A limb whose end no hold touches reaches to the
                // distance the smoothed bend gives.
                var bends = new float[frames.Count];
                for ( var f = 0; f < frames.Count; f++ ) { FkUtil.ToWorld( frames[f], rig, world ); bends[f] = Bend( world[a].Pos, world[b].Pos, world[e].Pos ); }
                corrected += Angles( bends, changed );
                for ( var f = 0; f < frames.Count; f++ )
                {
                    if ( !changed[f] || holding[c][f] > 0 ) continue;
                    FkUtil.ToWorld( frames[f], rig, world );
                    float upper = Vector3.Distance( world[a].Pos, world[b].Pos ), lower = Vector3.Distance( world[b].Pos, world[e].Pos );
                    var line = world[e].Pos - world[a].Pos; if ( line.LengthSquared() < 1e-10f ) continue;
                    var reach = MathF.Sqrt( MathF.Max( 0, upper * upper + lower * lower - 2 * upper * lower * MathF.Cos( bends[f] * MathF.PI / 180 ) ) );
                    var attitude = world[e].Rot;
                    var bend = Vector3.Cross( world[b].Pos - world[a].Pos, world[e].Pos - world[b].Pos );
                    if ( bend.LengthSquared() < 1e-8f ) bend = Vector3.Transform( Vector3.UnitX, world[a].Rot );
                    var ik = TwoBoneIk.Solve( world[a].Pos, world[b].Pos, world[e].Pos, world[a].Pos + Vector3.Normalize( line ) * reach, soften: 0, stableBendAxis: bend );
                    EffectorIk.ApplyWorldDeltas( frames[f], rig, a, b, e, ik.UpperWorldDelta, ik.LowerWorldDelta, world );
                    FkUtil.ToWorld( frames[f], rig, world );
                    frames[f][e].Rot = Quaternion.Normalize( Quaternion.Inverse( world[rig[e].ParentIndex].Rot ) * attitude );
                }
                // The elbow or knee, by turning the limb about the line from its root to its end, which stays put.
                for ( var f = 0; f < frames.Count; f++ ) { FkUtil.ToWorld( frames[f], rig, world ); path[f] = world[b].Pos; }
                corrected += Positions( path, changed, 1 / unitsPerCm );
                for ( var f = 0; f < frames.Count; f++ )
                {
                    if ( !changed[f] ) continue;
                    FkUtil.ToWorld( frames[f], rig, world );
                    var line = world[e].Pos - world[a].Pos; if ( line.LengthSquared() < 1e-8f ) continue;
                    line = Vector3.Normalize( line );
                    var from = world[b].Pos - world[a].Pos; from -= line * Vector3.Dot( from, line );
                    var to = path[f] - world[a].Pos; to -= line * Vector3.Dot( to, line );
                    if ( from.LengthSquared() < 1e-8f || to.LengthSquared() < 1e-8f ) continue;
                    var turn = MathF.Atan2( Vector3.Dot( Vector3.Cross( from, to ), line ), Vector3.Dot( from, to ) );
                    Turn( frames[f], rig, world, a, Quaternion.CreateFromAxisAngle( line, turn ), e );
                }
            }
        // The hands' attitude in the world, which carries the fingers: a wrist that flicks while its arm moves smoothly
        // flicks every fingertip.
        var attitudes = new Quaternion[frames.Count];
        foreach ( var hand in new[] { BoneRole.HandL, BoneRole.HandR }.Select( target.BoneForRole ) )
        {
            if ( hand is not int h || rig[h].ParentIndex < 0 ) continue;
            for ( var pass = 0; pass < Passes; pass++ )
            {
                for ( var f = 0; f < frames.Count; f++ ) { FkUtil.ToWorld( frames[f], rig, world ); attitudes[f] = world[h].Rot; }
                corrected += Rotations( attitudes, changed );
                for ( var f = 0; f < frames.Count; f++ )
                {
                    if ( !changed[f] ) continue;
                    FkUtil.ToWorld( frames[f], rig, world );
                    frames[f][h].Rot = Quaternion.Normalize( Quaternion.Inverse( world[rig[h].ParentIndex].Rot ) * attitudes[f] );
                }
            }
        }
        // The head and each finger's middle and last joint, by turning the bone above toward the fitted place; the joint
        // keeps its attitude. A finger's path is followed in its hand's frame, so a hand spinning on the floor does not read as
        // fingers flicking: fitted in the world, the arcs its fingertips swept at 30–40° a frame were pulled inward and tipped
        // the flat fingers up to 8 cm off the floor. The head's is followed in the world (-1), where it wobbles.
        var aimed = new List<(int Joint, int Frame)>();
        if ( target.BoneForRole( BoneRole.Head ) is int head ) aimed.Add( (head, -1) );
        foreach ( var side in new[] { "L", "R" } )
        {
            if ( target.BoneForRole( Enum.Parse<BoneRole>( "Hand" + side ) ) is not int hand ) continue;
            foreach ( var finger in new[] { "Thumb", "Index", "Middle", "Ring", "Pinky" } )
                foreach ( var joint in new[] { "Mid", "Dist" } )
                    if ( Enum.TryParse<BoneRole>( finger + joint + side, out var role ) && target.BoneForRole( role ) is int bone ) aimed.Add( (bone, hand) );
        }
        foreach ( var (joint, carrier) in aimed )
        {
            var above = rig[joint].ParentIndex; if ( above < 0 || rig[above].ParentIndex < 0 ) continue;
            for ( var pass = 0; pass < Passes; pass++ )
            {
                for ( var f = 0; f < frames.Count; f++ )
                {
                    FkUtil.ToWorld( frames[f], rig, world );
                    path[f] = carrier < 0 ? world[joint].Pos : Local( world[carrier], world[joint].Pos );
                }
                corrected += Positions( path, changed, 1 / unitsPerCm );
                for ( var f = 0; f < frames.Count; f++ )
                {
                    if ( !changed[f] ) continue;
                    FkUtil.ToWorld( frames[f], rig, world );
                    var place = carrier < 0 ? path[f] : Global( world[carrier], path[f] );
                    var from = world[joint].Pos - world[above].Pos; var to = place - world[above].Pos;
                    if ( from.LengthSquared() < 1e-10f || to.LengthSquared() < 1e-10f ) continue;
                    Turn( frames[f], rig, world, above, MathQ.FromTo( Vector3.Normalize( from ), Vector3.Normalize( to ) ), joint );
                }
            }
        }
        // Held toes back where they were, by turning their foot about the ankle.
        for ( var t = 0; t < toes.Length; t++ )
        {
            var foot = rig[toes[t]].ParentIndex;
            for ( var f = 0; f < frames.Count; f++ )
            {
                var w = toesHolding[t][f]; if ( w <= 0 ) continue;
                FkUtil.ToWorld( frames[f], rig, world );
                var from = world[toes[t]].Pos - world[foot].Pos; var to = Vector3.Lerp( world[toes[t]].Pos, toesHeld[t][f], w ) - world[foot].Pos;
                if ( from.LengthSquared() < 1e-10f || to.LengthSquared() < 1e-10f ) continue;
                Turn( frames[f], rig, world, foot, MathQ.FromTo( Vector3.Normalize( from ), Vector3.Normalize( to ) ), toes[t] );
            }
        }
        return corrected;
    }

    static Vector3 Local( XForm frame, Vector3 point ) => Vector3.Transform( point - frame.Pos, Quaternion.Inverse( frame.Rot ) );
    static Vector3 Global( XForm frame, Vector3 point ) => frame.Pos + Vector3.Transform( point, frame.Rot );

    /// <summary>Turns <paramref name="bone"/> in the world by <paramref name="turn"/> about its own joint, keeping the
    /// world attitude of <paramref name="kept"/> below it.</summary>
    static void Turn( XForm[] frame, Skeleton.Skeleton rig, XForm[] world, int bone, Quaternion turn, int kept )
    {
        var attitude = world[kept].Rot; var parent = rig[bone].ParentIndex;
        frame[bone].Rot = Quaternion.Normalize( (parent < 0 ? Quaternion.Identity : Quaternion.Inverse( world[parent].Rot )) * Quaternion.Normalize( turn * world[bone].Rot ) );
        FkUtil.ToWorld( frame, rig, world );
        frame[kept].Rot = Quaternion.Normalize( Quaternion.Inverse( world[rig[kept].ParentIndex].Rot ) * attitude );
        FkUtil.ToWorld( frame, rig, world );
    }

    /// <summary>Despikes retargeted frames in place: every bone's local rotation and each root bone's position, in target
    /// units with <paramref name="unitsPerCm"/> to the centimetre. Returns the number of corrected samples; 0 for fewer
    /// than <see cref="Window"/> frames.</summary>
    public static int Apply( List<XForm[]> frames, Skeleton.Skeleton rig, float unitsPerCm )
    {
        if ( frames.Count < Window ) return 0;
        var corrected = 0; var changed = new bool[frames.Count];
        var rotations = new Quaternion[frames.Count]; var positions = new Vector3[frames.Count];
        for ( var pass = 0; pass < Passes; pass++ )
            for ( var bone = 0; bone < rig.Count; bone++ )
            {
                for ( var f = 0; f < frames.Count; f++ ) rotations[f] = frames[f][bone].Rot;
                corrected += Rotations( rotations, changed );
                for ( var f = 0; f < frames.Count; f++ ) if ( changed[f] ) frames[f][bone].Rot = rotations[f];
                if ( rig[bone].ParentIndex >= 0 ) continue;
                for ( var f = 0; f < frames.Count; f++ ) positions[f] = frames[f][bone].Pos;
                corrected += Positions( positions, changed, 1 / unitsPerCm );
                for ( var f = 0; f < frames.Count; f++ ) if ( changed[f] ) frames[f][bone].Pos = positions[f];
            }
        return corrected;
    }

    /// <summary>Savitzky-Golay weights for the centre of a cubic fitted over seven samples.</summary>
    static readonly double[] Weights = { -2 / 21d, 3 / 21d, 6 / 21d, 7 / 21d, 6 / 21d, 3 / 21d, -2 / 21d };

    /// <summary>Pulls the rotations that leave their neighbours' cubic onto it, fitting on the track as given; marks them in
    /// <paramref name="changed"/> and returns how many.</summary>
    static int Rotations( Quaternion[] track, bool[] changed )
    {
        var frames = track.Length; var half = Window / 2;
        // Sign-continuous so neighbouring quaternions average sensibly.
        var given = new Quaternion[frames];
        for ( var f = 0; f < frames; f++ )
        {
            given[f] = track[f];
            if ( f > 0 && Quaternion.Dot( given[f], given[f - 1] ) < 0 ) given[f] = Quaternion.Negate( given[f] );
        }
        Array.Clear( changed ); var count = 0;
        // Only frames with a whole window: repeating an end frame bends steady fast motion off its path.
        for ( var f = half; f < frames - half; f++ )
        {
            var fit = Quaternion.Zero;
            for ( var k = -half; k <= half; k++ )
            {
                var q = given[f + k]; var w = (float)Weights[k + half];
                fit = new Quaternion( fit.X + q.X * w, fit.Y + q.Y * w, fit.Z + q.Z * w, fit.W + q.W * w );
            }
            if ( fit.LengthSquared() < 1e-12f ) continue;
            fit = Quaternion.Normalize( fit );
            var off = 2 * MathF.Acos( Math.Clamp( MathF.Abs( Quaternion.Dot( fit, given[f] ) ), 0, 1 ) ) * 180 / MathF.PI;
            var pull = Math.Clamp( (off - StartDegrees) / (FullDegrees - StartDegrees), 0, 1 );
            if ( pull <= 0 ) continue;
            track[f] = Quaternion.Normalize( Quaternion.Slerp( given[f], fit, pull ) );
            changed[f] = true; count++;
        }
        return count;
    }

    /// <summary>The angle at <paramref name="joint"/> between the bones to <paramref name="root"/> and <paramref name="end"/>,
    /// in degrees: 180 for a straight limb.</summary>
    static float Bend( Vector3 root, Vector3 joint, Vector3 end )
    {
        var a = root - joint; var b = end - joint;
        if ( a.LengthSquared() < 1e-12f || b.LengthSquared() < 1e-12f ) return 180;
        return MathF.Acos( Math.Clamp( Vector3.Dot( Vector3.Normalize( a ), Vector3.Normalize( b ) ), -1, 1 ) ) * 180 / MathF.PI;
    }

    /// <summary>As <see cref="Rotations"/>, for angles in degrees.</summary>
    static int Angles( float[] track, bool[] changed )
    {
        var frames = track.Length; var half = Window / 2;
        var given = new float[frames]; Array.Copy( track, given, frames );
        Array.Clear( changed ); var count = 0;
        for ( var f = half; f < frames - half; f++ )
        {
            var fit = 0f;
            for ( var k = -half; k <= half; k++ ) fit += given[f + k] * (float)Weights[k + half];
            var pull = Math.Clamp( (MathF.Abs( fit - given[f] ) - StartDegrees) / (FullDegrees - StartDegrees), 0, 1 );
            if ( pull <= 0 ) continue;
            track[f] = Math.Min( 180, given[f] + (fit - given[f]) * pull );
            changed[f] = true; count++;
        }
        return count;
    }

    /// <summary>As <see cref="Rotations"/>, for positions with <paramref name="cmPerUnit"/> centimetres to the unit.</summary>
    static int Positions( Vector3[] track, bool[] changed, float cmPerUnit )
    {
        var frames = track.Length; var half = Window / 2;
        var given = new Vector3[frames]; Array.Copy( track, given, frames );
        Array.Clear( changed ); var count = 0;
        for ( var f = half; f < frames - half; f++ )
        {
            var fit = Vector3.Zero;
            for ( var k = -half; k <= half; k++ ) fit += given[f + k] * (float)Weights[k + half];
            var off = Vector3.Distance( fit, given[f] ) * cmPerUnit;
            var pull = Math.Clamp( (off - StartCm) / (FullCm - StartCm), 0, 1 );
            if ( pull <= 0 ) continue;
            track[f] = Vector3.Lerp( given[f], fit, pull );
            changed[f] = true; count++;
        }
        return count;
    }
}