Editor tool that rebuilds a full-body MotionDocument using a MotionBricks generative model. It resamples and levels the capture, finds key poses (anchors), synthesizes AI in-betweens via the model, splits where generated motion deviates from the capture, blends/recovers torso/limb details, plants feet with IK, preserves fingers/head/wrists from the capture, and writes a rebuilt MotionDocument and diagnostics.
#nullable enable annotations
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using HumanoidMocap.Core.Mapping;
using HumanoidMocap.Core.Maths;
using HumanoidMocap.Core.Motion;
namespace HumanoidMocap.EditorTools.MotionBricks;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// Rebuilds a capture's body motion with MotionBricks, the way an animator keys a shot: key poses are
/// taken from the capture where the body briefly settles (0.6–1.9 s apart, at lengths the model plays at
/// its own timing), and the motion between them is generated by the model, each in-between continuing
/// the previous one and landing exactly on the next key pose. Where an in-between strays more than <see cref="Tolerance"/> from the
/// capture (a kick between two settled poses), the capture's pose at the worst frame becomes another
/// key and both halves are generated again, so the rebuild keeps the performance. The model has no fingers, neck or head, so those keep the capture. The
/// source document is not changed; the rebuilt one keeps its timestamps, contacts and evidence.
/// </summary>
public static class MotionBricksRebuild
{
/// <summary>The model's frame rate.</summary>
const float Fps = G1Motion.Fps;
/// <summary>Key pose spacing in model frames: long enough for the model to shape the motion, short enough to keep the performance.</summary>
public const int MinimumGap = 17, MaximumGap = 57;
/// <summary>The in-between lengths the model generates at its own timing: 6–16 tokens of 4 frames, less the 7 boundary frames.</summary>
public static readonly int[] NativeLengths = Enumerable.Range( MotionBricksModel.MinTokens, MotionBricksModel.MaxTokens - MotionBricksModel.MinTokens + 1 )
.Select( t => t * MotionBricksModel.FramesPerToken - 7 ).ToArray();
/// <summary>How many frames an in-between of <paramref name="length"/> must be stretched or squeezed by (0 at a native length).</summary>
public static int Warp( int length ) => NativeLengths.Min( n => Math.Abs( n - length ) );
/// <summary>Shortest gap the model fills (from AI Animator); shorter tails keep the capture.</summary>
const int ModelMinimumLength = 10;
/// <summary>How far (metres, relative to the hips) a body joint of an in-between may stray from the capture before a key pose
/// is added there: enough for the model's own timing and style, not for a missing kick or swing.</summary>
public const float Tolerance = .15f;
/// <summary>Tuning, for the offline comparison harness; the defaults are what the editor uses.</summary>
public sealed record Options
{
/// <summary>Take the model's most likely motion instead of a random sample (measured smoother, and repeatable).</summary>
public bool Argmax { get; init; } = true;
/// <summary>Gaussian smoothing (frames) of the capture the key poses and targets come from, so the model does not aim at
/// the capture's frame-to-frame jitter; 0 = raw.</summary>
public float KeySmoothing { get; init; } = 1.5f;
/// <summary>Instead of <see cref="KeySmoothing"/>: a quadratic Savitzky–Golay filter of this half-width (frames), which removes
/// jitter but keeps the height of quick peaks such as kicks; 0 = off.</summary>
public int KeyPeakKeeping { get; init; } = 4;
/// <summary>Shortest in-between a split may leave.</summary>
public int MinimumSplit { get; init; } = ModelMinimumLength;
public bool SmoothJoins { get; init; } = true;
public float Tolerance { get; init; } = MotionBricksRebuild.Tolerance;
/// <summary>Blend back toward the (smoothed) capture where the model still misses a move it could not be split around.</summary>
public bool RecoverMisses { get; init; } = true;
/// <summary>Raise the AI's leg to the capture's height at each kick's peak (the thigh only, eased in and out), keeping its timing and knee.</summary>
public bool KeepKickHeight { get; init; } = true;
/// <summary>Pin each foot where the capture's foot is planted (leg IK), so the AI's feet do not slide.</summary>
public bool PlantFeet { get; init; } = true;
/// <summary>Degrees the AI's pelvis may lean or turn away from the capture's before the pelvis and spine are eased back
/// toward it (fully at <see cref="TorsoFull"/>); 0 turns it off.</summary>
public float TorsoFrom { get; init; } = 4f;
public float TorsoFull { get; init; } = 10f;
/// <summary>A move counts as missed beyond this distance (metres, relative to the hips), and is fully recovered at <see cref="RecoverFull"/>.</summary>
public float RecoverFrom { get; init; } = .06f;
public float RecoverFull { get; init; } = .14f;
/// <summary>Frames either side a miss is held, and the smoothing (frames) of the blend weight.</summary>
public int RecoverHold { get; init; }
public float RecoverSigma { get; init; } = 2f;
}
public static Options Settings { get; set; } = new();
public sealed record Result( MotionDocument Motion, int KeyPoses, int InBetweens, TimeSpan Elapsed, IReadOnlyList<double> KeyTimes = null, float AiShare = 1 );
public static Result Rebuild( MotionDocument source, MotionBricksModel model, RetargeterLibrary library, IProgress<float>? progress = null, CancellationToken cancel = default )
{
var watch = System.Diagnostics.Stopwatch.StartNew();
var bones = source.Bones;
if ( !new[] { BoneRole.Hips, BoneRole.UpperLegL, BoneRole.UpperLegR, BoneRole.FootL, BoneRole.FootR, BoneRole.HandL, BoneRole.HandR }.All( r => bones.Any( b => b.Role == r ) ) )
throw new InvalidOperationException( "MotionBricks rebuilds whole-body captures; this capture has no legs." );
var root = bones.FindIndex( b => b.Parent < 0 );
var bridge = new CaptureMotionBridge( library, bones, model.Skeleton );
// Model frames: the capture resampled to 30 fps, levelled with gravity and standing on y = 0.
var start = source.Frames[0].Time;
var count = Math.Max( 2, (int)Math.Floor( (source.Frames[^1].Time - start) * Fps + 1e-6 ) + 1 );
var level = Level( source );
var original = new XForm[count][];
for ( var k = 0; k < count; k++ )
{
var pose = Sample( source, start + k / (double)Fps );
pose[root] = new XForm( Vector3.Transform( pose[root].Pos, level ), Quaternion.Normalize( level * pose[root].Rot ) );
original[k] = pose;
}
var floor = Floor( bones, original );
for ( var k = 0; k < count; k++ )
original[k][root] = new XForm( original[k][root].Pos - new Vector3( 0, floor, 0 ), original[k][root].Rot );
var capture = original;
if ( Settings.KeyPeakKeeping > 0 )
original = SmoothPoses( bones, original, SavitzkyGolay( Settings.KeyPeakKeeping ) );
else if ( Settings.KeySmoothing > 0 )
original = SmoothPoses( bones, original, Gaussian( Settings.KeySmoothing ) );
var anchors = Anchors( bones, original );
var working = original.Select( p => (XForm[])p.Clone() ).ToArray();
var detail = DetailBones( bones );
var body = Enumerable.Range( 0, bones.Count ).Where( j => !detail.Contains( j ) || bones[j].Role is BoneRole.Head or BoneRole.HandL or BoneRole.HandR ).ToArray();
var originalWorld = capture.Select( p => World( bones, p ) ).ToArray();
var generated = 0;
var keys = new SortedSet<int>( anchors );
// In order, so every in-between continues the one before it; a split re-queues both halves at the front.
var pending = new Stack<(int A, int B)>();
for ( var s = anchors.Count - 2; s >= 0; s-- )
pending.Push( (anchors[s], anchors[s + 1]) );
while ( pending.Count > 0 )
{
cancel.ThrowIfCancellationRequested();
var (a, b) = pending.Pop();
progress?.Report( a / (float)(count - 1) );
var length = b - a;
if ( length < ModelMinimumLength )
continue;
var context = Enumerable.Range( a - 3, 4 ).Select( f => working[Math.Clamp( f, 0, count - 1 )] ).ToList();
var target = Enumerable.Range( b, 4 ).Select( f => original[Math.Clamp( f, 0, count - 1 )] ).ToList();
var frames = Synthesize( model, bridge, context, target, length, 1UL + (ulong)a * 7919UL, cancel, Settings.Argmax );
HitKeys( frames, working[a], original[b] );
for ( var t = 0; t <= length; t++ )
foreach ( var bone in detail )
frames[t][bone] = capture[a + t][bone];
// The worst frame, comparing the pose shape (relative to the hips).
var worst = -1;
var worstDistance = Settings.Tolerance;
for ( var t = 1; t < length; t++ )
{
var w = World( bones, frames[t] );
var o = originalWorld[a + t];
var d = body.Max( j => Vector3.Distance( w[j] - w[root], o[j] - o[root] ) );
if ( d > worstDistance )
{
worstDistance = d;
worst = t;
}
}
var split = Math.Max( ModelMinimumLength, Settings.MinimumSplit );
if ( worst >= 0 && length >= 2 * split )
{
// Split near the worst frame, preferring halves the model plays at its own timing (no stretching);
// halves shorter than its shortest in-between are squeezed only when the gap allows nothing else.
var m = a + Math.Clamp( worst, split, length - split );
var best = float.MaxValue;
for ( var c = a + split; c <= b - split; c++ )
{
var cost = Math.Abs( c - (a + worst) ) + 2f * (Warp( c - a ) + Warp( b - c ));
if ( cost < best ) { best = cost; m = c; }
}
{
keys.Add( m );
pending.Push( (m, b) );
pending.Push( (a, m) );
continue;
}
}
for ( var t = 0; t <= length; t++ )
working[a + t] = frames[t];
generated++;
}
anchors = keys.ToList();
if ( Settings.SmoothJoins )
SmoothJoins( bones, working, anchors, detail, root );
var captureShare = Settings.RecoverMisses ? RecoverMisses( bones, working, original, originalWorld, detail, root ) : 0;
if ( Settings.TorsoFrom > 0 )
RecoverTorso( bones, working, original, detail, root );
var kicks = Settings.KeepKickHeight ? KeepKickHeight( bones, working, originalWorld ) : 0;
// Last, so nothing above moves a planted foot again.
if ( Settings.PlantFeet )
PlantFeet( bones, working, capture, originalWorld );
// Back to the capture's own frames, floor and tilt.
var unlevel = Quaternion.Conjugate( level );
var rebuilt = source.Copy();
for ( var i = 0; i < rebuilt.Frames.Count; i++ )
{
var frame = rebuilt.Frames[i];
var g = Math.Clamp( (frame.Time - start) * Fps, 0, count - 1 );
var lo = (int)Math.Floor( g );
var hi = Math.Min( lo + 1, count - 1 );
var blend = (float)(g - lo);
for ( var j = 0; j < bones.Count; j++ )
{
// Bones the model does not drive keep their exact captured values (no resampling).
if ( j != root && detail.Contains( j ) )
continue;
var p = Vector3.Lerp( working[lo][j].Pos, working[hi][j].Pos, blend );
var q = Quaternion.Normalize( Quaternion.Slerp( working[lo][j].Rot, working[hi][j].Rot, blend ) );
if ( j == root )
{
p = Vector3.Transform( p + new Vector3( 0, floor, 0 ), unlevel );
q = Quaternion.Normalize( unlevel * q );
}
frame.Positions[j] = MotionDocument.A( p );
frame.Rotations[j] = MotionDocument.A( q );
}
}
rebuilt.Diagnostics.Add( $"Rebuilt with MotionBricks: {anchors.Count} key poses from the capture, {generated} in-betweens generated{(captureShare >= .005f ? FormattableString.Invariant( $", {captureShare * 100:F0}% of the limb motion taken back from the capture where the model missed a move" ) : "")}{(kicks > 0 ? $", {kicks} kicks raised to the captured height" : "")}; fingers, wrists, neck and head kept from the capture." );
rebuilt.Corrections.Add( new MotionCorrection { Type = "motionbricks-rebuild", Start = source.Frames[0].Time, End = source.Frames[^1].Time,
Settings = new Dictionary<string, float> { ["keyPoses"] = anchors.Count, ["inBetweens"] = generated } } );
progress?.Report( 1 );
return new Result( rebuilt, anchors.Count, generated, watch.Elapsed, anchors.Select( k => start + k / (double)Fps ).ToList(), 1 - captureShare );
}
/// <summary>One in-between: port of AI Animator's <c>MotionBricksSynthesizer</c>. The boundary frames go to the G1 in the model's
/// canonical frame (first context frame at the origin, heading zero), are generated, restored and time-warped onto the gap.</summary>
static XForm[][] Synthesize( MotionBricksModel model, CaptureMotionBridge bridge, IReadOnlyList<XForm[]> context, IReadOnlyList<XForm[]> target, int length, ulong seed, CancellationToken cancel, bool argmax = false )
{
bridge.ResetProjection();
var boundary = bridge.ToG1( context.Concat( target ).ToList() );
var origin = boundary.Root[0];
var heading = MotionRepresentation.Heading( boundary.Local[0][0] );
var toCanonical = Quaternion.CreateFromAxisAngle( Vector3.UnitY, -heading );
var fromCanonical = Quaternion.Conjugate( toCanonical );
var roots = boundary.Root.Select( p => Vector3.Transform( new Vector3( p.X - origin.X, p.Y, p.Z - origin.Z ), toCanonical ) ).ToArray();
var locals = boundary.Local.Select( l =>
{
var copy = (Quaternion[])l.Clone();
copy[0] = Quaternion.Normalize( toCanonical * copy[0] );
return copy;
} ).ToArray();
var representation = model.Representation;
var encodedContext = representation.Encode( roots.AsSpan( 0, 4 ), locals.Take( 4 ).ToList(), repeatLastVelocity: false );
var encodedTarget = representation.Encode( roots.AsSpan( 4, 4 ), locals.Skip( 4 ).ToList(), repeatLastVelocity: true );
var tokens = Math.Clamp( (int)MathF.Round( (length + 7) / 4f ), MotionBricksModel.MinTokens, MotionBricksModel.MaxTokens );
var motion = model.RunTransition( new TransitionConstraints { Context = encodedContext, Target = encodedTarget, Tokens = tokens, Seed = seed, Argmax = argmax }, cancel );
for ( var f = 0; f < motion.FrameCount; f++ )
{
var p = Vector3.Transform( motion.Root[f], fromCanonical );
motion.Root[f] = new Vector3( p.X + origin.X, p.Y, p.Z + origin.Z );
motion.Local[f][0] = Quaternion.Normalize( fromCanonical * motion.Local[f][0] );
}
// Generated frame 3 is the gap's first frame and frame F-4 its last.
var span = motion.FrameCount - 7;
var warped = new G1Motion( length + 1 );
for ( var t = 0; t <= length; t++ )
{
var g = 3f + t * span / (float)length;
var lo = Math.Clamp( (int)MathF.Floor( g ), 0, motion.FrameCount - 1 );
var hi = Math.Min( lo + 1, motion.FrameCount - 1 );
var blend = g - lo;
warped.Root[t] = Vector3.Lerp( motion.Root[lo], motion.Root[hi], blend );
for ( var j = 0; j < G1Skeleton.JointCount; j++ )
warped.Local[t][j] = Quaternion.Normalize( Quaternion.Slerp( motion.Local[lo][j], motion.Local[hi][j], blend ) );
}
cancel.ThrowIfCancellationRequested();
return bridge.ToCapture( warped );
}
static float[] Gaussian( float sigma )
{
var radius = (int)MathF.Ceiling( 3 * sigma );
return Enumerable.Range( -radius, 2 * radius + 1 ).Select( k => MathF.Exp( -k * k / (2 * sigma * sigma) ) ).ToArray();
}
/// <summary>Quadratic Savitzky–Golay smoothing weights over 2·<paramref name="half"/>+1 frames (they sum to 1; the outer ones are negative).</summary>
static float[] SavitzkyGolay( int half )
{
float m = half;
var norm = (2 * m + 1) * (4 * m * m + 4 * m - 3) / 3f;
return Enumerable.Range( -half, 2 * half + 1 ).Select( k => (3 * m * m + 3 * m - 1 - 5f * k * k) / norm ).ToArray();
}
/// <summary>The capture filtered with <paramref name="kernel"/> (centred) on the bones the model drives and the hips' path.</summary>
static XForm[][] SmoothPoses( IReadOnlyList<MotionBone> bones, XForm[][] frames, float[] kernel )
{
var count = frames.Length;
var radius = kernel.Length / 2;
var result = frames.Select( f => (XForm[])f.Clone() ).ToArray();
for ( var t = 0; t < count; t++ )
for ( var j = 0; j < bones.Count; j++ )
{
if ( bones[j].Group == "fingers" )
continue;
var position = Vector3.Zero;
var rotation = new Quaternion( 0, 0, 0, 0 );
var total = 0f;
for ( var k = -radius; k <= radius; k++ )
{
var f = Math.Clamp( t + k, 0, count - 1 );
var w = kernel[k + radius];
var q = frames[f][j].Rot;
if ( Quaternion.Dot( q, frames[t][j].Rot ) < 0 )
q = Quaternion.Negate( q );
position += frames[f][j].Pos * w;
rotation += q * w;
total += w;
}
result[t][j] = new XForm( position / total, Quaternion.Normalize( rotation ) );
}
return result;
}
/// <summary>Where the rebuild still misses a move (one quicker than the model's shortest in-between, or a kick the model keeps
/// low), that limb is blended toward the smoothed capture. Each leg, each arm and the spine is judged on its own, by where its
/// end (foot, hand, neck) sits relative to where the limb starts: a miss beyond <see cref="Options.RecoverFrom"/> starts the
/// blend, fully at <see cref="Options.RecoverFull"/>, the weight smoothed over time so the hand-over never pops. The rest of the
/// body keeps the model's motion. Returns the share of the motion (limbs × frames) taken from the capture.</summary>
static float RecoverMisses( IReadOnlyList<MotionBone> bones, XForm[][] frames, XForm[][] target, Vector3[][] captureWorld, HashSet<int> detail, int root )
{
var count = frames.Length;
int Role( BoneRole r ) => bones.ToList().FindIndex( b => b.Role == r );
var groups = new List<(int Start, int End, int[] Bones)>();
void Group( int start, int end, Func<int, bool> include )
{
if ( start < 0 || end < 0 )
return;
var members = Enumerable.Range( 0, bones.Count ).Where( j => j != root && !detail.Contains( j ) && include( j ) ).ToArray();
if ( members.Length > 0 )
groups.Add( (start, end, members) );
}
var legL = Role( BoneRole.UpperLegL );
var legR = Role( BoneRole.UpperLegR );
var armL = Role( BoneRole.ClavicleL ) is var cl and >= 0 ? cl : Role( BoneRole.UpperArmL );
var armR = Role( BoneRole.ClavicleR ) is var cr and >= 0 ? cr : Role( BoneRole.UpperArmR );
var limbs = new[] { legL, legR, armL, armR };
Group( legL, Role( BoneRole.ToeL ) is var tl and >= 0 ? tl : Role( BoneRole.FootL ), j => Under( bones, j, legL ) );
Group( legR, Role( BoneRole.ToeR ) is var tr and >= 0 ? tr : Role( BoneRole.FootR ), j => Under( bones, j, legR ) );
Group( armL, Role( BoneRole.HandL ), j => Under( bones, j, armL ) );
Group( armR, Role( BoneRole.HandR ), j => Under( bones, j, armR ) );
Group( root, Role( BoneRole.Neck ), j => !limbs.Any( l => l >= 0 && Under( bones, j, l ) ) );
var sigma = Settings.RecoverSigma;
var hold = Settings.RecoverHold;
var radius = (int)MathF.Ceiling( 3 * sigma );
var rebuiltWorld = frames.Select( f => World( bones, f ) ).ToArray();
var capture = 0f;
foreach ( var (start, end, members) in groups )
{
var raw = new float[count];
for ( var t = 0; t < count; t++ )
{
var d = Vector3.Distance( rebuiltWorld[t][end] - rebuiltWorld[t][start], captureWorld[t][end] - captureWorld[t][start] );
raw[t] = Math.Clamp( (d - Settings.RecoverFrom) / Math.Max( .01f, Settings.RecoverFull - Settings.RecoverFrom ), 0, 1 );
}
var held = Enumerable.Range( 0, count ).Select( t => Enumerable.Range( Math.Max( 0, t - hold ), Math.Min( count, t + hold + 1 ) - Math.Max( 0, t - hold ) ).Max( k => raw[k] ) ).ToArray();
for ( var t = 0; t < count; t++ )
{
float sum = 0, total = 0;
for ( var k = -radius; k <= radius; k++ )
{
var w = MathF.Exp( -k * k / (2 * sigma * sigma) );
sum += held[Math.Clamp( t + k, 0, count - 1 )] * w;
total += w;
}
var weight = Math.Min( 1f, sum / total );
capture += weight;
if ( weight < .01f )
continue;
foreach ( var j in members )
frames[t][j] = new XForm( Vector3.Lerp( frames[t][j].Pos, target[t][j].Pos, weight ),
Quaternion.Normalize( Quaternion.Slerp( frames[t][j].Rot, target[t][j].Rot, weight ) ) );
}
}
return groups.Count == 0 ? 0 : capture / (groups.Count * count);
}
/// <summary>The model keeps the pelvis more upright than a performer who leans into a move (measured on a dance: 9° typical,
/// 23° at worst), which squares the whole body up against the video. Where the pelvis leans or turns more than
/// <see cref="Options.TorsoFrom"/> away from the capture's, its rotation and the spine's are eased toward the capture's, fully at
/// <see cref="Options.TorsoFull"/>, the weight smoothed over time. The pelvis position (the path) stays the model's.</summary>
static void RecoverTorso( IReadOnlyList<MotionBone> bones, XForm[][] frames, XForm[][] target, HashSet<int> detail, int root )
{
var count = frames.Length;
var limbs = new[] { BoneRole.UpperLegL, BoneRole.UpperLegR, BoneRole.ClavicleL, BoneRole.ClavicleR, BoneRole.UpperArmL, BoneRole.UpperArmR }
.Select( r => bones.ToList().FindIndex( b => b.Role == r ) ).Where( i => i >= 0 ).ToArray();
var spine = Enumerable.Range( 0, bones.Count ).Where( j => j != root && !detail.Contains( j ) && !limbs.Any( l => Under( bones, j, l ) ) ).ToArray();
float Degrees( Quaternion a, Quaternion b ) => 2 * MathF.Acos( Math.Clamp( MathF.Abs( Quaternion.Dot( Quaternion.Normalize( a ), Quaternion.Normalize( b ) ) ), 0f, 1f ) ) * 180 / MathF.PI;
var raw = Enumerable.Range( 0, count ).Select( t => Math.Clamp( (Degrees( frames[t][root].Rot, target[t][root].Rot ) - Settings.TorsoFrom)
/ Math.Max( .1f, Settings.TorsoFull - Settings.TorsoFrom ), 0, 1 ) ).ToArray();
var sigma = Settings.RecoverSigma;
var radius = (int)MathF.Ceiling( 3 * sigma );
for ( var t = 0; t < count; t++ )
{
float sum = 0, total = 0;
for ( var k = -radius; k <= radius; k++ )
{
var w = MathF.Exp( -k * k / (2 * sigma * sigma) );
sum += raw[Math.Clamp( t + k, 0, count - 1 )] * w;
total += w;
}
var weight = Math.Min( 1f, sum / total );
if ( weight < .01f )
continue;
frames[t][root] = new XForm( frames[t][root].Pos, Quaternion.Normalize( Quaternion.Slerp( frames[t][root].Rot, target[t][root].Rot, weight ) ) );
foreach ( var j in spine )
frames[t][j] = new XForm( Vector3.Lerp( frames[t][j].Pos, target[t][j].Pos, weight ),
Quaternion.Normalize( Quaternion.Slerp( frames[t][j].Rot, target[t][j].Rot, weight ) ) );
}
}
/// <summary>A foot counts as planted in the capture within this height (metres) of its lowest and below this horizontal speed (m/s).</summary>
const float PlantHeight = .03f, PlantSpeed = .2f;
/// <summary>Frames over which a plant lock eases in and out (measured: over 3 frames the foot snapped onto its spot at 40 cm/s),
/// and the longest lift inside a plant that is bridged.</summary>
const int PlantBlend = 6, PlantBridge = 3;
/// <summary>The model moves its own legs between key poses, and its hips wander a few centimetres from the capture's, so a foot the
/// performer kept planted slid (measured on a floss dance: 3 cm/s in the capture, 50 cm/s rebuilt). Wherever the capture's foot is
/// planted, the rebuilt leg is re-solved (two-bone IK, the knee kept on its side) so the ankle sits where the capture's does and the
/// foot and toe take the capture's angle, easing in and out over <see cref="PlantBlend"/> frames. Everything above the feet stays
/// the model's.</summary>
static void PlantFeet( IReadOnlyList<MotionBone> bones, XForm[][] frames, XForm[][] capture, Vector3[][] captureWorld )
{
var count = frames.Length;
var chains = new[]
{
(BoneRole.UpperLegL, BoneRole.LowerLegL, BoneRole.FootL, BoneRole.ToeL),
(BoneRole.UpperLegR, BoneRole.LowerLegR, BoneRole.FootR, BoneRole.ToeR),
};
int Role( BoneRole r ) => bones.ToList().FindIndex( b => b.Role == r );
var weights = chains.Select( c => PlantWeights( captureWorld, Role( c.Item3 ) ) ).ToArray();
// A planted leg can only reach the capture's spot if the hips are near where the capture had them (the model's hips
// wander up to 10-20 cm): while either foot is planted the hips ease to the capture's position.
var root = bones.ToList().FindIndex( b => b.Parent < 0 );
if ( root >= 0 )
for ( var t = 0; t < count; t++ )
{
var w = Math.Max( weights[0]?[t] ?? 0, weights[1]?[t] ?? 0 );
if ( w > 0 )
frames[t][root] = new XForm( Vector3.Lerp( frames[t][root].Pos, capture[t][root].Pos, w ), frames[t][root].Rot );
}
for ( var c = 0; c < chains.Length; c++ )
{
var (thighRole, shinRole, footRole, toeRole) = chains[c];
int thigh = Role( thighRole ), shin = Role( shinRole ), foot = Role( footRole ), toe = Role( toeRole );
if ( weights[c] is not { } plant || thigh < 0 || shin < 0 || bones[shin].Parent != thigh || bones[foot].Parent != shin )
continue;
for ( var t = 0; t < count; t++ )
{
var weight = plant[t];
if ( weight <= 0 )
continue;
var world = WorldTransforms( bones, frames[t] );
var target = Vector3.Lerp( world[foot].Pos, captureWorld[t][foot], weight );
Vector3 s = world[thigh].Pos, e = world[shin].Pos, w = world[foot].Pos;
float upper = Vector3.Distance( s, e ), lower = Vector3.Distance( e, w );
if ( upper < 1e-4f || lower < 1e-4f )
continue;
var toGoal = target - s;
var d = Math.Clamp( toGoal.Length(), MathF.Abs( upper - lower ) + 1e-4f, upper + lower - 1e-4f );
var direction = Vector3.Normalize( toGoal );
var pole = e - s;
pole -= direction * Vector3.Dot( pole, direction );
if ( pole.LengthSquared() < 1e-10f )
continue;
pole = Vector3.Normalize( pole );
var along = (upper * upper - lower * lower + d * d) / (2 * d);
var knee = s + direction * along + pole * MathF.Sqrt( Math.Max( 0, upper * upper - along * along ) );
var ankle = s + direction * d;
var bend = Between( e - s, knee - s );
var thighWorld = Quaternion.Normalize( bend * world[thigh].Rot );
var shinTurned = Quaternion.Normalize( bend * world[shin].Rot );
var shinWorld = Quaternion.Normalize( Between( Vector3.Transform( w - e, bend ), ankle - knee ) * shinTurned );
// The foot takes the capture's angle as the lock comes in.
var captureFoot = WorldTransforms( bones, capture[t] )[foot].Rot;
var footWorld = Quaternion.Normalize( Quaternion.Slerp( world[foot].Rot, captureFoot, weight ) );
var parentWorld = bones[thigh].Parent < 0 ? Quaternion.Identity : world[bones[thigh].Parent].Rot;
frames[t][thigh] = new XForm( frames[t][thigh].Pos, Quaternion.Normalize( Quaternion.Conjugate( parentWorld ) * thighWorld ) );
frames[t][shin] = new XForm( frames[t][shin].Pos, Quaternion.Normalize( Quaternion.Conjugate( thighWorld ) * shinWorld ) );
frames[t][foot] = new XForm( frames[t][foot].Pos, Quaternion.Normalize( Quaternion.Conjugate( shinWorld ) * footWorld ) );
if ( toe >= 0 && bones[toe].Parent == foot )
frames[t][toe] = new XForm( frames[t][toe].Pos, Quaternion.Normalize( Quaternion.Slerp( frames[t][toe].Rot, capture[t][toe].Rot, weight ) ) );
}
}
}
/// <summary>Per frame, how firmly the capture plants <paramref name="foot"/>: 1 while it is low and still, easing to 0 over
/// <see cref="PlantBlend"/> frames either side; lifts up to <see cref="PlantBridge"/> frames inside a plant are bridged. Null when there is no such bone.</summary>
static float[] PlantWeights( Vector3[][] captureWorld, int foot )
{
if ( foot < 0 )
return null;
var count = captureWorld.Length;
var heights = captureWorld.Select( w => w[foot].Y ).OrderBy( y => y ).ToArray();
var lowest = heights[Math.Min( heights.Length - 1, heights.Length / 20 )];
var planted = new bool[count];
for ( var t = 0; t < count; t++ )
{
int a = Math.Max( 0, t - 1 ), b = Math.Min( count - 1, t + 1 );
var step = captureWorld[b][foot] - captureWorld[a][foot];
var speed = MathF.Sqrt( step.X * step.X + step.Z * step.Z ) * Fps / Math.Max( 1, b - a );
planted[t] = captureWorld[t][foot].Y < lowest + PlantHeight && speed < PlantSpeed;
}
var bridged = (bool[])planted.Clone();
for ( var t = 0; t < count; t++ )
if ( !planted[t] )
{
var before = Enumerable.Range( 1, PlantBridge ).Any( k => t - k >= 0 && planted[t - k] );
var after = Enumerable.Range( 1, PlantBridge ).Any( k => t + k < count && planted[t + k] );
if ( before && after )
bridged[t] = true;
}
var result = new float[count];
for ( var t = 0; t < count; t++ )
{
if ( bridged[t] ) { result[t] = 1; continue; }
var distance = Enumerable.Range( 1, PlantBlend ).FirstOrDefault( k => (t - k >= 0 && bridged[t - k]) || (t + k < count && bridged[t + k]) );
if ( distance == 0 )
continue;
var w = 1f - distance / (PlantBlend + 1f);
result[t] = w * w * (3 - 2 * w);
}
return result;
}
static XForm[] WorldTransforms( IReadOnlyList<MotionBone> bones, XForm[] pose )
{
var world = new XForm[bones.Count];
for ( var j = 0; j < bones.Count; j++ )
world[j] = bones[j].Parent < 0 ? pose[j] : XForm.Compose( world[bones[j].Parent], pose[j] );
return world;
}
static Quaternion Between( Vector3 from, Vector3 to )
{
from = Vector3.Normalize( from );
to = Vector3.Normalize( to );
var dot = Vector3.Dot( from, to );
if ( dot > .999999f )
return Quaternion.Identity;
if ( dot < -.999999f )
{
var axis = Vector3.Cross( Vector3.UnitX, from );
if ( axis.LengthSquared() < 1e-6f )
axis = Vector3.Cross( Vector3.UnitY, from );
return Quaternion.CreateFromAxisAngle( Vector3.Normalize( axis ), MathF.PI );
}
var c = Vector3.Cross( from, to );
return Quaternion.Normalize( new Quaternion( c.X, c.Y, c.Z, 1 + dot ) );
}
/// <summary>Degrees a leg must swing from hanging straight down for its peak to count as a kick, and the least shortfall corrected.</summary>
const float KickDegrees = 30, KickShortfallDegrees = 3;
/// <summary>Frames either side of a kick's peak over which the correction eases in and out.</summary>
const int KickReach = 6;
/// <summary>Frames either side of the capture's kick peak searched for the AI's own peak.</summary>
const int KickSearch = 4;
/// <summary>The model tends to kick lower than the performer (its training motion and the G1's hips). For each peak of a leg's swing
/// in the capture (the leg's angle from straight down, at least <see cref="KickDegrees"/>), the AI's own peak nearby is found and its
/// thigh raised within its own swing plane by the missing angle, fully at the AI's peak and eased (Hann) over <see cref="KickReach"/>
/// frames either side. The timing, direction and knee bend stay the model's; only the height changes. Returns the kicks corrected.</summary>
static int KeepKickHeight( IReadOnlyList<MotionBone> bones, XForm[][] frames, Vector3[][] captureWorld )
{
var count = frames.Length;
var corrected = 0;
foreach ( var (thighRole, footRole) in new[] { (BoneRole.UpperLegL, BoneRole.FootL), (BoneRole.UpperLegR, BoneRole.FootR) } )
{
var thigh = bones.ToList().FindIndex( b => b.Role == thighRole );
var foot = bones.ToList().FindIndex( b => b.Role == footRole );
if ( thigh < 0 || foot < 0 )
continue;
float Swing( Vector3[] w ) => MathF.Acos( Math.Clamp( Vector3.Dot( Vector3.Normalize( w[foot] - w[thigh] ), -Vector3.UnitY ), -1f, 1f ) ) * 180 / MathF.PI;
var captured = captureWorld.Select( Swing ).ToArray();
// Each correction: the AI's peak frame and the world rotation raising its leg by the shortfall.
var rebuilt = frames.Select( f => World( bones, f ) ).ToArray();
var swing = rebuilt.Select( Swing ).ToArray();
var peaks = new List<(int Frame, Quaternion Turn)>();
for ( var t = 2; t < count - 2; t++ )
{
if ( captured[t] < KickDegrees || captured[t] < captured[t - 1] || captured[t] < captured[t + 1] || captured[t] < captured[t - 2] || captured[t] < captured[t + 2] )
continue;
var own = t;
for ( var k = Math.Max( 0, t - KickSearch ); k <= Math.Min( count - 1, t + KickSearch ); k++ )
if ( swing[k] > swing[own] )
own = k;
var shortfall = captured[t] - swing[own];
if ( shortfall < KickShortfallDegrees || peaks.Any( p => Math.Abs( p.Frame - own ) <= KickReach ) )
continue;
var leg = Vector3.Normalize( rebuilt[own][foot] - rebuilt[own][thigh] );
var axis = Vector3.Cross( -Vector3.UnitY, leg );
if ( axis.LengthSquared() < 1e-6f )
continue;
peaks.Add( (own, Quaternion.CreateFromAxisAngle( Vector3.Normalize( axis ), shortfall * MathF.PI / 180 )) );
}
foreach ( var (peak, turn) in peaks )
{
corrected++;
for ( var t = Math.Max( 0, peak - KickReach ); t <= Math.Min( count - 1, peak + KickReach ); t++ )
{
var weight = .5f + .5f * MathF.Cos( MathF.PI * (t - peak) / (KickReach + 1f) );
weight *= weight * (3 - 2 * weight);
var partial = Quaternion.Slerp( Quaternion.Identity, turn, weight );
// World turn of the thigh, written back as a local rotation under its (unchanged) parent.
var parentWorld = Quaternion.Identity;
for ( var b = bones[thigh].Parent; b >= 0; b = bones[b].Parent )
parentWorld = Quaternion.Normalize( frames[t][b].Rot * parentWorld );
var worldThigh = Quaternion.Normalize( parentWorld * frames[t][thigh].Rot );
var local = Quaternion.Normalize( Quaternion.Conjugate( parentWorld ) * Quaternion.Normalize( partial * worldThigh ) );
frames[t][thigh] = new XForm( frames[t][thigh].Pos, local );
}
}
}
return corrected;
}
/// <summary>Frames either side of a join that are eased.</summary>
const float JoinSigma = 2f, JoinReach = 4f;
/// <summary>Each in-between is time-warped onto its own gap, so neighbouring ones play at different speeds and the body
/// hitches where they meet (measured: acceleration at the joins 2–3× that inside the in-betweens). Around every join the body
/// bones and the hips' path are smoothed (Gaussian, <see cref="JoinSigma"/> frames), fading out over <see cref="JoinReach"/>
/// frames, so the generated motion elsewhere is left as the model made it.</summary>
static void SmoothJoins( IReadOnlyList<MotionBone> bones, XForm[][] frames, IReadOnlyList<int> anchors, HashSet<int> detail, int root )
{
var count = frames.Length;
var joins = anchors.Where( k => k > 0 && k < count - 1 ).ToArray();
if ( joins.Length == 0 )
return;
var source = frames.Select( f => (XForm[])f.Clone() ).ToArray();
var radius = (int)MathF.Ceiling( 3 * JoinSigma );
for ( var t = 1; t < count - 1; t++ )
{
var distance = joins.Min( k => Math.Abs( k - t ) );
if ( distance > JoinReach )
continue;
var weight = MathF.Exp( -distance * distance / (JoinReach * JoinReach / 2) );
for ( var j = 0; j < bones.Count; j++ )
{
if ( j != root && detail.Contains( j ) )
continue;
// Weighted average: positions directly, rotations as a sign-aligned normalized sum (close rotations).
var position = Vector3.Zero;
var rotation = new Quaternion( 0, 0, 0, 0 );
var reference = source[t][j].Rot;
var total = 0f;
for ( var k = -radius; k <= radius; k++ )
{
var f = Math.Clamp( t + k, 0, count - 1 );
var w = MathF.Exp( -k * k / (2 * JoinSigma * JoinSigma) );
var q = source[f][j].Rot;
if ( Quaternion.Dot( q, reference ) < 0 )
q = Quaternion.Negate( q );
position += source[f][j].Pos * w;
rotation += q * w;
total += w;
}
var smoothed = new XForm( position / total, Quaternion.Normalize( rotation ) );
frames[t][j] = new XForm( Vector3.Lerp( source[t][j].Pos, smoothed.Pos, weight ),
Quaternion.Normalize( Quaternion.Slerp( source[t][j].Rot, smoothed.Rot, weight ) ) );
}
}
}
/// <summary>Spreads the difference between the generated and the captured pose at both ends over the in-between (per bone,
/// parent-local), so the key poses are reproduced exactly and nothing pops.</summary>
static void HitKeys( XForm[][] frames, XForm[] first, XForm[] last )
{
var n = frames.Length - 1;
for ( var bone = 0; bone < first.Length; bone++ )
{
var rotA = first[bone].Rot * Quaternion.Conjugate( frames[0][bone].Rot );
var rotB = last[bone].Rot * Quaternion.Conjugate( frames[n][bone].Rot );
var posA = first[bone].Pos - frames[0][bone].Pos;
var posB = last[bone].Pos - frames[n][bone].Pos;
for ( var t = 0; t <= n; t++ )
{
var s = n == 0 ? 0f : t / (float)n;
s = s * s * (3f - 2f * s);
var correction = Quaternion.Normalize( Quaternion.Slerp( rotA, rotB, s ) );
frames[t][bone] = new XForm( frames[t][bone].Pos + Vector3.Lerp( posA, posB, s ), Quaternion.Normalize( correction * frames[t][bone].Rot ) );
}
}
}
/// <summary>Key poses: the first and last frame, and in between the frame where the body moves least within
/// <see cref="MinimumGap"/>–<see cref="MaximumGap"/> frames of the previous key (a settle, a turn-around, an extreme).</summary>
public static List<int> Anchors( IReadOnlyList<MotionBone> bones, IReadOnlyList<XForm[]> poses )
{
var count = poses.Count;
var body = Enumerable.Range( 0, bones.Count ).Where( j => bones[j].Group != "fingers" ).ToArray();
var world = poses.Select( p => World( bones, p ) ).ToArray();
var speed = new float[count];
for ( var k = 0; k < count; k++ )
{
int a = Math.Max( 0, k - 1 ), b = Math.Min( count - 1, k + 1 );
speed[k] = body.Sum( j => Vector3.Distance( world[a][j], world[b][j] ) );
}
var anchors = new List<int> { 0 };
var last = 0;
while ( count - 1 - last > MaximumGap )
{
// Leave room for a whole in-between after the next key.
var from = last + MinimumGap;
var to = Math.Min( last + MaximumGap, count - 1 - MinimumGap );
if ( to < from )
break;
// The stillest frame at a length the model plays at its own timing; any frame if none fits.
var next = -1;
for ( var k = from; k <= to; k++ )
if ( Warp( k - last ) == 0 && (next < 0 || speed[k] < speed[next]) )
next = k;
if ( next < 0 )
{
next = from;
for ( var k = from + 1; k <= to; k++ )
if ( speed[k] < speed[next] )
next = k;
}
anchors.Add( next );
last = next;
}
anchors.Add( count - 1 );
return anchors;
}
/// <summary>Bones kept from the capture: fingers, neck and head (the model has none), and the wrists. The model's G1 wrist is three
/// robot hinges it turns freely (measured: the rebuilt wrist turned nearly twice as fast as the capture's and wobbled 3×), while the
/// capture's wrist was fitted to the hand seen in the video. The hand still follows the model's forearm; only its bend at the wrist
/// is the capture's.</summary>
static HashSet<int> DetailBones( IReadOnlyList<MotionBone> bones )
{
var neck = bones.ToList().FindIndex( b => b.Role == BoneRole.Neck );
var head = bones.ToList().FindIndex( b => b.Role == BoneRole.Head );
var hands = bones.Select( ( b, i ) => (b, i) ).Where( x => x.b.Role is BoneRole.HandL or BoneRole.HandR ).Select( x => x.i ).ToArray();
var result = new HashSet<int>();
for ( var j = 0; j < bones.Count; j++ )
if ( Under( bones, j, neck ) || Under( bones, j, head ) || hands.Any( h => Under( bones, j, h ) ) || bones[j].Group == "fingers" )
result.Add( j );
return result;
}
static bool Under( IReadOnlyList<MotionBone> bones, int bone, int ancestor )
{
if ( ancestor < 0 )
return false;
for ( var b = bone; b >= 0; b = bones[b].Parent )
if ( b == ancestor )
return true;
return false;
}
/// <summary>The capture's pose at <paramref name="time"/>, per bone parent-local, interpolated between its frames.</summary>
static XForm[] Sample( MotionDocument doc, double time )
{
var frames = doc.Frames;
var lo = 0;
var hi = frames.Count - 1;
while ( lo < hi )
{
var mid = (lo + hi + 1) / 2;
if ( frames[mid].Time <= time ) lo = mid; else hi = mid - 1;
}
var a = frames[lo];
var b = frames[Math.Min( lo + 1, frames.Count - 1 )];
var t = b.Time > a.Time ? (float)Math.Clamp( (time - a.Time) / (b.Time - a.Time), 0, 1 ) : 0f;
var pose = new XForm[doc.Bones.Count];
for ( var j = 0; j < pose.Length; j++ )
pose[j] = new XForm( Vector3.Lerp( MotionDocument.V( a.Positions[j] ), MotionDocument.V( b.Positions[j] ), t ),
Quaternion.Normalize( Quaternion.Slerp( MotionDocument.Q( a.Rotations[j] ), MotionDocument.Q( b.Rotations[j] ), t ) ) );
return pose;
}
/// <summary>Turns a capture that stays relative to a tilted camera upright (the body network's gravity estimate); identity otherwise.</summary>
static Quaternion Level( MotionDocument doc )
{
if ( doc.Space != MotionSpace.CameraRelative || doc.Cameras.FirstOrDefault( c => c.Id == "video" )?.Up is not { Length: 3 } values )
return Quaternion.Identity;
var up = MotionDocument.V( values );
if ( !(up.LengthSquared() > 1e-6f) )
return Quaternion.Identity;
up = Vector3.Normalize( up );
var axis = Vector3.Cross( up, Vector3.UnitY );
if ( axis.LengthSquared() < 1e-8f )
return Quaternion.Identity;
return Quaternion.CreateFromAxisAngle( Vector3.Normalize( axis ), MathF.Acos( Math.Clamp( up.Y, -1f, 1f ) ) );
}
/// <summary>The ground: the height the feet and toes reach in the lowest 2% of frames.</summary>
static float Floor( IReadOnlyList<MotionBone> bones, IReadOnlyList<XForm[]> poses )
{
var feet = Enumerable.Range( 0, bones.Count ).Where( j => bones[j].Role is BoneRole.FootL or BoneRole.FootR or BoneRole.ToeL or BoneRole.ToeR ).ToArray();
var lows = poses.Select( p => { var w = World( bones, p ); return feet.Min( j => w[j].Y ); } ).OrderBy( y => y ).ToArray();
return lows[Math.Min( lows.Length - 1, lows.Length / 50 )];
}
static Vector3[] World( IReadOnlyList<MotionBone> bones, XForm[] pose )
{
var world = new XForm[bones.Count];
var result = new Vector3[bones.Count];
for ( var j = 0; j < bones.Count; j++ )
{
world[j] = bones[j].Parent < 0 ? pose[j] : XForm.Compose( world[bones[j].Parent], pose[j] );
result[j] = world[j].Pos;
}
return result;
}
}