Editor/Core/Rigging/ReferenceFitting.cs
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Fit a reference template through semantic anchors. Extra joints and
/// controls are carried by their anatomical segment; reference frames are kept.</summary>
internal static class ReferenceFitting
{
internal static GeneratedRig Create(Anatomy anatomy,RigProfile profile)
{
var reference=profile.Reference??throw new ArgumentException("Missing reference.");
var source=reference.Bones;var bones=(RigBone[])source.Clone();
var warp=new Warp(source,anatomy.Points.ToDictionary(p=>p.Key,p=>p.Value.Position));
bool Digit(string role)=>Profiles.Fingers.Any(f=>role.StartsWith(f,StringComparison.Ordinal));
bool HasDigitGeometry(int index)
{
if(Digit(source[index].Role))return anatomy.Points.ContainsKey(source[index].Role);
if(!source[index].Role.StartsWith("Reference:"))return true;
var children=Enumerable.Range(0,source.Length).Where(i=>source[i].Parent==index&&Digit(source[i].Role)).ToArray();
return children.Length==0||children.Any(i=>anatomy.Points.ContainsKey(source[i].Role));
}
foreach(int i in Enumerable.Range(0,source.Length))
{
var bone=source[i];
bones[i]=bone with{Position=anatomy.Points.TryGetValue(bone.Role,out var anchor)?anchor.Position:warp.Map(bone.Position,i),Deform=bone.Deform&&HasDigitGeometry(i)};
}
return new GeneratedRig{Profile=profile,Bones=bones,Anatomy=anatomy};
}
internal sealed class Warp
{
readonly RigBone[] source;
readonly IReadOnlyDictionary<string,Vector3> target;
readonly HashSet<int> mapped;
readonly (int End,int Start)[] segments;
readonly float scale;
internal Warp(RigBone[] source,IReadOnlyDictionary<string,Vector3> target)
{
this.source=source;this.target=target;
mapped=Enumerable.Range(0,source.Length).Where(i=>source[i].Role!="Root"&&!source[i].Role.StartsWith("Reference:")&&target.ContainsKey(source[i].Role)).ToHashSet();
if(mapped.Count<12)throw new InvalidOperationException("The reference cannot be fitted to the detected anatomy.");
segments=mapped.Select(i=>(End:i,Start:Parent(i))).Where(s=>s.Start>=0&&Vector3.DistanceSquared(source[s.Start].Position,source[s.End].Position)>1e-6f).ToArray();
float height=source[mapped.Single(i=>source[i].Role=="Head")].Position.Y-source.Where(b=>b.Role.StartsWith("Foot.")).Average(b=>b.Position.Y);
scale=(target["Head"].Y-(target["Foot.L"].Y+target["Foot.R"].Y)*.5f)/height;
if(!float.IsFinite(scale)||scale<=0)throw new InvalidOperationException("The reference has invalid anatomical proportions.");
}
int Parent(int i)
{
for(int p=source[i].Parent;p>=0;p=source[p].Parent)if(mapped.Contains(p))return p;
return -1;
}
internal Vector3 Map(Vector3 point,int bone)
{
int ancestor=mapped.Contains(bone)?bone:Parent(bone);
var candidates=segments.Where(s=>ancestor<0||s.Start==ancestor||s.End==ancestor).ToArray();
if(candidates.Length==0)candidates=segments;
var best=candidates.OrderBy(s=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,source[s.Start].Position,source[s.End].Position))).First();
return Carry(point,best.Start,best.End);
}
Vector3 Carry(Vector3 point,int start,int end)
{
var a=source[start].Position;var b=source[end].Position;var c=target[source[start].Role];var d=target[source[end].Role];
float along=Vector3.Dot(point-a,b-a)/(b-a).LengthSquared();
var offset=point-Vector3.Lerp(a,b,along);
var from=Vector3.Normalize(b-a);var to=Vector3.Normalize(d-c);float dot=Vector3.Dot(from,to);
var rotation=dot<-.9999f?SkeletonSolver.Frame(to,"X",0)*Quaternion.Inverse(SkeletonSolver.Frame(from,"X",0)):
Quaternion.Normalize(new Quaternion(Vector3.Cross(from,to),1+dot));
return Vector3.Lerp(c,d,along)+Vector3.Transform(offset,rotation)*scale;
}
}
/// <summary>Resolve the semantic ownership of source-specific helper bones.</summary>
internal static string Owner(GeneratedRig rig,int bone)
{
for(int i=bone;i>=0;i=rig.Bones[i].Parent)if(!rig.Bones[i].Role.StartsWith("Reference:"))return rig.Bones[i].Role;
return rig.Bones[bone].Role;
}
}