Editor/Core/RigProfiles/ReferenceSkin.cs
namespace HumanoidRigger.EditorTools.Core.RigProfiles;
using Vector3=System.Numerics.Vector3;
public static class ReferenceSkin
{
/// <summary>Actual skin clusters decide deform membership. Control names and
/// proximity alone cannot tell an attachment from a deforming helper.</summary>
public static HashSet<string> WeightedBones(byte[] fbx)
{
var root=FbxTokenizer.Parse(fbx);var scene=FbxScene.Build(root);
var clusters=scene.ObjectsById.Values.Where(o=>o.SubClass=="Cluster"&&o.Node.Child("Weights")?.AsDoubleArray(0).Any(w=>w>0)==true).Select(o=>o.Id).ToHashSet();
var result=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach(var c in root.Child("Connections")?.ChildrenNamed("C")??[])
if(c.Properties.Count>=3&&c.Prop<string>(0)=="OO"&&clusters.Contains(c.Prop<long>(2))&&scene.ObjectsById.TryGetValue(c.Prop<long>(1),out var model)&&model.NodeType=="Model")result.Add(model.Name);
if(result.Count==0)throw new FormatException("The reference contains no skin clusters.");
return result;
}
/// <summary>The stock knee, ankle and elbow weight profiles: the share of each limb bone at each
/// position along the limb, measured on the reference mesh. Joints whose bones are missing are skipped.</summary>
public static JointProfile[] JointProfiles(byte[] fbx)
{
var root=FbxTokenizer.Parse(fbx);var objects=root.Child("Objects");if(objects is null)return [];
var names=new Dictionary<long,string>();var geometry=new Dictionary<long,double[]>();
var clusters=new Dictionary<long,(int[] Index,double[] Weight,Vector3 Link)>();
foreach(var node in objects.Children)
{
if(node.Properties.Count<2)continue;
long id=node.Prop<long>(0);names[id]=FbxNode.SplitName(node.AsString(1)).Name;
if(node.Name=="Geometry"&&node.Child("Vertices") is {} vertices)geometry[id]=vertices.AsDoubleArray(0);
if(node.Name=="Deformer"&&node.Properties.Count>2&&node.AsString(2)=="Cluster"&&node.Child("Indexes") is {} indexes&&node.Child("Weights") is {} weights&&node.Child("TransformLink") is {} link)
{
var m=link.AsDoubleArray(0);
clusters[id]=(indexes.AsIntArray(0),weights.AsDoubleArray(0),new Vector3((float)m[12],(float)m[13],(float)m[14]));
}
}
var bone=new Dictionary<long,string>();var skinOf=new Dictionary<long,long>();var geometryOf=new Dictionary<long,long>();
foreach(var c in root.Child("Connections")?.ChildrenNamed("C")??[])
{
if(c.Properties.Count<3)continue;
long child=c.Prop<long>(1),parent=c.Prop<long>(2);
if(clusters.ContainsKey(parent)&&names.TryGetValue(child,out var name))bone[parent]=name;
if(clusters.ContainsKey(child))skinOf[child]=parent;
if(geometry.ContainsKey(parent))geometryOf[child]=parent;
}
var links=clusters.Where(c=>bone.ContainsKey(c.Key)).GroupBy(c=>bone[c.Key]).ToDictionary(g=>g.Key,g=>g.First().Value.Link,StringComparer.OrdinalIgnoreCase);
var result=new List<JointProfile>();
foreach(string side in new[]{"L","R"})
foreach(var (name,above,joint,below,prefix,end) in new[]{
("Knee","leg_upper_"+side+"_twist0","leg_lower_"+side+"_twist0","ankle_"+side,"leg_","ankle_"+side),
("Ankle","leg_lower_"+side+"_twist0","ankle_"+side,"","leg_lower_","ankle_"+side),
("Elbow","arm_upper_"+side+"_twist0","arm_lower_"+side+"_twist0","hand_"+side,"arm_","hand_"+side)})
{
if(!links.TryGetValue(above,out var a)||!links.TryGetValue(joint,out var j))continue;
var b=below.Length>0&&links.TryGetValue(below,out var next)?next:j+(j-a);
float length=Vector3.Distance(j,b);if(length<1e-4f)continue;
bool Member(string n)=>n.Equals(end,StringComparison.OrdinalIgnoreCase)||prefix.StartsWith("leg_")&&n.StartsWith("ball_"+side,StringComparison.OrdinalIgnoreCase)
||n.StartsWith(prefix,StringComparison.OrdinalIgnoreCase)&&n.EndsWith("_"+side,StringComparison.OrdinalIgnoreCase)
||n.StartsWith(prefix,StringComparison.OrdinalIgnoreCase)&&n.Contains("_"+side+"_",StringComparison.OrdinalIgnoreCase);
var members=links.Keys.Where(Member).OrderBy(n=>n,StringComparer.Ordinal).ToArray();
var sums=new double[2*JointProfile.Half+1,members.Length];
var vertexSums=new Dictionary<(long Geometry,int Vertex),double[]>();
foreach(var (cluster,data) in clusters)
{
if(!bone.TryGetValue(cluster,out var bn)||Array.IndexOf(members,bn) is var m&&m<0)continue;
if(!skinOf.TryGetValue(cluster,out var skin)||!geometryOf.TryGetValue(skin,out var geo))continue;
for(int k=0;k<data.Index.Length;k++)
{
var key=(geo,data.Index[k]);
if(!vertexSums.TryGetValue(key,out var row))vertexSums[key]=row=new double[members.Length];
row[m]+=data.Weight[k];
}
}
foreach(var ((geo,v),row) in vertexSums)
{
var p=geometry[geo];var x=new Vector3((float)p[v*3],(float)p[v*3+1],(float)p[v*3+2]);
if(Math.Min(Vector3.Distance(x,Geometry.ClosestOnSegment(x,a,j)),Vector3.Distance(x,Geometry.ClosestOnSegment(x,j,b)))>length*.35f)continue;
float u=Position(x,a,j,b,length);
int sample=(int)MathF.Round(u/JointProfile.Step)+JointProfile.Half;
if(sample<0||sample>2*JointProfile.Half)continue;
for(int m=0;m<members.Length;m++)sums[sample,m]+=row[m];
}
var shares=new float[2*JointProfile.Half+1][];
for(int s=0;s<shares.Length;s++)
{
double total=Enumerable.Range(0,members.Length).Sum(m=>sums[s,m]);
shares[s]=Enumerable.Range(0,members.Length).Select(m=>total>0?(float)(sums[s,m]/total):0).ToArray();
}
// Fill samples no vertex reached from their nearest measured neighbour.
for(int s=0;s<shares.Length;s++)if(shares[s].Sum()==0)
{
int nearest=Enumerable.Range(0,shares.Length).Where(t=>shares[t].Sum()>0).OrderBy(t=>Math.Abs(t-s)).DefaultIfEmpty(-1).First();
if(nearest>=0)shares[s]=shares[nearest];
}
if(shares.All(s=>s.Sum()>0))result.Add(new JointProfile(name+"."+side,above,joint,below,members,shares));
}
return result.ToArray();
}
/// <summary>The stock skinned mesh in reference space: vertices, triangles and each vertex's weights,
/// indexed into <paramref name="bones"/> (the reference armature). Clusters on bones not in the
/// armature are dropped; each vertex's weights are normalised.</summary>
public static ReferenceSurface Surface(byte[] fbx,RigBone[] bones)
{
var root=FbxTokenizer.Parse(fbx);var objects=root.Child("Objects")??throw new FormatException("The reference has no objects.");
var names=new Dictionary<long,string>();var geometry=new Dictionary<long,(double[] Points,int[] Polygons)>();
var clusters=new Dictionary<long,(int[] Index,double[] Weight)>();
foreach(var node in objects.Children)
{
if(node.Properties.Count<2)continue;
long id=node.Prop<long>(0);names[id]=FbxNode.SplitName(node.AsString(1)).Name;
if(node.Name=="Geometry"&&node.Child("Vertices") is {} points&&node.Child("PolygonVertexIndex") is {} polygons)geometry[id]=(points.AsDoubleArray(0),polygons.AsIntArray(0));
if(node.Name=="Deformer"&&node.Properties.Count>2&&node.AsString(2)=="Cluster"&&node.Child("Indexes") is {} indexes&&node.Child("Weights") is {} weights)
clusters[id]=(indexes.AsIntArray(0),weights.AsDoubleArray(0));
}
var bone=new Dictionary<long,int>();var skinOf=new Dictionary<long,long>();var geometryOf=new Dictionary<long,long>();
foreach(var c in root.Child("Connections")?.ChildrenNamed("C")??[])
{
if(c.Properties.Count<3)continue;
long child=c.Prop<long>(1),parent=c.Prop<long>(2);
if(clusters.ContainsKey(parent)&&names.TryGetValue(child,out var name)&&Array.FindIndex(bones,b=>b.Name.Equals(name,StringComparison.OrdinalIgnoreCase)) is var index&&index>=0)bone[parent]=index;
if(clusters.ContainsKey(child))skinOf[child]=parent;
if(geometry.ContainsKey(parent))geometryOf[child]=parent;
}
var vertices=new List<Vector3>();var triangles=new List<int>();var influences=new List<Dictionary<int,float>>();
var offsets=new Dictionary<long,int>();
foreach(var (id,(points,polygons)) in geometry)
{
offsets[id]=vertices.Count;
for(int i=0;i+2<points.Length;i+=3){vertices.Add(new Vector3((float)points[i],(float)points[i+1],(float)points[i+2]));influences.Add(new());}
var polygon=new List<int>();
foreach(int raw in polygons)
{
polygon.Add(offsets[id]+(raw<0?~raw:raw));
if(raw>=0)continue;
for(int k=1;k+1<polygon.Count;k++){triangles.Add(polygon[0]);triangles.Add(polygon[k]);triangles.Add(polygon[k+1]);}
polygon.Clear();
}
}
foreach(var (id,(index,weight)) in clusters)
{
if(!bone.TryGetValue(id,out int b)||!skinOf.TryGetValue(id,out var skin)||!geometryOf.TryGetValue(skin,out var geo))continue;
for(int k=0;k<index.Length;k++)
{
int v=offsets[geo]+index[k];if(v<influences.Count&&weight[k]>0)influences[v][b]=influences[v].GetValueOrDefault(b)+(float)weight[k];
}
}
var result=influences.Select(d=>{float total=d.Values.Sum();return total>0?d.Select(p=>new Influence(p.Key,p.Value/total)).OrderByDescending(w=>w.Weight).ToArray():[];}).ToArray();
if(triangles.Count==0||result.All(r=>r.Length==0))throw new FormatException("The reference has no skinned surface.");
return new ReferenceSurface(vertices.ToArray(),triangles.ToArray(),result);
}
/// <summary>Position along a limb in lower-limb lengths from the joint: negative above, positive below.</summary>
public static float Position(Vector3 x,Vector3 above,Vector3 joint,Vector3 below,float length)
{
var down=Vector3.Normalize(below-joint);var up=Vector3.Normalize(joint-above);var d=x-joint;
float along=Vector3.Dot(d,down);
return along>0?along/length:Vector3.Dot(d,up)/length;
}
}