tests/test_fingers.cs
using System.Numerics;
using System.Text;
using HumanoidMocapX.Core;
using HumanoidMocapX;
using HumanoidMocapX.Core.Formats.Fbx;
using HumanoidMocapX.Core.Mapping;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Core.Target;
static class FingerTests
{
internal static void Run()
{
var fixture = MotionTests.Fixture();
for (var frame = 0; frame < fixture.Poses.Length; frame++)
for (var joint = 22; joint < 52; joint++) fixture.Poses[frame][joint * 3 + 2] = frame / (float)(fixture.Poses.Length - 1) * (.25f + (joint - 22) * .015f);
var motion = fixture.ToDocument(); var source = FbxImporter.Import(MotionFbxExport.WriteForModelDoc(motion));
for (var joint = 22; joint < 52; joint++)
{
var index = source.Skeleton.IndexOf(motion.Bones[joint].Name);
MotionTests.Check(index >= 0, "Source FBX lost a finger bone");
var expected = Similarity(MotionDocument.Q(motion.Frames[0].Rotations[joint]), MotionDocument.Q(motion.Frames[^1].Rotations[joint]));
MotionTests.Near(expected, Similarity(source.Clips[0].Frames[0][index].Rot, source.Clips[0].Frames[^1][index].Rot), 1e-4);
}
Retarget(motion, false); Retarget(motion, true);
Console.WriteLine("PASS: all 30 source finger rotations survive FBX; Human/Citizen finger articulation survives retargeting and target FBX export");
}
static void Retarget(MotionDocument motion, bool citizen)
{
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, citizen ? "target_rig_sbox_citizen.json" : "target_rig_sbox.json"));
var target = citizen ? RetargetTargetSpec.SboxCitizen(json) : RetargetTargetSpec.SboxDefault(json);
var result = Retargeter.Convert(new RetargetRequest
{
SourceData = Encoding.UTF8.GetBytes(motion.ToJson()),
SourceFileName = "fingers.hmotion",
FootPlantCleanup = false,
MocapCorrections = new TargetCorrectionSettings { FirstPerson = false, StabilizeFeet = false }
}, target);
MotionTests.Check(result.Success, string.Join("; ", result.Errors.Concat(result.Clips.Select(c => c.Error))));
var clip = result.Clips.Single(); var times = Enumerable.Range(0, clip.SolvedFrames.Count).Select(i => i / (double)clip.Fps).ToArray();
var parsed = FbxImporter.Import(ModelDocFbxExport.Write(target.Rig.Skeleton, "fingers", clip.SolvedFrames, times, clip.Fps, target.UpAxis, "WorldRelative"));
var mapped = 0;
foreach (var bone in motion.Bones.Skip(22))
{
var index = target.Rig.BoneForRole(bone.Role!.Value);
if (index is null)
{
MotionTests.Check(citizen && bone.Role is BoneRole.PinkyProxL or BoneRole.PinkyMidL or BoneRole.PinkyDistL
or BoneRole.PinkyProxR or BoneRole.PinkyMidR or BoneRole.PinkyDistR, "Target is missing an expected finger joint");
continue;
}
mapped++;
var similarity = Similarity(clip.SolvedFrames[0][index.Value].Rot, clip.SolvedFrames[^1][index.Value].Rot);
MotionTests.Check(similarity < .9999f, "Retargeting froze finger " + bone.Role);
var exported = parsed.Skeleton.IndexOf(target.Rig.Skeleton[index.Value].Name);
MotionTests.Check(exported >= 0, "Target FBX lost finger " + bone.Role);
MotionTests.Near(similarity, Similarity(parsed.Clips[0].Frames[0][exported].Rot, parsed.Clips[0].Frames[^1][exported].Rot), 1e-4);
}
MotionTests.Equal(citizen ? 24 : 30, mapped);
}
static float Similarity(Quaternion first, Quaternion second) => MathF.Abs(Quaternion.Dot(Quaternion.Normalize(first), Quaternion.Normalize(second)));
}