Editor/Core/Validation/JointCoverage.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Check every deforming joint in both directions on each axis.
/// Only faces reached by that joint's skin weights need geometric measurement.</summary>
internal static class JointCoverage
{
internal static (StressPose Pose,StressResult Result)[] Measure(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
{
// Each joint's probes are independent and read only frozen weights.
// Share the ordinary worker budget; results keep their joint order.
var joints=Enumerable.Range(0,rig.Bones.Length).Where(j=>RigGeometry.CanPose(rig,j)).ToArray();
var result=new (StressPose,StressResult)[joints.Length*6];var bindFaces=geometry.Faces;
RigWork.For(joints.Length,RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length)),()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray(),(index,buffer)=>
{
int joint=joints[index],probe=index*6;
var moving=new bool[rig.Bones.Length];moving[joint]=true;
for(int b=joint+1;b<moving.Length;b++)moving[b]=rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
var touched=rig.Weights.Select(p=>p.Select(w=>w.Any(i=>moving[i.Bone])).ToArray()).ToArray();
var faces=bindFaces.Select((p,m)=>p.Where(f=>touched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();
foreach(var (axis,name) in new[]{(Vector3.UnitX,"X"),(Vector3.UnitY,"Y"),(Vector3.UnitZ,"Z")})foreach(float degrees in new[]{-30f,30f})
{
var pose=new StressPose($"Joint {rig.Bones[joint].Role} {name} {degrees:+0;-0}",rig.Bones[joint].Role,axis,degrees);
result[probe++]=(pose,RigValidator.MeasurePose(character,rig,pose,faces,buffer,geometry.Height));
}
});
return result;
}
internal static bool Bad(StressResult result)=>result.ReversedTriangles>0||result.NonFiniteVertices>0||result.NonFiniteMeasurements>0||result.MaximumStretch>4||result.MinimumAreaRatio<.025f;
static double Score(IEnumerable<StressResult> results)=>results.Sum(r=>r.ReversedTriangles+1000000d*(r.NonFiniteVertices+r.NonFiniteMeasurements)+100*Math.Max(0,r.MaximumStretch/4-1)+100*Math.Max(0,1-r.MinimumAreaRatio/.025f));
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)
{
if(!rig.Report.Passed)return rig;
geometry??=new ValidationGeometry(character);var checks=Measure(character,rig,geometry);
if(checks.Any(c=>Bad(c.Result)))
{
var repaired=PoseWeightRepair.Improve(character,rig,geometry,checks);
if(!ReferenceEquals(repaired,rig)){rig=repaired;checks=Measure(character,rig,geometry);}
}
var trunk=new TrunkRegion(character,rig);
var normalHeat=new Lazy<Influence[][][]>(()=>HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());
var heat=new Lazy<Influence[][][]>(()=>HeatSkinning.Solve(character,rig));
var constraints=new Dictionary<string,StressPose>();
for(int pass=0;pass<4&&checks.Any(c=>Bad(c.Result));pass++)
{
var failed=checks.Where(c=>Bad(c.Result)).OrderByDescending(c=>c.Result.ReversedTriangles).Take(24).Select(c=>c.Pose).ToArray();
// Retain previously discovered failures after they are repaired.
// Otherwise a spine correction can undo the adjacent chest repair.
foreach(var pose in failed)constraints.TryAdd(pose.Name,pose);
var scope=new ValidationGeometry(character,Deformation.Poses.Concat(constraints.Values).ToArray(),trunk,geometry.InfluenceAllowed);
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};
candidate.Report=RigValidator.ValidateAndRepair(character,candidate,scope);
candidate=JointWeightRepair.Improve(character,candidate,scope);
if(candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0))
RefineSources(character,candidate,scope,failed,normalHeat,heat);
if(!candidate.Report.Passed||trunk.HasBleeding(character,candidate))break;
var standard=RigValidator.Validate(character,candidate);
if(!standard.Passed||standard.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))break;
var next=Measure(character,candidate,geometry);
bool discovered=false;
foreach(var check in next.Where(c=>Bad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);
if(Score(next.Select(c=>c.Result))>=Score(checks.Select(c=>c.Result)))
{if(discovered)continue;break;}
standard.Repairs=rig.Report.Repairs+candidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses+candidate.Report.RepairPasses+1;
candidate.Report=standard;rig=candidate;checks=next;
}
rig.Report.JointStressTests.AddRange(checks.Select(c=>c.Result));
var remaining=checks.Where(c=>Bad(c.Result)).ToArray();
if(remaining.Length>0)rig.Report.Issues.Add(new("joint-deformation",$"{remaining.Length} joint motions still have unsafe deformation: {string.Join(", ",remaining.Select(c=>c.Pose.Role).Distinct())}.",true));
return rig;
}
static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy<Influence[][][]>[] fields)
{
var roots=failed.Select(p=>p.Role).ToHashSet();var moving=new bool[rig.Bones.Length];
for(int b=0;b<moving.Length;b++)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
foreach(var field in fields)
{
Influence[][][] source;
try{source=field.Value;}catch(InvalidOperationException){continue;}
foreach(float amount in new[]{1f,.5f,.25f})
{
var proposed=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();
for(int p=0;p<proposed.Length;p++)for(int v=0;v<proposed[p].Length;v++)
{
float support=Math.Min(1,rig.Weights[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight)+source[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight))*amount;
if(support<.001f)continue;
var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=>w with{Weight=w.Weight*(1-support)})
.Concat(source[p][v].Select(w=>w with{Weight=w.Weight*support})),rig.Bones.Length,rig.Profile.MaximumInfluences);
if(!geometry.Allows(p,v,character.Meshes[p].Vertices[v],weights))continue;
proposed[p][v]=weights;
}
rig.Report=SurfaceRepair.TryWeights(character,rig,rig.Report,proposed,geometry);
rig.Report=SurfaceRepair.Improve(character,rig,rig.Report,geometry);
if(rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return;
}
}
}
}