Editor/Core/Validation/JointWeightRepair.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Regularize a limb's blend with its parent using the measured joint
/// thickness. Trials retain connectivity and are accepted after complete stress
/// testing and local repair; reviewed bones and source geometry never move.</summary>
internal static class JointWeightRepair
{
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)
{
var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
var specifications=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,roles)).ToArray();
var expected=specifications.Select(p=>p.Name).Order().ToArray();
if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return rig;
geometry??=new ValidationGeometry(character);var faces=geometry.Faces;
var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
float height=character.AnatomicalHeight;
for(int pass=0;pass<4;pass++)
{
int before=rig.Report.StressTests.Sum(p=>p.ReversedTriangles);
foreach(var scheduled in rig.Report.StressTests.Where(p=>p.ReversedTriangles>0)
.OrderByDescending(p=>p.MaximumStretch>4||p.MinimumAreaRatio<.025f).ThenByDescending(p=>p.ReversedAreaFraction).ToArray())
{
var stress=rig.Report.StressTests.Single(p=>p.Pose==scheduled.Pose);
if(stress.ReversedTriangles==0)continue;
var specification=specifications.Single(p=>p.Name==stress.Pose);
if(!new[]{"UpperArm.","LowerArm.","UpperLeg.","LowerLeg."}.Any(specification.Role.StartsWith))continue;
int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);var bone=rig.Bones[joint];
var child=rig.Bones.FirstOrDefault(b=>b.Parent==joint&&b.Deform);
if(child is null||bone.Parent<0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)<1e-8f)continue;
var axis=Vector3.Normalize(child.Position-bone.Position);var moving=new bool[rig.Bones.Length];moving[joint]=true;
for(int i=joint+1;i<moving.Length;i++)moving[i]=rig.Bones[i].Parent>=0&&moving[rig.Bones[i].Parent];
float sum=0;int count=0;
foreach(var mesh in character.Meshes)foreach(var p in mesh.Vertices)
{
var delta=p-bone.Position;float along=Vector3.Dot(delta,axis),radial=(delta-axis*along).Length();
if(Math.Abs(along)<height*.006f&&radial<height*.07f){sum+=radial;count++;}
}
float radius=Math.Clamp(count>0?sum/count:height*.025f,height*.01f,height*.065f);
var totals=rig.Weights.Select(part=>part.Select(weights=>MovingTotal(weights,moving)).ToArray()).ToArray();
var axial=character.Meshes.Select(mesh=>mesh.Vertices.Select(point=>Vector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();
var trials=new List<(GeneratedRig Rig,StressResult Stress)>();
var blends=stress.ReversedTriangles<16?new[]{-.025f,-.05f,-.1f,-.2f,-.35f,.025f,.05f,.1f,.2f,.35f}:new[]{.5f,1f};
foreach(float width in new[]{2f,3f,4f,6f})foreach(float blend in blends)
{
var weights=character.Meshes.Select((mesh,part)=>mesh.Vertices.Select((point,vertex)=>
Blend(rig.Weights[part][vertex],rig.Profile.MaximumInfluences,rig.Bones.Length,moving,bone.Parent,
totals[part][vertex],axial[part][vertex]/(2*width*radius),blend)).ToArray()).ToArray();
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Weights=weights,Anatomy=rig.Anatomy};
var result=RigValidator.MeasurePose(character,candidate,specification,faces,buffer,height);
if(result.ReversedTriangles>=stress.ReversedTriangles||result.ReversedAreaFraction>stress.ReversedAreaFraction)continue;
trials.Add((candidate,result));
// Only the best three trials are tested below. Release other
// dense weight buffers immediately, preserving stable tie order.
if(trials.Count>3)trials=trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3).ToList();
}
// A promising broad correction can expose a local seam. Run the
// normal cleanup and repair before deciding whether it is better.
foreach(var trial in trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3))
{
var report=RigValidator.ValidateAndRepair(character,trial.Rig,geometry);
if(!SkeletonSolver.BetterSkinning(report,rig.Report,expected))continue;
report.Repairs+=rig.Report.Repairs;report.RepairPasses+=rig.Report.RepairPasses+1;
trial.Rig.Report=report;rig=trial.Rig;break;
}
}
if(before==rig.Report.StressTests.Sum(p=>p.ReversedTriangles))break;
}
return rig;
}
static float MovingTotal(Influence[] weights,bool[] moving)
{
double total=0;foreach(var w in weights)if(moving[w.Bone])total+=w.Weight;
return(float)total;
}
static Influence[] Blend(Influence[] source,int maximum,int boneCount,bool[] moving,int parent,float total,float axial,float amount)
{
if(total<.0001f)return source;
float t=Math.Clamp(.5f+axial,0,1),envelope=t*t*(3-2*t);
if(amount<0)
{
if(total>.9999f)return source;
float target=total+(-amount)*total*(1-total)*(1-envelope);
var scaled=new Influence[source.Length];
for(int i=0;i<source.Length;i++){var w=source[i];scaled[i]=w with{Weight=w.Weight*(moving[w.Bone]?target/total:(1-target)/(1-total))};}
return Skinning.Cleanup(scaled,boneCount,maximum);
}
float retained=1-amount+envelope*amount;
var blended=new Influence[source.Length+1];
for(int i=0;i<source.Length;i++){var w=source[i];blended[i]=w with{Weight=w.Weight*(moving[w.Bone]?retained:1)};}
blended[^1]=new(parent,total*(1-retained));
return Skinning.Cleanup(blended,boneCount,maximum);
}
}