The file contains logic to validate and repair humanoid character rigs in the editor. It performs checks on bone hierarchies, skinning weights, and mesh deformation under various poses.
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger.EditorTools.Core.Validation;
using Vector3 = System.Numerics.Vector3;
public static class RigValidator
{
public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)
=>ValidateAndRepair(character,rig,new ValidationGeometry(character));
internal static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
{
int repairs=0;
foreach(var part in rig.Weights) for(int v=0;v<part.Length;v++)
{
var cleaned=Skinning.Cleanup(part[v],rig.Bones.Length,rig.Profile.MaximumInfluences);
if(!cleaned.SequenceEqual(part[v])) {part[v]=cleaned;repairs++;}
}
var report=Validate(character,rig,null,geometry);report.Repairs=repairs;
return SurfaceRepair.Improve(character,rig,WeightRepair.Improve(character,rig,report,geometry),geometry);
}
public static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig)
=>Validate(character,rig,null);
internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)
=>Validate(character,rig,faces,null);
internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces,ValidationGeometry? geometry)
{
var height=geometry?.Height??character.AnatomicalHeight;var r=new ValidationReport();void Error(string code,string text)=>r.Issues.Add(new(code,text,true));
try{rig.Profile.Validate();}catch(Exception e){Error("profile",e.Message);return r;}
var roles=new HashSet<string>();var names=new HashSet<string>();
var body=geometry?.Body??character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
for(int i=0;i<rig.Bones.Length;i++)
{
var b=rig.Bones[i];
if(!roles.Add(b.Role)||!names.Add(b.Name))Error("duplicate","Duplicate bone assignment.");
bool validParent=b.Parent<i&&b.Parent>=-1;
if(!validParent)Error("hierarchy","Invalid skeleton hierarchy.");
if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)>.001f)Error("frame","Invalid bone orientation or position.");
if(b.Deform&&body.Length>0&&(geometry?.JointDistanceSquared(b.Position)??body.Min(p=>Vector3.DistanceSquared(p,b.Position)))>height*height*.15f*.15f)
Error("joint-placement",$"{b.Role} is too far from the character. Check its landmark.");
var definition=rig.Profile.Bones.FirstOrDefault(d=>d.Role==b.Role);
if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent<0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error("mapping","Skeleton differs from the selected profile.");
}
foreach(var b in rig.Profile.Bones.Where(b=>b.Required))if(!roles.Contains(b.Role))Error("required",$"Missing {b.Role}.");
if(rig.Weights.Length!=character.Meshes.Length){Error("weights","Missing mesh skinning.");return r;}
for(int p=0;p<rig.Weights.Length;p++)
{
if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error("weights","Skinning vertex count mismatch.");continue;}
foreach(var vertex in rig.Weights[p])
{
if(vertex.Length==0||vertex.Length>rig.Profile.MaximumInfluences)Error("influences","Invalid influence count.");
if(vertex.Any(i=>i.Bone<0||i.Bone>=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight<=0))Error("influences","Invalid weight or bone index.");
if(Math.Abs(vertex.Sum(i=>i.Weight)-1)>.0001f)Error("normalization","Weights do not sum to one.");
}
}
if(r.Issues.Any(i=>i.Error))return r;
if(geometry?.InfluenceAllowed is {} allowed&&rig.Weights.Where((part,p)=>part.Where((weights,v)=>weights.Any(w=>w.Weight>1e-6f&&!allowed(p,v,w.Bone))).Any()).Any())
{Error("weight-ownership","A weight repair crossed a confirmed anatomical boundary.");return r;}
if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error("weight-bleeding","A weight repair reintroduced a remote torso attachment.");return r;}
var ends=RigGeometry.SegmentEnds(rig);
var locality=new SkinningLocality(character,rig,ends);
for(int p=0;p<character.Meshes.Length;p++)
{
var mesh=character.Meshes[p];if(mesh.Kind==MeshKind.Accessory)continue;
bool remote=false;
for(int v=0;v<mesh.Vertices.Length&&!remote;v++)
foreach(var influence in rig.Weights[p][v])
{
if(!rig.Bones[influence.Bone].Deform){Error("nondeforming-influence","Skinning references a non-deforming bone.");remote=true;break;}
if(influence.Weight>.05f&&Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))>locality.Limit(influence.Bone,mesh.Vertices[v]))
{Error("weight-region",$"Mesh '{mesh.Name}' is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).");remote=true;break;}
}
}
if(r.Issues.Any(i=>i.Error))return r;
faces??=geometry?.Faces??character.Meshes.Select(BindTriangle.Measure).ToArray();
var specifications=(geometry?.Poses??Deformation.Poses).ToArray();
var tests=new StressResult[specifications.Length];
Vector3[][] Buffers()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
void Measure(int i,Vector3[][] buffer)
{
if(Deformation.IsApplicable(specifications[i],roles))
{
var transforms=geometry?.Transforms(rig,specifications[i])??Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,specifications[i]));
Deformation.ApplyTransforms(character,rig,transforms.Positions,transforms.Rotations,buffer);
tests[i]=MeasureDeformed(character,rig,specifications[i].Name,faces,buffer,transforms.Rotations,height,Still(rig,transforms,height));
}
}
// Poses read the same frozen weights and write separate buffers. Keep
// report order and each pose's arithmetic serial and deterministic.
int workers=RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length));
RigWork.For(specifications.Length,workers,Buffers,Measure);
for(int i=0;i<specifications.Length;i++)
{
var pose=specifications[i];var test=tests[i];
if(test is null){r.Issues.Add(new("optional-pose",$"{pose.Name}: optional joints absent.",false));continue;}
r.StressTests.Add(test);
if(test.ReversedTriangles>0)r.Issues.Add(new("surface-reversal",$"{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.",false));
if(test.NonFiniteVertices>0||test.NonFiniteMeasurements>0)Error("deformation",$"{pose.Name}: deformation produced non-finite coordinates or measurements.");
else if(test.MaximumStretch>4||test.MinimumAreaRatio<.025f)Error("deformation",$"{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).");
}
return r;
}
internal static StressResult MeasurePose(ImportedCharacter character,GeneratedRig rig,StressPose pose,BindTriangle[][] faces,Vector3[][] deformed,float height)
{
var transforms=Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,pose));
var rotations=transforms.Rotations;
Deformation.ApplyTransforms(character,rig,transforms.Positions,rotations,deformed);
return MeasureDeformed(character,rig,pose.Name,faces,deformed,rotations,height);
}
/// <summary>Bones a pose leaves at their bind transform. A vertex weighted
/// only to those does not move, and a face of such vertices keeps its bind
/// shape exactly: it can neither stretch nor reverse.</summary>
static bool[] Still(GeneratedRig rig,(Vector3[] Positions,System.Numerics.Quaternion[] Rotations) transforms,float height)
=>rig.Bones.Select((bone,b)=>transforms.Rotations[b]==System.Numerics.Quaternion.Identity&&Vector3.Distance(transforms.Positions[b],bone.Position)<=height*1e-6f).ToArray();
/// <summary>The scale a deformation is judged at. A sculpt's faces are a
/// millimetre across, and the pores and wrinkles that give a bodybuilder
/// his skin fold one by one under any blend of two bones: nothing a viewer
/// could see. Faces finer than the patch are judged together, patch by
/// patch, on the same tests; coarser meshes are judged face by face.</summary>
internal const float PatchScale=.006f;
static readonly System.Runtime.CompilerServices.ConditionalWeakTable<ImportedCharacter,object> fineness=new();
/// <summary>Whether the character as a whole is finer than the patch. A
/// head or a hand judged on its own has small faces on any character.</summary>
internal static bool Fine(ImportedCharacter character,float height)
{
if(fineness.TryGetValue(character,out var known))return (bool)known;
double total=0;long count=0;
foreach(var mesh in character.Meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)
{
float length=Vector3.Distance(mesh.Vertices[mesh.Triangles[t]],mesh.Vertices[mesh.Triangles[t+1]]);
if(float.IsFinite(length)){total+=length;count++;}
}
bool fine=count>0&&total/count<height*PatchScale*.4f;
fineness.AddOrUpdate(character,fine);return fine;
}
internal static StressResult MeasureDeformed(ImportedCharacter character,GeneratedRig rig,string name,BindTriangle[][] faces,Vector3[][] deformed,System.Numerics.Quaternion[] rotations,float height,bool[]? still=null)
{
if(Fine(character,height))return MeasurePatches(character,rig,name,faces,deformed,rotations,height,still);
float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int nonFinite=0,invalidMeasurements=0;
int reversed=0;double surfaceArea=0,reversedArea=0;
for(int p=0;p<character.Meshes.Length;p++)
{
var mesh=character.Meshes[p];var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));
var weights=rig.Weights[p];
bool Moves(int v){foreach(var w in weights[v])if(!still![w.Bone])return true;return false;}
foreach(var face in faces[p])
{
var i=face.A;var j=face.B;var k=face.C;
if(still is not null&&!Moves(i)&&!Moves(j)&&!Moves(k))
{
// Keep the surface total in its original order; the face
// contributes an area ratio of exactly one and no stretch.
// Its measurements can still be invalid, and that is reported.
if(!float.IsFinite(face.Area))invalidMeasurements++;
else if(face.Area>height*height*1e-10f)surfaceArea+=face.Area;
for(int edge=0;edge<3;edge++)if(!float.IsFinite(face.Edge(edge).Length))invalidMeasurements++;
continue;
}
var normal=face.Normal;
var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);
float area=face.Area,posedArea=posedNormal.Length();
if(!float.IsFinite(area)||!float.IsFinite(posedArea))invalidMeasurements++;
else if(area>height*height*1e-10f)
{
float ratio=posedArea/area;
if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;
surfaceArea+=area;
float alignment=SurfaceOrientation.Alignment(normal,posedNormal,rig.Weights[p][i],rig.Weights[p][j],rig.Weights[p][k],rotations);
if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=area;}
}
for(int edge=0;edge<3;edge++)
{
var (a,b,length)=face.Edge(edge);var posedLength=Vector3.Distance(dst[a],dst[b]);
if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}
if(length<=height*1e-6f)continue;
float ratio=posedLength/length;
if(!float.IsFinite(ratio)){invalidMeasurements++;continue;}
if(ratio>stretch){stretch=ratio;sourceEdge=length;deformedEdge=posedLength;}
}
}
}
return new(name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);
}
struct Patch{public Vector3 Posed,Transported;public double BindArea,PosedArea,BindEdges,PosedEdges;}
sealed class PatchMap{public int[][] Index=[];public int Count;}
static readonly System.Runtime.CompilerServices.ConditionalWeakTable<ImportedCharacter,PatchMap> patchIndex=new();
/// <summary>The patch each face belongs to, by its bind centroid; the bind
/// surface does not change between poses.</summary>
static PatchMap Patches(ImportedCharacter character,BindTriangle[][] faces,float height)
{
if(patchIndex.TryGetValue(character,out var known)&&known.Index.Length==faces.Length&&known.Index.Select((part,p)=>part.Length==faces[p].Length).All(x=>x))return known;
// Two skins meet within a cell at an armpit or an elbow crease, and
// one moves with the arm while the other stays: their summed normals
// would disagree although neither folds. Faces facing different ways
// form patches of their own.
float cell=height*PatchScale;var ids=new Dictionary<(long,long,long,int),int>();
var index=new int[faces.Length][];
for(int p=0;p<faces.Length;p++)
{
var src=character.Meshes[p].Vertices;index[p]=new int[faces[p].Length];
for(int f=0;f<faces[p].Length;f++)
{
var face=faces[p][f];var center=(src[face.A]+src[face.B]+src[face.C])/3;
var n=face.Normal;float ax=Math.Abs(n.X),ay=Math.Abs(n.Y),az=Math.Abs(n.Z);
int facing=ax>=ay&&ax>=az?(n.X<0?0:1):ay>=az?(n.Y<0?2:3):(n.Z<0?4:5);
var key=((long)MathF.Floor(center.X/cell),(long)MathF.Floor(center.Y/cell),(long)MathF.Floor(center.Z/cell),facing);
if(!ids.TryGetValue(key,out int id))ids[key]=id=ids.Count;
index[p][f]=id;
}
}
var result=new PatchMap{Index=index,Count=ids.Count};patchIndex.AddOrUpdate(character,result);return result;
}
static StressResult MeasurePatches(ImportedCharacter character,GeneratedRig rig,string name,BindTriangle[][] faces,Vector3[][] deformed,System.Numerics.Quaternion[] rotations,float height,bool[]? still)
{
int nonFinite=0,invalidMeasurements=0;double surfaceArea=0;
var map=Patches(character,faces,height);var index=map.Index;var patches=new Patch[map.Count];
for(int p=0;p<character.Meshes.Length;p++)
{
var mesh=character.Meshes[p];var src=mesh.Vertices;var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));
var weights=rig.Weights[p];
bool Moves(int v){foreach(var w in weights[v])if(!still![w.Bone])return true;return false;}
var ownIndex=index[p];
for(int f=0;f<faces[p].Length;f++)
{
var face=faces[p][f];var i=face.A;var j=face.B;var k=face.C;
if(!float.IsFinite(face.Area)){invalidMeasurements++;continue;}
if(face.Area<=height*height*1e-10f)continue;
if(still is not null&&!Moves(i)&&!Moves(j)&&!Moves(k)){surfaceArea+=face.Area;continue;}
var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);float posedArea=posedNormal.Length();
if(!float.IsFinite(posedArea)){invalidMeasurements++;continue;}
ref var patch=ref patches[ownIndex[f]];
patch.Posed+=posedNormal;patch.Transported+=SurfaceOrientation.Transported(face.Normal,weights[i],weights[j],weights[k],rotations);
patch.BindArea+=face.Area;patch.PosedArea+=posedArea;surfaceArea+=face.Area;
for(int edge=0;edge<3;edge++)
{
var (a,b,length)=face.Edge(edge);float posedLength=Vector3.Distance(dst[a],dst[b]);
if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}
if(length<=height*1e-6f)continue;
patch.BindEdges+=length*(double)length;patch.PosedEdges+=posedLength*(double)posedLength;
}
}
}
float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int reversed=0;double reversedArea=0;
foreach(var patch in patches)
{
if(patch.BindArea<=0&&patch.BindEdges<=0)continue;
if(patch.BindArea>0)
{
float ratio=(float)(patch.PosedArea/patch.BindArea);
if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;
}
float denominator=patch.Posed.Length()*patch.Transported.Length();
float alignment=denominator>0&&float.IsFinite(denominator)?Math.Clamp(Vector3.Dot(patch.Posed,patch.Transported)/denominator,-1,1):float.NaN;
if(float.IsNaN(alignment))invalidMeasurements++;
else if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=patch.BindArea;}
if(patch.BindEdges>0)
{
float edgeRatio=(float)Math.Sqrt(patch.PosedEdges/patch.BindEdges);
if(!float.IsFinite(edgeRatio))invalidMeasurements++;
else if(edgeRatio>stretch){stretch=edgeRatio;sourceEdge=(float)Math.Sqrt(patch.BindEdges);deformedEdge=(float)Math.Sqrt(patch.PosedEdges);}
}
}
return new(name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);
}
}
public sealed record ValidationIssue(string Code,string Message,bool Error);
public sealed record StressResult(string Pose,float MaximumStretch,float MinimumAreaRatio,int NonFiniteVertices,float SourceEdgeLength=0,float DeformedEdgeLength=0,int NonFiniteMeasurements=0,int ReversedTriangles=0,float ReversedAreaFraction=0);
public sealed class ValidationReport
{
public List<ValidationIssue> Issues {get;}=[];
public List<StressResult> StressTests {get;}=[];
public List<StressResult> JointStressTests {get;}=[];
public List<StressResult> NativeStressTests {get;}=[];
public int Repairs {get;set;}
public int RepairPasses {get;set;}
public bool Passed=>Issues.All(i=>!i.Error) && StressTests.Count>0;
}