glTF importer classes and parser. Defines GltfNode/Primitive/Material/Skin/Mesh and GltfDocument.Parse which reads GLB or .gltf JSON, resolves buffers (embedded base64 or GLB BIN chunk), decodes nodes, meshes, materials and skins into in-memory arrays.
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;
}
}