Code/AutoRig/Formats/Gltf/GltfMeshImporter.cs

Importer that parses a glTF/GLB document and produces a RigMesh. Walks scene nodes depth-first, accumulates world transforms, bakes vertex positions and normals into world space, copies UVs and triangle indices, and tags triangles with node/mesh names.

File Access
using System.Numerics;
using AutoRig.Mesh;

namespace AutoRig.Formats.Gltf;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;


/// <summary>
/// Extracts a combined <see cref="RigMesh"/> from a glTF/GLB document: walks the node
/// tree accumulating world transforms, bakes each mesh-carrying node's primitives into
/// world space, and tags triangles with the node name.
/// </summary>
public static class GltfMeshImporter
{
    /// <exception cref="FormatException">Malformed file, external .bin buffers, or no
    /// triangle geometry.</exception>
    public static RigMesh Import( byte[] data, string sourceName )
    {
        var document = GltfDocument.Parse( data );

        var outPositions = new List<Vector3>();
        var outNormals = new List<Vector3>();
        var outUvs = new List<Vector2>();
        var outTriangles = new List<int>();
        var outTriangleTags = new List<int>();
        var outTriangleMaterials = new List<int>();
        var tags = new List<string>();
        var anyNormal = false;
        var anyUv = false;

        // Depth-first from roots, accumulating world = local * parentWorld (row vectors).
        var stack = new Stack<(int Node, Matrix4x4 ParentWorld)>();
        for ( var i = document.Nodes.Count - 1; i >= 0; i-- )
            if ( document.Nodes[i].Parent < 0 )
                stack.Push( (i, Matrix4x4.Identity) );

        while ( stack.Count > 0 )
        {
            var (nodeIndex, parentWorld) = stack.Pop();
            var node = document.Nodes[nodeIndex];
            var world = node.LocalMatrix * parentWorld;

            if ( node.Mesh >= 0 && node.Mesh < document.Meshes.Count )
            {
                var mesh = document.Meshes[node.Mesh];
                var tagName = node.Name ?? mesh.Name ?? $"node{nodeIndex}";
                var tag = tags.IndexOf( tagName );
                if ( tag < 0 ) { tag = tags.Count; tags.Add( tagName ); }

                // Normals need the inverse-transpose of the world's 3x3 part.
                Matrix4x4.Invert( world, out var inverse );
                var normalMatrix = Matrix4x4.Transpose( inverse );

                foreach ( var primitive in mesh.Primitives )
                {
                    var baseVertex = outPositions.Count;
                    var vertexCount = primitive.Positions.Length / 3;
                    for ( var v = 0; v < vertexCount; v++ )
                    {
                        var p = new Vector3(
                            primitive.Positions[v * 3],
                            primitive.Positions[v * 3 + 1],
                            primitive.Positions[v * 3 + 2] );
                        outPositions.Add( Vector3.Transform( p, world ) );

                        if ( primitive.Normals is { } normals )
                        {
                            var n = new Vector3( normals[v * 3], normals[v * 3 + 1], normals[v * 3 + 2] );
                            var transformed = Vector3.TransformNormal( n, normalMatrix );
                            var length = transformed.Length();
                            outNormals.Add( length > 1e-8f ? transformed / length : Vector3.UnitZ );
                            anyNormal = true;
                        }
                        else
                        {
                            outNormals.Add( Vector3.UnitZ );
                        }

                        if ( primitive.Uvs is { } uvs )
                        {
                            outUvs.Add( new Vector2( uvs[v * 2], uvs[v * 2 + 1] ) );
                            anyUv = true;
                        }
                        else
                        {
                            outUvs.Add( Vector2.Zero );
                        }
                    }

                    var material = primitive.Material >= 0
                        && primitive.Material < document.Materials.Count ? primitive.Material : -1;
                    for ( var i = 0; i < primitive.Indices.Length; i += 3 )
                    {
                        outTriangles.Add( baseVertex + primitive.Indices[i] );
                        outTriangles.Add( baseVertex + primitive.Indices[i + 1] );
                        outTriangles.Add( baseVertex + primitive.Indices[i + 2] );
                        outTriangleTags.Add( tag );
                        outTriangleMaterials.Add( material );
                    }
                }
            }

            foreach ( var child in node.Children )
                stack.Push( (child, world) );
        }

        if ( outTriangles.Count == 0 )
            throw new FormatException( $"glTF: '{sourceName}' contains no triangle geometry." );

        var result = new RigMesh
        {
            SourceName = sourceName,
            Positions = [ .. outPositions ],
            Normals = anyNormal ? [ .. outNormals ] : [],
            Uvs = anyUv ? [ .. outUvs ] : [],
            Triangles = [ .. outTriangles ],
            TriangleTags = [ .. outTriangleTags ],
            Tags = [ .. tags ],
            Materials = document.Materials.Select( m => new RigMaterial
            {
                Name = m.Name,
                BaseColorImage = m.BaseColorImage,
                Tint = m.BaseColorFactor,
            } ).ToArray(),
            TriangleMaterials = [ .. outTriangleMaterials ],
        };
        result.Validate();
        return result;
    }
}