Editor/WeaponImporter/Core/Hands/HandPose.cs
#nullable enable annotations
using System.Numerics;
using WeaponImporter.Core.Maths;
namespace WeaponImporter.Core.Hands;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// A hand pose: wrist transform plus per-joint flexion (and base spread) angles. Stored as
/// angles so blending, limits and "smallest change" comparisons stay meaningful; converted to
/// bone rotations with <see cref="LocalRotation"/>.
/// </summary>
public sealed class HandPose
{
/// <summary>Hand bone transform in the space the pose was solved in (weapon space for grips).</summary>
public XForm Wrist;
/// <summary>Flexion per finger joint (radians), keyed by finger then joint index.</summary>
public Dictionary<FingerKind, float[]> Flex { get; } = new();
/// <summary>Base-joint spread per finger (radians, + toward the thumb).</summary>
public Dictionary<FingerKind, float> Spread { get; } = new();
/// <summary>Thumb opposition (radians): swings the thumb across the palm.</summary>
public float ThumbOpposition;
public HandPose(XForm wrist) => Wrist = wrist;
public static HandPose Open(HandRig rig, XForm wrist)
{
var pose = new HandPose(wrist);
foreach (var f in rig.Fingers)
{
pose.Flex[f.Kind] = new float[f.Joints.Length];
pose.Spread[f.Kind] = 0f;
}
return pose;
}
public HandPose Clone()
{
var c = new HandPose(Wrist) { ThumbOpposition = ThumbOpposition };
foreach (var (k, v) in Flex)
c.Flex[k] = v.ToArray();
foreach (var (k, v) in Spread)
c.Spread[k] = v;
return c;
}
public float[] FlexOf(FingerKind kind) => Flex.TryGetValue(kind, out var f) ? f : Array.Empty<float>();
/// <summary>Local rotation of finger joint <paramref name="j"/> for this pose.</summary>
public Quaternion LocalRotation(HandRig rig, FingerRig finger, int j)
{
var rest = rig.Skeleton[finger.Joints[j]].RestLocal.Rot;
// What the character shows: a pinky its model copies from the ring takes the ring's angles.
var kind = rig.PinkyFollowsRing && finger.Kind == FingerKind.Pinky && Flex.ContainsKey(FingerKind.Ring) ? FingerKind.Ring : finger.Kind;
var flex = Flex.TryGetValue(kind, out var f) && j < f.Length ? f[j] : 0f;
var q = Quaternion.CreateFromAxisAngle(finger.FlexAxes[j], flex);
if (j == 0)
{
var spread = Spread.TryGetValue(kind, out var sp) ? sp : 0f;
if (MathF.Abs(spread) > 1e-5f)
q = Quaternion.CreateFromAxisAngle(finger.SpreadAxis, spread) * q;
if (finger.Kind == FingerKind.Thumb && MathF.Abs(ThumbOpposition) > 1e-5f)
{
// Opposition rolls the thumb about its own length while swinging it across the palm.
q = Quaternion.CreateFromAxisAngle(finger.Along[0], ThumbOpposition * 0.6f) * Quaternion.CreateFromAxisAngle(finger.SpreadAxis, -ThumbOpposition * (rig.Side == Side.Right ? 1f : -1f) * 0.5f) * q;
}
}
return MathQ.Normalize(rest * q);
}
/// <summary>Linear blend between two poses (angles and wrist).</summary>
public static HandPose Lerp(HandPose a, HandPose b, float t)
{
var r = new HandPose(new XForm(Vector3.Lerp(a.Wrist.Pos, b.Wrist.Pos, t), MathQ.Slerp(a.Wrist.Rot, b.Wrist.Rot, t)))
{
ThumbOpposition = a.ThumbOpposition + (b.ThumbOpposition - a.ThumbOpposition) * t,
};
foreach (var (k, fa) in a.Flex)
{
var fb = b.Flex.TryGetValue(k, out var x) ? x : fa;
r.Flex[k] = fa.Select((v, i) => v + ((i < fb.Length ? fb[i] : v) - v) * t).ToArray();
}
foreach (var (k, sa) in a.Spread)
r.Spread[k] = sa + ((b.Spread.TryGetValue(k, out var sb) ? sb : sa) - sa) * t;
return r;
}
}
/// <summary>World-space samples of a posed hand for contact and penetration tests.</summary>
public sealed class HandShape
{
public readonly record struct Segment(FingerKind Finger, int Joint, Vector3 A, Vector3 B, float Radius)
{
public Vector3 Point(float t) => Vector3.Lerp(A, B, t);
}
public List<Segment> Segments { get; } = new();
/// <summary>
/// Contact radius of a finger segment: tapers toward the tip. The thumb's first segment is
/// the fleshy metacarpal web, which yields against a grip, so it is modelled thinner.
/// </summary>
public static float SegmentRadius(FingerRig finger, int joint)
=> finger.Radius * (1f - 0.12f * joint) * (finger.Kind == FingerKind.Thumb && joint == 0 ? 0.55f : 1f);
/// <summary>Points on the palm skin, with the outward palm normal.</summary>
public List<Vector3> PalmPoints { get; } = new();
public Vector3 PalmNormal { get; private set; }
/// <summary>World transform of every finger joint bone (plus the hand).</summary>
public Dictionary<int, XForm> JointWorld { get; } = new();
public static HandShape Of(HandRig rig, HandPose pose)
{
var shape = new HandShape();
var skeleton = rig.Skeleton;
var handWorld = pose.Wrist;
shape.JointWorld[rig.Hand] = handWorld;
foreach (var finger in rig.Fingers)
{
// Walk from the hand down to the first joint through any metacarpal at rest.
var chainToBase = new List<int>();
for (var b = skeleton[finger.Joints[0]].ParentIndex; b >= 0 && b != rig.Hand; b = skeleton[b].ParentIndex)
chainToBase.Add(b);
var world = handWorld;
for (var i = chainToBase.Count - 1; i >= 0; i--)
{
world = XForm.Compose(world, skeleton[chainToBase[i]].RestLocal);
shape.JointWorld[chainToBase[i]] = world;
}
for (var j = 0; j < finger.Joints.Length; j++)
{
var local = skeleton[finger.Joints[j]].RestLocal;
local.Rot = pose.LocalRotation(rig, finger, j);
world = XForm.Compose(world, local);
shape.JointWorld[finger.Joints[j]] = world;
var a = world.Pos;
var b = world.TransformPoint(finger.Along[j] * finger.Lengths[j]);
// Taper toward the tip.
var radius = HandShape.SegmentRadius(finger, j);
shape.Segments.Add(new Segment(finger.Kind, j, a, b, radius));
}
}
// Palm: a small grid on the palm skin between wrist and knuckles.
var n = handWorld.TransformVector(rig.PalmNormal);
shape.PalmNormal = n;
var k = rig.KnuckleLine;
var skin = rig.PalmNormal * (rig.PalmThickness * 0.5f);
for (var u = 0; u <= 3; u++)
for (var v = 0; v <= 3; v++)
{
var along = Vector3.Lerp(rig.PalmCenter - (rig.KnuckleCenter - rig.PalmCenter) * 0.6f, rig.KnuckleCenter, u / 3f);
var across = k * (rig.PalmWidth * (v / 3f - 0.5f) * 0.85f);
shape.PalmPoints.Add(handWorld.TransformPoint(along + across + skin));
}
return shape;
}
/// <summary>Fingertip position of a finger.</summary>
public Vector3 Tip(FingerKind finger) => Segments.Where(s => s.Finger == finger).OrderBy(s => s.Joint).Last().B;
}