Editor/Core/Validation/HeadWeightRepair.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Separate skull ownership at a measured neck contour. Nearby shoulders
/// must not pull the face; disconnected cranial surfaces retain their topology.</summary>
internal static class HeadWeightRepair
{
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,Action<string>? diagnostic=null)
{
if(!rig.Report.Passed)return rig;
int head=Array.FindIndex(rig.Bones,b=>b.Role=="Head"),neck=Array.FindIndex(rig.Bones,b=>b.Role=="Neck");
if(head<0||neck<0)return rig;
float height=character.AnatomicalHeight;var origin=rig.Bones[neck].Position;
var section=NeckSection(character,origin,height);
if(section is null)return rig;
origin=section.Center;
var skull=new bool[rig.Bones.Length];skull[head]=true;
for(int b=head+1;b<skull.Length;b++)skull[b]=rig.Bones[b].Parent>=0&&skull[rig.Bones[b].Parent];
var axial=new bool[rig.Bones.Length];for(int b=neck;b>=0;b=rig.Bones[b].Parent)axial[b]=true;
float transitionHeight=Math.Max(section.Radius*.5f,height*.005f);
diagnostic?.Invoke($"neck joint {rig.Bones[neck].Position/height} section center {section.Center/height} radius {section.Radius/height:F3}H transition {transitionHeight/height:F3}H head joint {rig.Bones[head].Position/height}");
var mesh=Geometry.Merge(character.Meshes);var graph=Geometry.Neighbors(mesh,height*1e-5f);
for(int v=0;v<graph.Length;v++)graph[v].RemoveAll(n=>
{
float a=mesh.Vertices[v].Y-origin.Y,b=mesh.Vertices[n].Y-origin.Y;
if(!((a<=0&&b>0)||(b<=0&&a>0)))return false;
// Cut the analysis graph across outer hair shells too. Anatomy
// classifies each resulting component before it receives weights.
return true;
});
var components=Geometry.Components(graph);var ends=RigGeometry.SegmentEnds(rig);
var cranial=new bool[components.Max()+1];var below=new bool[cranial.Length];
var minimum=Enumerable.Repeat(new Vector3(float.PositiveInfinity),cranial.Length).ToArray();
var maximum=Enumerable.Repeat(new Vector3(float.NegativeInfinity),cranial.Length).ToArray();
bool NearHead(Vector3 point)
{
float distance=Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[head].Position,ends[head]));
float other=Enumerable.Range(0,rig.Bones.Length).Where(b=>rig.Bones[b].Deform&&!skull[b]&&b!=neck)
.Select(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b]))).DefaultIfEmpty(float.PositiveInfinity).Min();
return distance<other*.5f;
}
for(int v=0;v<mesh.Vertices.Length;v++)
{
var point=mesh.Vertices[v];int component=components[v];
minimum[component]=Vector3.Min(minimum[component],point);maximum[component]=Vector3.Max(maximum[component],point);
if(point.Y<origin.Y-height*.001f)below[component]=true;
if(point.Y<=origin.Y)continue;
if(NearHead(point))cranial[component]=true;
}
// Sparse skulls may have only distant corner vertices. Confirm their
// bounds center against the volume without relying on vertex density.
var volume=new Lazy<SurfaceVisibility>(()=>new(character.Meshes.Where(m=>m.Kind==MeshKind.Body),height*1e-5f));
for(int c=0;c<cranial.Length;c++)if(!below[c]&&!cranial[c])
{
var center=(minimum[c]+maximum[c])*.5f;
cranial[c]=NearHead(center)&&volume.Value.Contains(center,height*1e-5f);
}
var selected=character.Meshes.Select(m=>new bool[m.Vertices.Length]).ToArray();int offset=0;
for(int p=0;p<selected.Length;offset+=selected[p++].Length)
if(character.Meshes[p].Kind is MeshKind.Body or MeshKind.Clothing or MeshKind.Hair)
for(int v=0;v<selected[p].Length;v++){int c=components[offset+v];selected[p][v]=cranial[c]&&!below[c];}
// Within the transition any bone may linger: a limb's small tail on the
// trapezius fades out there instead of stopping at the neck plane.
bool Allows(int p,int v,int b)=>!selected[p][v]||skull[b]||character.Meshes[p].Vertices[v].Y<origin.Y+transitionHeight;
bool Bleeds(GeneratedRig candidate)
{
for(int p=0;p<selected.Length;p++)for(int v=0;v<selected[p].Length;v++)if(selected[p][v])
foreach(var w in candidate.Weights[p][v])if(w.Weight>1e-6f&&!Allows(p,v,w.Bone))return true;
return false;
}
if(!Bleeds(rig))return rig;
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};
for(int p=0;p<selected.Length;p++)for(int v=0;v<selected[p].Length;v++)if(selected[p][v])
{
var values=new float[rig.Bones.Length];float removed=0;
float blend=TrunkRegion.Smooth((character.Meshes[p].Vertices[v].Y-origin.Y)/transitionHeight);
foreach(var w in rig.Weights[p][v])
{
if(skull[w.Bone])values[w.Bone]+=w.Weight;
else {values[w.Bone]+=w.Weight*(1-blend);removed+=w.Weight*blend;}
}
// What the torso held goes where the neck goes: to the neck at the
// section and to the head above the transition. Handing it to the
// head at the section itself steps the head weight across the plane.
values[neck]+=removed*(1-blend);values[head]+=removed*blend;candidate.Weights[p][v]=Skinning.Cleanup(values,rig.Profile.MaximumInfluences);
}
// Guard the trunk only where the reviewed rig already keeps it: a rig
// that carries the closed-pose warning would otherwise see every head
// candidate rejected for a bleed it did not introduce.
var trunk=new TrunkRegion(character,rig);
var geometry=new ValidationGeometry(character,trunk:trunk.HasBleeding(character,rig)?null:trunk,influenceAllowed:Allows);
candidate.Report=RigValidator.ValidateAndRepair(character,candidate,geometry);
// A fold the rig already carries elsewhere is no reason to leave the
// face on the torso; only a fold the head candidate adds is.
if(!candidate.Report.Passed||candidate.Report.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles))
candidate=PoseWeightRepair.Improve(character,candidate,geometry,JointCoverage.Measure(character,candidate,geometry));
if(candidate.Report.Passed)candidate=SeamWeightRepair.Improve(character,JointCoverage.Improve(character,candidate,geometry),geometry);
diagnostic?.Invoke($"candidate: passed {candidate.Report.Passed} bleeds {Bleeds(candidate)} selected {selected.Sum(p=>p.Count(x=>x))} folds {candidate.Report.StressTests.Sum(p=>p.ReversedTriangles)} vs {rig.Report.StressTests.Sum(p=>p.ReversedTriangles)} errors {string.Join(" | ",candidate.Report.Issues.Where(i=>i.Error).Select(i=>i.Code+": "+i.Message))}");
// A joint probe the reviewed rig fails as well is no verdict on the
// head; the candidate carries that report on, with the face fixed.
bool acceptable=candidate.Report.Passed;
if(!acceptable&&candidate.Report.Issues.Where(i=>i.Error).All(i=>i.Code=="joint-deformation"))
{
var before=JointCoverage.Measure(character,rig,geometry).Where(c=>JointCoverage.Bad(c.Result)).Select(c=>c.Pose.Name).ToHashSet();
acceptable=JointCoverage.Measure(character,candidate,geometry).Where(c=>JointCoverage.Bad(c.Result)).All(c=>before.Contains(c.Pose.Name));
diagnostic?.Invoke($"probes: candidate fails only probes the rig fails too: {acceptable} (rig fails {before.Count})");
}
if(acceptable&&!Bleeds(candidate))
{
candidate.Report.Repairs+=rig.Report.Repairs;candidate.Report.RepairPasses+=rig.Report.RepairPasses+1;return candidate;
}
rig.Report.Issues.Add(new("head-weight-bleeding","The face still follows a shoulder or torso joint. Check the neck and head landmarks.",true));
return rig;
}
static MeshSections.Section? NeckSection(ImportedCharacter character,Vector3 origin,float height)
{
MeshSections.Section? At(Vector3 point)
{
// Separate material parts can quantize the same seam differently.
// Retry the contour with a small relative gap tolerance;
// this joins analysis endpoints without welding source geometry.
foreach(float tolerance in new[]{1e-5f,1e-4f})
{
var found=MeshSections.Cut(character,point,Vector3.UnitY,height*.15f,height*tolerance)
.Where(s=>s.Radius>height*.005f&&s.Radius<height*.08f&&Vector3.Distance(s.Center,point)<s.Radius*.5f)
.MinBy(s=>Vector3.DistanceSquared(s.Center,point));
if(found is not null)return found;
}
return null;
}
var section=At(origin);if(section is not null)return section;
// A coarse or block-shaped skull can contain the predicted neck joint.
// Confirm a narrower neck shaft below it before assigning skull weights.
var lower=Enumerable.Range(1,16).Select(i=>At(origin-Vector3.UnitY*(height*.005f*i))).OfType<MeshSections.Section>().ToArray();
if(lower.Length<2)return null;
float radius=lower.Min(s=>s.Radius);var shaft=lower.Where(s=>s.Radius<radius*1.1f).ToArray();
if(shaft.Length<2)return null;
float middle=shaft.Average(s=>s.Center.Y);
return shaft.MinBy(s=>Math.Abs(s.Center.Y-middle));
}
}