tests/test_capture_spin.cs
using System.Numerics;
using HumanoidMocapX;
using HumanoidMocapX.Core.Motion;
static class CaptureSpinTests
{
internal static void Run()
{
// 60 frames (2 s) turning 40° a frame on average, unevenly: 20° and 60° in turn.
var spinning = Turning(f => f * 40 + (f % 2 == 0 ? 0 : 20)).ToDocument();
var before = Headings(spinning);
MotionTests.Check(CaptureSpin.Apply(spinning) > 40, "A sustained fast spin must be smoothed");
var after = Headings(spinning);
var steps = after.Zip(after.Skip(1), (a, b) => b - a).Skip(10).Take(40).ToArray();
MotionTests.Check(steps.Max() - steps.Min() < 6, $"Spin rate still uneven: {steps.Min():F1}..{steps.Max():F1} degrees per frame");
MotionTests.Near(before[30], after[30], 12);
// Where the hip line stands near vertical (a windmill) its heading means nothing; those frames are left alone, and
// no frame is turned further than MaxTurnDegrees.
var tumbling = Turning(f => f * 40 + (f % 2 == 0 ? 0 : 20)).ToDocument(); var roll = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2);
for (var f = 28; f <= 31; f++) tumbling.Frames[f].Rotations[0] = Q(MotionDocument.Q(tumbling.Frames[f].Rotations[0]) * roll);
var captured = tumbling.Frames.Select(f => MotionDocument.Q(f.Rotations[0])).ToArray();
CaptureSpin.Apply(tumbling);
for (var f = 0; f < captured.Length; f++)
{
var turned = 2 * Math.Acos(Math.Min(1, Math.Abs(Quaternion.Dot(captured[f], MotionDocument.Q(tumbling.Frames[f].Rotations[0]))))) * 180 / Math.PI;
MotionTests.Check(f is < 28 or > 31 ? turned <= CaptureSpin.MaxTurnDegrees + .01 : turned < .1, $"Frame {f} turned {turned:F1} degrees");
}
var slow = Turning(f => f * 5).ToDocument(); var original = Turning(f => f * 5).ToDocument();
MotionTests.Equal(0, CaptureSpin.Apply(slow));
MotionTests.Near(1, Quaternion.Dot(MotionDocument.Q(original.Frames[30].Rotations[0]), MotionDocument.Q(slow.Frames[30].Rotations[0])), 1e-7);
Console.WriteLine("PASS: capture spin evens out fast uneven turns, keeps their heading, caps each turn, skips frames with an upright hip line and leaves slow turns alone");
}
static float[] Q(Quaternion q) => new[] { q.X, q.Y, q.Z, q.W };
/// <summary>The motion fixture turned about the vertical by <paramref name="degrees"/> per frame index.</summary>
static FlowMotion Turning(Func<int, float> degrees)
{
var fixture = MotionTests.Fixture();
for (var f = 0; f < fixture.Poses.Length; f++) fixture.Poses[f][1] = degrees(f) * MathF.PI / 180;
return fixture;
}
/// <summary>Unwrapped heading of the hip line in degrees, positive about +Y.</summary>
static double[] Headings(MotionDocument motion)
{
var left = ObservationFixture.Bone(motion, "UpperLegL"); var right = ObservationFixture.Bone(motion, "UpperLegR");
var headings = new double[motion.Frames.Count];
for (var f = 0; f < headings.Length; f++)
{
var position = ObservationFixture.Positions(motion, motion.Frames[f], out _, out _);
var line = position[right] - position[left];
headings[f] = Math.Atan2(-line.Z, line.X) * 180 / Math.PI;
if (f > 0) headings[f] = headings[f - 1] + Math.IEEERemainder(headings[f] - headings[f - 1], 360);
}
return headings;
}
}