Editor/WeaponImporter/Core/Grip/GripLibrary.cs
#nullable enable annotations
using System.Numerics;
using System.Text.Json;
using System.Text.Json.Serialization;
using WeaponImporter.Core.Analysis;
using WeaponImporter.Core.Geometry;
using WeaponImporter.Core.Hands;
using WeaponImporter.Core.Maths;
using WeaponImporter.Core.Setup;
namespace WeaponImporter.Core.Grip;
using Vector3 = System.Numerics.Vector3;
/// <summary>
/// A hand grip captured from an animation (any rig) in a form that transfers to another hand
/// and another weapon: finger angles, and the palm's place and orientation relative to the grip
/// surface it holds, normalised by hand length. Anatomical axes (fingers, palm) are used, never
/// bone axes, so rigs with different bone orientations agree.
/// </summary>
public sealed class GripPreset
{
public string Name { get; set; } = "";
/// <summary>Where it came from: "this weapon's first-person animation", a pack file, "built-in".</summary>
public string Source { get; set; } = "";
public Side Side { get; set; }
public GripStyle Style { get; set; }
/// <summary>Palm centre in the grip surface frame, divided by the hand length.</summary>
public float[] Palm { get; set; } = new float[3];
/// <summary>Anatomical hand frame (x = fingers, y = out of the palm) in the grip surface frame.</summary>
public float[] Orientation { get; set; } = { 0, 0, 0, 1 };
/// <summary>
/// Each finger's segment directions (x,y,z per joint, proximal first) in the anatomical hand
/// frame. Applied by fitting the target hand's joint angles to them.
/// </summary>
public Dictionary<FingerKind, float[]> Directions { get; set; } = new();
/// <summary>
/// Preference added to the fit score when grips compete. A grip captured from this very
/// weapon's own first-person animation gets <see cref="GripExtractor.OwnPriority"/>: it was
/// authored for exactly this geometry, so it wins unless it fits clearly worse.
/// </summary>
[JsonIgnore] public float Priority { get; set; }
/// <summary>Weapon type it was captured on (ranking prefers the same kind).</summary>
public string WeaponType { get; set; } = "";
}
/// <summary>Captures grips from posed hands and applies them to other hands and weapons.</summary>
public static class GripLibrary
{
/// <summary>
/// Grip surface frame with a canonical sign: z out of the surface, x along the grip axis
/// pointing up/forward (so a pistol grip and a handguard never flip between weapons).
/// </summary>
public static XForm SurfaceFrame(GripSurface s)
{
var n = Vector3.Normalize(s.Normal);
var axis = s.Axis - n * Vector3.Dot(s.Axis, n);
if (axis.LengthSquared() < 1e-6f)
axis = MathQ.Perpendicular(n);
axis = Vector3.Normalize(axis);
if (Vector3.Dot(axis, Vector3.UnitZ + Vector3.UnitX * 0.3f) < 0f)
axis = -axis;
var y = Vector3.Cross(n, axis);
return new XForm(s.Contact, MathQ.FromAxes(axis, y));
}
/// <summary>The hand's anatomical frame in the hand bone's own frame.</summary>
public static Quaternion AnatomicalLocal(HandRig rig) => MathQ.FromAxes(rig.FingerDirection, rig.PalmNormal);
/// <summary>
/// Captures the grip of a posed hand. <paramref name="world"/> are the posed bone transforms
/// of <paramref name="rig"/>'s skeleton in the weapon's space; <paramref name="surface"/> is
/// the surface the hand holds, in the same space.
/// </summary>
public static GripPreset Capture(HandRig rig, IReadOnlyList<XForm> world, GripSurface surface, GripStyle style, string name, string source)
{
var frame = SurfaceFrame(surface);
var wrist = world[rig.Hand];
var palm = wrist.TransformPoint(rig.PalmCenter);
var anat = MathQ.Normalize(wrist.Rot * AnatomicalLocal(rig));
var palmLocal = Vector3.Transform(palm - frame.Pos, Quaternion.Conjugate(frame.Rot)) / MathF.Max(0.5f, rig.HandLength);
var orientation = MathQ.Normalize(Quaternion.Conjugate(frame.Rot) * anat);
var preset = new GripPreset
{
Name = name,
Source = source,
Side = rig.Side,
Style = style,
Palm = V.A(palmLocal),
Orientation = V.A(orientation),
};
// Fingers as segment directions in the anatomical hand frame: independent of how each
// rig's bones are oriented and of its rest pose (first-person rigs are often bound with
// the fingers already curled).
foreach (var finger in rig.Fingers)
preset.Directions[finger.Kind] = FingerDirections(rig, finger, world, anat).SelectMany(d => new[] { d.X, d.Y, d.Z }).ToArray();
return preset;
}
/// <summary>Direction of each segment of a finger in the anatomical hand frame.</summary>
private static Vector3[] FingerDirections(HandRig rig, FingerRig finger, IReadOnlyList<XForm> world, Quaternion anatWorld)
{
var inverse = Quaternion.Conjugate(anatWorld);
var dirs = new Vector3[finger.Joints.Length];
for (var j = 0; j < dirs.Length; j++)
{
var along = Vector3.Transform(finger.Along[j], world[finger.Joints[j]].Rot);
dirs[j] = Vector3.Normalize(Vector3.Transform(along, inverse));
}
return dirs;
}
/// <summary>
/// Finger angles of <paramref name="rig"/> that reproduce captured segment directions, fitted
/// per finger (flex per joint, base spread, and thumb opposition) by coordinate descent.
/// </summary>
private static void FitFingers(GripPreset preset, HandRig rig, HandPose pose)
{
var anat = AnatomicalLocal(rig);
foreach (var finger in rig.Fingers)
{
if (!preset.Directions.TryGetValue(finger.Kind, out var flat) || flat.Length < 3)
continue;
var target = Enumerable.Range(0, flat.Length / 3).Select(i => new Vector3(flat[i * 3], flat[i * 3 + 1], flat[i * 3 + 2])).ToArray();
var n = finger.Joints.Length;
var thumb = finger.Kind == FingerKind.Thumb;
var x = new float[n + 1 + (thumb ? 1 : 0)];
var lo = new float[x.Length];
var hi = new float[x.Length];
for (var j = 0; j < n; j++)
{
lo[j] = -0.35f;
hi[j] = finger.MaxFlex[j];
}
lo[n] = -0.6f;
hi[n] = 0.6f;
if (thumb)
{
lo[n + 1] = -1.2f;
hi[n + 1] = 1.2f;
}
float Error()
{
pose.Flex[finger.Kind] = x.Take(n).ToArray();
pose.Spread[finger.Kind] = x[n];
if (thumb)
pose.ThumbOpposition = x[n + 1];
var probe = pose.Clone();
probe.Wrist = XForm.Identity;
var world = HandShape.Of(rig, probe).JointWorld;
var e = 0f;
for (var j = 0; j < n; j++)
{
var t = target[Math.Min(j, target.Length - 1)];
var along = Vector3.Normalize(Vector3.Transform(Vector3.Transform(finger.Along[j], world[finger.Joints[j]].Rot), Quaternion.Conjugate(anat)));
// Base segments matter most: they place the whole finger.
e += (1f - Vector3.Dot(along, t)) * (j == 0 ? 2f : 1f);
}
return e;
}
var best = Error();
for (var step = 0.5f; step > 2e-3f; step *= 0.5f)
{
var improved = true;
for (var guard = 0; improved && guard < 50; guard++)
{
improved = false;
for (var k = 0; k < x.Length; k++)
foreach (var sign in new[] { 1f, -1f })
{
var old = x[k];
x[k] = Math.Clamp(old + sign * step, lo[k], hi[k]);
if (x[k] == old)
continue;
var e = Error();
if (e < best - 1e-7f)
{
best = e;
improved = true;
}
else
{
x[k] = old;
}
}
}
}
pose.Flex[finger.Kind] = x.Take(n).ToArray();
pose.Spread[finger.Kind] = x[n];
if (thumb)
pose.ThumbOpposition = x[n + 1];
}
}
/// <summary>A preset placed on a surface for a (possibly different) hand, in the surface's space.</summary>
public static HandPose Apply(GripPreset preset, HandRig rig, GripSurface surface)
{
var frame = SurfaceFrame(surface);
var palm = frame.TransformPoint(V.Of(preset.Palm) * rig.HandLength);
var anat = MathQ.Normalize(frame.Rot * V.Q(preset.Orientation));
var wristRot = MathQ.Normalize(anat * Quaternion.Conjugate(AnatomicalLocal(rig)));
var wristPos = palm - Vector3.Transform(rig.PalmCenter, wristRot);
var pose = HandPose.Open(rig, new XForm(wristPos, wristRot));
FitFingers(preset, rig, pose);
return pose;
}
// ------------------------------------------------------------------ files
public static readonly JsonSerializerOptions Json = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
public static string ToJson(IEnumerable<GripPreset> presets) => JsonSerializer.Serialize(presets.ToList(), Json);
public static List<GripPreset> FromJson(string json)
{
try
{
return JsonSerializer.Deserialize<List<GripPreset>>(json, Json) ?? new List<GripPreset>();
}
catch (JsonException e)
{
throw new FormatException($"Grip library is not valid JSON: {e.Message}", e);
}
}
}