GLTF importer classes for AutoRig: defines GltfNode/Primitive/Material/Skin/Mesh and a GltfDocument parser that reads .glb or .gltf JSON bytes, resolves buffers (embedded base64 or GLB BIN chunk), and decodes nodes, meshes, materials and skins into flattened arrays (positions, normals, uvs, indices, joints, weights). It validates accessor layouts and throws FormatException on malformed input.
using System.Numerics;
using System.Text;
using System.Text.Json;
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>One glTF node: hierarchy + local transform + optional mesh reference.</summary>
internal sealed class GltfNode
{
public string? Name;
public int[] Children = [];
public int Parent = -1;
public int Mesh = -1;
public int Skin = -1;
public Vector3 Translation;
public Quaternion Rotation = Quaternion.Identity;
public Vector3 Scale = Vector3.One;
/// <summary>Local transform as a row-vector matrix (v * M).</summary>
public Matrix4x4 LocalMatrix =>
Matrix4x4.CreateScale( Scale )
* Matrix4x4.CreateFromQuaternion( Rotation )
* Matrix4x4.CreateTranslation( Translation );
}
/// <summary>One triangle primitive of a glTF mesh, decoded to flat arrays.</summary>
internal sealed class GltfPrimitive
{
public required float[] Positions; // xyz per vertex
public float[]? Normals; // xyz per vertex
public float[]? Uvs; // uv per vertex
public required int[] Indices; // 3 per triangle
public int[]? Joints; // 4 skin-local joint ids per vertex (JOINTS_0)
public float[]? Weights; // 4 weights per vertex (WEIGHTS_0)
public int Material = -1; // index into GltfDocument.Materials
}
/// <summary>One glTF material, reduced to what export carries through.</summary>
internal sealed class GltfMaterial
{
public string Name = "";
public byte[]? BaseColorImage; // png/jpg bytes as stored
public System.Numerics.Vector3 BaseColorFactor = new( 1f, 1f, 1f );
}
/// <summary>One glTF skin: joint node ids and optional inverse bind matrices.</summary>
internal sealed class GltfSkin
{
public required int[] Joints; // node indices
public Matrix4x4[]? InverseBindMatrices; // parallel to Joints (row-vector layout)
}
/// <summary>One glTF mesh: a name and its triangle primitives.</summary>
internal sealed class GltfMesh
{
public string? Name;
public List<GltfPrimitive> Primitives { get; } = new();
}
/// <summary>
/// Container + JSON layer of the glTF mesh importer (adapted from humanoid-retargeter's
/// GltfDocument, same author): parses a GLB container or plain .gltf JSON, resolves
/// buffers (GLB BIN chunk and base64 data: URIs; external file URIs throw — no file IO
/// in Code/), and decodes nodes and triangle-mesh primitives (POSITION/NORMAL/TEXCOORD_0
/// + indices). Throws <see cref="FormatException"/> on anything malformed.
/// </summary>
internal sealed class GltfDocument
{
private const uint GlbMagic = 0x46546C67; // 'glTF' little-endian
private const uint ChunkJson = 0x4E4F534A; // 'JSON'
private const uint ChunkBin = 0x004E4942; // 'BIN\0'
public List<GltfNode> Nodes { get; } = new();
public List<GltfMesh> Meshes { get; } = new();
public List<GltfSkin> Skins { get; } = new();
public List<GltfMaterial> Materials { get; } = new();
private GltfDocument()
{
}
/// <summary>Parses GLB or plain-JSON glTF bytes.</summary>
/// <exception cref="FormatException">Truncated/malformed container, invalid JSON,
/// unresolvable buffers, or unsupported accessor layouts.</exception>
public static GltfDocument Parse( byte[] data )
{
ArgumentNullException.ThrowIfNull( data );
byte[] json;
byte[]? bin = null;
if ( data.Length >= 4 && ReadU32( data, 0 ) == GlbMagic )
(json, bin) = ParseGlbContainer( data );
else
json = data;
JsonElement root;
try
{
var text = Encoding.UTF8.GetString( json ).TrimStart( '' );
using var doc = JsonDocument.Parse( text );
root = doc.RootElement.Clone();
}
catch ( JsonException e )
{
throw new FormatException( $"glTF: invalid JSON ({e.Message})" );
}
if ( root.ValueKind != JsonValueKind.Object || !root.TryGetProperty( "asset", out _ ) )
throw new FormatException( "glTF: missing required 'asset' object (not a glTF file?)." );
var document = new GltfDocument();
var buffers = ResolveBuffers( root, bin );
document.ReadNodes( root );
document.ReadMeshes( root, buffers );
document.ReadSkins( root, buffers );
document.ReadMaterials( root, buffers );
return document;
}
// ================================================================== GLB container
private static (byte[] Json, byte[]? Bin) ParseGlbContainer( byte[] data )
{
if ( data.Length < 12 )
throw new FormatException( "GLB: truncated header (need 12 bytes)." );
var version = ReadU32( data, 4 );
if ( version != 2 )
throw new FormatException( $"GLB: unsupported container version {version} (expected 2)." );
long declared = ReadU32( data, 8 );
if ( declared > data.Length )
throw new FormatException(
$"GLB: truncated file (header declares {declared} bytes, got {data.Length})." );
byte[]? json = null, bin = null;
long offset = 12;
while ( offset + 8 <= declared )
{
long length = ReadU32( data, (int)offset );
var type = ReadU32( data, (int)offset + 4 );
offset += 8;
if ( offset + length > data.Length )
throw new FormatException( "GLB: truncated chunk (declared length exceeds the file)." );
if ( type == ChunkJson && json is null )
json = data.AsSpan( (int)offset, (int)length ).ToArray();
else if ( type == ChunkBin && bin is null )
bin = data.AsSpan( (int)offset, (int)length ).ToArray();
offset += length + (length % 4 == 0 ? 0 : 4 - length % 4);
}
if ( json is null )
throw new FormatException( "GLB: no JSON chunk found." );
return (json, bin);
}
private static uint ReadU32( byte[] data, int offset )
=> (uint)(data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16 | data[offset + 3] << 24);
// ================================================================== buffers
private static List<byte[]> ResolveBuffers( JsonElement root, byte[]? bin )
{
var buffers = new List<byte[]>();
if ( !root.TryGetProperty( "buffers", out var array ) || array.ValueKind != JsonValueKind.Array )
return buffers;
foreach ( var buffer in array.EnumerateArray() )
{
if ( !buffer.TryGetProperty( "uri", out var uriProp ) )
{
buffers.Add( bin ?? throw new FormatException(
"glTF: buffer has no uri but the file has no GLB BIN chunk." ) );
continue;
}
var uri = uriProp.GetString() ?? "";
if ( uri.StartsWith( "data:", StringComparison.OrdinalIgnoreCase ) )
{
var comma = uri.IndexOf( ',' );
if ( comma < 0 || !uri[..comma].EndsWith( ";base64", StringComparison.OrdinalIgnoreCase ) )
throw new FormatException( "glTF: only base64 data: URIs are supported for buffers." );
try
{
buffers.Add( Convert.FromBase64String( uri[(comma + 1)..] ) );
}
catch ( Exception e ) when ( e is FormatException or ArgumentException )
{
throw new FormatException( "glTF: invalid base64 in buffer data: URI." );
}
}
else
{
throw new FormatException(
$"glTF: buffer references an external file ('{uri}') which this importer cannot "
+ "read (no file IO). Export as .glb (binary, self-contained) instead." );
}
}
return buffers;
}
// ================================================================== nodes
private void ReadNodes( JsonElement root )
{
if ( !root.TryGetProperty( "nodes", out var array ) || array.ValueKind != JsonValueKind.Array )
return;
Span<float> m = stackalloc float[16];
foreach ( var n in array.EnumerateArray() )
{
var node = new GltfNode
{
Name = n.TryGetProperty( "name", out var name ) ? name.GetString() : null,
Mesh = n.TryGetProperty( "mesh", out var mesh ) && mesh.TryGetInt32( out var mi ) ? mi : -1,
Skin = n.TryGetProperty( "skin", out var skin ) && skin.TryGetInt32( out var si ) ? si : -1,
};
if ( n.TryGetProperty( "children", out var children ) && children.ValueKind == JsonValueKind.Array )
{
var list = new List<int>();
foreach ( var c in children.EnumerateArray() )
list.Add( c.GetInt32() );
node.Children = list.ToArray();
}
if ( n.TryGetProperty( "matrix", out var matrix ) && matrix.ValueKind == JsonValueKind.Array )
{
var i = 0;
foreach ( var v in matrix.EnumerateArray() )
{
if ( i >= 16 ) break;
m[i++] = v.GetSingle();
}
if ( i < 16 )
throw new FormatException( "glTF: node matrix has fewer than 16 elements." );
var local = new Matrix4x4(
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] );
if ( Matrix4x4.Decompose( local, out var scale, out var rot, out var pos ) )
{
node.Translation = pos;
node.Rotation = rot;
node.Scale = scale;
}
else
{
node.Translation = local.Translation;
}
}
else
{
node.Translation = ReadVec3( n, "translation", Vector3.Zero );
node.Scale = ReadVec3( n, "scale", Vector3.One );
if ( n.TryGetProperty( "rotation", out var r ) && r.ValueKind == JsonValueKind.Array
&& r.GetArrayLength() >= 4 )
{
node.Rotation = new Quaternion(
r[0].GetSingle(), r[1].GetSingle(), r[2].GetSingle(), r[3].GetSingle() );
}
}
Nodes.Add( node );
}
for ( var i = 0; i < Nodes.Count; i++ )
{
foreach ( var child in Nodes[i].Children )
{
if ( child < 0 || child >= Nodes.Count )
throw new FormatException( $"glTF: node {i} references nonexistent child {child}." );
if ( Nodes[child].Parent < 0 )
Nodes[child].Parent = i;
}
}
}
private static Vector3 ReadVec3( JsonElement element, string property, Vector3 fallback )
{
if ( !element.TryGetProperty( property, out var v ) || v.ValueKind != JsonValueKind.Array
|| v.GetArrayLength() < 3 )
return fallback;
return new Vector3( v[0].GetSingle(), v[1].GetSingle(), v[2].GetSingle() );
}
// ================================================================== meshes
private void ReadMeshes( JsonElement root, List<byte[]> buffers )
{
if ( !root.TryGetProperty( "meshes", out var array ) || array.ValueKind != JsonValueKind.Array )
return;
root.TryGetProperty( "accessors", out var accessors );
root.TryGetProperty( "bufferViews", out var views );
foreach ( var m in array.EnumerateArray() )
{
var mesh = new GltfMesh
{
Name = m.TryGetProperty( "name", out var name ) ? name.GetString() : null,
};
if ( m.TryGetProperty( "primitives", out var primitives )
&& primitives.ValueKind == JsonValueKind.Array )
{
foreach ( var p in primitives.EnumerateArray() )
{
var mode = p.TryGetProperty( "mode", out var modeProp ) ? modeProp.GetInt32() : 4;
if ( mode != 4 )
continue; // only TRIANGLES
if ( !p.TryGetProperty( "attributes", out var attributes )
|| !attributes.TryGetProperty( "POSITION", out var posProp ) )
continue;
var positions = ReadFloats( accessors, views, buffers, posProp.GetInt32(), 3 );
var vertexCount = positions.Length / 3;
float[]? normals = null;
if ( attributes.TryGetProperty( "NORMAL", out var normProp ) )
{
normals = ReadFloats( accessors, views, buffers, normProp.GetInt32(), 3 );
if ( normals.Length != positions.Length ) normals = null;
}
float[]? uvs = null;
if ( attributes.TryGetProperty( "TEXCOORD_0", out var uvProp )
&& TryReadFloatUvs( accessors, views, buffers, uvProp.GetInt32(), out var read )
&& read.Length == vertexCount * 2 )
{
// glTF UV origin is TOP-left; the rest of the pipeline (FBX
// export especially) is bottom-left - flip V here or every
// texture maps upside down/garbled on the compiled model.
for ( var uv = 1; uv < read.Length; uv += 2 )
read[uv] = 1f - read[uv];
uvs = read;
}
int[] indices;
if ( p.TryGetProperty( "indices", out var idxProp ) )
indices = ReadIndices( accessors, views, buffers, idxProp.GetInt32(), vertexCount );
else
{
indices = new int[vertexCount - vertexCount % 3];
for ( var i = 0; i < indices.Length; i++ ) indices[i] = i;
}
int[]? joints = null;
float[]? weights = null;
if ( attributes.TryGetProperty( "JOINTS_0", out var jointsProp )
&& attributes.TryGetProperty( "WEIGHTS_0", out var weightsProp ) )
{
joints = ReadJoints( accessors, views, buffers, jointsProp.GetInt32() );
weights = ReadWeights( accessors, views, buffers, weightsProp.GetInt32() );
if ( joints.Length != vertexCount * 4 || weights.Length != vertexCount * 4 )
{
joints = null;
weights = null;
}
}
mesh.Primitives.Add( new GltfPrimitive
{
Positions = positions,
Normals = normals,
Uvs = uvs,
Indices = indices,
Joints = joints,
Weights = weights,
Material = p.TryGetProperty( "material", out var mat )
&& mat.TryGetInt32( out var materialIndex ) ? materialIndex : -1,
} );
}
}
Meshes.Add( mesh );
}
}
// ================================================================== materials
/// <summary>materials → base color factor + texture image bytes (via textures[].source
/// and images[].bufferView). External image URIs are skipped, not fatal.</summary>
private void ReadMaterials( JsonElement root, List<byte[]> buffers )
{
if ( !root.TryGetProperty( "materials", out var array ) || array.ValueKind != JsonValueKind.Array )
return;
root.TryGetProperty( "textures", out var textures );
root.TryGetProperty( "images", out var images );
root.TryGetProperty( "bufferViews", out var views );
byte[]? ImageBytes( int textureIndex )
{
if ( textures.ValueKind != JsonValueKind.Array || textureIndex < 0
|| textureIndex >= textures.GetArrayLength()
|| !textures[textureIndex].TryGetProperty( "source", out var sourceProp )
|| images.ValueKind != JsonValueKind.Array )
return null;
var imageIndex = sourceProp.GetInt32();
if ( imageIndex < 0 || imageIndex >= images.GetArrayLength()
|| !images[imageIndex].TryGetProperty( "bufferView", out var viewProp )
|| views.ValueKind != JsonValueKind.Array )
return null;
var view = views[viewProp.GetInt32()];
var buffer = buffers[RequiredInt( view, "buffer", "bufferView" )];
var offset = view.TryGetProperty( "byteOffset", out var vo ) ? vo.GetInt32() : 0;
var length = RequiredInt( view, "byteLength", "bufferView" );
if ( offset < 0 || offset + length > buffer.Length )
return null;
return buffer.AsSpan( offset, length ).ToArray();
}
var index = 0;
foreach ( var m in array.EnumerateArray() )
{
var material = new GltfMaterial
{
Name = m.TryGetProperty( "name", out var name )
? name.GetString() ?? $"material{index}" : $"material{index}",
};
if ( m.TryGetProperty( "pbrMetallicRoughness", out var pbr ) )
{
if ( pbr.TryGetProperty( "baseColorFactor", out var factor )
&& factor.ValueKind == JsonValueKind.Array && factor.GetArrayLength() >= 3 )
material.BaseColorFactor = new System.Numerics.Vector3(
factor[0].GetSingle(), factor[1].GetSingle(), factor[2].GetSingle() );
if ( pbr.TryGetProperty( "baseColorTexture", out var tex )
&& tex.TryGetProperty( "index", out var texIndex ) )
material.BaseColorImage = ImageBytes( texIndex.GetInt32() );
}
Materials.Add( material );
index++;
}
}
// ================================================================== skins
private void ReadSkins( JsonElement root, List<byte[]> buffers )
{
if ( !root.TryGetProperty( "skins", out var array ) || array.ValueKind != JsonValueKind.Array )
return;
root.TryGetProperty( "accessors", out var accessors );
root.TryGetProperty( "bufferViews", out var views );
foreach ( var s in array.EnumerateArray() )
{
if ( !s.TryGetProperty( "joints", out var jointsProp )
|| jointsProp.ValueKind != JsonValueKind.Array )
throw new FormatException( "glTF: skin has no joints array." );
var joints = new List<int>();
foreach ( var j in jointsProp.EnumerateArray() )
{
var index = j.GetInt32();
if ( index < 0 || index >= Nodes.Count )
throw new FormatException( $"glTF: skin references nonexistent joint node {index}." );
joints.Add( index );
}
Matrix4x4[]? inverseBind = null;
if ( s.TryGetProperty( "inverseBindMatrices", out var ibmProp ) )
{
var values = ReadFloats( accessors, views, buffers, ibmProp.GetInt32(), 16 );
if ( values.Length != joints.Count * 16 )
throw new FormatException(
$"glTF: skin has {joints.Count} joints but {values.Length / 16} inverse bind matrices." );
inverseBind = new Matrix4x4[joints.Count];
for ( var i = 0; i < joints.Count; i++ )
{
var o = i * 16;
inverseBind[i] = new Matrix4x4(
values[o], values[o + 1], values[o + 2], values[o + 3],
values[o + 4], values[o + 5], values[o + 6], values[o + 7],
values[o + 8], values[o + 9], values[o + 10], values[o + 11],
values[o + 12], values[o + 13], values[o + 14], values[o + 15] );
}
}
Skins.Add( new GltfSkin { Joints = joints.ToArray(), InverseBindMatrices = inverseBind } );
}
}
/// <summary>JOINTS_0: u8/u16 VEC4 per vertex.</summary>
private static int[] ReadJoints( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex )
{
if ( accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength() )
throw new FormatException( $"glTF: accessor {accessorIndex} does not exist." );
var componentType = RequiredInt( accessors[accessorIndex], "componentType", "accessor" );
var compSize = componentType switch
{
5121 => 1, // UNSIGNED_BYTE
5123 => 2, // UNSIGNED_SHORT
_ => throw new FormatException(
$"glTF: unsupported JOINTS_0 componentType {componentType}." ),
};
var (_, buffer, start, stride, count) =
ResolveAccessor( accessors, views, buffers, accessorIndex, compSize * 4 );
var result = new int[checked(count * 4)];
for ( var element = 0; element < count; element++ )
{
var offset = (int)(start + (long)element * stride);
for ( var c = 0; c < 4; c++ )
result[element * 4 + c] = compSize == 1
? buffer[offset + c]
: BitConverter.ToUInt16( buffer, offset + c * 2 );
}
return result;
}
/// <summary>WEIGHTS_0: f32 or normalized u8/u16 VEC4 per vertex.</summary>
private static float[] ReadWeights( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex )
{
if ( accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength() )
throw new FormatException( $"glTF: accessor {accessorIndex} does not exist." );
var componentType = RequiredInt( accessors[accessorIndex], "componentType", "accessor" );
if ( componentType == 5126 )
return ReadFloats( accessors, views, buffers, accessorIndex, 4 );
var (compSize, divisor) = componentType switch
{
5121 => (1, 255f), // UNSIGNED_BYTE normalized
5123 => (2, 65535f), // UNSIGNED_SHORT normalized
_ => throw new FormatException(
$"glTF: unsupported WEIGHTS_0 componentType {componentType}." ),
};
var (_, buffer, start, stride, count) =
ResolveAccessor( accessors, views, buffers, accessorIndex, compSize * 4 );
var result = new float[checked(count * 4)];
for ( var element = 0; element < count; element++ )
{
var offset = (int)(start + (long)element * stride);
for ( var c = 0; c < 4; c++ )
result[element * 4 + c] = (compSize == 1
? buffer[offset + c]
: BitConverter.ToUInt16( buffer, offset + c * 2 )) / divisor;
}
return result;
}
// ================================================================== accessors
private static int RequiredInt( JsonElement element, string property, string context )
{
if ( !element.TryGetProperty( property, out var v ) )
throw new FormatException( $"glTF: {context} is missing '{property}'." );
return v.GetInt32();
}
private static (JsonElement Accessor, byte[] Buffer, long Start, int Stride, int Count)
ResolveAccessor( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex, int elementSize )
{
if ( accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength() )
throw new FormatException( $"glTF: accessor {accessorIndex} does not exist." );
var accessor = accessors[accessorIndex];
if ( accessor.TryGetProperty( "sparse", out _ ) )
throw new FormatException( "glTF: sparse accessors are not supported." );
var count = RequiredInt( accessor, "count", "accessor" );
if ( count < 0 )
throw new FormatException( $"glTF: accessor {accessorIndex} has a negative count ({count})." );
if ( !accessor.TryGetProperty( "bufferView", out var viewIndexProp ) )
throw new FormatException( $"glTF: accessor {accessorIndex} has no bufferView (unsupported for meshes)." );
var viewIndex = viewIndexProp.GetInt32();
if ( views.ValueKind != JsonValueKind.Array || viewIndex < 0 || viewIndex >= views.GetArrayLength() )
throw new FormatException( $"glTF: bufferView {viewIndex} does not exist." );
var view = views[viewIndex];
var bufferIndex = RequiredInt( view, "buffer", "bufferView" );
if ( bufferIndex < 0 || bufferIndex >= buffers.Count )
throw new FormatException( $"glTF: buffer {bufferIndex} does not exist." );
var buffer = buffers[bufferIndex];
var viewOffset = view.TryGetProperty( "byteOffset", out var vo ) ? vo.GetInt32() : 0;
var accessorOffset = accessor.TryGetProperty( "byteOffset", out var ao ) ? ao.GetInt32() : 0;
var stride = view.TryGetProperty( "byteStride", out var st ) ? st.GetInt32() : elementSize;
if ( stride < elementSize )
throw new FormatException( "glTF: bufferView byteStride is smaller than the element size." );
var start = (long)viewOffset + accessorOffset;
var end = start + (long)(count - 1) * stride + elementSize;
if ( count > 0 && (start < 0 || end > buffer.Length) )
throw new FormatException(
$"glTF: accessor {accessorIndex} reads past the end of its buffer (truncated file?)." );
return (accessor, buffer, start, stride, count);
}
/// <summary>Reads a float accessor with the given component count (VEC2=2, VEC3=3).</summary>
private static float[] ReadFloats( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex, int comps )
{
if ( accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength() )
throw new FormatException( $"glTF: accessor {accessorIndex} does not exist." );
var componentType = RequiredInt( accessors[accessorIndex], "componentType", "accessor" );
if ( componentType != 5126 )
throw new FormatException( $"glTF: accessor {accessorIndex} must be FLOAT for mesh attributes." );
var (_, buffer, start, stride, count) =
ResolveAccessor( accessors, views, buffers, accessorIndex, comps * 4 );
var result = new float[checked(count * comps)];
for ( var element = 0; element < count; element++ )
{
var offset = (int)(start + (long)element * stride);
for ( var c = 0; c < comps; c++ )
result[element * comps + c] = BitConverter.ToSingle( buffer, offset + c * 4 );
}
return result;
}
private static bool TryReadFloatUvs( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex, out float[] uvs )
{
try
{
uvs = ReadFloats( accessors, views, buffers, accessorIndex, 2 );
return true;
}
catch ( FormatException )
{
uvs = [];
return false; // normalized-integer UVs etc. — skip UVs rather than fail the mesh
}
}
/// <summary>Reads an index accessor (u8/u16/u32 SCALAR) and validates the index range.</summary>
private static int[] ReadIndices( JsonElement accessors, JsonElement views, List<byte[]> buffers,
int accessorIndex, int vertexCount )
{
if ( accessors.ValueKind != JsonValueKind.Array || accessorIndex < 0
|| accessorIndex >= accessors.GetArrayLength() )
throw new FormatException( $"glTF: accessor {accessorIndex} does not exist." );
var componentType = RequiredInt( accessors[accessorIndex], "componentType", "accessor" );
var compSize = componentType switch
{
5121 => 1, // UNSIGNED_BYTE
5123 => 2, // UNSIGNED_SHORT
5125 => 4, // UNSIGNED_INT
_ => throw new FormatException(
$"glTF: unsupported index componentType {componentType}." ),
};
var (_, buffer, start, stride, count) =
ResolveAccessor( accessors, views, buffers, accessorIndex, compSize );
var result = new int[count - count % 3];
for ( var i = 0; i < result.Length; i++ )
{
var offset = (int)(start + (long)i * stride);
var value = componentType switch
{
5121 => buffer[offset],
5123 => BitConverter.ToUInt16( buffer, offset ),
_ => (long)BitConverter.ToUInt32( buffer, offset ),
};
if ( value < 0 || value >= vertexCount )
throw new FormatException( $"glTF: index {value} out of range (have {vertexCount} vertices)." );
result[i] = (int)value;
}
return result;
}
}