tests/test_capture_despike.cs
using System.Numerics;
using HumanoidMocapX;
using HumanoidMocapX.Core;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Core.Target;
static class CaptureDespikeTests
{
internal static void Run()
{
// A steady fast spin, 40° a frame, is not a snap.
var spin = MotionTests.Fixture();
for (var f = 0; f < spin.Poses.Length; f++) spin.Poses[f][1] = f * 40 * MathF.PI / 180;
var spinning = spin.ToDocument();
MotionTests.Equal(0, CaptureDespike.Apply(spinning));
// One frame where a knee snaps 20° out of line and the body jumps 6 cm.
var snap = MotionTests.Fixture();
snap.Poses[30][3 * 4] = 20 * MathF.PI / 180; snap.Translations[30][0] = .06f;
var snapped = snap.ToDocument();
var bone = ObservationFixture.Bone(snapped, "LowerLegL");
MotionTests.Check(CaptureDespike.Apply(snapped) > 0, "The snap was not corrected");
var q = MotionDocument.Q(snapped.Frames[30].Rotations[bone]);
var off = 2 * Math.Acos(Math.Min(1, Math.Abs(q.W))) * 180 / Math.PI;
MotionTests.Check(off < 7, $"The knee still snaps {off:F1} degrees out of line");
var root = MotionDocument.V(snapped.Frames[30].Positions[0]) - MotionDocument.V(snapped.Frames[29].Positions[0]);
MotionTests.Check(root.Length() < .02f, $"The body still jumps {root.Length() * 100:F1} cm");
// On the retargeted Human: a one-frame 25° snap of the right arm is pulled back, while the planted feet and the left
// hand, held still by earlier cleanup as the body sways, stay exactly where they are.
var rig = TargetRig.SboxDefault(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "target_rig_sbox.json"))); var skeleton = rig.Skeleton;
int Bone(BoneRole role) => rig.BoneForRole(role) ?? throw new Exception("missing " + role);
var frames = Enumerable.Range(0, 40).Select(f =>
{
var pose = Enumerable.Range(0, skeleton.Count).Select(b => skeleton[b].RestLocal).ToArray();
pose[Bone(BoneRole.Spine1)].Rot = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitY, .15f * MathF.Sin(f * .2f)) * pose[Bone(BoneRole.Spine1)].Rot);
return pose;
}).ToList();
var held = new[] { BoneRole.HandL, BoneRole.FootL, BoneRole.FootR }.Select(Bone).ToArray();
var anchors = held.Select(b => World(skeleton, frames[0])[b].Pos).ToArray();
// A quarter of the way in from the rest pose's straight arm, so the arm is bent and can hold the hand as the body sways.
anchors[0] = Vector3.Lerp(anchors[0], World(skeleton, frames[0])[Bone(BoneRole.UpperArmL)].Pos, .25f);
// The swaying body's left hand pinned the way the hand plant pins it.
HumanoidMocapX.Core.Cleanup.EffectorIk.ApplyGoals(frames, skeleton, new HumanoidMocapX.Core.Cleanup.LimbChain { Upper = Bone(BoneRole.UpperArmL), Lower = Bone(BoneRole.LowerArmL), End = Bone(BoneRole.HandL) },
Enumerable.Repeat(anchors[0], frames.Count).ToArray(), Vector3.UnitX, soften: 0);
var shoulder = Bone(BoneRole.UpperArmR); var handR = Bone(BoneRole.HandR);
frames[20][shoulder].Rot = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(Vector3.UnitX, 25 * MathF.PI / 180) * frames[20][shoulder].Rot);
var heldBefore = held.Select(b => frames.Select(f => World(skeleton, f)[b].Pos).ToArray()).ToArray();
var swung = frames.Select(f => World(skeleton, f)[handR].Pos * 2.54f).ToArray();
var swing = Vector3.Distance(swung[20], (swung[19] + swung[21]) / 2);
MotionTests.Check(CaptureDespike.Apply(frames, rig, 1 / 2.54f) > 0, "The retargeted snap was not corrected");
var path = frames.Select(f => World(skeleton, f)[handR].Pos * 2.54f).ToArray();
var jump = Vector3.Distance(path[20], (path[19] + path[21]) / 2);
MotionTests.Check(swing > 10 && jump < 2, $"The right hand still snaps {jump:F1} cm out of line (from {swing:F1} cm)");
for (var h = 0; h < held.Length; h++)
for (var f = 0; f < frames.Count; f++)
{
var moved = Vector3.Distance(World(skeleton, frames[f])[held[h]].Pos, heldBefore[h][f]) * 2.54f;
MotionTests.Check(moved < .05f, $"{rig.RoleOf(held[h])} moved {moved:F2} cm at frame {f}");
}
Console.WriteLine("PASS: capture despike pulls a one-frame snap back onto its neighbours' path, leaves a steady spin alone, and on a retargeted rig keeps held hands and planted feet in place");
}
static XForm[] World(HumanoidMocapX.Core.Skeleton.Skeleton skeleton, XForm[] locals)
{
var world = new XForm[skeleton.Count];
for (var i = 0; i < world.Length; i++) world[i] = skeleton[i].ParentIndex < 0 ? locals[i] : XForm.Compose(world[skeleton[i].ParentIndex], locals[i]);
return world;
}
}