Editor/Core/Validation/PoseWeightRepair.cs
namespace HumanoidRigger;
/// <summary>Bounded pose-constrained skinning repair. Intermediate trials may be
/// unsafe; only a completely validated candidate can replace the reviewed rig.</summary>
internal static class PoseWeightRepair
{
const int MaximumFolds=1000;
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,
(StressPose Pose,StressResult Result)[] joints)
{
var poses=geometry.Poses.Concat(joints.Select(j=>j.Pose)).ToArray();
var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
var expected=poses.Where(p=>Deformation.IsApplicable(p,roles)).Select(p=>p.Name).ToArray();
var initial=new ValidationReport();initial.Issues.AddRange(rig.Report.Issues);
initial.StressTests.AddRange(rig.Report.StressTests);initial.StressTests.AddRange(joints.Select(j=>j.Result));
if(!WeightRepair.HasCompleteEvidence(initial,expected.Order().ToArray())||!initial.StressTests.Select(p=>p.Pose).SequenceEqual(expected))return rig;
if(initial.Passed&&initial.StressTests.All(p=>p.ReversedTriangles==0&&p.MaximumStretch<=4&&p.MinimumAreaRatio>=.025f))return rig;
// This fits the faces around each fold against every pose, so its cost
// grows with their number. It answers a seam or a socket boundary: a few
// dozen folds, a couple of hundred on the hardest characters. Thousands
// mean the weights are wrong at a scale a local fit does not mend, and
// trying takes tens of minutes on a dense mesh before being rejected.
// A fine character is judged patch by patch, and a patch holds many
// faces; the fit's cost is in faces. Count the faces its folds cover.
long folds=RigValidator.Fine(character,character.AnatomicalHeight)
?(long)Math.Round(initial.StressTests.Sum(p=>(double)p.ReversedAreaFraction)*character.Meshes.Sum(m=>m.Triangles.Length/3))
:initial.StressTests.Sum(p=>(long)p.ReversedTriangles);
if(folds>MaximumFolds)return rig;
var trunk=geometry.Trunk;
if(trunk is not null&&trunk.HasBleeding(character,rig))return rig;
if(trunk is null)
{
var existing=new TrunkRegion(character,rig);
// Preserve already established ownership. A standalone deformation
// repair can precede the separate trunk-ownership stage.
if(!existing.HasBleeding(character,rig))trunk=existing;
}
var scope=new ValidationGeometry(character,poses,trunk,geometry.InfluenceAllowed){SampledPoses=geometry.SampledPoses};
var seams=new WeightSeams(character,rig.Weights);
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights,Report=initial};
// Ordinary defects retain their influence set. Broaden it only after
// that fails; the next pass fits the profile's limited influence set.
var bestWeights=rig.Weights;var bestReport=initial;
double Score(ValidationReport r)=>r.StressTests.Sum(p=>p.ReversedTriangles+100*Math.Max(0,p.MaximumStretch/4-1)+100*Math.Max(0,1-p.MinimumAreaRatio/.025f));
for(int attempt=0;attempt<2;attempt++)
{
if(attempt==1)
{
candidate.Weights=bestWeights;
candidate.Weights=SpineWeightSupport.Compact(candidate);
candidate.Report=RigValidator.Validate(character,candidate,null,scope);
if(!WeightRepair.HasCompleteEvidence(candidate.Report,expected.Order().ToArray()))return rig;
}
var attemptWeights=candidate.Weights;var attemptReport=candidate.Report;
foreach(var (expand,support,iterations) in new[]{(false,false,240),(true,false,240),(false,false,600),(false,true,600),(false,true,600)})
{
if(expand){candidate.Weights=attemptWeights;candidate.Report=attemptReport;}
candidate.Weights=PoseWeightFit.Solve(character,candidate,scope,candidate.Report,seams,expand,support,iterations,attempt==1);
candidate.Report=RigValidator.Validate(character,candidate,null,scope);
// The continuous fit can unlock a discrete influence transfer
// that was impossible before it. Finish locally, then recheck.
candidate.Report=SurfaceRepair.Improve(character,candidate,candidate.Report,scope);
if(!WeightRepair.HasCompleteEvidence(candidate.Report,expected.Order().ToArray()))return rig;
if(Score(candidate.Report)<Score(bestReport)){bestWeights=candidate.Weights;bestReport=candidate.Report;}
if(!candidate.Report.Passed||candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0)||!seams.Preserved(candidate.Weights))continue;
// Rebuild the ordinary report separately so the wizard retains its
// existing standard-pose presentation and independent joint audit.
var report=RigValidator.Validate(character,candidate,null,geometry);
if(!report.Passed||report.StressTests.Any(p=>p.ReversedTriangles>0)||trunk?.HasBleeding(character,candidate)==true)return rig;
report.Repairs=rig.Report.Repairs+candidate.Weights.SelectMany((p,m)=>p.Select((w,v)=>w.SequenceEqual(rig.Weights[m][v])?0:1)).Sum();
report.RepairPasses=rig.Report.RepairPasses+1;
candidate.Report=report;
return candidate;
}
}
return rig;
}
}