Editor/Core/Rigging/SurfaceHugging.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Parts that are not skin of their own. A part that lies on another
/// everywhere, a strap, a belt, a patch or a skin-tight garment, moves with the
/// surface beneath it: each of its vertices copies the nearest vertex of a
/// larger part. A small part that no bone passes through, a pouch, a buckle or
/// a badge, is a rigid object: all of it moves as the surface it touches does.
/// Solved on their own such parts go to whichever bone happens to be nearest,
/// and a strap crossing an armpit takes the arm while a pouch beside an elbow
/// tears between the arm and the spine. A sleeve or a glove wraps a limb, its
/// far side lies near nothing larger, and a bone runs through it: it keeps its
/// own solve.</summary>
internal sealed class SurfaceHugging
{
readonly (int Part,int Vertex)?[][] host;
readonly bool[] rigid;
readonly int[] order;
internal SurfaceHugging(ImportedCharacter character,GeneratedRig rig)
{
var meshes=character.Meshes;float height=character.AnatomicalHeight,cell=height*.012f;
host=meshes.Select(m=>new (int,int)?[m.Vertices.Length]).ToArray();rigid=new bool[meshes.Length];
order=Enumerable.Range(0,meshes.Length).OrderByDescending(p=>meshes[p].Vertices.Length).ToArray();
if(cell<=0||meshes.Length<2)return;
var cells=new Dictionary<(long,long,long),List<(int Part,int Vertex)>>();
(long,long,long) Key(Vector3 p)=>((long)MathF.Floor(p.X/cell),(long)MathF.Floor(p.Y/cell),(long)MathF.Floor(p.Z/cell));
for(int p=0;p<meshes.Length;p++)for(int v=0;v<meshes[p].Vertices.Length;v++)
{
var key=Key(meshes[p].Vertices[v]);
if(!cells.TryGetValue(key,out var list))cells[key]=list=[];
list.Add((p,v));
}
var ends=RigGeometry.SegmentEnds(rig);
for(int p=0;p<meshes.Length;p++)
{
var mesh=meshes[p];if(mesh.Vertices.Length==0)continue;
var found=new (int Part,int Vertex)?[mesh.Vertices.Length];int hugging=0;
for(int v=0;v<mesh.Vertices.Length;v++)
{
var point=mesh.Vertices[v];var key=Key(point);float best=cell*cell;
for(long x=-1;x<=1;x++)for(long y=-1;y<=1;y++)for(long z=-1;z<=1;z++)
if(cells.TryGetValue((key.Item1+x,key.Item2+y,key.Item3+z),out var near))
foreach(var (q,w) in near)
{
if(q==p||meshes[q].Vertices.Length<=mesh.Vertices.Length)continue;
float d=Vector3.DistanceSquared(point,meshes[q].Vertices[w]);
if(d<best){best=d;found[v]=(q,w);}
}
if(found[v] is not null)hugging++;
}
if(hugging>=mesh.Vertices.Length*.9f){host[p]=found;continue;}
// A rigid object is small and carries no bone inside it. One that
// hangs free of anything larger keeps the mean of its own solve.
var center=Geometry.Mean(mesh.Vertices);float radius=MathF.Sqrt(mesh.Vertices.Max(q=>Vector3.DistanceSquared(q,center)));
if(radius>height*.08f)continue;
bool threaded=false;
for(int b=0;b<rig.Bones.Length&&!threaded;b++)
if(rig.Bones[b].Deform&&Vector3.Distance(center,Geometry.ClosestOnSegment(center,rig.Bones[b].Position,ends[b]))<radius)threaded=true;
if(threaded)continue;
host[p]=found;rigid[p]=true;
}
}
internal bool Any=>host.Any(part=>part.Any(h=>h is not null));
/// <summary>Copy each hugging vertex from its host, larger parts first so
/// that a strap on a jacket on a body reads the jacket after the jacket
/// has read the body. A rigid part takes one row: the mean of what it
/// touches.</summary>
internal Influence[][][] Apply(Influence[][][] weights,int bones,int maximumInfluences)
{
if(!Any)return weights;
var result=weights.Select(part=>(Influence[][])part.Clone()).ToArray();
foreach(int p in order)
{
if(rigid[p])
{
var sum=new float[bones];int count=0;
for(int v=0;v<host[p].Length;v++)if(host[p][v] is {} from){count++;foreach(var w in result[from.Part][from.Vertex])sum[w.Bone]+=w.Weight;}
if(count==0)foreach(var row0 in weights[p])foreach(var w in row0)sum[w.Bone]+=w.Weight;
var row=Skinning.Cleanup(Skinning.Limit(sum,maximumInfluences),maximumInfluences);
for(int v=0;v<result[p].Length;v++)result[p][v]=(Influence[])row.Clone();
continue;
}
for(int v=0;v<host[p].Length;v++)
if(host[p][v] is {} from)result[p][v]=(Influence[])result[from.Part][from.Vertex].Clone();
}
return result;
}
}