Code/HumanoidMocap/Core/Motion/CaptureHandPlant.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>Holds hands that rest on the floor in place, the way planted feet are held. Push-ups, crawling,
/// cartwheels and handstands put weight on the hands, but the capture has no contact track for them, so a hand on
/// the floor slid and hovered with the arm's small errors. After the floor is settled (CaptureGround, CaptureHover,
/// CaptureSeat), a hand whose lowest point (fingers included) stays within <see cref="NearFloorCm"/> of the floor
/// and moves slower than <see cref="StillMetresPerSecond"/> for at least <see cref="MinimumSeconds"/> rests on it:
/// through that stretch it is held where it landed, palm on the floor, re-solved with two-bone arm IK; its
/// orientation is kept. The slide the capture built up is let go over <see cref="BlendSeconds"/> after the hand lifts:
/// held at the stretch's middle instead, a hand that slid while resting jumped to it at up to 2 m/s. A hand that travels more than <see cref="MaximumTravelCm"/> while low is wiping or
/// sliding on purpose and is left alone, unless both feet are at least <see cref="SupportFeetCm"/> off the floor: then the
/// hand carries the body (handstands, freezes, one-handed spins) and cannot slide, however far the capture drifts. The body
/// is moved to stay over such a hand, a body on one hand is tilted about that wrist until its weight is over it, the
/// palm turns flat onto the floor and the fingers straighten along it.</summary>
public static class CaptureHandPlant
{
/// <summary>A hand comes to rest within NearFloorCm of the floor and stays at rest until it rises past LiftCm: the capture
/// lifted a hand planted behind a seated dancer 9 cm as he got up, and a single threshold let it float.</summary>
public const float MaximumShoulderDegrees = 25, MaximumLeanDegrees = 15;
public const float NearFloorCm = 8, LiftCm = 15, StillMetresPerSecond = 1, MinimumSeconds = .15f, MaximumTravelCm = 25, BlendSeconds = .25f;
/// <summary>Both feet this far off the floor leave the hands carrying the body. The breakin clip's one-handed spin
/// holds its feet 0.8–2 m up while the captured support hand slid up to 32 cm a frame.</summary>
public const float SupportFeetCm = 40;
/// <summary>Hands this close in height both count as the lowest; a higher hand under a raised body carries weight
/// only while it rests still.</summary>
public const float BothHandsCm = 2;
static readonly BoneRole[][] Fingers =
{
new[] { BoneRole.IndexProxL, BoneRole.IndexMidL, BoneRole.IndexDistL, BoneRole.MiddleProxL, BoneRole.MiddleMidL, BoneRole.MiddleDistL,
BoneRole.RingProxL, BoneRole.RingMidL, BoneRole.RingDistL, BoneRole.PinkyProxL, BoneRole.PinkyMidL, BoneRole.PinkyDistL },
new[] { BoneRole.IndexProxR, BoneRole.IndexMidR, BoneRole.IndexDistR, BoneRole.MiddleProxR, BoneRole.MiddleMidR, BoneRole.MiddleDistR,
BoneRole.RingProxR, BoneRole.RingMidR, BoneRole.RingDistR, BoneRole.PinkyProxR, BoneRole.PinkyMidR, BoneRole.PinkyDistR }
};
/// <returns>The number of hand samples held.</returns>
/// <param name="observed">Frames in which the video shows each hand on the floor under a raised body, by hand role;
/// such a hand is planted even where the capture lifted it.</param>
/// <param name="spread">Per frame, how far apart the performer's hands are on the floor, in shoulder widths; a second
/// hand landing beside a held one keeps that spacing on this rig.</param>
public static int Apply( List<XForm[]> frames, TargetRig target, TargetUpAxis axis, float fps, IReadOnlyDictionary<BoneRole, bool[]> observed = null, float[] spread = null )
{
if ( frames.Count < 3 || fps <= 0 ) return 0;
var rig = target.Skeleton; var up = axis == TargetUpAxis.YUpCm ? Vector3.UnitY : Vector3.UnitZ;
var cm = axis == TargetUpAxis.ZUpEngine ? 1 / 2.54f : 1f;
var arms = new[] { (BoneRole.UpperArmL, BoneRole.LowerArmL, BoneRole.HandL), (BoneRole.UpperArmR, BoneRole.LowerArmR, BoneRole.HandR) }
.Select( r => target.BoneForRole( r.Item1 ) is int a && target.BoneForRole( r.Item2 ) is int b && target.BoneForRole( r.Item3 ) is int c ? (A: a, B: b, C: c) : ((int A, int B, int C)?)null )
.Where( c => c is not null ).Select( c => c.Value ).ToArray();
if ( arms.Length == 0 ) return 0;
var world = new XForm[rig.Count];
// Each hand's wrist and the height of its lowest part, per frame, before anything is held.
var parts = arms.ToDictionary( a => a.C, a => CaptureSeat.HandParts( target, a.C ) );
var feet = new[] { BoneRole.FootL, BoneRole.ToeL, BoneRole.FootR, BoneRole.ToeR }.Select( target.BoneForRole ).ToArray();
var supported = new bool[frames.Count];
// Finger chains (proximal, middle, distal) of each hand: index and middle, then ring and pinky where the rig has them.
// The Citizen has no pinky; without it none of its hands lay flat, and a planted wrist bobbed up to 21 cm as the
// capture's long fingers swung under it.
var chains = arms.ToDictionary( a => a.C, a =>
{
var fingers = Fingers[target.BoneForRole( BoneRole.HandL ) == a.C ? 0 : 1].Chunk( 3 ).Select( f => f.Select( target.BoneForRole ).ToArray() ).ToArray();
if ( fingers.Take( 2 ).Any( f => f.Any( b => b is null ) ) ) return null;
return fingers.Where( f => f.All( b => b is int ) ).SelectMany( f => f.Select( b => b!.Value ) ).ToArray();
} );
var thumbs = arms.ToDictionary( a => a.C, a => (target.BoneForRole( BoneRole.HandL ) == a.C
? new[] { BoneRole.ThumbProxL, BoneRole.ThumbMidL, BoneRole.ThumbDistL } : new[] { BoneRole.ThumbProxR, BoneRole.ThumbMidR, BoneRole.ThumbDistR } )
.Select( target.BoneForRole ).ToArray() is var bones && bones.All( b => b is int ) ? bones.Select( b => b!.Value ).ToArray() : null );
var wrist = arms.ToDictionary( a => a.C, _ => new Vector3[frames.Count] );
var lowest = arms.ToDictionary( a => a.C, _ => new float[frames.Count] );
for ( var f = 0; f < frames.Count; f++ )
{
FkUtil.ToWorld( frames[f], rig, world );
foreach ( var a in arms ) { wrist[a.C][f] = world[a.C].Pos; lowest[a.C][f] = parts[a.C].Min( b => Vector3.Dot( world[b].Pos, up ) ); }
if ( feet[0] is int footL && feet[2] is int footR )
supported[f] = Math.Min( Height( footL, feet[1] ), Height( footR, feet[3] ) ) > SupportFeetCm * cm;
}
float Height( int foot, int? toe ) => Math.Min( Vector3.Dot( world[foot].Pos, up ), toe is int t ? Vector3.Dot( world[t].Pos, up ) : float.MaxValue );
var minimum = Math.Max( 3, (int)MathF.Ceiling( MinimumSeconds * fps ) ); var blend = Math.Max( 1, (int)MathF.Round( BlendSeconds * fps ) );
var still = StillMetresPerSecond * 100 * cm / fps;
var goals = arms.ToDictionary( a => a.C, _ => new (Vector3 Position, float Weight, bool Bearing, bool Lifting)[frames.Count] );
// Which weight-bearing stretch (index into bearings) wrote each goal, -1 for none.
var owner = arms.ToDictionary( a => a.C, _ => Enumerable.Repeat( -1, frames.Count ).ToArray() );
// Weight-bearing stretches (hand, first, last, landing point), to move the body over them below.
var bearings = new List<(int Hand, int First, int Last, Vector3 Anchor)>();
foreach ( var a in arms )
{
var p = wrist[a.C]; var low = lowest[a.C];
bool Lowest( int f ) => arms.All( other => low[f] <= lowest[other.C][f] + BothHandsCm * cm );
bool Still( int f ) => f == 0 || Flat( p[f] - p[f - 1], up ).Length() < still;
// The lowest hand under a raised body rests however the capture drags it; any other hand must also be still,
// so the free hand swinging low through a one-handed spin is not planted.
bool Resting( int f, float height ) => low[f] < height * cm && (supported[f] && Lowest( f ) || Still( f ));
for ( var f = 0; f < frames.Count; )
{
if ( !Resting( f, NearFloorCm ) ) { f++; continue; }
var e = f;
while ( true )
{
while ( e + 1 < frames.Count && Resting( e + 1, LiftCm ) ) e++;
// The lowest hand under a raised body still carries it while the capture lifts it for a moment: through the
// breakin clip's one-handed spin it lifted the support hand up to 22 cm for three to five frames, and each
// touch after that landed somewhere new. Up to BlendSeconds of that stays one hold.
// A hand that touches down again within ShortLiftFrames never really left: on the Citizen two of the breakin
// clip's touches, a frame apart, each landed elsewhere and the hand jumped 18–30 cm between them.
var g = e + 1; while ( g < frames.Count && g - e <= blend && (supported[g] && Lowest( g ) || g - e <= ShortLiftFrames) && !Resting( g, NearFloorCm ) ) g++;
if ( g == e + 1 || g >= frames.Count || g - e > blend || !Resting( g, NearFloorCm ) ) break;
e = g;
}
if ( e - f + 1 >= minimum )
{
// Held where it landed: a hand that touches down stays there. The capture's slide builds up while it rests
// and is let go over BlendSeconds after the hand lifts, while it is moving anyway.
var length = e - f + 1; var anchor = Flat( p[f], up );
// The lower hand under a raised body carries it. A second hand that rests beside it is found below,
// once the body has been carried: until then the whole body slides with the capture.
var bearing = Enumerable.Range( f, length ).Count( i => supported[i] && Lowest( i ) ) * 2 >= length;
if ( bearing || Enumerable.Range( f, length ).All( i => (Flat( p[i], up ) - anchor).Length() <= MaximumTravelCm * cm ) )
{
if ( bearing ) bearings.Add( (a.C, f, e, anchor) );
// The palm settles onto the floor over a few frames rather than dropping in one.
var settle = Math.Max( 1, Math.Min( 3, (length - 1) / 3 ) );
for ( var i = f; i <= e; i++ )
{
// Palm on the floor: the wrist at the held place, as high as puts the hand's lowest point on the floor.
var w = Math.Clamp( (i - f + 1) / (float)(settle + 1), 0, 1 ); w = w * w * (3 - 2 * w);
var goal = anchor + up * (Vector3.Dot( p[i], up ) - low[i]);
if ( w > goals[a.C][i].Weight ) { goals[a.C][i] = (goal, w, bearing, false); owner[a.C][i] = bearing ? bearings.Count - 1 : -1; }
}
// Everything held at the last frame, height included, is let go gradually; a flat palm curls back
// over the same frames instead of snapping.
var carried = anchor + up * (Vector3.Dot( p[e], up ) - low[e]) - p[e];
for ( var i = e + 1; i <= Math.Min( frames.Count - 1, e + blend ); i++ )
{
var w = 1 - (i - e) / (float)(blend + 1); w = w * w * (3 - 2 * w);
if ( w > goals[a.C][i].Weight ) { goals[a.C][i] = (p[i] + carried, w, bearing, true); owner[a.C][i] = bearing ? bearings.Count - 1 : -1; }
}
}
}
f = e + 1;
}
}
// Each stretch lands where the carried body puts its hand, and a hand being let go keeps its offset from where the
// carried body puts it at the release, fading, rather than from the uncarried capture: while the carry eased out, a
// Citizen's lifting hand was sent out of reach and its arm swung 45 cm.
var (landed, moved) = CarryBody( frames, rig, bearings, wrist, up, blend );
foreach ( var a in arms )
for ( var i = 0; i < frames.Count; i++ )
if ( owner[a.C][i] is var b && b >= 0 )
goals[a.C][i].Position += landed[b] + (goals[a.C][i].Lifting ? moved[i] - moved[bearings[b].Last] : Vector3.Zero);
for ( var b = 0; b < bearings.Count; b++ ) bearings[b] = bearings[b] with { Anchor = bearings[b].Anchor + landed[b] };
var seen = arms.ToDictionary( a => a.C, a => observed?.GetValueOrDefault( target.BoneForRole( BoneRole.HandL ) == a.C ? BoneRole.HandL : BoneRole.HandR ) is { } mask
&& mask.Length == frames.Count ? mask : new bool[frames.Count] );
SecondHands( frames, rig, arms, parts, supported, seen, goals, bearings, up, cm, still, minimum, blend, spread is { } s0 && s0.Length == frames.Count ? s0 : null );
Balance( frames, target, bearings, goals, up, fps );
KeepArmBend( frames, target, arms, goals, supported, up, blend );
var heading = arms.ToDictionary( a => a.C, a => chains[a.C] is { } chain
? Headings( frames, rig, a.C, chain, target.BoneForRole( BoneRole.HandL ) == a.C, goals[a.C], up ) : null );
var held = 0; var lift = arms.ToDictionary( a => a.C, _ => 0f );
for ( var f = 0; f < frames.Count; f++ )
{
var frame = frames[f];
// With both hands bearing weight, the second hand's small lean is followed by the first hand again, so neither
// ends off the floor.
var bothBearing = arms.Count( a => goals[a.C][f].Bearing && goals[a.C][f].Weight > 0 ) == arms.Length && arms.Length > 1;
foreach ( var a in bothBearing ? arms.Concat( arms.Take( 1 ) ) : arms )
{
var (position, weight, bearing, lifting) = goals[a.C][f]; if ( weight <= 0 ) continue;
FkUtil.ToWorld( frame, rig, world );
var rotation = world[a.C].Rot;
// Any hand held on the floor lies flat on it, carrying the body or not: a crouching dancer's hand resting on the
// floor stood on its fingertips with the wrist 8–14 cm up where the clip shows the palm flat.
if ( chains[a.C] is { } chain )
{
// A hand carrying the body lies flat: palm onto the floor, fingers straight along it, its lowest point on the floor.
var down = Quaternion.Slerp( Quaternion.Identity, FromTo( PalmNormal( world, chain, a.C, target.BoneForRole( BoneRole.HandL ) == a.C ), -up ), weight );
rotation = Quaternion.Normalize( down * rotation );
if ( heading[a.C] is { } turning && !double.IsNaN( turning[f] ) )
{
var turn = (float)Math.IEEERemainder( turning[f] - Heading( Vector3.Transform( frame[chain[3]].Pos, rotation ), up ), 2 * Math.PI ) * weight;
rotation = Quaternion.Normalize( Quaternion.CreateFromAxisAngle( up, turn ) * rotation );
}
Straighten( frame, rig, chain, weight );
var parent0 = rig[a.C].ParentIndex;
frame[a.C].Rot = Quaternion.Normalize( (parent0 < 0 ? Quaternion.Identity : Quaternion.Inverse( world[parent0].Rot )) * rotation );
FkUtil.ToWorld( frame, rig, world );
if ( thumbs[a.C] is { } thumb ) LayThumb( frame, rig, world, thumb, up, weight );
var depth = parts[a.C].Min( b => Vector3.Dot( world[b].Pos - world[a.C].Pos, up ) );
// Down, the flat hand's lowest point is on the floor. Lifting, the hand rises as the capture lifts it from
// where it was held, never through the floor: forced down, a Citizen's lifting arm overreached and flipped.
if ( !lifting ) { lift[a.C] = Math.Max( 0, -depth ) - Vector3.Dot( position, up ); position = Flat( position, up ) + up * Math.Max( 0, -depth ); }
else position = Flat( position, up ) + up * Math.Max( Math.Max( 0, -depth ), Vector3.Dot( position, up ) + lift[a.C] );
}
// A hand that is down stays on its spot from the frame it lands; only its height eases in. Easing the whole
// place in slid the landing hand on with the capture for up to three frames, up to 6 cm a frame.
var goal = lifting ? Vector3.Lerp( world[a.C].Pos, position, weight )
: Flat( position, up ) + up * float.Lerp( Vector3.Dot( world[a.C].Pos, up ), Vector3.Dot( position, up ), weight );
// Out of the straight arm's reach (a seated dancer rising off a planted hand), the shoulder drops
// toward the hand as a person's does: the clavicle turns, up to MaximumShoulderDegrees.
var reach = Vector3.Distance( world[a.A].Pos, world[a.B].Pos ) + Vector3.Distance( world[a.B].Pos, world[a.C].Pos );
void Lean( int bone, float limit ) => CaptureHandPlant.Lean( frame, rig, world, bone, a.A, goal, reach, limit );
Lean( rig[a.A].ParentIndex, MaximumShoulderDegrees );
// Still out of reach (pushing up off the hand), the body leans onto it from the base of the spine; only a third
// as far while the other hand also bears weight, since the body is fixed on that hand.
var otherBearing = arms.Any( o => o.C != a.C && goals[o.C][f].Bearing && goals[o.C][f].Weight > 0 );
if ( target.BoneForRole( BoneRole.Spine0 ) is int spine ) Lean( spine, otherBearing ? MaximumLeanDegrees / 3 : MaximumLeanDegrees );
// Still out of reach, a hand that is down comes straight up off its spot as the body rises, rather than being
// dragged across the floor by the straightened arm.
if ( !lifting ) goal = Above( goal, world[a.A].Pos, reach * .999f, up );
var upper = world[a.A]; var middle = world[a.B]; var end = world[a.C];
var bend = Vector3.Cross( middle.Pos - upper.Pos, end.Pos - middle.Pos );
if ( bend.LengthSquared() < 1e-8f ) bend = Vector3.Transform( Vector3.UnitX, upper.Rot );
// A soft lock-out: near full stretch the elbow eases straight instead of snapping between bent and locked.
var ik = TwoBoneIk.Solve( upper.Pos, middle.Pos, end.Pos, goal, stableBendAxis: bend );
EffectorIk.ApplyWorldDeltas( frame, rig, a.A, a.B, a.C, ik.UpperWorldDelta, ik.LowerWorldDelta, world );
FkUtil.ToWorld( frame, rig, world );
var parent = rig[a.C].ParentIndex;
frame[a.C].Rot = Quaternion.Normalize( (parent < 0 ? Quaternion.Identity : Quaternion.Inverse( world[parent].Rot )) * rotation );
}
held += arms.Count( a => goals[a.C][f].Weight > 0 );
}
return held;
}
static Vector3 Flat( Vector3 v, Vector3 up ) => v - up * Vector3.Dot( v, up );
/// <summary>The lowest point straight above <paramref name="goal"/> within <paramref name="reach"/> of
/// <paramref name="shoulder"/>; <paramref name="goal"/> itself when it is within reach or nothing above it is.</summary>
static Vector3 Above( Vector3 goal, Vector3 shoulder, float reach, Vector3 up )
{
var d = shoulder - goal; var along = Vector3.Dot( d, up ); var room = along * along - d.LengthSquared() + reach * reach;
if ( d.LengthSquared() <= reach * reach || room < 0 ) return goal;
return goal + up * (along - MathF.Sqrt( room ));
}
/// <summary>Turns <paramref name="bone"/> about its own joint, up to <paramref name="limitDegrees"/>, just far enough
/// to bring <paramref name="shoulder"/> within <paramref name="reach"/> of <paramref name="goal"/>; no turn when it
/// already is or the bone is a root. Updates <paramref name="world"/>.</summary>
internal static void Lean( XForm[] frame, HumanoidMocapX.Core.Skeleton.Skeleton rig, XForm[] world, int bone, int shoulder, Vector3 goal, float reach, float limitDegrees )
{
if ( bone < 0 || rig[bone].ParentIndex < 0 || Vector3.Distance( world[shoulder].Pos, goal ) <= reach * .999f ) return;
var pivot = world[bone].Pos; var from = world[shoulder].Pos - pivot; var to = goal - pivot;
var hinge = Vector3.Cross( from, to );
if ( hinge.LengthSquared() < 1e-10f || from.LengthSquared() < 1e-8f ) return;
hinge = Vector3.Normalize( hinge ); var turn = 0f;
for ( var step = 1; step <= 25; step++ )
{
turn = step * limitDegrees / 25 * MathF.PI / 180;
if ( Vector3.Distance( pivot + Vector3.Transform( from, Quaternion.CreateFromAxisAngle( hinge, turn ) ), goal ) <= reach * .999f ) break;
}
var turned = Quaternion.Normalize( Quaternion.CreateFromAxisAngle( hinge, turn ) * world[bone].Rot );
frame[bone].Rot = Quaternion.Normalize( Quaternion.Inverse( world[rig[bone].ParentIndex].Rot ) * turned );
FkUtil.ToWorld( frame, rig, world );
}
/// <summary>Moves the whole body so each weight-bearing hand stays where it landed, the way a support hand pins a
/// handstand; the move eases out over <paramref name="blend"/> frames after the hand lifts. Where both hands carry
/// the body, the one lower on average leads and the other is reached with the arm. Returns, per stretch, how far
/// its landing point moved with the carried body, and the body's move per frame.</summary>
static (Vector3[] Landed, Vector3[] Moved) CarryBody( List<XForm[]> frames, HumanoidMocapX.Core.Skeleton.Skeleton rig, List<(int Hand, int First, int Last, Vector3 Anchor)> bearings,
Dictionary<int, Vector3[]> wrist, Vector3 up, int blend )
{
var landed = new Vector3[bearings.Count];
if ( bearings.Count == 0 ) return (landed, new Vector3[frames.Count]);
// The move per frame, which stretch leads it, and how much of that stretch's move remains (1 while its hand is down).
var shift = new Vector3?[frames.Count]; var lead = Enumerable.Repeat( -1, frames.Count ).ToArray(); var share = new float[frames.Count];
// A hand that is down leads before any other hand's letting go.
var order = Enumerable.Range( 0, bearings.Count ).OrderBy( s => Enumerable.Range( bearings[s].First, bearings[s].Last - bearings[s].First + 1 ).Average( i => Vector3.Dot( wrist[bearings[s].Hand][i], up ) ) ).ToArray();
foreach ( var s in order )
{
var (hand, first, last, anchor) = bearings[s];
for ( var i = first; i <= last; i++ ) if ( shift[i] is null ) { shift[i] = anchor - Flat( wrist[hand][i], up ); lead[i] = s; share[i] = 1; }
}
foreach ( var s in order )
{
var (hand, _, last, anchor) = bearings[s];
var carried = anchor - Flat( wrist[hand][last], up );
for ( var i = last + 1; i <= Math.Min( frames.Count - 1, last + blend ); i++ )
{
var w = 1 - (i - last) / (float)(blend + 1); w = w * w * (3 - 2 * w);
if ( shift[i] is null ) { shift[i] = carried * w; lead[i] = s; share[i] = w; }
}
}
// In time order, a stretch that takes over while another still moves the body carries that move on instead of
// dropping it, and lands where the moved body puts its hand; so does a hand that only follows another's lead. On
// the Citizen the breakin clip's left hand touched down again one frame after lifting and jumped 30 cm.
foreach ( var s in Enumerable.Range( 0, bearings.Count ).OrderBy( s => bearings[s].First ) )
{
var first = bearings[s].First;
if ( lead[first] != s ) { landed[s] = shift[first] ?? Vector3.Zero; continue; }
if ( first == 0 || lead[first - 1] < 0 || lead[first - 1] == s ) continue;
landed[s] = shift[first - 1]!.Value;
for ( var i = 0; i < frames.Count; i++ ) if ( lead[i] == s ) shift[i] += landed[s] * share[i];
}
// Smoothed over time: the capture's frame-to-frame jitter at the hand would otherwise shake the whole body. The window
// stays symmetric and inside the carried frames, shrinking toward their ends, so the body is not moved before the hand
// lands (a hand landing on a 3 cm-a-frame slide settled 5 cm sideways) and a steady slide is followed exactly. The
// arm's IK below keeps the hand itself exactly where it landed.
var smoothed = new Vector3[frames.Count]; var radius = (int)MathF.Ceiling( 3 * CarrySigmaFrames );
for ( var f = 0; f < frames.Count; f++ )
{
if ( shift[f] is null ) continue;
var reach = 0;
while ( reach < radius && f - reach - 1 >= 0 && f + reach + 1 < frames.Count && shift[f - reach - 1] is not null && shift[f + reach + 1] is not null ) reach++;
var sum = Vector3.Zero; var total = 0f;
for ( var j = f - reach; j <= f + reach; j++ )
{
var g = MathF.Exp( -.5f * (j - f) * (j - f) / (CarrySigmaFrames * CarrySigmaFrames) ); sum += shift[j]!.Value * g; total += g;
}
smoothed[f] = sum / total;
}
var roots = Enumerable.Range( 0, rig.Count ).Where( b => rig[b].ParentIndex < 0 ).ToArray();
for ( var f = 0; f < frames.Count; f++ )
if ( smoothed[f].LengthSquared() > 0 ) foreach ( var root in roots ) frames[f][root].Pos += smoothed[f];
return (landed, smoothed);
}
/// <summary>Plants a hand that bears weight beside the one carrying the body: where the video shows it on the floor
/// under a raised body, or where it rests still near the floor there. The breakin clip's closing headstand had both
/// hands down in the video but the capture's right hand 22–26 cm up. Stillness is judged after the body was carried
/// over the first hand, so the capture's slide does not hide it; the free hand sweeping low through a one-handed
/// spin is never still and stays free. The hand is let go over <paramref name="blend"/> frames.</summary>
static void SecondHands( List<XForm[]> frames, HumanoidMocapX.Core.Skeleton.Skeleton rig, (int A, int B, int C)[] arms, Dictionary<int, int[]> parts,
bool[] supported, Dictionary<int, bool[]> seen, Dictionary<int, (Vector3 Position, float Weight, bool Bearing, bool Lifting)[]> goals,
List<(int Hand, int First, int Last, Vector3 Anchor)> bearings, Vector3 up, float cm, float still, int minimum, int blend, float[] spread )
{
var world = new XForm[rig.Count]; var hands = arms.Select( a => a.C ).ToArray();
var wrist = hands.ToDictionary( h => h, _ => new Vector3[frames.Count] ); var low = hands.ToDictionary( h => h, _ => new float[frames.Count] );
// Whether the floor under each wrist lies within the arm's reach of its shoulder.
var reachable = hands.ToDictionary( h => h, _ => new bool[frames.Count] ); var shoulders = new float[frames.Count];
for ( var f = 0; f < frames.Count; f++ )
{
FkUtil.ToWorld( frames[f], rig, world );
foreach ( var (a, b, h) in arms )
{
wrist[h][f] = world[h].Pos; low[h][f] = parts[h].Min( p => Vector3.Dot( world[p].Pos, up ) );
var floor = world[h].Pos - up * low[h][f];
reachable[h][f] = Vector3.Distance( world[a].Pos, floor ) <= 1.02f * (Vector3.Distance( world[a].Pos, world[b].Pos ) + Vector3.Distance( world[b].Pos, world[h].Pos ));
shoulders[f] = arms.Length > 1 ? Vector3.Distance( world[arms[0].A].Pos, world[arms[1].A].Pos ) : 0;
}
}
foreach ( var h in hands )
{
var p = wrist[h];
bool Resting( int f, float height ) => !goals[h][f].Bearing && (seen[h][f]
|| supported[f] && low[h][f] < height * cm && (f == 0 || Flat( p[f] - p[f - 1], up ).Length() < still));
for ( var f = 0; f < frames.Count; )
{
if ( !Resting( f, NearFloorCm ) ) { f++; continue; }
var e = f; while ( e + 1 < frames.Count && Resting( e + 1, LiftCm ) ) e++;
// A hand the video puts on the floor out of the arm's reach is a mismatch (SAM swaps hands upside down);
// reaching for it would only drag the body off the hand carrying it.
if ( e - f + 1 >= minimum && Enumerable.Range( f, e - f + 1 ).Count( i => reachable[h][i] ) * 2 >= e - f + 1 )
{
// A hand that never leaves the floor is one touch: it keeps the place of a hold ending just before, and a hold
// starting just after carries on at this one's. The breakin clip's right hand, held through the closing freeze,
// jumped 66 cm to where the first pass had put it as the dancer pushed up.
var before = bearings.FindIndex( s => s.Hand == h && s.Last == f - 1 );
var anchor = before >= 0 ? bearings[before].Anchor : Flat( p[f], up ); var settle = Math.Max( 1, Math.Min( 3, (e - f) / 3 ) );
// Landing beside the other hand, held down, it keeps the performer's spacing for this rig's shoulders: the
// Citizen's short arms brought the breakin clip's freeze hands together under its face, 1.1 shoulder widths
// apart where the dancer's were 1.4.
var other = hands.FirstOrDefault( o => o != h && goals[o][f] is { Weight: >= 1, Lifting: false } );
if ( before < 0 && spread is not null && other != 0 && spread[f] > 0 && shoulders[f] > 0 )
{
var held = Flat( goals[other][f].Position, up ); var apart = Flat( anchor - held, up );
if ( apart.LengthSquared() > 1e-8f ) anchor = held + Vector3.Normalize( apart ) * spread[f] * shoulders[f];
}
var after = bearings.FindIndex( s => s.Hand == h && s.First == e + 1 );
var last = after >= 0 ? bearings[after].Last : e;
if ( after >= 0 ) bearings.RemoveAt( after );
for ( var i = f; i <= last; i++ )
{
var goal = anchor + up * (Vector3.Dot( p[i], up ) - low[h][i]);
var w = before >= 0 ? 1 : Math.Clamp( (i - f + 1) / (float)(settle + 1), 0, 1 ); w = w * w * (3 - 2 * w);
goals[h][i] = (goal, w, true, false);
}
e = last;
bearings.Add( (h, f, e, anchor) );
var carried = anchor + up * (Vector3.Dot( p[e], up ) - low[h][e]) - p[e];
for ( var i = e + 1; i <= Math.Min( frames.Count - 1, e + blend ); i++ )
{
var w = 1 - (i - e) / (float)(blend + 1); w = w * w * (3 - 2 * w);
if ( w >= goals[h][i].Weight ) goals[h][i] = (p[i] + carried, w, true, true);
}
}
f = e + 1;
}
}
}
/// <summary>Moves the body along each arm whose hand is held down, so the arm keeps the bend the capture gave it and
/// the body rides on it: the Citizen's arms are short for its body, so its straight support arm through the breakin
/// clip's spin folded to reach the floor, and crouching it fell 8 cm short of the floor where the dancer put a hand.
/// Feet on the floor are kept where they were by their legs; the move is smoothed over CarrySigmaFrames.</summary>
static void KeepArmBend( List<XForm[]> frames, TargetRig target, (int A, int B, int C)[] arms,
Dictionary<int, (Vector3 Position, float Weight, bool Bearing, bool Lifting)[]> goals, bool[] supported, Vector3 up, int blend )
{
var rig = target.Skeleton; var world = new XForm[rig.Count];
var move = new Vector3?[frames.Count];
for ( var f = 0; f < frames.Count; f++ )
{
FkUtil.ToWorld( frames[f], rig, world );
var sum = Vector3.Zero; var total = 0f;
foreach ( var (a, _, c) in arms )
{
var (position, weight, _, lifting) = goals[c][f];
if ( weight <= 0 || lifting ) continue;
var toShoulder = world[a].Pos - position; var length = toShoulder.Length(); if ( length < 1e-4f ) continue;
var natural = Vector3.Distance( world[a].Pos, world[c].Pos );
sum += toShoulder / length * (natural - length) * weight; total += weight;
}
if ( total > 0 ) move[f] = sum / total;
}
// Eased out over blend frames once the hands lift, as the carried body is: dropped at once, the body sprang back
// and a lifting Citizen's elbow snapped from straight to 123° in a frame.
for ( var f = 0; f + 1 < frames.Count; f++ )
{
if ( move[f] is not { } last || move[f + 1] is not null ) continue;
for ( var i = f + 1; i <= Math.Min( frames.Count - 1, f + blend ) && move[i] is null; i++ )
{
var w = 1 - (i - f) / (float)(blend + 1); w = w * w * (3 - 2 * w);
move[i] = last * w;
}
f += blend;
}
var smoothed = new Vector3[frames.Count]; var radius = (int)MathF.Ceiling( 3 * CarrySigmaFrames );
for ( var f = 0; f < frames.Count; f++ )
{
if ( move[f] is null ) continue;
// Symmetric and inside the held frames, as the carry: a hold starts and ends with the body where the capture put it.
var reach = 0;
while ( reach < radius && f - reach - 1 >= 0 && f + reach + 1 < frames.Count && move[f - reach - 1] is not null && move[f + reach + 1] is not null ) reach++;
var sum = Vector3.Zero; var total = 0f;
for ( var j = f - reach; j <= f + reach; j++ ) { var g = MathF.Exp( -.5f * (j - f) * (j - f) / (CarrySigmaFrames * CarrySigmaFrames) ); sum += move[j]!.Value * g; total += g; }
smoothed[f] = sum / total;
}
var roots = Enumerable.Range( 0, rig.Count ).Where( b => rig[b].ParentIndex < 0 ).ToArray();
var legs = new[] { (BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL), (BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR) }
.Select( l => target.BoneForRole( l.Item1 ) is int h && target.BoneForRole( l.Item2 ) is int k && target.BoneForRole( l.Item3 ) is int ft ? (H: h, K: k, F: ft) : ((int H, int K, int F)?)null )
.Where( l => l is not null ).Select( l => l!.Value ).ToArray();
for ( var f = 0; f < frames.Count; f++ )
{
if ( smoothed[f].LengthSquared() < 1e-10f ) continue;
FkUtil.ToWorld( frames[f], rig, world );
var feet = legs.Select( l => world[l.F] ).ToArray();
foreach ( var root in roots ) frames[f][root].Pos += smoothed[f];
if ( supported[f] ) continue;
// On its feet, the body moves on its legs: each foot stays where it was.
FkUtil.ToWorld( frames[f], rig, world );
for ( var i = 0; i < legs.Length; i++ )
{
var (h, k, ft) = legs[i];
var bend = Vector3.Cross( world[k].Pos - world[h].Pos, world[ft].Pos - world[k].Pos );
if ( bend.LengthSquared() < 1e-8f ) bend = Vector3.Transform( Vector3.UnitX, world[h].Rot );
var ik = TwoBoneIk.Solve( world[h].Pos, world[k].Pos, world[ft].Pos, feet[i].Pos, soften: 0, stableBendAxis: bend );
EffectorIk.ApplyWorldDeltas( frames[f], rig, h, k, ft, ik.UpperWorldDelta, ik.LowerWorldDelta, world );
FkUtil.ToWorld( frames[f], rig, world );
frames[f][ft].Rot = Quaternion.Normalize( Quaternion.Inverse( world[rig[ft].ParentIndex].Rot ) * feet[i].Rot );
FkUtil.ToWorld( frames[f], rig, world );
}
}
}
/// <summary>Segment mass fractions (de Leva 1996) between joint pairs, for the body's centre of mass.</summary>
static readonly (BoneRole From, BoneRole To, float Mass)[] Segments =
{
(BoneRole.Hips, BoneRole.Neck, .43f), (BoneRole.Head, BoneRole.Head, .08f),
(BoneRole.UpperArmL, BoneRole.LowerArmL, .027f), (BoneRole.UpperArmR, BoneRole.LowerArmR, .027f),
(BoneRole.LowerArmL, BoneRole.HandL, .022f), (BoneRole.LowerArmR, BoneRole.HandR, .022f),
(BoneRole.UpperLegL, BoneRole.LowerLegL, .14f), (BoneRole.UpperLegR, BoneRole.LowerLegR, .14f),
(BoneRole.LowerLegL, BoneRole.FootL, .06f), (BoneRole.LowerLegR, BoneRole.FootR, .06f)
};
/// <summary>A body on one hand is tilted at most this far to bring its weight over the hand, only where it stays on
/// that hand at least BalanceMinimumSeconds (a windmill's brief touches are not held long enough to tip the body
/// over), and the tilt is smoothed over BalanceSigmaFrames so it eases in and out.</summary>
public const float MaximumBalanceDegrees = 30, BalanceMinimumSeconds = .4f, BalanceSigmaFrames = 4;
/// <summary>The body's move over a carrying hand is smoothed over this many frames.</summary>
public const float CarrySigmaFrames = 3;
/// <summary>A hand off the floor for at most this many frames between two touches stays where it was.</summary>
public const int ShortLiftFrames = 2;
/// <summary>Frames over which a flat hand's heading on the floor is smoothed.</summary>
public const float HeadingSigmaFrames = 3;
/// <summary>Tilts a body that rests on a single hand about that wrist until its centre of mass is over it: on one
/// hand it cannot be anywhere else without falling. The breakin clip's one-handed spin leaned 14° off the hand
/// in the capture where SAM saw 5°, which swung the free hand on a circle a quarter wider. Two hands or a hand and
/// the head down share the weight, and the body is left as captured.</summary>
static void Balance( List<XForm[]> frames, TargetRig target, List<(int Hand, int First, int Last, Vector3 Anchor)> bearings,
Dictionary<int, (Vector3 Position, float Weight, bool Bearing, bool Lifting)[]> goals, Vector3 up, float fps )
{
var rig = target.Skeleton; var segments = Segments.Select( s => (A: target.BoneForRole( s.From ), B: target.BoneForRole( s.To ), s.Mass) ).ToArray();
if ( bearings.Count == 0 || segments.Any( s => s.A is null || s.B is null ) ) return;
var roots = Enumerable.Range( 0, rig.Count ).Where( b => rig[b].ParentIndex < 0 ).ToArray();
var world = new XForm[rig.Count]; var minimum = (int)MathF.Ceiling( BalanceMinimumSeconds * fps );
// The tilt each frame needs, as an axis times an angle, and the hand it turns about; zero where none is needed.
var needed = new Vector3[frames.Count]; var pivotHand = new int[frames.Count]; Array.Fill( pivotHand, -1 );
for ( var f = 0; f < frames.Count; f++ )
{
var on = bearings.Where( s => s.First <= f && f <= s.Last ).ToArray();
if ( on.Length != 1 || on[0].Last - on[0].First + 1 < minimum ) continue;
var hand = on[0].Hand; pivotHand[f] = hand;
FkUtil.ToWorld( frames[f], rig, world );
var centre = Vector3.Zero; var mass = 0f;
foreach ( var (a, b, m) in segments ) { centre += (world[a!.Value].Pos + world[b!.Value].Pos) * .5f * m; mass += m; }
var lever = centre / mass - world[hand].Pos;
if ( Vector3.Dot( lever, up ) <= 0 ) continue;
var axis = Vector3.Cross( Vector3.Normalize( lever ), up );
if ( axis.LengthSquared() < 1e-12f ) continue;
var angle = MathF.Min( MathF.Acos( Math.Clamp( Vector3.Dot( Vector3.Normalize( lever ), up ), -1, 1 ) ), MaximumBalanceDegrees * MathF.PI / 180 );
needed[f] = Vector3.Normalize( axis ) * angle * goals[hand][f].Weight;
}
// Smoothed over time, zeros included, so the tilt eases in and out instead of switching with the hand.
var radius = (int)MathF.Ceiling( 3 * BalanceSigmaFrames );
for ( var f = 0; f < frames.Count; f++ )
{
var sum = Vector3.Zero; var total = 0f; var hand = -1; var nearest = int.MaxValue;
for ( var j = Math.Max( 0, f - radius ); j <= Math.Min( frames.Count - 1, f + radius ); j++ )
{
var g = MathF.Exp( -.5f * (j - f) * (j - f) / (BalanceSigmaFrames * BalanceSigmaFrames) ); sum += needed[j] * g; total += g;
if ( pivotHand[j] >= 0 && Math.Abs( j - f ) < nearest ) { nearest = Math.Abs( j - f ); hand = pivotHand[j]; }
}
var turn = sum / total;
if ( hand < 0 || turn.LengthSquared() < 1e-10f ) continue;
var tilt = Quaternion.CreateFromAxisAngle( Vector3.Normalize( turn ), turn.Length() );
FkUtil.ToWorld( frames[f], rig, world );
var pivot = world[hand].Pos;
foreach ( var root in roots )
{
frames[f][root].Pos = pivot + Vector3.Transform( frames[f][root].Pos - pivot, tilt );
frames[f][root].Rot = Quaternion.Normalize( tilt * frames[f][root].Rot );
}
}
}
/// <summary>The direction the palm faces, from the wrist and the index, middle and outermost knuckles. The back of a
/// right hand faces along (knuckle line × index-to-outermost); a left hand mirrors it.</summary>
static Vector3 PalmNormal( XForm[] world, int[] chain, int hand, bool left )
{
var along = world[chain[3]].Pos - world[hand].Pos; var across = world[chain[0]].Pos - world[chain[^3]].Pos;
var normal = Vector3.Cross( along, across );
if ( normal.LengthSquared() < 1e-10f ) return Vector3.Zero;
normal = Vector3.Normalize( normal );
return left ? normal : -normal;
}
/// <summary>Turns each finger joint toward straight, by <paramref name="weight"/>: each segment continues the one
/// before, so a flat palm puts the fingers along the floor. The roll is the rig's rest roll, not the capture's: kept,
/// the capture's roll twisted the fingers of a flat hand back and forth as it turned through a spin.
/// <paramref name="first"/> segments of each finger are left as they are.</summary>
static void Straighten( XForm[] frame, HumanoidMocapX.Core.Skeleton.Skeleton rig, int[] chain, float weight, int first = 0 )
{
for ( var finger = 0; finger < chain.Length / 3; finger++ )
for ( var segment = first; segment < 3; segment++ )
{
var bone = chain[finger * 3 + segment];
// The parent's bone runs along this joint's offset; this bone runs along its child's (the tip along its own).
var inherited = frame[bone].Pos; var own = segment < 2 ? frame[chain[finger * 3 + segment + 1]].Pos : frame[bone].Pos;
if ( inherited.LengthSquared() < 1e-10f || own.LengthSquared() < 1e-10f ) continue;
var rest = rig[bone].RestLocal.Rot;
var straight = Quaternion.Normalize( FromTo( Vector3.Normalize( Vector3.Transform( own, rest ) ), Vector3.Normalize( inherited ) ) * rest );
frame[bone].Rot = Quaternion.Normalize( Quaternion.Slerp( frame[bone].Rot, straight, weight ) );
}
}
/// <summary>Lays the thumb of a flat hand out along the floor beside the fingers, by <paramref name="weight"/>: its
/// first bone at the rig's rest splay from the hand, turned level with the floor, the rest of it straight. The
/// capture's thumb, curled for gripping, stayed tucked under the flat palm.</summary>
static void LayThumb( XForm[] frame, HumanoidMocapX.Core.Skeleton.Skeleton rig, XForm[] world, int[] thumb, Vector3 up, float weight )
{
var first = thumb[0];
frame[first].Rot = Quaternion.Normalize( Quaternion.Slerp( frame[first].Rot, rig[first].RestLocal.Rot, weight ) );
FkUtil.ToWorld( frame, rig, world );
var along = world[thumb[1]].Pos - world[first].Pos; var level = Flat( along, up );
if ( level.LengthSquared() > 1e-8f && along.LengthSquared() > 1e-8f )
{
var turn = Quaternion.Slerp( Quaternion.Identity, FromTo( Vector3.Normalize( along ), Vector3.Normalize( level ) ), weight );
var parent = rig[first].ParentIndex;
frame[first].Rot = Quaternion.Normalize( Quaternion.Inverse( world[parent].Rot ) * Quaternion.Normalize( turn * world[first].Rot ) );
}
Straighten( frame, rig, thumb, weight, first: 1 );
FkUtil.ToWorld( frame, rig, world );
}
/// <summary>Per frame of each hold, the heading a flat hand has on the floor (toward its middle knuckle), smoothed over
/// <see cref="HeadingSigmaFrames"/> within the hold; NaN outside holds. Through the breakin clip's spin on one hand the
/// support hand turned with the body, 30° a frame, which smoothing keeps; in the closing freeze the capture wobbled it
/// 33° over three frames and back, which it removes.</summary>
static double[] Headings( List<XForm[]> frames, HumanoidMocapX.Core.Skeleton.Skeleton rig, int hand, int[] chain, bool left,
(Vector3 Position, float Weight, bool Bearing, bool Lifting)[] goals, Vector3 up )
{
var raw = new double[frames.Count]; Array.Fill( raw, double.NaN ); var world = new XForm[rig.Count];
for ( var f = 0; f < frames.Count; f++ )
{
if ( !goals[f].Bearing || goals[f].Weight <= 0 ) continue;
FkUtil.ToWorld( frames[f], rig, world );
var flat = Quaternion.Normalize( FromTo( PalmNormal( world, chain, hand, left ), -up ) * world[hand].Rot );
raw[f] = Heading( Vector3.Transform( frames[f][chain[3]].Pos, flat ), up );
if ( f > 0 && !double.IsNaN( raw[f - 1] ) ) raw[f] = raw[f - 1] + Math.IEEERemainder( raw[f] - raw[f - 1], 2 * Math.PI );
}
var smoothed = new double[frames.Count]; var radius = (int)Math.Ceiling( 3 * HeadingSigmaFrames );
for ( var f = 0; f < frames.Count; f++ )
{
smoothed[f] = double.NaN;
if ( double.IsNaN( raw[f] ) ) continue;
double sum = 0, total = 0;
// Only within this hold: the unwrapped heading is continuous there.
for ( var j = f; j >= Math.Max( 0, f - radius ) && !double.IsNaN( raw[j] ); j-- ) Add( j );
for ( var j = f + 1; j <= Math.Min( frames.Count - 1, f + radius ) && !double.IsNaN( raw[j] ); j++ ) Add( j );
smoothed[f] = sum / total;
void Add( int j ) { var g = Math.Exp( -.5 * (j - f) * (j - f) / (HeadingSigmaFrames * HeadingSigmaFrames) ); sum += g * raw[j]; total += g; }
}
return smoothed;
}
/// <summary>The angle of <paramref name="v"/> on the floor, increasing with turns about <paramref name="up"/>.</summary>
static float Heading( Vector3 v, Vector3 up ) => MathF.Atan2( Vector3.Dot( v, Vector3.Cross( up, Vector3.UnitX ) ), Vector3.Dot( v, Vector3.UnitX ) );
static Quaternion FromTo( Vector3 from, Vector3 to )
{
if ( from.LengthSquared() < 1e-10f || to.LengthSquared() < 1e-10f ) return Quaternion.Identity;
return MathQ.FromTo( from, to );
}
static bool IsWithin( HumanoidMocapX.Core.Skeleton.Skeleton rig, int bone, int ancestor )
{
for ( var b = bone; b >= 0; b = rig[b].ParentIndex ) if ( b == ancestor ) return true;
return false;
}
}