tests/CitizenAnimationSetup/SmartPortMissingFingerTests.cs
using System.Numerics;
using HumanoidRetargeter.Maths;
using HumanoidRetargeter.Skeleton;
using HumanoidRetargeter.Target;
using HumanoidRetargeter.Tests.Skeleton;
using Xunit;
using SkeletonModel = HumanoidRetargeter.Skeleton.Skeleton;
namespace HumanoidRetargeter.Tests.Target;
public class SmartPortMissingFingerTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MissingPinkyFollowsRingButAuthoredPinkyIsPreserved(bool sourceHasPinky)
{
var target = TargetRig.Load(TargetRigGenerator.Generate(File.ReadAllText(SkeletonTests.FixturePath("rig_human_male.json")))).Skeleton;
var source = SkeletonModel.Create(target.Bones.Where(b => sourceHasPinky || !b.Name.Contains("pinky"))
.Select(b => new BoneDefinition(b.Name, b.ParentIndex < 0 ? null : target[b.ParentIndex].Name, b.RestLocal)).ToArray());
var rig = new SmartPortRig(source, target);
var pose = source.Bones.Select(b => b.RestLocal).ToArray();
foreach (var side in new[] { "L", "R" })
{
var ring = source.IndexOf("finger_ring_1_" + side);
pose[ring].Rot *= Quaternion.CreateFromAxisAngle(Vector3.UnitY, .7f);
}
var result = rig.Transfer(pose);
foreach (var side in new[] { "L", "R" })
{
var index = rig.Target.IndexOf("finger_pinky_1_" + side);
var rest = rig.Target[index].RestLocal;
var curl = MathQ.AngleBetween(rest.Rot, result[index].Rot);
if (sourceHasPinky) Assert.True(curl < .001f); // Authored straight pinky, despite curled ring.
else Assert.True(curl > .1f, "A missing source pinky must not leave the target pinky stuck in bind pose.");
Assert.True(Vector3.Distance(rest.Pos, result[index].Pos) < .001f);
}
Assert.All(rig.Transfer(Enumerable.Repeat(XForm.Identity, source.Count).ToArray(), true), delta =>
{
Assert.True(delta.Pos.Length() < .0001f);
Assert.True(MathQ.AngleBetween(delta.Rot, Quaternion.Identity) < .001f);
});
}
}