Editor/Core/Rigging/SkinTransfer.cs
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger.EditorTools.Core.Rigging;
using Vector3=System.Numerics.Vector3;

/// <summary>Reference rigs: copy the stock Citizen skin onto the fitted body at the knees, ankles and elbows,
/// so they deform like the stock model, including the volume the joint helpers keep and the stock weighting
/// behind the knee and inside the elbow. Vertices are matched in limb coordinates: position along the limb
/// and angle around it, measured in the bone's own frame. Fitted frames are the stock frames swung onto the
/// fitted bones, so the same coordinates are the same anatomical spot on both bodies whatever the limb's
/// thickness or rest pose (a nearest-surface match crossed the crease of a slim, straight arm). The trunk,
/// hips, neck, head and hands keep the solved weights.</summary>
internal static class SkinTransfer
{
    internal static string LastOutcome="";
    const float Reach=.65f,Fade=.2f;

    /// <summary>Transfers each joint type that validates; <paramref name="kept"/> lists them.</summary>
    internal static GeneratedRig Apply(ImportedCharacter character,GeneratedRig rig,out string[] kept)
    {
        var done=new List<string>();kept=[];
        if(rig.Profile.Reference?.Surface is null||rig.Weights is null||!rig.Report.Passed){LastOutcome="skipped";return rig;}
        // Each joint type is kept only when it validates at least as well on its own.
        var outcomes=new List<string>();
        foreach(string group in new[]{"Knee","Ankle","Elbow"})
        {
            var next=Transfer(character,rig,group,out var outcome);outcomes.Add(group+": "+outcome);
            if(!ReferenceEquals(next,rig))done.Add(group);
            rig=next;
        }
        kept=done.ToArray();
        LastOutcome=string.Join(" | ",outcomes);
        return rig;
    }

    /// <summary>One limb joint: the bone above it, the joint bone, and the point the lower segment aims at.</summary>
    internal sealed record Limb(int Upper,int Joint,int Below,string[] Roles);

    /// <summary>Limb coordinates of a point: along the limb in lower-segment lengths from the joint
    /// (negative above), angle around the segment's bone axis, and distance from that axis in segment lengths.</summary>
    internal static (float U,float Angle,float Radius) Coordinates(Vector3 x,RigBone[] bones,Limb limb)
    {
        var a=bones[limb.Upper].Position;var j=bones[limb.Joint].Position;
        var b=limb.Below>=0?bones[limb.Below].Position:j+(j-a);float length=Vector3.Distance(j,b);
        float u=ReferenceSkin.Position(x,a,j,b,length);
        var (bone,child)=u<0?(limb.Upper,j):(limb.Joint,b);
        var inverse=Quaternion.Inverse(bones[bone].Rotation);var origin=bones[bone].Position;
        var local=Vector3.Transform(x-origin,inverse);var axis=Vector3.Normalize(Vector3.Transform(child-origin,inverse));
        var perpendicular=local-axis*Vector3.Dot(local,axis);
        var e1=Vector3.Normalize(Vector3.Cross(axis,Math.Abs(axis.X)<.9f?Vector3.UnitX:Vector3.UnitY));var e2=Vector3.Cross(axis,e1);
        return (u,MathF.Atan2(Vector3.Dot(perpendicular,e2),Vector3.Dot(perpendicular,e1)),perpendicular.Length()/length);
    }

    static GeneratedRig Transfer(ImportedCharacter character,GeneratedRig rig,string group,out string outcome)
    {
        var reference=rig.Profile.Reference!;var surface=reference.Surface!;var stockBones=reference.Bones;
        int Role(string role)=>Array.FindIndex(rig.Bones,b=>b.Role==role);
        var owner=Enumerable.Range(0,rig.Bones.Length).Select(b=>ReferenceFitting.Owner(rig,b)).ToArray();
        var limbs=new List<Limb>();
        foreach(string side in new[]{"L","R"})
        {
            var (upper,joint,below)=group switch
            {
                "Knee"=>("UpperLeg."+side,"LowerLeg."+side,"Foot."+side),
                "Ankle"=>("LowerLeg."+side,"Foot."+side,"Toe."+side),
                _=>("UpperArm."+side,"LowerArm."+side,"Hand."+side),
            };
            int u=Role(upper),j=Role(joint),b=Role(below);
            if(u<0||j<0)continue;
            limbs.Add(new Limb(u,j,b,group=="Ankle"?[upper,joint,below]:[upper,joint]));
        }
        int Dominant(IEnumerable<Influence> row)=>row.OrderByDescending(w=>w.Weight).Select(w=>w.Bone).DefaultIfEmpty(-1).First();
        var weights=rig.Weights!.Select(p=>p.Select(v=>(Influence[])v.Clone()).ToArray()).ToArray();
        int matched=0,eligible=0;
        foreach(var limb in limbs)
        {
            // Stock samples on this limb, bucketed by position along it.
            var buckets=new Dictionary<int,List<(float U,float Angle,Influence[] Weights)>>();
            for(int v=0;v<surface.Vertices.Length;v++)
            {
                int d=Dominant(surface.Weights[v]);if(d<0||!limb.Roles.Contains(owner[d]))continue;
                var (u,angle,radius)=Coordinates(surface.Vertices[v],stockBones,limb);
                if(Math.Abs(u)>Reach+.05f||radius>.8f)continue;
                int key=(int)MathF.Floor(u/.02f);
                if(!buckets.TryGetValue(key,out var list))buckets[key]=list=[];
                list.Add((u,angle,surface.Weights[v]));
            }
            if(buckets.Count==0)continue;
            for(int part=0;part<character.Meshes.Length;part++)
            for(int v=0;v<character.Meshes[part].Vertices.Length;v++)
            {
                int d=Dominant(rig.Weights![part][v]);if(d<0||!limb.Roles.Contains(owner[d]))continue;
                var (u,angle,radius)=Coordinates(character.Meshes[part].Vertices[v],rig.Bones,limb);
                if(radius>.8f)continue;
                float fade=Math.Clamp((Reach-Math.Abs(u))/Fade,0,1);if(fade<=0)continue;
                eligible++;
                // Nearest stock samples in (along, around) coordinates.
                var nearest=new List<(float Distance,Influence[] Weights)>();
                int key=(int)MathF.Floor(u/.02f);
                for(int k=key-3;k<=key+3;k++)
                {
                    if(!buckets.TryGetValue(k,out var list))continue;
                    foreach(var s in list)
                    {
                        float du=(s.U-u)/.03f,da=MathF.IEEERemainder(s.Angle-angle,2*MathF.PI)/.25f;
                        nearest.Add((du*du+da*da,s.Weights));
                    }
                }
                if(nearest.Count==0)continue;
                var blended=new Dictionary<int,float>();float total=0;
                foreach(var (distance,stock) in nearest.OrderBy(n=>n.Distance).Take(4))
                {
                    float w=1/(distance+1e-3f);total+=w;
                    foreach(var i in stock)blended[i.Bone]=blended.GetValueOrDefault(i.Bone)+i.Weight*w;
                }
                if(blended.Keys.Any(b=>!rig.Bones[b].Deform))continue;
                foreach(var bone in blended.Keys.ToArray())blended[bone]=blended[bone]/total*fade;
                if(fade<1)foreach(var w in rig.Weights[part][v])blended[w.Bone]=blended.GetValueOrDefault(w.Bone)+w.Weight*(1-fade);
                weights[part][v]=Skinning.Cleanup(blended.Where(p=>p.Value>0).Select(p=>new Influence(p.Key,p.Value)),rig.Bones.Length,rig.Profile.MaximumInfluences);
                matched++;
            }
        }
        if(matched==0){outcome="no matches";return rig;}
        var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=weights};
        // The rigger's local repair resolves the few faces the copied skin folds in its stress poses.
        candidate.Report=RigValidator.ValidateAndRepair(character,candidate);
        var geometry=new ValidationGeometry(character);
        int Folds(ValidationReport r)=>r.StressTests.Sum(t=>t.ReversedTriangles);
        int badAfter=JointCoverage.Measure(character,candidate,geometry).Count(c=>JointCoverage.Bad(c.Result));
        int badBefore=JointCoverage.Measure(character,rig,geometry).Count(c=>JointCoverage.Bad(c.Result));
        bool keep=candidate.Report.Passed&&Folds(candidate.Report)<=Folds(rig.Report)&&badAfter<=badBefore;
        outcome=$"matched {matched}/{eligible} vertices, folds {Folds(rig.Report)}->{Folds(candidate.Report)}, bad joints {badBefore}->{badAfter}, {(keep?"kept":"rejected")}"
            +string.Join(";",candidate.Report.StressTests.Where(t=>t.ReversedTriangles>0||t.MaximumStretch>4||t.MinimumAreaRatio<.025f).Select(t=>$" {t.Pose}:{t.ReversedTriangles}"));
        if(!keep)return rig;
        candidate.Report.JointStressTests.AddRange(rig.Report.JointStressTests);
        candidate.Report.Repairs=rig.Report.Repairs;candidate.Report.RepairPasses=rig.Report.RepairPasses;
        return candidate;
    }
}