Editor/Core/Import/Gltf/GltfMeshImporter.cs
// Mesh transforms, skin baking and triangle modes adapted from the retargeter's
// GltfModelDmxWriter; outputs the rigger's canonical mesh contract instead of DMX.
#nullable enable annotations
using System.Numerics;
using System.Text.Json;
namespace HumanoidRigger.Formats.Gltf;
using Vector2=System.Numerics.Vector2;
using Vector3=System.Numerics.Vector3;
using Vector4=System.Numerics.Vector4;
internal static class GltfMeshImporter
{
public static ImportedCharacter Import(byte[] bytes,string name,string path)
{
var doc=GltfDocument.Parse(bytes,uri=>ModelImporter.ReadDependency(path,uri));var root=doc.Root;
var nodes=root.GetProperty("nodes");int count=nodes.GetArrayLength();
var parents=Enumerable.Repeat(-1,count).ToArray();var worlds=new Matrix4x4[count];var states=new byte[count];
for(int i=0;i<count;i++)if(nodes[i].TryGetProperty("children",out var children))foreach(var child in children.EnumerateArray())
{
int c=child.GetInt32();if(c<0||c>=count||parents[c]!=-1)throw new FormatException("glTF node has an invalid child or multiple parents.");parents[c]=i;
}
Matrix4x4 World(int i,int depth=0)
{
if(states[i]==2)return worlds[i];
if(states[i]==1||depth>256)throw new FormatException("glTF node hierarchy is cyclic or too deep.");
states[i]=1;var node=nodes[i];Matrix4x4 local;
if(node.TryGetProperty("matrix",out var matrix))
{
if(matrix.GetArrayLength()!=16||node.TryGetProperty("rotation",out _)||node.TryGetProperty("translation",out _)||node.TryGetProperty("scale",out _))throw new FormatException("Invalid glTF node matrix/TRS combination.");
local=Matrix(matrix.EnumerateArray().Select(v=>v.GetSingle()).ToArray());
}
else
{
Vector3 V(string key,Vector3 fallback)=>node.TryGetProperty(key,out var v)&&v.GetArrayLength()==3?new(v[0].GetSingle(),v[1].GetSingle(),v[2].GetSingle()):fallback;
var q=node.TryGetProperty("rotation",out var r)?new Quaternion(r[0].GetSingle(),r[1].GetSingle(),r[2].GetSingle(),r[3].GetSingle()):Quaternion.Identity;
if(!float.IsFinite(q.LengthSquared())||q.LengthSquared()<1e-10f)throw new FormatException("Invalid glTF node rotation.");
local=Matrix4x4.CreateScale(V("scale",Vector3.One))*Matrix4x4.CreateFromQuaternion(Quaternion.Normalize(q))*Matrix4x4.CreateTranslation(V("translation",Vector3.Zero));
}
worlds[i]=parents[i]<0?local:local*World(parents[i],depth+1);states[i]=2;return worlds[i];
}
for(int i=0;i<count;i++)World(i);
var active=new HashSet<int>();
void Include(int i){if(i<0||i>=count)throw new FormatException("Invalid glTF scene root.");if(!active.Add(i))return;if(nodes[i].TryGetProperty("children",out var children))foreach(var child in children.EnumerateArray())Include(child.GetInt32());}
if(root.TryGetProperty("scenes",out var scenes)&&scenes.GetArrayLength()>0)
{
int selected=root.TryGetProperty("scene",out var scene)?scene.GetInt32():0;
if(scenes[selected].TryGetProperty("nodes",out var roots))foreach(var node in roots.EnumerateArray())Include(node.GetInt32());
}
else for(int i=0;i<count;i++)if(parents[i]<0)Include(i);
var embedded=new Dictionary<string,byte[]>(StringComparer.OrdinalIgnoreCase);var warnings=new List<string>();var materials=GltfMaterials.Read(doc,path,embedded,warnings).ToList();
var parts=new List<MeshPart>();var boneNodes=new HashSet<int>();bool hasSkin=false;
foreach(int nodeIndex in active.Order())
{
var node=nodes[nodeIndex];if(!node.TryGetProperty("mesh",out var meshId))continue;
var mesh=root.GetProperty("meshes")[meshId.GetInt32()];int primitiveIndex=0;
Matrix4x4[]? skin=null;
if(node.TryGetProperty("skin",out var skinId))
{
hasSkin=true;var definition=root.GetProperty("skins")[skinId.GetInt32()];var joints=definition.GetProperty("joints");
var inverse=definition.TryGetProperty("inverseBindMatrices",out var binds)?new GltfAccessor(doc,binds.GetInt32(),16):null;
if(inverse is not null&&inverse.Count!=joints.GetArrayLength())throw new FormatException("glTF inverse bind count differs from its joints.");
skin=new Matrix4x4[joints.GetArrayLength()];
for(int j=0;j<skin.Length;j++)
{
int id=joints[j].GetInt32();if(id<0||id>=count||!boneNodes.Add(id)&&joints.EnumerateArray().Count(v=>v.GetInt32()==id)>1)throw new FormatException("Invalid or duplicate glTF skin joint.");
var bind=inverse is null?Matrix4x4.Identity:Matrix(Enumerable.Range(0,16).Select(c=>inverse.Float(j,c)).ToArray());
skin[j]=bind*worlds[id];
}
}
foreach(var primitive in mesh.GetProperty("primitives").EnumerateArray())
{
var attributes=primitive.GetProperty("attributes");
var positions=new GltfAccessor(doc,attributes.GetProperty("POSITION").GetInt32(),3);int length=positions.Count;
GltfAccessor? Attribute(string semantic,int components)
{
if(!attributes.TryGetProperty(semantic,out var a))return null;
var accessor=new GltfAccessor(doc,a.GetInt32(),components);
if(accessor.Count!=length)throw new FormatException("glTF vertex attribute counts disagree: "+semantic);return accessor;
}
int mat=primitive.TryGetProperty("material",out var m)?m.GetInt32():-1;
if(mat>=materials.Count||mat< -1)throw new FormatException("Invalid glTF material index.");
var uvTransform=mat>=0?GltfMaterials.UvTransform(root.GetProperty("materials")[mat]):(Set:0,Offset:Vector2.Zero,Scale:Vector2.One,Rotation:0f);
var normals=Attribute("NORMAL",3);var uv=Attribute("TEXCOORD_"+uvTransform.Set,2);
GltfAccessor? colors=null;
if(attributes.TryGetProperty("COLOR_0",out var colorId))colors=Attribute("COLOR_0",root.GetProperty("accessors")[colorId.GetInt32()].GetProperty("type").GetString()=="VEC3"?3:4);
var jointSets=new List<(GltfAccessor Joints,GltfAccessor Weights)>();
if(skin is not null)
{
for(int set=0;attributes.TryGetProperty("JOINTS_"+set,out _);set++)jointSets.Add((Attribute("JOINTS_"+set,4)!,Attribute("WEIGHTS_"+set,4)??throw new FormatException("glTF joints have no matching weights.")));
if(jointSets.Count==0)throw new FormatException("A skinned glTF mesh has no joint/weight attributes.");
}
var targets=primitive.TryGetProperty("targets",out var morphs)?morphs.EnumerateArray().ToArray():[];
var defaultWeights=node.TryGetProperty("weights",out var nw)?nw:mesh.TryGetProperty("weights",out var mw)?mw:default;
var morphData=targets.Select(t=>(Position:t.TryGetProperty("POSITION",out var p)?new GltfAccessor(doc,p.GetInt32(),3):null,Normal:t.TryGetProperty("NORMAL",out var n)?new GltfAccessor(doc,n.GetInt32(),3):null)).ToArray();
if(morphData.Any(t=>t.Position is not null&&t.Position.Count!=length||t.Normal is not null&&t.Normal.Count!=length))throw new FormatException("Invalid glTF morph target count.");
var vertices=new Vector3[length];var vertexNormals=normals is null?[]:new Vector3[length];
for(int v=0;v<length;v++)
{
var p=new Vector3(positions.Float(v,0),positions.Float(v,1),positions.Float(v,2));
var n=normals is null?Vector3.Zero:new(normals.Float(v,0),normals.Float(v,1),normals.Float(v,2));
for(int target=0;target<morphData.Length;target++)
{
float weight=defaultWeights.ValueKind==JsonValueKind.Array?defaultWeights[target].GetSingle():0;
if(weight==0)continue;var delta=morphData[target];
if(delta.Position is {} mp)p+=new Vector3(mp.Float(v,0),mp.Float(v,1),mp.Float(v,2))*weight;
if(delta.Normal is {} mn)n+=new Vector3(mn.Float(v,0),mn.Float(v,1),mn.Float(v,2))*weight;
}
var transform=worlds[nodeIndex];
if(skin is not null)
{
transform=default;float total=0;
foreach(var set in jointSets)for(int k=0;k<4;k++)
{
int joint=set.Joints.Unsigned(v,k);float weight=set.Weights.Float(v,k);
if(joint<0||joint>=skin.Length||!float.IsFinite(weight)||weight<0)throw new FormatException("Invalid glTF skin influence.");
transform+=skin[joint]*weight;total+=weight;
}
if(total<=1e-8f)throw new FormatException("glTF skin has an unweighted vertex.");transform*=1/total;
}
vertices[v]=Vector3.Transform(p,transform)*100;
if(normals is not null)
{
if(!Matrix4x4.Invert(transform,out var inverse))throw new FormatException("Singular glTF mesh/skin transform.");
var normal=Vector3.TransformNormal(n,Matrix4x4.Transpose(inverse));vertexNormals[v]=normal.LengthSquared()>1e-12f?Vector3.Normalize(normal):Vector3.Zero;
}
}
var indices=primitive.TryGetProperty("indices",out var indexId)?ReadIndices(new GltfAccessor(doc,indexId.GetInt32(),1),length):Enumerable.Range(0,length).ToArray();
var triangles=Triangulate(indices,primitive.TryGetProperty("mode",out var mode)?mode.GetInt32():4);
if(skin is null&&worlds[nodeIndex].GetDeterminant()<0)for(int t=0;t<triangles.Length;t+=3)(triangles[t+1],triangles[t+2])=(triangles[t+2],triangles[t+1]);
if(colors is not null&&mat<0){mat=materials.Count;materials.Add(new(){Name="Vertex colors",ColorFactor=Vector3.One,AuthoredPbr=true,MetallicFactor=0});}
if(colors is not null)materials[mat].VertexColors=true;
string partName=(node.TryGetProperty("name",out var label)?label.GetString():mesh.TryGetProperty("name",out var ml)?ml.GetString():"mesh_"+nodeIndex)+"_"+primitiveIndex++;
Vector2 UV(int v)
{
var p=new Vector2(uv!.Float(v,0),uv.Float(v,1))*uvTransform.Scale;float c=MathF.Cos(uvTransform.Rotation),s=MathF.Sin(uvTransform.Rotation);
p=new Vector2(c*p.X-s*p.Y,s*p.X+c*p.Y)+uvTransform.Offset;return new(p.X,1-p.Y);
}
parts.Add(new(partName,vertices,triangles,ModelImporter.Classify(partName))
{
CornerNormals=normals is null?[]:triangles.Select(v=>vertexNormals[v]).ToArray(),CornerTexCoords=uv is null?[]:triangles.Select(UV).ToArray(),
CornerColors=colors is null?[]:triangles.Select(v=>new Vector4(colors.Float(v,0),colors.Float(v,1),colors.Float(v,2),colors.Components==4?colors.Float(v,3):1)).ToArray(),TriangleMaterials=Enumerable.Repeat(mat,triangles.Length/3).ToArray()
});
}
}
int Depth(int i){int depth=0;while(parents[i]>=0){depth++;i=parents[i];}return depth;}
var ordered=boneNodes.OrderBy(Depth).ThenBy(i=>i).ToArray();var boneIds=ordered.Select((n,i)=>(n,i)).ToDictionary(p=>p.n,p=>p.i);
var bones=ordered.Select(i=>{int parent=parents[i];while(parent>=0&&!boneIds.ContainsKey(parent))parent=parents[parent];return new SourceBone(nodes[i].TryGetProperty("name",out var n)?n.GetString()!:"node_"+i,parent<0?-1:boneIds[parent],worlds[i].Translation*100);}).ToArray();
var result=new ImportedCharacter{Name=name,SourcePath=path,SourceUnitCm=100,Meshes=parts.ToArray(),Materials=materials.ToArray(),EmbeddedTextures=embedded,ExistingBones=bones,HasExistingSkin=hasSkin,ImportWarnings=warnings.Distinct().ToArray()};result.Validate();return result;
}
internal static Matrix4x4 Matrix(float[] m)=>new(m[0],m[1],m[2],m[3],m[4],m[5],m[6],m[7],m[8],m[9],m[10],m[11],m[12],m[13],m[14],m[15]);
static int[] ReadIndices(GltfAccessor source,int count)
{
var result=new int[source.Count];for(int i=0;i<result.Length;i++){result[i]=source.Unsigned(i,0);if(result[i]>=count)throw new FormatException("glTF index exceeds vertex count.");}return result;
}
internal static int[] Triangulate(int[] indices,int mode)
{
if(mode==4){if(indices.Length%3!=0)throw new FormatException("Invalid glTF triangle count.");return indices;}
if(mode is not (5 or 6))throw new FormatException("glTF lines/points must be converted to triangle meshes before importing.");
var result=new List<int>();for(int i=2;i<indices.Length;i++)
{
int a=mode==6?indices[0]:indices[i-2],b=indices[i-1],c=indices[i];if(mode==5&&(i&1)!=0)(a,b)=(b,a);
if(a!=b&&b!=c&&a!=c)result.AddRange([a,b,c]);
}
return result.ToArray();
}
}