@galfus/gltf-loader 0.0.0-stage → 0.31.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/scene.ts ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Scene graph construction, ECS entity spawning, hierarchy resolution, and GPU instancing.
3
+ */
4
+
5
+ import type { Model3DComponent, Realm3D } from '@galfus/engine';
6
+ import { mat4, quat, vec3 } from '@galfus/engine';
7
+ import type {
8
+ ModelImportOptions,
9
+ ModelInstantiateOptions,
10
+ ModelNodeDesc
11
+ } from '@galfus/model-loader-types';
12
+ import { readAccessor } from './parser';
13
+ import type { UploadedGltfResources } from './resources';
14
+ import type { GlTF, GlTFNode } from './types';
15
+
16
+ export interface SpawnedSceneResult {
17
+ rootEntityId: number;
18
+ entities: number[];
19
+ nodeEntityMap: Map<number, number>; // gltfNodeIndex -> ecsEntityId
20
+ primitiveEntities: Map<number, number[]>;
21
+ instancedModelIds: number[];
22
+ }
23
+
24
+ /**
25
+ * Checks whether a glTF node passes the user-provided import filter.
26
+ */
27
+ function shouldImportNode(nodeDesc: ModelNodeDesc, options?: ModelImportOptions): boolean {
28
+ if (!options?.filter) return true;
29
+ if (typeof options.filter === 'function') {
30
+ return options.filter(nodeDesc);
31
+ }
32
+ if (typeof options.filter === 'string') {
33
+ return nodeDesc.name === options.filter;
34
+ }
35
+ if (options.filter instanceof RegExp) {
36
+ return options.filter.test(nodeDesc.name ?? '');
37
+ }
38
+ return true;
39
+ }
40
+
41
+ /**
42
+ * Spawns a complete glTF scene graph or sub-tree into the ECS and GPU.
43
+ *
44
+ * @param realm The target Realm3D.
45
+ * @param gltf The root glTF descriptor.
46
+ * @param buffers Array of resolved binary buffers.
47
+ * @param resources Pre-uploaded GPU geometries, materials, and textures.
48
+ * @param instantiateOptions Spatial offsets and optional parent hierarchy attachment.
49
+ * @param importOptions Filtering rules, node renamers, and camera/light toggles.
50
+ * @param rootNodeIndices Optional specific node indices to instantiate (for sub-factories).
51
+ * @returns Object holding the root entity, entity collections, and node mappings.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const result = spawnSceneGraph(realm, gltf, buffers, resources, options);
56
+ * console.log(`Spawned ${result.entities.length} entities in ECS`);
57
+ * ```
58
+ */
59
+ export function spawnSceneGraph(
60
+ realm: Realm3D,
61
+ gltf: GlTF,
62
+ buffers: Uint8Array[],
63
+ resources: UploadedGltfResources,
64
+ instantiateOptions?: ModelInstantiateOptions,
65
+ importOptions?: ModelImportOptions,
66
+ rootNodeIndices?: number[]
67
+ ): SpawnedSceneResult {
68
+ const nodeCount = gltf.nodes?.length ?? 0;
69
+ const nodeEntityMap = new Map<number, number>();
70
+ const entities: number[] = [];
71
+ const instancedModelIds: number[] = [];
72
+ const primitiveEntities = new Map<number, number[]>();
73
+
74
+ // Determine root nodes: either user-specified or from active glTF scene
75
+ let candidateRoots: number[] = [];
76
+ if (rootNodeIndices && rootNodeIndices.length > 0) {
77
+ candidateRoots = rootNodeIndices;
78
+ } else {
79
+ const activeSceneIdx = gltf.scene ?? 0;
80
+ const activeScene = gltf.scenes?.[activeSceneIdx];
81
+ candidateRoots = activeScene?.nodes ?? (nodeCount > 0 ? [0] : []);
82
+ }
83
+
84
+ // 1. Create a root container entity for this instance
85
+ const rootEntityId = realm.createSpatialNode({
86
+ position: instantiateOptions?.transform?.position ?? [0, 0, 0],
87
+ rotation: instantiateOptions?.transform?.rotation ?? [0, 0, 0, 1],
88
+ scale: instantiateOptions?.transform?.scale ?? [1, 1, 1]
89
+ });
90
+ entities.push(rootEntityId);
91
+ realm.setEntityName(rootEntityId, 'GltfInstance_Root');
92
+
93
+ if (instantiateOptions?.parentEntity !== undefined) {
94
+ realm.hierarchy.attach(rootEntityId, instantiateOptions.parentEntity);
95
+ }
96
+
97
+ // Fallback default material if a primitive omits material definition
98
+ let defaultMaterialId: number | undefined;
99
+ function getDefaultMaterial(): number {
100
+ if (defaultMaterialId === undefined) {
101
+ defaultMaterialId = realm.createMaterial({ baseColor: [1, 1, 1, 1] }).materialId;
102
+ }
103
+ return defaultMaterialId;
104
+ }
105
+
106
+ // Helper to extract TRS from glTF node
107
+ function getNodeTRS(node: GlTFNode): {
108
+ position: [number, number, number];
109
+ rotation: [number, number, number, number];
110
+ scale: [number, number, number];
111
+ } {
112
+ if (node.matrix) {
113
+ // Decompose 4x4 column-major matrix
114
+ const m = node.matrix;
115
+ const pos: [number, number, number] = [m[12] ?? 0, m[13] ?? 0, m[14] ?? 0];
116
+ const scl: [number, number, number] = [
117
+ Math.hypot(m[0] ?? 1, m[1] ?? 0, m[2] ?? 0),
118
+ Math.hypot(m[4] ?? 0, m[5] ?? 1, m[6] ?? 0),
119
+ Math.hypot(m[8] ?? 0, m[9] ?? 0, m[10] ?? 1)
120
+ ];
121
+ // Orientation from rotation matrix
122
+ const rot: [number, number, number, number] = [0, 0, 0, 1];
123
+ quat.fromMat4(rot as any, m as any);
124
+ return { position: pos, rotation: rot, scale: scl };
125
+ }
126
+
127
+ return {
128
+ position: node.translation ?? [0, 0, 0],
129
+ rotation: node.rotation ?? [0, 0, 0, 1],
130
+ scale: node.scale ?? [1, 1, 1]
131
+ };
132
+ }
133
+
134
+ // 2. Instantiate Nodes as ECS entities (or GPU Instanced Models)
135
+ for (let nIdx = 0; nIdx < nodeCount; nIdx++) {
136
+ const node = gltf.nodes![nIdx]!;
137
+ const nodeDesc: ModelNodeDesc = {
138
+ index: nIdx,
139
+ name: node.name,
140
+ hasMesh: node.mesh !== undefined,
141
+ hasSkin: node.skin !== undefined,
142
+ hasCamera: node.camera !== undefined,
143
+ hasLight: node.extensions?.['KHR_lights_punctual'] !== undefined,
144
+ extras:
145
+ typeof node.extras === 'object' && node.extras !== null
146
+ ? (node.extras as Record<string, unknown>)
147
+ : undefined
148
+ };
149
+
150
+ if (!shouldImportNode(nodeDesc, importOptions)) {
151
+ continue;
152
+ }
153
+
154
+ const normalizedName = importOptions?.renameNode
155
+ ? importOptions.renameNode(node.name ?? `Node_${nIdx}`, nodeDesc)
156
+ : (node.name ?? `Node_${nIdx}`);
157
+
158
+ // A) Check for Hardware GPU Instancing (EXT_mesh_gpu_instancing)
159
+ const gpuInstancingExt = node.extensions?.EXT_mesh_gpu_instancing;
160
+ if (gpuInstancingExt && node.mesh !== undefined) {
161
+ const mesh = gltf.meshes?.[node.mesh];
162
+ if (mesh) {
163
+ const transAcc = gpuInstancingExt.attributes['TRANSLATION'];
164
+ const rotAcc = gpuInstancingExt.attributes['ROTATION'];
165
+ const scaleAcc = gpuInstancingExt.attributes['SCALE'];
166
+
167
+ const translations =
168
+ transAcc !== undefined
169
+ ? (readAccessor(gltf, buffers, transAcc) as Float32Array)
170
+ : undefined;
171
+ const rotations =
172
+ rotAcc !== undefined ? (readAccessor(gltf, buffers, rotAcc) as Float32Array) : undefined;
173
+ const scales =
174
+ scaleAcc !== undefined
175
+ ? (readAccessor(gltf, buffers, scaleAcc) as Float32Array)
176
+ : undefined;
177
+
178
+ const count = translations
179
+ ? translations.length / 3
180
+ : rotations
181
+ ? rotations.length / 4
182
+ : scales
183
+ ? scales.length / 3
184
+ : 0;
185
+ // 32 floats per instance record required by Galfus Core
186
+ const instanceData = new Float32Array(count * 32);
187
+
188
+ const tempMat = mat4.create();
189
+ const tempPos = vec3.create();
190
+ const tempRot = quat.create();
191
+ const tempScale = vec3.create();
192
+
193
+ for (let i = 0; i < count; i++) {
194
+ tempPos[0] = translations ? translations[i * 3 + 0]! : 0;
195
+ tempPos[1] = translations ? translations[i * 3 + 1]! : 0;
196
+ tempPos[2] = translations ? translations[i * 3 + 2]! : 0;
197
+
198
+ if (rotations) {
199
+ tempRot[0] = rotations[i * 4 + 0]!;
200
+ tempRot[1] = rotations[i * 4 + 1]!;
201
+ tempRot[2] = rotations[i * 4 + 2]!;
202
+ tempRot[3] = rotations[i * 4 + 3]!;
203
+ } else {
204
+ quat.identity(tempRot);
205
+ }
206
+
207
+ if (scales) {
208
+ tempScale[0] = scales[i * 3 + 0]!;
209
+ tempScale[1] = scales[i * 3 + 1]!;
210
+ tempScale[2] = scales[i * 3 + 2]!;
211
+ } else {
212
+ vec3.set(tempScale, 1, 1, 1);
213
+ }
214
+
215
+ mat4.fromRotationTranslationScale(tempMat, tempRot, tempPos, tempScale);
216
+
217
+ const offset = i * 32;
218
+ for (let m = 0; m < 16; m++) {
219
+ instanceData[offset + m] = tempMat[m]!;
220
+ }
221
+ // Color (default white)
222
+ instanceData[offset + 16] = 1;
223
+ instanceData[offset + 17] = 1;
224
+ instanceData[offset + 18] = 1;
225
+ instanceData[offset + 19] = 1;
226
+ // Emissive (default black)
227
+ instanceData[offset + 20] = 0;
228
+ instanceData[offset + 21] = 0;
229
+ instanceData[offset + 22] = 0;
230
+ instanceData[offset + 23] = 0;
231
+ // PBR params (metallic, roughness, etc.)
232
+ instanceData[offset + 24] = 1;
233
+ instanceData[offset + 25] = 1;
234
+ instanceData[offset + 26] = 0;
235
+ instanceData[offset + 27] = 0;
236
+ }
237
+
238
+ for (let primIdx = 0; primIdx < mesh.primitives.length; primIdx++) {
239
+ const prim = mesh.primitives[primIdx]!;
240
+ const key = `${node.mesh}_${primIdx}`;
241
+ const geometryId = resources.geometryMap.get(key);
242
+ const materialId =
243
+ (prim.material !== undefined ? resources.materialMap.get(prim.material) : undefined) ??
244
+ getDefaultMaterial();
245
+
246
+ if (geometryId !== undefined) {
247
+ const instancedEntityId = realm.createInstancedModel({
248
+ geometryId,
249
+ materialId
250
+ });
251
+ realm.setInstancedModelData(instancedEntityId, instanceData);
252
+ instancedModelIds.push(instancedEntityId);
253
+ }
254
+ }
255
+ }
256
+ continue;
257
+ }
258
+
259
+ // B) Standard ECS Model Entity
260
+ const trs = getNodeTRS(node);
261
+ const entityId = realm.createSpatialNode(trs);
262
+ entities.push(entityId);
263
+ nodeEntityMap.set(nIdx, entityId);
264
+
265
+ realm.setEntityName(entityId, normalizedName);
266
+
267
+ // Attach metadata extras if present
268
+ if (nodeDesc.extras) {
269
+ for (const [k, v] of Object.entries(nodeDesc.extras)) {
270
+ realm.setMetadataKV(entityId, k, v);
271
+ }
272
+ }
273
+
274
+ // Attach Renderable Mesh if present
275
+ if (node.mesh !== undefined) {
276
+ const mesh = gltf.meshes?.[node.mesh];
277
+ if (mesh) {
278
+ for (let primIdx = 0; primIdx < mesh.primitives.length; primIdx++) {
279
+ const prim = mesh.primitives[primIdx]!;
280
+ const key = `${node.mesh}_${primIdx}`;
281
+ const geometryId = resources.geometryMap.get(key);
282
+ const materialId =
283
+ (prim.material !== undefined ? resources.materialMap.get(prim.material) : undefined) ??
284
+ getDefaultMaterial();
285
+
286
+ if (geometryId !== undefined) {
287
+ let primitiveEntityId = entityId;
288
+ if (primIdx === 0) {
289
+ realm.createMesh(entityId, {
290
+ geometryId,
291
+ materialId
292
+ });
293
+ } else {
294
+ // Sub-primitive: spawn a child spatial node attached to the primary entity
295
+ const subEntityId = realm.createSpatialNode();
296
+ primitiveEntityId = subEntityId;
297
+ realm.setEntityName(subEntityId, `${normalizedName}_prim${primIdx}`);
298
+ realm.createMesh(subEntityId, {
299
+ geometryId,
300
+ materialId
301
+ });
302
+ realm.hierarchy.attach(subEntityId, entityId);
303
+ entities.push(subEntityId);
304
+ }
305
+ const mapped = primitiveEntities.get(nIdx) ?? [];
306
+ mapped.push(primitiveEntityId);
307
+ primitiveEntities.set(nIdx, mapped);
308
+ if (prim.targets?.length) {
309
+ const weights =
310
+ node.weights ?? mesh.weights ?? new Array(prim.targets.length).fill(0);
311
+ const model = realm.world.getComponent<Model3DComponent>(
312
+ primitiveEntityId,
313
+ 'Model3D'
314
+ );
315
+ if (model) realm.setModelMorphWeights(model.modelId, new Float32Array(weights));
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ // 3. Connect Hierarchy Relationships via Anchor HierarchyCoordinator
324
+ for (let nIdx = 0; nIdx < nodeCount; nIdx++) {
325
+ const parentEntityId = nodeEntityMap.get(nIdx);
326
+ if (parentEntityId === undefined) continue;
327
+
328
+ const node = gltf.nodes![nIdx]!;
329
+ if (node.children) {
330
+ for (const childIdx of node.children) {
331
+ const childEntityId = nodeEntityMap.get(childIdx);
332
+ if (childEntityId !== undefined) {
333
+ realm.hierarchy.attach(childEntityId, parentEntityId);
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ // 4. Attach Candidate Roots to Instance Root Container
340
+ for (const rootIdx of candidateRoots) {
341
+ const rootChildEntityId = nodeEntityMap.get(rootIdx);
342
+ if (rootChildEntityId !== undefined) {
343
+ realm.hierarchy.attach(rootChildEntityId, rootEntityId);
344
+ }
345
+ }
346
+
347
+ return {
348
+ rootEntityId,
349
+ entities,
350
+ nodeEntityMap,
351
+ primitiveEntities,
352
+ instancedModelIds
353
+ };
354
+ }
package/src/types.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * glTF 2.0 canonical schema specifications and extension types.
3
+ *
4
+ * Implements the official Khronos glTF 2.0 standard:
5
+ * https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
6
+ */
7
+
8
+ export const GLTF_COMPONENT_TYPE = {
9
+ BYTE: 5120,
10
+ UNSIGNED_BYTE: 5121,
11
+ SHORT: 5122,
12
+ UNSIGNED_SHORT: 5123,
13
+ UNSIGNED_INT: 5125,
14
+ FLOAT: 5126
15
+ } as const;
16
+
17
+ export type GlTFComponentType = (typeof GLTF_COMPONENT_TYPE)[keyof typeof GLTF_COMPONENT_TYPE];
18
+
19
+ export type GlTFAccessorType = 'SCALAR' | 'VEC2' | 'VEC3' | 'VEC4' | 'MAT2' | 'MAT3' | 'MAT4';
20
+
21
+ export interface GlTFAccessor {
22
+ bufferView?: number;
23
+ byteOffset?: number;
24
+ componentType: GlTFComponentType;
25
+ normalized?: boolean;
26
+ count: number;
27
+ type: GlTFAccessorType;
28
+ max?: number[];
29
+ min?: number[];
30
+ sparse?: {
31
+ count: number;
32
+ indices: { bufferView: number; byteOffset?: number; componentType: GlTFComponentType };
33
+ values: { bufferView: number; byteOffset?: number };
34
+ };
35
+ name?: string;
36
+ extensions?: Record<string, unknown>;
37
+ extras?: unknown;
38
+ }
39
+
40
+ export interface GlTFBufferView {
41
+ buffer: number;
42
+ byteOffset?: number;
43
+ byteLength: number;
44
+ byteStride?: number;
45
+ target?: number;
46
+ name?: string;
47
+ extensions?: {
48
+ EXT_meshopt_compression?: {
49
+ buffer: number;
50
+ byteOffset?: number;
51
+ byteLength: number;
52
+ byteStride: number;
53
+ count: number;
54
+ mode: 'ATTRIBUTES' | 'TRIANGLES' | 'INDICES';
55
+ filter?: 'NONE' | 'OCTAHEDRAL' | 'QUATERNION' | 'EXPONENTIAL';
56
+ };
57
+ } & Record<string, unknown>;
58
+ extras?: unknown;
59
+ }
60
+
61
+ export interface GlTFBuffer {
62
+ uri?: string;
63
+ byteLength: number;
64
+ name?: string;
65
+ extensions?: Record<string, unknown>;
66
+ extras?: unknown;
67
+ }
68
+
69
+ export interface GlTFPrimitive {
70
+ attributes: Record<string, number>;
71
+ indices?: number;
72
+ material?: number;
73
+ mode?: number;
74
+ targets?: Array<Record<string, number>>;
75
+ extensions?: {
76
+ KHR_draco_mesh_compression?: {
77
+ bufferView: number;
78
+ attributes: Record<string, number>;
79
+ };
80
+ } & Record<string, unknown>;
81
+ extras?: unknown;
82
+ }
83
+
84
+ export interface GlTFMesh {
85
+ primitives: GlTFPrimitive[];
86
+ weights?: number[];
87
+ name?: string;
88
+ extensions?: Record<string, unknown>;
89
+ extras?: unknown;
90
+ }
91
+
92
+ export interface GlTFTextureInfo {
93
+ index: number;
94
+ texCoord?: number;
95
+ extensions?: Record<string, unknown>;
96
+ }
97
+
98
+ export interface GlTFMaterialNormalTextureInfo extends GlTFTextureInfo {
99
+ scale?: number;
100
+ }
101
+
102
+ export interface GlTFMaterialOcclusionTextureInfo extends GlTFTextureInfo {
103
+ strength?: number;
104
+ }
105
+
106
+ export interface GlTFKHRMaterialsPbrSpecularGlossiness {
107
+ diffuseFactor?: [number, number, number, number];
108
+ diffuseTexture?: GlTFTextureInfo;
109
+ specularFactor?: [number, number, number];
110
+ glossinessFactor?: number;
111
+ specularGlossinessTexture?: GlTFTextureInfo;
112
+ }
113
+
114
+ export interface GlTFPbrMetallicRoughness {
115
+ baseColorFactor?: [number, number, number, number];
116
+ baseColorTexture?: GlTFTextureInfo;
117
+ metallicFactor?: number;
118
+ roughnessFactor?: number;
119
+ metallicRoughnessTexture?: GlTFTextureInfo;
120
+ extensions?: Record<string, unknown>;
121
+ }
122
+
123
+ export interface GlTFMaterial {
124
+ name?: string;
125
+ pbrMetallicRoughness?: GlTFPbrMetallicRoughness;
126
+ normalTexture?: GlTFMaterialNormalTextureInfo;
127
+ occlusionTexture?: GlTFMaterialOcclusionTextureInfo;
128
+ emissiveTexture?: GlTFTextureInfo;
129
+ emissiveFactor?: [number, number, number];
130
+ alphaMode?: 'OPAQUE' | 'MASK' | 'BLEND';
131
+ alphaCutoff?: number;
132
+ doubleSided?: boolean;
133
+ extensions?: {
134
+ KHR_materials_pbrSpecularGlossiness?: GlTFKHRMaterialsPbrSpecularGlossiness;
135
+ KHR_materials_ior?: { ior?: number };
136
+ } & Record<string, unknown>;
137
+ extras?: unknown;
138
+ }
139
+
140
+ export type GltfMaterialAlphaModeResolver = (
141
+ material: GlTFMaterial,
142
+ index: number,
143
+ baseColorImage?: Uint8Array
144
+ ) => GlTFMaterial['alphaMode'];
145
+
146
+ export interface GlTFTexture {
147
+ sampler?: number;
148
+ source?: number;
149
+ name?: string;
150
+ extensions?: {
151
+ KHR_texture_basisu?: {
152
+ source: number;
153
+ };
154
+ } & Record<string, unknown>;
155
+ extras?: unknown;
156
+ }
157
+
158
+ export interface GlTFImage {
159
+ uri?: string;
160
+ mimeType?: string;
161
+ bufferView?: number;
162
+ name?: string;
163
+ extensions?: Record<string, unknown>;
164
+ extras?: unknown;
165
+ }
166
+
167
+ export interface GlTFNode {
168
+ camera?: number;
169
+ children?: number[];
170
+ skin?: number;
171
+ matrix?: [
172
+ number,
173
+ number,
174
+ number,
175
+ number,
176
+ number,
177
+ number,
178
+ number,
179
+ number,
180
+ number,
181
+ number,
182
+ number,
183
+ number,
184
+ number,
185
+ number,
186
+ number,
187
+ number
188
+ ];
189
+ mesh?: number;
190
+ rotation?: [number, number, number, number];
191
+ scale?: [number, number, number];
192
+ translation?: [number, number, number];
193
+ weights?: number[];
194
+ name?: string;
195
+ extensions?: {
196
+ EXT_mesh_gpu_instancing?: {
197
+ attributes: Record<string, number>;
198
+ };
199
+ } & Record<string, unknown>;
200
+ extras?: unknown;
201
+ }
202
+
203
+ export interface GlTFSkin {
204
+ inverseBindMatrices?: number;
205
+ skeleton?: number;
206
+ joints: number[];
207
+ name?: string;
208
+ extensions?: Record<string, unknown>;
209
+ extras?: unknown;
210
+ }
211
+
212
+ export interface GlTFAnimationChannel {
213
+ sampler: number;
214
+ target: {
215
+ node?: number;
216
+ path: 'translation' | 'rotation' | 'scale' | 'weights';
217
+ extensions?: Record<string, unknown>;
218
+ };
219
+ extensions?: Record<string, unknown>;
220
+ }
221
+
222
+ export interface GlTFAnimationSampler {
223
+ input: number;
224
+ interpolation?: 'LINEAR' | 'STEP' | 'CUBICSPLINE';
225
+ output: number;
226
+ extensions?: Record<string, unknown>;
227
+ }
228
+
229
+ export interface GlTFAnimation {
230
+ name?: string;
231
+ channels: GlTFAnimationChannel[];
232
+ samplers: GlTFAnimationSampler[];
233
+ extensions?: Record<string, unknown>;
234
+ extras?: unknown;
235
+ }
236
+
237
+ export interface GlTFScene {
238
+ nodes?: number[];
239
+ name?: string;
240
+ extensions?: Record<string, unknown>;
241
+ extras?: unknown;
242
+ }
243
+
244
+ export interface GlTFAsset {
245
+ version: string;
246
+ generator?: string;
247
+ minVersion?: string;
248
+ extensions?: Record<string, unknown>;
249
+ extras?: unknown;
250
+ }
251
+
252
+ export interface GlTF {
253
+ asset: GlTFAsset;
254
+ scene?: number;
255
+ scenes?: GlTFScene[];
256
+ nodes?: GlTFNode[];
257
+ meshes?: GlTFMesh[];
258
+ materials?: GlTFMaterial[];
259
+ textures?: GlTFTexture[];
260
+ images?: GlTFImage[];
261
+ accessors?: GlTFAccessor[];
262
+ bufferViews?: GlTFBufferView[];
263
+ buffers?: GlTFBuffer[];
264
+ skins?: GlTFSkin[];
265
+ animations?: GlTFAnimation[];
266
+ extensionsUsed?: string[];
267
+ extensionsRequired?: string[];
268
+ extensions?: Record<string, unknown>;
269
+ extras?: unknown;
270
+ }