@aura3d/assets 1.4.0 → 1.4.2

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.
Files changed (69) hide show
  1. package/package.json +38 -5
  2. package/AGENTS.md +0 -46
  3. package/src/AdvancedAssetCorpus.ts +0 -454
  4. package/src/AssetBundleCacheFixtures.ts +0 -290
  5. package/src/AssetCache.ts +0 -76
  6. package/src/AssetCompatibility.ts +0 -224
  7. package/src/AssetCorpus.ts +0 -329
  8. package/src/AssetDependencyGraph.ts +0 -83
  9. package/src/AssetHandle.ts +0 -74
  10. package/src/AssetImportPreflight.ts +0 -179
  11. package/src/AssetInspection.ts +0 -706
  12. package/src/AssetLoader.ts +0 -36
  13. package/src/AssetManager.ts +0 -211
  14. package/src/AssetRegistry.ts +0 -44
  15. package/src/AssetRenderDefaults.ts +0 -36
  16. package/src/AudioLoader.ts +0 -41
  17. package/src/BlenderExportValidation.ts +0 -227
  18. package/src/CarConceptMaterialStability.ts +0 -1405
  19. package/src/EXRLoader.ts +0 -1
  20. package/src/ExternalParityGLTFCorpus.ts +0 -65
  21. package/src/GLTFAnimationRuntime.ts +0 -1416
  22. package/src/GLTFAutoFit.ts +0 -133
  23. package/src/GLTFCompressionDecoders.ts +0 -190
  24. package/src/GLTFExtensionSupport.ts +0 -138
  25. package/src/GLTFLoader.ts +0 -4060
  26. package/src/GLTFRenderResources.ts +0 -2270
  27. package/src/HDRLoader.ts +0 -1
  28. package/src/ImageLoader.ts +0 -52
  29. package/src/ImportPipeline.ts +0 -148
  30. package/src/KTX2BasisTextureTranscoder.ts +0 -225
  31. package/src/LoadContext.ts +0 -60
  32. package/src/MaterialLoader.ts +0 -166
  33. package/src/MeshOptimization.ts +0 -93
  34. package/src/OBJLoader.ts +0 -551
  35. package/src/SceneAnalysisFixtures.ts +0 -438
  36. package/src/SceneLoader.ts +0 -62
  37. package/src/ShaderLoader.ts +0 -30
  38. package/src/TextureLoader.ts +0 -30
  39. package/src/TexturePipeline.ts +0 -96
  40. package/src/TextureStreaming.ts +0 -20
  41. package/src/WorkerAssetJobs.ts +0 -56
  42. package/src/advanced-gallery/index.ts +0 -18
  43. package/src/asset-corpus/GLTFSceneLoader.ts +0 -9
  44. package/src/asset-corpus/ProductionAssetCorpus.ts +0 -230
  45. package/src/asset-corpus/ProductionAssetPipeline.ts +0 -5
  46. package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +0 -182
  47. package/src/asset-corpus/ProductionHDRTextureLoader.ts +0 -2
  48. package/src/asset-corpus/ProductionKTX2TextureLoader.ts +0 -2
  49. package/src/asset-corpus/ProductionTextureLoader.ts +0 -2
  50. package/src/asset-corpus/index.ts +0 -29
  51. package/src/browser-index.ts +0 -229
  52. package/src/createRenderableScene.ts +0 -128
  53. package/src/index.ts +0 -325
  54. package/src/loadRenderableAsset.ts +0 -67
  55. package/src/loaders/CubeTextureLoader.ts +0 -8
  56. package/src/loaders/EXRLoader.ts +0 -10
  57. package/src/loaders/HDRLoader.ts +0 -7
  58. package/src/loaders/KTX2Loader.ts +0 -7
  59. package/src/loaders/LoaderDiagnostics.ts +0 -36
  60. package/src/loaders/MTLLoader.ts +0 -20
  61. package/src/loaders/OBJLoader.ts +0 -24
  62. package/src/loaders/TextureLoader.ts +0 -14
  63. package/src/loaders/ThreeCompatGLTFLoader.ts +0 -19
  64. package/src/loaders/index.ts +0 -14
  65. package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +0 -51
  66. package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +0 -61
  67. package/src/threejs-example-parity/index.ts +0 -262
  68. package/tests/assets.test.ts +0 -393
  69. package/tsconfig.json +0 -7
package/src/GLTFLoader.ts DELETED
@@ -1,4060 +0,0 @@
1
- import {
2
- PointLight,
3
- composeMat4,
4
- decomposeMat4,
5
- invertMat4,
6
- multiplyMat4,
7
- Renderable,
8
- Scene,
9
- SpotLight,
10
- type Mat4 as SceneMat4,
11
- type Quat as SceneQuat,
12
- type SceneNode,
13
- type Vec3 as SceneVec3
14
- } from "@aura3d/scene";
15
- import { AnimationClip, AnimationTrack, Skeleton, type Mat4, type Quat, type SerializedAnimationClip, type SkinningPalette, type TrackValueType, type Vec3 } from "@aura3d/animation";
16
- import type { AssetLoadProgress, AssetLoadRequest, AssetLoader } from "./AssetLoader";
17
- import {
18
- evaluateGLTFExtensionSupport,
19
- type GLTFExtensionSupportEvaluation
20
- } from "./GLTFExtensionSupport";
21
- import type { LoadContext } from "./LoadContext";
22
-
23
- type GLTFComponentType = 5120 | 5121 | 5122 | 5123 | 5125 | 5126;
24
- type GLTFSparseIndexComponentType = 5121 | 5123 | 5125;
25
- type GLTFAccessorType = "SCALAR" | "VEC2" | "VEC3" | "VEC4" | "MAT4";
26
-
27
- const DEFAULT_GLTF_MATERIAL_NAME = "default-material";
28
- const RUNTIME_MATERIAL_KEY_MARKER = "#aura3d-runtime:";
29
- const DISPOSE_GLTF_ASSET = Symbol("disposeGLTFAsset");
30
- const MAX_RENDERABLE_SKIN_JOINTS = 96;
31
-
32
- interface GLTFBuffer {
33
- readonly uri?: string;
34
- readonly byteLength: number;
35
- }
36
-
37
- interface GLTFBufferView {
38
- readonly buffer: number;
39
- readonly byteOffset?: number;
40
- readonly byteLength: number;
41
- readonly byteStride?: number;
42
- readonly extensions?: {
43
- readonly EXT_meshopt_compression?: GLTFMeshoptCompressionExtension;
44
- readonly KHR_meshopt_compression?: GLTFMeshoptCompressionExtension;
45
- };
46
- }
47
-
48
- interface GLTFMeshoptCompressionExtension {
49
- readonly buffer: number;
50
- readonly byteOffset?: number;
51
- readonly byteLength: number;
52
- readonly byteStride: number;
53
- readonly count: number;
54
- readonly mode: "ATTRIBUTES" | "TRIANGLES" | "INDICES";
55
- readonly filter?: "NONE" | "OCTAHEDRAL" | "QUATERNION" | "EXPONENTIAL" | "COLOR";
56
- }
57
-
58
- interface GLTFAccessor {
59
- readonly bufferView?: number;
60
- readonly byteOffset?: number;
61
- readonly componentType: GLTFComponentType;
62
- readonly count: number;
63
- readonly type: GLTFAccessorType;
64
- readonly normalized?: boolean;
65
- readonly sparse?: GLTFSparseAccessor;
66
- }
67
-
68
- interface GLTFSparseAccessor {
69
- readonly count: number;
70
- readonly indices: {
71
- readonly bufferView: number;
72
- readonly byteOffset?: number;
73
- readonly componentType: GLTFSparseIndexComponentType;
74
- };
75
- readonly values: {
76
- readonly bufferView: number;
77
- readonly byteOffset?: number;
78
- };
79
- }
80
-
81
- interface GLTFPrimitive {
82
- readonly attributes: Readonly<Record<string, number>>;
83
- readonly indices?: number;
84
- readonly material?: number;
85
- readonly targets?: readonly Readonly<Record<string, number>>[];
86
- readonly mode?: number;
87
- readonly extensions?: {
88
- readonly KHR_draco_mesh_compression?: {
89
- readonly bufferView: number;
90
- readonly attributes: Readonly<Record<string, number>>;
91
- };
92
- readonly KHR_materials_variants?: {
93
- readonly mappings?: readonly {
94
- readonly material: number;
95
- readonly variants: readonly number[];
96
- }[];
97
- };
98
- };
99
- }
100
-
101
- interface GLTFMesh {
102
- readonly name?: string;
103
- readonly primitives: readonly GLTFPrimitive[];
104
- readonly weights?: readonly number[];
105
- }
106
-
107
- interface GLTFNode {
108
- readonly name?: string;
109
- readonly camera?: number;
110
- readonly mesh?: number;
111
- readonly skin?: number;
112
- readonly children?: readonly number[];
113
- readonly translation?: readonly [number, number, number];
114
- readonly scale?: readonly [number, number, number];
115
- readonly rotation?: readonly [number, number, number, number];
116
- readonly matrix?: readonly number[];
117
- readonly extras?: Readonly<Record<string, unknown>>;
118
- readonly extensions?: {
119
- readonly KHR_lights_punctual?: {
120
- readonly light: number;
121
- };
122
- readonly EXT_mesh_gpu_instancing?: {
123
- readonly attributes: Readonly<Record<string, number>>;
124
- };
125
- };
126
- }
127
-
128
- interface GLTFMaterial {
129
- readonly name?: string;
130
- readonly pbrMetallicRoughness?: {
131
- readonly baseColorFactor?: readonly [number, number, number, number];
132
- readonly baseColorTexture?: GLTFTextureInfo;
133
- readonly metallicFactor?: number;
134
- readonly roughnessFactor?: number;
135
- readonly metallicRoughnessTexture?: GLTFTextureInfo;
136
- };
137
- readonly normalTexture?: GLTFTextureInfo & { readonly scale?: number };
138
- readonly occlusionTexture?: GLTFTextureInfo & { readonly strength?: number };
139
- readonly emissiveTexture?: GLTFTextureInfo;
140
- readonly emissiveFactor?: readonly [number, number, number];
141
- readonly alphaMode?: "OPAQUE" | "MASK" | "BLEND";
142
- readonly alphaCutoff?: number;
143
- readonly doubleSided?: boolean;
144
- readonly extensions?: {
145
- readonly KHR_materials_unlit?: Record<string, never>;
146
- readonly KHR_materials_emissive_strength?: {
147
- readonly emissiveStrength?: number;
148
- };
149
- readonly KHR_materials_clearcoat?: {
150
- readonly clearcoatFactor?: number;
151
- readonly clearcoatTexture?: GLTFTextureInfo;
152
- readonly clearcoatRoughnessFactor?: number;
153
- readonly clearcoatRoughnessTexture?: GLTFTextureInfo;
154
- readonly clearcoatNormalTexture?: GLTFTextureInfo & { readonly scale?: number };
155
- };
156
- readonly KHR_materials_transmission?: {
157
- readonly transmissionFactor?: number;
158
- readonly transmissionTexture?: GLTFTextureInfo;
159
- };
160
- readonly KHR_materials_diffuse_transmission?: {
161
- readonly diffuseTransmissionFactor?: number;
162
- readonly diffuseTransmissionTexture?: GLTFTextureInfo;
163
- readonly diffuseTransmissionColorFactor?: readonly [number, number, number];
164
- readonly diffuseTransmissionColorTexture?: GLTFTextureInfo;
165
- };
166
- readonly KHR_materials_volume?: {
167
- readonly thicknessFactor?: number;
168
- readonly thicknessTexture?: GLTFTextureInfo;
169
- readonly attenuationDistance?: number;
170
- readonly attenuationColor?: readonly [number, number, number];
171
- };
172
- readonly KHR_materials_ior?: {
173
- readonly ior?: number;
174
- };
175
- readonly KHR_materials_specular?: {
176
- readonly specularFactor?: number;
177
- readonly specularTexture?: GLTFTextureInfo;
178
- readonly specularColorFactor?: readonly [number, number, number];
179
- readonly specularColorTexture?: GLTFTextureInfo;
180
- };
181
- readonly KHR_materials_sheen?: {
182
- readonly sheenColorFactor?: readonly [number, number, number];
183
- readonly sheenColorTexture?: GLTFTextureInfo;
184
- readonly sheenRoughnessFactor?: number;
185
- readonly sheenRoughnessTexture?: GLTFTextureInfo;
186
- };
187
- readonly KHR_materials_anisotropy?: {
188
- readonly anisotropyStrength?: number;
189
- readonly anisotropyRotation?: number;
190
- readonly anisotropyTexture?: GLTFTextureInfo;
191
- };
192
- readonly KHR_materials_iridescence?: {
193
- readonly iridescenceFactor?: number;
194
- readonly iridescenceTexture?: GLTFTextureInfo;
195
- readonly iridescenceIor?: number;
196
- readonly iridescenceThicknessMinimum?: number;
197
- readonly iridescenceThicknessMaximum?: number;
198
- readonly iridescenceThicknessTexture?: GLTFTextureInfo;
199
- };
200
- readonly KHR_materials_dispersion?: {
201
- readonly dispersion?: number;
202
- };
203
- readonly KHR_materials_pbrSpecularGlossiness?: {
204
- readonly diffuseFactor?: readonly [number, number, number, number];
205
- readonly diffuseTexture?: GLTFTextureInfo;
206
- readonly specularFactor?: readonly [number, number, number];
207
- readonly glossinessFactor?: number;
208
- readonly specularGlossinessTexture?: GLTFTextureInfo;
209
- };
210
- };
211
- }
212
-
213
- interface GLTFTextureInfo {
214
- readonly index: number;
215
- readonly texCoord?: number;
216
- readonly extensions?: {
217
- readonly KHR_texture_transform?: GLTFTextureTransform;
218
- };
219
- }
220
-
221
- interface GLTFTextureTransform {
222
- readonly offset?: readonly [number, number];
223
- readonly scale?: readonly [number, number];
224
- readonly rotation?: number;
225
- readonly texCoord?: number;
226
- }
227
-
228
- interface GLTFImage {
229
- readonly name?: string;
230
- readonly uri?: string;
231
- readonly mimeType?: string;
232
- readonly bufferView?: number;
233
- }
234
-
235
- interface GLTFTexture {
236
- readonly name?: string;
237
- readonly sampler?: number;
238
- readonly source?: number;
239
- readonly extensions?: {
240
- readonly EXT_texture_avif?: {
241
- readonly source: number;
242
- };
243
- readonly EXT_texture_webp?: {
244
- readonly source: number;
245
- };
246
- readonly KHR_texture_basisu?: {
247
- readonly source: number;
248
- };
249
- };
250
- }
251
-
252
- interface GLTFSampler {
253
- readonly name?: string;
254
- readonly magFilter?: number;
255
- readonly minFilter?: number;
256
- readonly wrapS?: number;
257
- readonly wrapT?: number;
258
- }
259
-
260
- interface GLTFScene {
261
- readonly name?: string;
262
- readonly nodes?: readonly number[];
263
- }
264
-
265
- interface GLTFSkin {
266
- readonly name?: string;
267
- readonly joints: readonly number[];
268
- readonly inverseBindMatrices?: number;
269
- readonly skeleton?: number;
270
- }
271
-
272
- interface GLTFAnimationSampler {
273
- readonly input: number;
274
- readonly output: number;
275
- readonly interpolation?: "LINEAR" | "STEP" | "CUBICSPLINE";
276
- }
277
-
278
- interface GLTFAnimationChannel {
279
- readonly sampler: number;
280
- readonly target: {
281
- readonly node?: number;
282
- readonly path: "translation" | "rotation" | "scale" | "weights" | "pointer";
283
- readonly extensions?: {
284
- readonly KHR_animation_pointer?: {
285
- readonly pointer?: string;
286
- };
287
- };
288
- };
289
- }
290
-
291
- interface GLTFAnimation {
292
- readonly name?: string;
293
- readonly samplers: readonly GLTFAnimationSampler[];
294
- readonly channels: readonly GLTFAnimationChannel[];
295
- }
296
-
297
- interface GLTFCamera {
298
- readonly name?: string;
299
- readonly type: "perspective" | "orthographic";
300
- readonly perspective?: {
301
- readonly aspectRatio?: number;
302
- readonly yfov: number;
303
- readonly znear: number;
304
- readonly zfar?: number;
305
- };
306
- readonly orthographic?: {
307
- readonly xmag: number;
308
- readonly ymag: number;
309
- readonly znear: number;
310
- readonly zfar: number;
311
- };
312
- }
313
-
314
- interface GLTFLight {
315
- readonly name?: string;
316
- readonly type: "directional" | "point" | "spot";
317
- readonly color?: readonly number[];
318
- readonly intensity?: number;
319
- readonly range?: number;
320
- readonly spot?: {
321
- readonly innerConeAngle?: number;
322
- readonly outerConeAngle?: number;
323
- };
324
- }
325
-
326
- interface GLTFJson {
327
- readonly asset?: { readonly version?: string };
328
- readonly extensionsUsed?: readonly string[];
329
- readonly extensionsRequired?: readonly string[];
330
- readonly buffers?: readonly GLTFBuffer[];
331
- readonly bufferViews?: readonly GLTFBufferView[];
332
- readonly accessors?: readonly GLTFAccessor[];
333
- readonly images?: readonly GLTFImage[];
334
- readonly textures?: readonly GLTFTexture[];
335
- readonly samplers?: readonly GLTFSampler[];
336
- readonly meshes?: readonly GLTFMesh[];
337
- readonly materials?: readonly GLTFMaterial[];
338
- readonly cameras?: readonly GLTFCamera[];
339
- readonly nodes?: readonly GLTFNode[];
340
- readonly scenes?: readonly GLTFScene[];
341
- readonly scene?: number;
342
- readonly skins?: readonly GLTFSkin[];
343
- readonly animations?: readonly GLTFAnimation[];
344
- readonly extensions?: {
345
- readonly KHR_lights_punctual?: {
346
- readonly lights?: readonly GLTFLight[];
347
- };
348
- readonly KHR_materials_variants?: {
349
- readonly variants?: readonly {
350
- readonly name?: string;
351
- }[];
352
- };
353
- };
354
- }
355
-
356
- export interface GLTFMeshAsset {
357
- readonly name: string;
358
- readonly sourceMeshIndex: number;
359
- readonly primitiveIndex: number;
360
- readonly topology: "triangles" | "lines" | "points";
361
- readonly geometry: GLTFGeometryAsset;
362
- readonly positions: readonly (readonly [number, number, number])[];
363
- readonly normals: readonly (readonly [number, number, number])[];
364
- readonly texcoords: readonly (readonly [number, number])[];
365
- readonly texcoordSets: readonly (readonly (readonly [number, number])[])[];
366
- readonly tangents: readonly (readonly [number, number, number, number])[];
367
- readonly colors: readonly (readonly [number, number, number, number])[];
368
- readonly joints: readonly (readonly [number, number, number, number])[];
369
- readonly weights: readonly (readonly [number, number, number, number])[];
370
- readonly morphTargets: readonly GLTFMorphTargetAsset[];
371
- readonly morphWeights: readonly number[];
372
- readonly indices?: readonly number[];
373
- readonly material: string;
374
- readonly materialIndex?: number;
375
- readonly materialVariants: readonly GLTFMaterialVariantMappingAsset[];
376
- readonly skinIndex?: number;
377
- }
378
-
379
- export interface GLTFRuntimeMaterialContract {
380
- readonly skinned?: boolean;
381
- readonly instanced?: boolean;
382
- }
383
-
384
- export interface GLTFRuntimeMaterialKey {
385
- readonly material: string;
386
- readonly contract: GLTFRuntimeMaterialContract;
387
- }
388
-
389
- export interface GLTFMorphTargetAsset {
390
- /** Morph target / blendshape name (from `mesh.extras.targetNames`), when authored. */
391
- readonly name?: string;
392
- readonly positions: readonly (readonly [number, number, number])[];
393
- readonly normals: readonly (readonly [number, number, number])[];
394
- readonly tangents: readonly (readonly [number, number, number])[];
395
- }
396
-
397
- export interface GLTFGeometryAsset {
398
- readonly vertexCount: number;
399
- readonly indexCount: number;
400
- readonly bounds: {
401
- readonly min: readonly [number, number, number];
402
- readonly max: readonly [number, number, number];
403
- };
404
- }
405
-
406
- export interface GLTFLoaderDiagnostics {
407
- readonly schemaVersion: "gltf-loader-diagnostics";
408
- readonly features: readonly string[];
409
- readonly extensionsUsed: readonly string[];
410
- readonly extensionsRequired: readonly string[];
411
- readonly unsupportedExtensions: readonly string[];
412
- readonly extensionSupport?: GLTFExtensionSupportEvaluation;
413
- readonly unsupportedFeatures: readonly string[];
414
- readonly meshCount: number;
415
- readonly primitiveCount: number;
416
- readonly vertexCount: number;
417
- readonly indexCount: number;
418
- readonly materialCount: number;
419
- readonly textureCount: number;
420
- readonly imageCount: number;
421
- readonly animationCount: number;
422
- readonly skinCount: number;
423
- readonly morphTargetCount: number;
424
- readonly materialFeatures: readonly string[];
425
- readonly textureSlots: readonly string[];
426
- readonly compression: {
427
- readonly draco: boolean;
428
- readonly meshopt: boolean;
429
- readonly ktx2Basis: boolean;
430
- };
431
- readonly loadProfile?: GLTFLoaderLoadProfileDiagnostics;
432
- }
433
-
434
- export interface GLTFLoaderLoadProfileDiagnostics {
435
- readonly schemaVersion: "gltf-loader-load-profile";
436
- readonly documentBytes: number;
437
- readonly binaryChunkBytes: number;
438
- readonly bufferBytes: number;
439
- readonly imageBytes: number;
440
- readonly embeddedImageBytes: number;
441
- readonly externalImageCount: number;
442
- readonly bufferViewImageCount: number;
443
- readonly accessorReadCount: number;
444
- readonly uniqueAccessorReadCount: number;
445
- readonly accessorCacheHitCount: number;
446
- readonly accessorRowsRead: number;
447
- readonly accessorValuesRead: number;
448
- readonly accessorReadMs: number;
449
- readonly dataViewCacheEntries: number;
450
- readonly largestAccessors: readonly GLTFAccessorLoadProfileEntry[];
451
- }
452
-
453
- export interface GLTFAccessorLoadProfileEntry {
454
- readonly accessorIndex: number;
455
- readonly count: number;
456
- readonly type: "SCALAR" | "VEC2" | "VEC3" | "VEC4" | "MAT4";
457
- readonly componentType: 5120 | 5121 | 5122 | 5123 | 5125 | 5126;
458
- readonly componentCount: number;
459
- readonly byteLength: number;
460
- readonly bufferView?: number;
461
- }
462
-
463
- export interface GLTFAsset {
464
- readonly url: string;
465
- readonly disposed: boolean;
466
- readonly loaderDiagnostics: GLTFLoaderDiagnostics;
467
- readonly images: readonly GLTFImageAsset[];
468
- readonly textures: readonly GLTFTextureAsset[];
469
- readonly materials: readonly GLTFMaterialAsset[];
470
- readonly materialVariants: readonly GLTFMaterialVariantAsset[];
471
- readonly scenes: readonly GLTFSceneAsset[];
472
- readonly defaultScene: number;
473
- readonly meshes: readonly GLTFMeshAsset[];
474
- readonly cameras: readonly GLTFCameraAsset[];
475
- readonly lights: readonly GLTFLightAsset[];
476
- readonly skins: readonly GLTFSkinAsset[];
477
- readonly animations: readonly AnimationClip[];
478
- createScene(options?: GLTFSceneCreateOptions): Scene;
479
- toJSON(): SerializedGLTFAsset;
480
- }
481
-
482
- interface DisposableGLTFAsset extends GLTFAsset {
483
- [DISPOSE_GLTF_ASSET](): void;
484
- }
485
-
486
- export interface GLTFSceneCreateOptions {
487
- readonly materialVariant?: string;
488
- readonly sceneIndex?: number;
489
- readonly sceneName?: string;
490
- }
491
-
492
- export interface GLTFImageAsset {
493
- readonly name: string;
494
- readonly uri?: string;
495
- readonly mimeType?: string;
496
- readonly data?: ArrayBuffer;
497
- }
498
-
499
- export interface GLTFTextureAsset {
500
- readonly name: string;
501
- readonly source: number;
502
- readonly sampler?: number;
503
- readonly samplerDescriptor?: GLTFSamplerAsset;
504
- }
505
-
506
- export interface GLTFSceneAsset {
507
- readonly name: string;
508
- readonly nodeIndices: readonly number[];
509
- }
510
-
511
- export interface GLTFSamplerAsset {
512
- readonly minFilter: "nearest" | "linear" | "nearest-mipmap-nearest" | "linear-mipmap-nearest" | "nearest-mipmap-linear" | "linear-mipmap-linear";
513
- readonly magFilter: "nearest" | "linear";
514
- readonly addressU: "clamp-to-edge" | "repeat" | "mirror-repeat";
515
- readonly addressV: "clamp-to-edge" | "repeat" | "mirror-repeat";
516
- }
517
-
518
- export interface GLTFMaterialAsset {
519
- readonly name: string;
520
- readonly unlit: boolean;
521
- readonly baseColorFactor: readonly [number, number, number, number];
522
- readonly baseColorTexture?: GLTFResolvedTextureInfo;
523
- readonly metallicFactor: number;
524
- readonly roughnessFactor: number;
525
- readonly metallicRoughnessTexture?: GLTFResolvedTextureInfo;
526
- readonly normalTexture?: GLTFResolvedTextureInfo & { readonly scale: number };
527
- readonly occlusionTexture?: GLTFResolvedTextureInfo & { readonly strength: number };
528
- readonly emissiveTexture?: GLTFResolvedTextureInfo;
529
- readonly emissiveFactor: readonly [number, number, number];
530
- readonly emissiveStrength: number;
531
- readonly clearcoat?: GLTFClearcoatMaterialExtension;
532
- readonly transmission?: GLTFTransmissionMaterialExtension;
533
- readonly diffuseTransmission?: GLTFDiffuseTransmissionMaterialExtension;
534
- readonly volume?: GLTFVolumeMaterialExtension;
535
- readonly ior?: number;
536
- readonly specular?: GLTFSpecularMaterialExtension;
537
- readonly pbrSpecularGlossiness?: GLTFPBRSpecularGlossinessMaterialExtension;
538
- readonly sheen?: GLTFSheenMaterialExtension;
539
- readonly anisotropy?: GLTFAnisotropyMaterialExtension;
540
- readonly iridescence?: GLTFIridescenceMaterialExtension;
541
- readonly dispersion?: number;
542
- readonly alphaMode: "OPAQUE" | "MASK" | "BLEND";
543
- readonly alphaCutoff: number;
544
- readonly doubleSided: boolean;
545
- }
546
-
547
- export interface GLTFMaterialVariantAsset {
548
- readonly name: string;
549
- }
550
-
551
- export interface GLTFMaterialVariantMappingAsset {
552
- readonly variantIndex: number;
553
- readonly variant: string;
554
- readonly materialIndex: number;
555
- readonly material: string;
556
- }
557
-
558
- export interface GLTFClearcoatMaterialExtension {
559
- readonly factor: number;
560
- readonly texture?: GLTFResolvedTextureInfo;
561
- readonly roughnessFactor: number;
562
- readonly roughnessTexture?: GLTFResolvedTextureInfo;
563
- readonly normalTexture?: GLTFResolvedTextureInfo & { readonly scale: number };
564
- }
565
-
566
- export interface GLTFTransmissionMaterialExtension {
567
- readonly factor: number;
568
- readonly texture?: GLTFResolvedTextureInfo;
569
- }
570
-
571
- export interface GLTFDiffuseTransmissionMaterialExtension {
572
- readonly factor: number;
573
- readonly texture?: GLTFResolvedTextureInfo;
574
- readonly colorFactor: readonly [number, number, number];
575
- readonly colorTexture?: GLTFResolvedTextureInfo;
576
- }
577
-
578
- export interface GLTFVolumeMaterialExtension {
579
- readonly thicknessFactor: number;
580
- readonly thicknessTexture?: GLTFResolvedTextureInfo;
581
- readonly attenuationDistance: number;
582
- readonly attenuationColor: readonly [number, number, number];
583
- }
584
-
585
- export interface GLTFSpecularMaterialExtension {
586
- readonly factor: number;
587
- readonly texture?: GLTFResolvedTextureInfo;
588
- readonly colorFactor: readonly [number, number, number];
589
- readonly colorTexture?: GLTFResolvedTextureInfo;
590
- }
591
-
592
- export interface GLTFPBRSpecularGlossinessMaterialExtension {
593
- readonly diffuseFactor: readonly [number, number, number, number];
594
- readonly diffuseTexture?: GLTFResolvedTextureInfo;
595
- readonly specularFactor: readonly [number, number, number];
596
- readonly glossinessFactor: number;
597
- readonly specularGlossinessTexture?: GLTFResolvedTextureInfo;
598
- }
599
-
600
- function createSpecularFromSpecularGlossiness(
601
- extension: GLTFPBRSpecularGlossinessMaterialExtension | undefined
602
- ): GLTFSpecularMaterialExtension | undefined {
603
- if (!extension) return undefined;
604
- return {
605
- factor: 1,
606
- colorFactor: extension.specularFactor,
607
- colorTexture: extension.specularGlossinessTexture
608
- };
609
- }
610
-
611
- export interface GLTFSheenMaterialExtension {
612
- readonly colorFactor: readonly [number, number, number];
613
- readonly colorTexture?: GLTFResolvedTextureInfo;
614
- readonly roughnessFactor: number;
615
- readonly roughnessTexture?: GLTFResolvedTextureInfo;
616
- }
617
-
618
- export interface GLTFAnisotropyMaterialExtension {
619
- readonly strength: number;
620
- readonly rotation: number;
621
- readonly texture?: GLTFResolvedTextureInfo;
622
- }
623
-
624
- export interface GLTFIridescenceMaterialExtension {
625
- readonly factor: number;
626
- readonly texture?: GLTFResolvedTextureInfo;
627
- readonly ior: number;
628
- readonly thicknessMinimum: number;
629
- readonly thicknessMaximum: number;
630
- readonly thicknessTexture?: GLTFResolvedTextureInfo;
631
- }
632
-
633
- export interface GLTFLightAsset {
634
- readonly name: string;
635
- readonly type: "directional" | "point" | "spot";
636
- readonly color: readonly [number, number, number];
637
- readonly intensity: number;
638
- readonly range?: number;
639
- readonly spot?: {
640
- readonly innerConeAngle: number;
641
- readonly outerConeAngle: number;
642
- };
643
- }
644
-
645
- export interface GLTFCameraAsset {
646
- readonly name: string;
647
- readonly type: "perspective" | "orthographic";
648
- readonly perspective?: {
649
- readonly aspectRatio: number;
650
- readonly yfov: number;
651
- readonly znear: number;
652
- readonly zfar: number;
653
- };
654
- readonly orthographic?: {
655
- readonly xmag: number;
656
- readonly ymag: number;
657
- readonly znear: number;
658
- readonly zfar: number;
659
- };
660
- }
661
-
662
- export interface GLTFResolvedTextureInfo {
663
- readonly texture: number;
664
- readonly image: number;
665
- readonly texCoord: number;
666
- readonly transform?: GLTFResolvedTextureTransform;
667
- }
668
-
669
- export interface GLTFResolvedTextureTransform {
670
- readonly offset: readonly [number, number];
671
- readonly scale: readonly [number, number];
672
- readonly rotation: number;
673
- }
674
-
675
- export interface GLTFSkinAsset {
676
- readonly name: string;
677
- readonly joints: readonly number[];
678
- readonly jointNames: readonly string[];
679
- readonly inverseBindMatrices: readonly Mat4[];
680
- readonly skeletonRoot?: number;
681
- readonly skeleton: Skeleton;
682
- readonly jointBindMatrices: readonly Mat4[];
683
- readonly skinningPalette?: SkinningPalette;
684
- }
685
-
686
- export interface SerializedGLTFAsset {
687
- readonly url: string;
688
- readonly loaderDiagnostics: GLTFLoaderDiagnostics;
689
- readonly images: readonly {
690
- readonly name: string;
691
- readonly uri?: string;
692
- readonly mimeType?: string;
693
- readonly byteLength?: number;
694
- }[];
695
- readonly textures: readonly GLTFTextureAsset[];
696
- readonly materials: readonly GLTFMaterialAsset[];
697
- readonly materialVariants: readonly GLTFMaterialVariantAsset[];
698
- readonly scenes: readonly GLTFSceneAsset[];
699
- readonly defaultScene: number;
700
- readonly meshes: readonly GLTFMeshAsset[];
701
- readonly cameras: readonly GLTFCameraAsset[];
702
- readonly lights: readonly GLTFLightAsset[];
703
- readonly skins: readonly {
704
- readonly name: string;
705
- readonly joints: readonly number[];
706
- readonly jointNames: readonly string[];
707
- readonly inverseBindMatrices: readonly Mat4[];
708
- readonly skeletonRoot?: number;
709
- readonly bones: readonly {
710
- readonly name: string;
711
- readonly parentIndex: number;
712
- readonly translation: Vec3;
713
- readonly rotation: Quat;
714
- readonly scale: Vec3;
715
- readonly inverseBindMatrix: Mat4;
716
- }[];
717
- }[];
718
- readonly animations: readonly SerializedAnimationClip[];
719
- }
720
-
721
- export interface GLTFMeshoptDecodeDescriptor {
722
- readonly bufferViewIndex: number;
723
- readonly byteStride: number;
724
- readonly count: number;
725
- readonly mode: "ATTRIBUTES" | "TRIANGLES" | "INDICES";
726
- readonly filter: "NONE" | "OCTAHEDRAL" | "QUATERNION" | "EXPONENTIAL" | "COLOR";
727
- }
728
-
729
- export type GLTFMeshoptDecoder = (
730
- source: Uint8Array,
731
- descriptor: GLTFMeshoptDecodeDescriptor
732
- ) => ArrayBuffer | Uint8Array | Promise<ArrayBuffer | Uint8Array>;
733
-
734
- export interface GLTFDracoDecodeDescriptor {
735
- readonly meshIndex: number;
736
- readonly primitiveIndex: number;
737
- readonly bufferViewIndex: number;
738
- readonly attributes: Readonly<Record<string, number>>;
739
- }
740
-
741
- export interface GLTFDracoDecodedPrimitive {
742
- readonly attributes: Readonly<Record<string, readonly (readonly number[])[]>>;
743
- readonly indices?: readonly number[];
744
- }
745
-
746
- export type GLTFDracoDecoder = (
747
- source: Uint8Array,
748
- descriptor: GLTFDracoDecodeDescriptor
749
- ) => GLTFDracoDecodedPrimitive | Promise<GLTFDracoDecodedPrimitive>;
750
-
751
- export interface GLTFLoaderOptions {
752
- readonly meshoptDecoder?: GLTFMeshoptDecoder;
753
- readonly dracoDecoder?: GLTFDracoDecoder;
754
- }
755
-
756
- interface MutableGLTFLoaderLoadProfileDiagnostics {
757
- accessorReadCount: number;
758
- uniqueAccessorReadCount: number;
759
- accessorCacheHitCount: number;
760
- accessorRowsRead: number;
761
- accessorValuesRead: number;
762
- accessorReadMs: number;
763
- dataViewCacheEntries: number;
764
- largestAccessors: GLTFAccessorLoadProfileEntry[];
765
- }
766
-
767
- interface GLTFAccessorReadCache {
768
- readonly rowsByAccessor: Map<number, number[][]>;
769
- readonly dataViews: Map<ArrayBuffer, DataView>;
770
- readonly profile: MutableGLTFLoaderLoadProfileDiagnostics;
771
- }
772
-
773
- export class GLTFLoader implements AssetLoader<GLTFAsset> {
774
- readonly type = "gltf";
775
-
776
- constructor(private readonly options: GLTFLoaderOptions = {}) {}
777
-
778
- canLoad(request: AssetLoadRequest): boolean {
779
- return /\.(?:gltf|glb)(?:\?.*)?$/i.test(request.url) || request.url.startsWith("data:model/gltf");
780
- }
781
-
782
- async load(request: AssetLoadRequest, context: LoadContext): Promise<GLTFAsset> {
783
- context.throwIfAborted(request.url);
784
- const document = await loadDocument(request);
785
- let json = document.json;
786
- if (json.asset?.version && !json.asset.version.startsWith("2.")) {
787
- throw new Error(`Unsupported glTF version: ${json.asset.version}`);
788
- }
789
- const extensionSupport = evaluateGLTFExtensionSupport(json.extensionsUsed, json.extensionsRequired);
790
- const unsupportedRequiredExtensions = extensionSupport.unsupportedRequired;
791
- if (unsupportedRequiredExtensions.length > 0) {
792
- throw new Error(`Unsupported required glTF extensions: ${unsupportedRequiredExtensions.join(", ")}`);
793
- }
794
-
795
- const rawBuffers = await Promise.all((json.buffers ?? []).map((buffer, index) => loadBuffer(buffer, index, document, request)));
796
- const prepared = await prepareBufferViews(json, rawBuffers, this.options.meshoptDecoder);
797
- const buffers = prepared.buffers;
798
- json = prepared.json;
799
- const accessorCache = createGLTFAccessorReadCache();
800
- const meshQuantizationEnabled = usesGLTFExtension(json, "KHR_mesh_quantization");
801
- const images = createImageAssets(json, buffers);
802
- const textures = createTextureAssets(json);
803
- const materials = createMaterialAssets(json, textures);
804
- const materialVariants = createMaterialVariantAssets(json);
805
- const scenes = createSceneAssets(json);
806
- const defaultScene = resolveDefaultSceneIndex(json);
807
- const cameras = createCameraAssets(json);
808
- const lights = createLightAssets(json);
809
- const skins = createSkinAssets(json, buffers, accessorCache);
810
- const nodeInstanceTransforms = createNodeInstanceTransforms(json, buffers, accessorCache);
811
- const meshes = await Promise.all((json.meshes ?? []).flatMap((mesh, meshIndex) => {
812
- validateMeshDescriptor(mesh, meshIndex);
813
- return mesh.primitives.map(async (primitive, primitiveIndex) => {
814
- validatePrimitiveDescriptor(primitive, meshIndex, primitiveIndex);
815
- validatePrimitiveAttributeAccessors(json, primitive, meshQuantizationEnabled, meshIndex, primitiveIndex);
816
- const positionAccessorIndex = primitive.attributes.POSITION;
817
- if (positionAccessorIndex === undefined) {
818
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} is missing POSITION`);
819
- }
820
-
821
- const dracoPrimitive = await decodeDracoPrimitive(json, buffers, primitive, meshIndex, primitiveIndex, this.options.dracoDecoder);
822
- const positions = dracoPrimitive?.attributes.POSITION ?? readAccessor(json, buffers, positionAccessorIndex, accessorCache);
823
- const normals = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "NORMAL", accessorCache);
824
- const texcoordSets = readTexcoordSets(json, buffers, primitive, positions.length, meshIndex, primitiveIndex, dracoPrimitive, accessorCache);
825
- const texcoords = texcoordSets[0] ?? [];
826
- const tangents = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "TANGENT", accessorCache);
827
- const colors = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "COLOR_0", accessorCache);
828
- const joints = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "JOINTS_0", accessorCache);
829
- const weights = readPrimitiveAttribute(json, buffers, primitive, dracoPrimitive, "WEIGHTS_0", accessorCache);
830
- const indices = dracoPrimitive
831
- ? dracoPrimitive.indices?.map((index) => [index])
832
- : primitive.indices === undefined ? undefined : readAccessor(json, buffers, primitive.indices, accessorCache);
833
- validateAttributeCount("NORMAL", normals, positions.length, meshIndex, primitiveIndex);
834
- validateAttributeCount("TANGENT", tangents, positions.length, meshIndex, primitiveIndex);
835
- validateAttributeCount("COLOR_0", colors, positions.length, meshIndex, primitiveIndex);
836
- validateAttributeCount("JOINTS_0", joints, positions.length, meshIndex, primitiveIndex);
837
- validateAttributeCount("WEIGHTS_0", weights, positions.length, meshIndex, primitiveIndex);
838
- const primitiveMaterialIndex = resolvePrimitiveMaterialIndex(primitive, materials, meshIndex, primitiveIndex);
839
- const primitiveMaterial = primitiveMaterialIndex === undefined ? undefined : materials[primitiveMaterialIndex];
840
- const primitiveMaterialVariants = resolvePrimitiveMaterialVariants(primitive, materialVariants, materials, meshIndex, primitiveIndex);
841
- validateMaterialTexCoordSets(primitiveMaterial, texcoordSets, meshIndex, primitiveIndex);
842
- const typedPositions = positions.map((position) => [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0] as const);
843
- const typedNormals = normals.map((normal) => [normal[0] ?? 0, normal[1] ?? 0, normal[2] ?? 0] as const);
844
- const typedTangents = tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0, tangent[3] ?? 1] as const);
845
- const typedColors = colors.map((color) => [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1, color[3] ?? 1] as const);
846
- const typedJoints = joints.map((joint) => [joint[0] ?? 0, joint[1] ?? 0, joint[2] ?? 0, joint[3] ?? 0] as const);
847
- const weightNormalization: SkinWeightNormalizationStats = { degenerate: 0, rescaled: 0 };
848
- const typedWeights = weights.map((weight) => normalizeSkinWeights([
849
- weight[0] ?? 0,
850
- weight[1] ?? 0,
851
- weight[2] ?? 0,
852
- weight[3] ?? 0
853
- ], weightNormalization));
854
- reportSkinWeightNormalization(meshIndex, primitiveIndex, weightNormalization);
855
- const morphTargets = readMorphTargets(json, buffers, primitive, positions.length, meshIndex, primitiveIndex, accessorCache, readMorphTargetNames(mesh));
856
- const resolvedPrimitive = resolvePrimitiveMode(primitive, indices?.map((index) => index[0] ?? 0), positions.length, meshIndex, primitiveIndex);
857
- const geometry = createGeometryAsset(typedPositions, resolvedPrimitive.indices);
858
- return {
859
- name: meshPrimitiveName(mesh, meshIndex, primitiveIndex),
860
- sourceMeshIndex: meshIndex,
861
- primitiveIndex,
862
- topology: resolvedPrimitive.topology,
863
- geometry,
864
- positions: typedPositions,
865
- normals: typedNormals,
866
- texcoords,
867
- texcoordSets,
868
- tangents: typedTangents,
869
- colors: typedColors,
870
- joints: typedJoints,
871
- weights: typedWeights,
872
- morphTargets,
873
- morphWeights: resolveMorphWeights(mesh, meshIndex, primitiveIndex, morphTargets.length),
874
- indices: resolvedPrimitive.indices,
875
- material: materialName(primitiveMaterial, primitiveMaterialIndex),
876
- materialIndex: primitiveMaterialIndex,
877
- materialVariants: primitiveMaterialVariants,
878
- skinIndex: findSkinForMesh(json, meshIndex)
879
- };
880
- });
881
- }));
882
- const animations = createAnimationClips(json, buffers, accessorCache);
883
- const loadProfile = createGLTFLoaderLoadProfileDiagnostics(document, buffers, images, accessorCache);
884
- const loaderDiagnostics = createGLTFLoaderDiagnostics(json, {
885
- images,
886
- textures,
887
- materials,
888
- meshes,
889
- animations,
890
- skins
891
- }, loadProfile);
892
-
893
- let disposed = false;
894
- const assertAlive = (): void => {
895
- if (disposed) {
896
- throw new Error(`glTF asset ${request.url} has been disposed`);
897
- }
898
- };
899
- const asset: DisposableGLTFAsset = {
900
- url: request.url,
901
- get disposed() {
902
- return disposed;
903
- },
904
- loaderDiagnostics,
905
- images,
906
- textures,
907
- materials,
908
- materialVariants,
909
- scenes,
910
- defaultScene,
911
- meshes,
912
- cameras,
913
- lights,
914
- skins,
915
- animations,
916
- createScene: (options) => {
917
- assertAlive();
918
- return createScene(json, meshes, cameras, lights, skins, nodeInstanceTransforms, materialVariants, options);
919
- },
920
- toJSON: () => {
921
- assertAlive();
922
- return serializeGLTFAsset(request.url, loaderDiagnostics, images, textures, materials, materialVariants, scenes, defaultScene, meshes, cameras, lights, skins, animations);
923
- },
924
- [DISPOSE_GLTF_ASSET]: () => {
925
- disposed = true;
926
- }
927
- };
928
- return asset;
929
- }
930
-
931
- dispose(asset: GLTFAsset): void {
932
- (asset as DisposableGLTFAsset)[DISPOSE_GLTF_ASSET]?.();
933
- }
934
- }
935
-
936
- function createGLTFAccessorReadCache(): GLTFAccessorReadCache {
937
- return {
938
- rowsByAccessor: new Map(),
939
- dataViews: new Map(),
940
- profile: {
941
- accessorReadCount: 0,
942
- uniqueAccessorReadCount: 0,
943
- accessorCacheHitCount: 0,
944
- accessorRowsRead: 0,
945
- accessorValuesRead: 0,
946
- accessorReadMs: 0,
947
- dataViewCacheEntries: 0,
948
- largestAccessors: []
949
- }
950
- };
951
- }
952
-
953
- function createGLTFLoaderLoadProfileDiagnostics(
954
- document: GLTFDocument,
955
- buffers: readonly ArrayBuffer[],
956
- images: readonly GLTFImageAsset[],
957
- accessorCache: GLTFAccessorReadCache
958
- ): GLTFLoaderLoadProfileDiagnostics {
959
- const profile = accessorCache.profile;
960
- const embeddedImageBytes = images.reduce((sum, image) => sum + (image.data?.byteLength ?? 0), 0);
961
- return {
962
- schemaVersion: "gltf-loader-load-profile",
963
- documentBytes: document.byteLength,
964
- binaryChunkBytes: document.binaryChunk?.byteLength ?? 0,
965
- bufferBytes: buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0),
966
- imageBytes: embeddedImageBytes,
967
- embeddedImageBytes,
968
- externalImageCount: images.filter((image) => Boolean(image.uri) && !image.uri!.startsWith("data:")).length,
969
- bufferViewImageCount: images.filter((image) => image.data !== undefined).length,
970
- accessorReadCount: profile.accessorReadCount,
971
- uniqueAccessorReadCount: profile.uniqueAccessorReadCount,
972
- accessorCacheHitCount: profile.accessorCacheHitCount,
973
- accessorRowsRead: profile.accessorRowsRead,
974
- accessorValuesRead: profile.accessorValuesRead,
975
- accessorReadMs: roundDiagnosticMs(profile.accessorReadMs),
976
- dataViewCacheEntries: profile.dataViewCacheEntries,
977
- largestAccessors: [...profile.largestAccessors]
978
- };
979
- }
980
-
981
- function roundDiagnosticMs(value: number): number {
982
- return Math.round(value * 1000) / 1000;
983
- }
984
-
985
- function nowMs(): number {
986
- const performance = globalThis.performance;
987
- return performance && typeof performance.now === "function" ? performance.now() : Date.now();
988
- }
989
-
990
- function createGeometryAsset(
991
- positions: readonly (readonly [number, number, number])[],
992
- indices?: readonly number[]
993
- ): GLTFGeometryAsset {
994
- const min: [number, number, number] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
995
- const max: [number, number, number] = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
996
- for (const position of positions) {
997
- min[0] = Math.min(min[0], position[0]);
998
- min[1] = Math.min(min[1], position[1]);
999
- min[2] = Math.min(min[2], position[2]);
1000
- max[0] = Math.max(max[0], position[0]);
1001
- max[1] = Math.max(max[1], position[1]);
1002
- max[2] = Math.max(max[2], position[2]);
1003
- }
1004
- return { vertexCount: positions.length, indexCount: indices?.length ?? 0, bounds: { min, max } };
1005
- }
1006
-
1007
- function createGLTFLoaderDiagnostics(
1008
- json: GLTFJson,
1009
- asset: {
1010
- readonly images: readonly GLTFImageAsset[];
1011
- readonly textures: readonly GLTFTextureAsset[];
1012
- readonly materials: readonly GLTFMaterialAsset[];
1013
- readonly meshes: readonly GLTFMeshAsset[];
1014
- readonly animations: readonly AnimationClip[];
1015
- readonly skins: readonly GLTFSkinAsset[];
1016
- },
1017
- loadProfile?: GLTFLoaderLoadProfileDiagnostics
1018
- ): GLTFLoaderDiagnostics {
1019
- const extensionsUsed = [...new Set([...(json.extensionsUsed ?? []), ...(json.extensionsRequired ?? [])])].sort();
1020
- const extensionSupport = evaluateGLTFExtensionSupport(json.extensionsUsed, json.extensionsRequired);
1021
- const materialFeatures = [...new Set(asset.materials.flatMap(materialFeatureNames))].sort();
1022
- const textureSlots = [...new Set(asset.materials.flatMap(materialTextureSlotNames))].sort();
1023
- const unsupportedFeatures = collectUnsupportedFeatureDiagnostics(json);
1024
- const compression = {
1025
- draco: usesGLTFExtension(json, "KHR_draco_mesh_compression"),
1026
- meshopt: usesGLTFMeshoptExtension(json),
1027
- ktx2Basis: usesGLTFExtension(json, "KHR_texture_basisu") || asset.images.some((image) => /\.ktx2(?:[?#]|$)/i.test(image.uri ?? "") || image.mimeType === "image/ktx2")
1028
- };
1029
- const features = new Set<string>([
1030
- "gltf",
1031
- ...extensionsUsed.map((extension) => `extension:${extension}`),
1032
- ...materialFeatures.map((feature) => `material:${feature}`),
1033
- ...textureSlots.map((slot) => `texture-slot:${slot}`)
1034
- ]);
1035
- if (asset.meshes.length > 0) features.add("mesh");
1036
- if (asset.meshes.some((mesh) => mesh.geometry.indexCount > 0)) features.add("indexed-geometry");
1037
- if (asset.meshes.some((mesh) => mesh.normals.length > 0)) features.add("normals");
1038
- if (asset.meshes.some((mesh) => mesh.tangents.length > 0)) features.add("tangents");
1039
- if (asset.meshes.some((mesh) => mesh.colors.length > 0)) features.add("vertex-colors");
1040
- if (asset.meshes.some((mesh) => mesh.texcoordSets.some((set) => set.length > 0))) features.add("texcoords");
1041
- if (asset.meshes.some((mesh) => mesh.texcoordSets.length > 1)) features.add("multi-uv");
1042
- if (asset.meshes.some((mesh) => mesh.morphTargets.length > 0)) features.add("morph-targets");
1043
- if (asset.meshes.some((mesh) => mesh.skinIndex !== undefined || mesh.joints.length > 0 || mesh.weights.length > 0)) features.add("skinning");
1044
- if (asset.skins.some((skin) => skin.joints.length > MAX_RENDERABLE_SKIN_JOINTS)) features.add("skinning-palette-limit-fallback");
1045
- if ((json.skins ?? []).some((skin) => skin.inverseBindMatrices === undefined)) features.add("skinning-default-inverse-bind-matrices");
1046
- for (const unsupportedFeature of unsupportedFeatures) features.add(`unsupported:${unsupportedFeature}`);
1047
- if (asset.animations.length > 0) features.add("animations");
1048
- if (asset.skins.length > 0) features.add("skins");
1049
- if (asset.textures.length > 0) features.add("textures");
1050
- if (compression.draco) features.add("compression:draco");
1051
- if (compression.meshopt) features.add("compression:meshopt");
1052
- if (compression.ktx2Basis) features.add("compression:ktx2-basis");
1053
-
1054
- return {
1055
- schemaVersion: "gltf-loader-diagnostics",
1056
- features: [...features].sort(),
1057
- extensionsUsed,
1058
- extensionsRequired: [...(json.extensionsRequired ?? [])].sort(),
1059
- unsupportedExtensions: extensionSupport.notAcceptedUsed,
1060
- extensionSupport,
1061
- unsupportedFeatures,
1062
- meshCount: asset.meshes.length,
1063
- primitiveCount: asset.meshes.length,
1064
- vertexCount: asset.meshes.reduce((sum, mesh) => sum + mesh.geometry.vertexCount, 0),
1065
- indexCount: asset.meshes.reduce((sum, mesh) => sum + mesh.geometry.indexCount, 0),
1066
- materialCount: asset.materials.length,
1067
- textureCount: asset.textures.length,
1068
- imageCount: asset.images.length,
1069
- animationCount: asset.animations.length,
1070
- skinCount: asset.skins.length,
1071
- morphTargetCount: asset.meshes.reduce((sum, mesh) => sum + mesh.morphTargets.length, 0),
1072
- materialFeatures,
1073
- textureSlots,
1074
- compression,
1075
- ...(loadProfile ? { loadProfile } : {})
1076
- };
1077
- }
1078
-
1079
- function collectUnsupportedFeatureDiagnostics(json: GLTFJson): readonly string[] {
1080
- const unsupported = new Set<string>();
1081
- for (const mesh of json.meshes ?? []) {
1082
- for (const primitive of mesh.primitives) {
1083
- if (primitive.attributes.JOINTS_1 !== undefined || primitive.attributes.WEIGHTS_1 !== undefined) {
1084
- unsupported.add("skinning-extra-influences:JOINTS_1/WEIGHTS_1");
1085
- }
1086
- }
1087
- }
1088
- return [...unsupported].sort();
1089
- }
1090
-
1091
- function materialFeatureNames(material: GLTFMaterialAsset): readonly string[] {
1092
- return [
1093
- material.unlit ? "unlit" : undefined,
1094
- material.metallicFactor > 0 ? "metallic" : undefined,
1095
- material.roughnessFactor < 1 ? "roughness" : undefined,
1096
- material.emissiveFactor.some((value) => value > 0) || material.emissiveStrength !== 1 ? "emissive" : undefined,
1097
- material.alphaMode !== "OPAQUE" ? `alpha-${material.alphaMode.toLowerCase()}` : undefined,
1098
- material.doubleSided ? "double-sided" : undefined,
1099
- material.normalTexture ? "normal-texture" : undefined,
1100
- material.occlusionTexture ? "occlusion-texture" : undefined,
1101
- material.clearcoat ? "clearcoat" : undefined,
1102
- material.transmission ? "transmission" : undefined,
1103
- material.diffuseTransmission ? "diffuse-transmission" : undefined,
1104
- material.volume ? "volume" : undefined,
1105
- material.ior !== undefined ? "ior" : undefined,
1106
- material.specular ? "specular" : undefined,
1107
- material.sheen ? "sheen" : undefined,
1108
- material.anisotropy ? "anisotropy" : undefined,
1109
- material.iridescence ? "iridescence" : undefined,
1110
- material.dispersion !== undefined ? "dispersion" : undefined,
1111
- material.pbrSpecularGlossiness ? "pbr-specular-glossiness" : undefined
1112
- ].filter((feature): feature is string => feature !== undefined);
1113
- }
1114
-
1115
- function materialTextureSlotNames(material: GLTFMaterialAsset): readonly string[] {
1116
- return [
1117
- material.baseColorTexture ? "base-color" : undefined,
1118
- material.metallicRoughnessTexture ? "metallic-roughness" : undefined,
1119
- material.normalTexture ? "normal" : undefined,
1120
- material.occlusionTexture ? "occlusion" : undefined,
1121
- material.emissiveTexture ? "emissive" : undefined,
1122
- material.clearcoat?.texture ? "clearcoat" : undefined,
1123
- material.clearcoat?.roughnessTexture ? "clearcoat-roughness" : undefined,
1124
- material.clearcoat?.normalTexture ? "clearcoat-normal" : undefined,
1125
- material.transmission?.texture ? "transmission" : undefined,
1126
- material.diffuseTransmission?.texture ? "diffuse-transmission" : undefined,
1127
- material.diffuseTransmission?.colorTexture ? "diffuse-transmission-color" : undefined,
1128
- material.volume?.thicknessTexture ? "volume-thickness" : undefined,
1129
- material.specular?.texture ? "specular" : undefined,
1130
- material.specular?.colorTexture ? "specular-color" : undefined,
1131
- material.sheen?.colorTexture ? "sheen-color" : undefined,
1132
- material.sheen?.roughnessTexture ? "sheen-roughness" : undefined,
1133
- material.anisotropy?.texture ? "anisotropy" : undefined,
1134
- material.iridescence?.texture ? "iridescence" : undefined,
1135
- material.iridescence?.thicknessTexture ? "iridescence-thickness" : undefined
1136
- ].filter((slot): slot is string => slot !== undefined);
1137
- }
1138
-
1139
- function validateAttributeCount(
1140
- semantic: string,
1141
- values: readonly (readonly number[])[],
1142
- vertexCount: number,
1143
- meshIndex: number,
1144
- primitiveIndex: number
1145
- ): void {
1146
- if (values.length > 0 && values.length !== vertexCount) {
1147
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} count mismatch`);
1148
- }
1149
- }
1150
-
1151
- function validateMeshDescriptor(mesh: GLTFMesh, meshIndex: number): void {
1152
- if (!Array.isArray(mesh.primitives) || mesh.primitives.length === 0) {
1153
- throw new Error(`glTF mesh ${meshIndex} primitives must be a non-empty array`);
1154
- }
1155
- }
1156
-
1157
- function validatePrimitiveDescriptor(primitive: GLTFPrimitive, meshIndex: number, primitiveIndex: number): void {
1158
- if (!isObjectRecord(primitive.attributes) || Array.isArray(primitive.attributes)) {
1159
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} attributes must be an object`);
1160
- }
1161
- for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
1162
- if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
1163
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} attribute ${semantic} accessor must be a non-negative integer`);
1164
- }
1165
- }
1166
- if (primitive.indices !== undefined && (!Number.isInteger(primitive.indices) || primitive.indices < 0)) {
1167
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} indices accessor must be a non-negative integer`);
1168
- }
1169
- if (primitive.mode !== undefined && (!Number.isInteger(primitive.mode) || primitive.mode < 0)) {
1170
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} mode must be a non-negative integer`);
1171
- }
1172
- const variantExtension = primitive.extensions?.KHR_materials_variants;
1173
- if (variantExtension !== undefined && (!Array.isArray(variantExtension.mappings) || variantExtension.mappings.length === 0)) {
1174
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants.mappings must be a non-empty array`);
1175
- }
1176
- }
1177
-
1178
- function readTexcoordSets(
1179
- json: GLTFJson,
1180
- buffers: readonly ArrayBuffer[],
1181
- primitive: GLTFPrimitive,
1182
- vertexCount: number,
1183
- meshIndex: number,
1184
- primitiveIndex: number,
1185
- decodedPrimitive: GLTFDracoDecodedPrimitive | undefined,
1186
- accessorCache: GLTFAccessorReadCache
1187
- ): readonly (readonly (readonly [number, number])[])[] {
1188
- const sets: (readonly (readonly [number, number])[])[] = [];
1189
- for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
1190
- const match = /^TEXCOORD_(\d+)$/.exec(semantic);
1191
- if (!match) continue;
1192
- const setIndex = Number(match[1]);
1193
- if (!Number.isInteger(setIndex)) {
1194
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} has invalid texture coordinate set index`);
1195
- }
1196
- const values = decodedPrimitive?.attributes[semantic] ?? readAccessor(json, buffers, accessorIndex, accessorCache);
1197
- validateAttributeCount(semantic, values, vertexCount, meshIndex, primitiveIndex);
1198
- sets[setIndex] = values.map((texcoord) => [texcoord[0] ?? 0, texcoord[1] ?? 0] as const);
1199
- }
1200
- const normalized: (readonly (readonly [number, number])[])[] = [];
1201
- for (let index = 0; index < sets.length; index += 1) {
1202
- normalized[index] = sets[index] ?? [];
1203
- }
1204
- return normalized;
1205
- }
1206
-
1207
- function readPrimitiveAttribute(
1208
- json: GLTFJson,
1209
- buffers: readonly ArrayBuffer[],
1210
- primitive: GLTFPrimitive,
1211
- decodedPrimitive: GLTFDracoDecodedPrimitive | undefined,
1212
- semantic: string,
1213
- accessorCache: GLTFAccessorReadCache
1214
- ): number[][] {
1215
- const decoded = decodedPrimitive?.attributes[semantic];
1216
- if (decoded) return normalizeDecodedPrimitiveAttribute(json, primitive, semantic, decoded);
1217
- return readOptionalAccessor(json, buffers, primitive.attributes[semantic], accessorCache);
1218
- }
1219
-
1220
- function normalizeDecodedPrimitiveAttribute(
1221
- json: GLTFJson,
1222
- primitive: GLTFPrimitive,
1223
- semantic: string,
1224
- rows: readonly (readonly number[])[]
1225
- ): number[][] {
1226
- const copied = rows.map((row) => [...row]);
1227
- const accessorIndex = primitive.attributes[semantic];
1228
- const accessor = accessorIndex === undefined ? undefined : json.accessors?.[accessorIndex];
1229
- if (!accessor?.normalized || !semantic.startsWith("COLOR_")) {
1230
- return copied;
1231
- }
1232
- const normalizer = normalizedComponentScale(accessor.componentType);
1233
- if (!normalizer) {
1234
- return copied;
1235
- }
1236
- const needsNormalization = copied.some((row) => row.some((value) => Math.abs(value) > 1));
1237
- if (!needsNormalization) {
1238
- return copied.map((row) => row.map(clampColorChannel));
1239
- }
1240
- return copied.map((row) => row.map((value) => clampColorChannel(normalizer(value))));
1241
- }
1242
-
1243
- function normalizedComponentScale(componentType: GLTFComponentType): ((value: number) => number) | null {
1244
- if (componentType === 5120) return (value) => Math.max(value / 127, -1);
1245
- if (componentType === 5121) return (value) => value / 255;
1246
- if (componentType === 5122) return (value) => Math.max(value / 32767, -1);
1247
- if (componentType === 5123) return (value) => value / 65535;
1248
- return null;
1249
- }
1250
-
1251
- function clampColorChannel(value: number): number {
1252
- return Math.max(0, Math.min(1, value));
1253
- }
1254
-
1255
- async function decodeDracoPrimitive(
1256
- json: GLTFJson,
1257
- buffers: readonly ArrayBuffer[],
1258
- primitive: GLTFPrimitive,
1259
- meshIndex: number,
1260
- primitiveIndex: number,
1261
- dracoDecoder: GLTFDracoDecoder | undefined
1262
- ): Promise<GLTFDracoDecodedPrimitive | undefined> {
1263
- const extension = primitive.extensions?.KHR_draco_mesh_compression;
1264
- if (!extension) return undefined;
1265
- if (!usesGLTFExtension(json, "KHR_draco_mesh_compression")) {
1266
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} uses KHR_draco_mesh_compression but the extension is not declared`);
1267
- }
1268
- if (!dracoDecoder) {
1269
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression requires a dracoDecoder`);
1270
- }
1271
- validateDracoDescriptor(extension, meshIndex, primitiveIndex);
1272
- const { view, buffer } = getBufferView(json, buffers, extension.bufferView);
1273
- const start = view.byteOffset ?? 0;
1274
- const source = new Uint8Array(buffer, start, view.byteLength);
1275
- const decoded = await dracoDecoder(source, {
1276
- meshIndex,
1277
- primitiveIndex,
1278
- bufferViewIndex: extension.bufferView,
1279
- attributes: { ...extension.attributes }
1280
- });
1281
- validateDecodedDracoPrimitive(decoded, primitive, meshIndex, primitiveIndex);
1282
- return decoded;
1283
- }
1284
-
1285
- function validateDracoDescriptor(
1286
- extension: NonNullable<NonNullable<GLTFPrimitive["extensions"]>["KHR_draco_mesh_compression"]>,
1287
- meshIndex: number,
1288
- primitiveIndex: number
1289
- ): void {
1290
- if (!Number.isInteger(extension.bufferView) || extension.bufferView < 0) {
1291
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression bufferView must be a non-negative integer`);
1292
- }
1293
- if (!isObjectRecord(extension.attributes) || Array.isArray(extension.attributes)) {
1294
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression attributes must be an object`);
1295
- }
1296
- if (!Number.isInteger(extension.attributes.POSITION) || extension.attributes.POSITION < 0) {
1297
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression requires POSITION attribute mapping`);
1298
- }
1299
- for (const [semantic, attributeId] of Object.entries(extension.attributes)) {
1300
- if (!Number.isInteger(attributeId) || attributeId < 0) {
1301
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression attribute ${semantic} id must be a non-negative integer`);
1302
- }
1303
- }
1304
- }
1305
-
1306
- function validateDecodedDracoPrimitive(
1307
- decoded: GLTFDracoDecodedPrimitive,
1308
- primitive: GLTFPrimitive,
1309
- meshIndex: number,
1310
- primitiveIndex: number
1311
- ): void {
1312
- if (!isObjectRecord(decoded.attributes) || Array.isArray(decoded.attributes)) {
1313
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoder must return attributes`);
1314
- }
1315
- const positions = decoded.attributes.POSITION;
1316
- if (!Array.isArray(positions) || positions.length === 0) {
1317
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoder must return POSITION rows`);
1318
- }
1319
- for (const [semantic, rows] of Object.entries(decoded.attributes)) {
1320
- if (primitive.attributes[semantic] === undefined) {
1321
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded unexpected attribute ${semantic}`);
1322
- }
1323
- if (!Array.isArray(rows) || rows.length !== positions.length) {
1324
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded ${semantic} count must match POSITION count`);
1325
- }
1326
- for (const [rowIndex, row] of rows.entries()) {
1327
- if (!Array.isArray(row) || row.some((value) => !Number.isFinite(value))) {
1328
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded ${semantic} row ${rowIndex} must contain finite values`);
1329
- }
1330
- }
1331
- }
1332
- if (decoded.indices !== undefined) {
1333
- for (const index of decoded.indices) {
1334
- if (!Number.isInteger(index) || index < 0 || index >= positions.length) {
1335
- throw new RangeError(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_draco_mesh_compression decoded index ${index} is outside vertex count ${positions.length}`);
1336
- }
1337
- }
1338
- }
1339
- }
1340
-
1341
- function validateMaterialTexCoordSets(
1342
- material: GLTFMaterialAsset | undefined,
1343
- texcoordSets: readonly (readonly (readonly [number, number])[])[],
1344
- meshIndex: number,
1345
- primitiveIndex: number
1346
- ): void {
1347
- if (!material) return;
1348
- for (const textureInfo of materialTextureInfos(material)) {
1349
- if (textureInfo.texCoord > 0 && (texcoordSets[textureInfo.texCoord]?.length ?? 0) === 0) {
1350
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} material ${material.name} references missing TEXCOORD_${textureInfo.texCoord}`);
1351
- }
1352
- }
1353
- }
1354
-
1355
- function materialTextureInfos(material: GLTFMaterialAsset): readonly GLTFResolvedTextureInfo[] {
1356
- return [
1357
- material.baseColorTexture,
1358
- material.metallicRoughnessTexture,
1359
- material.normalTexture,
1360
- material.occlusionTexture,
1361
- material.emissiveTexture,
1362
- material.clearcoat?.texture,
1363
- material.clearcoat?.roughnessTexture,
1364
- material.clearcoat?.normalTexture,
1365
- material.transmission?.texture,
1366
- material.diffuseTransmission?.texture,
1367
- material.diffuseTransmission?.colorTexture,
1368
- material.volume?.thicknessTexture,
1369
- material.specular?.texture,
1370
- material.specular?.colorTexture,
1371
- material.sheen?.colorTexture,
1372
- material.sheen?.roughnessTexture,
1373
- material.anisotropy?.texture,
1374
- material.iridescence?.texture,
1375
- material.iridescence?.thicknessTexture
1376
- ].filter((info): info is GLTFResolvedTextureInfo => info !== undefined);
1377
- }
1378
-
1379
- function resolvePrimitiveMode(
1380
- primitive: GLTFPrimitive,
1381
- rawIndices: readonly number[] | undefined,
1382
- vertexCount: number,
1383
- meshIndex: number,
1384
- primitiveIndex: number
1385
- ): { readonly topology: "triangles" | "lines" | "points"; readonly indices?: readonly number[] } {
1386
- const mode = primitive.mode ?? 4;
1387
- const source = rawIndices ?? [...Array(vertexCount).keys()];
1388
- validatePrimitiveIndices(source, vertexCount, meshIndex, primitiveIndex);
1389
- switch (mode) {
1390
- case 1:
1391
- if (source.length % 2 !== 0) throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} LINES index count must be even`);
1392
- return { topology: "lines", indices: rawIndices };
1393
- case 2:
1394
- return { topology: "lines", indices: expandLineLoop(source) };
1395
- case 3:
1396
- return { topology: "lines", indices: expandLineStrip(source) };
1397
- case 4:
1398
- if (source.length % 3 !== 0) throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} TRIANGLES index count must be a multiple of 3`);
1399
- return { topology: "triangles", indices: rawIndices };
1400
- case 5:
1401
- return { topology: "triangles", indices: expandTriangleStrip(source) };
1402
- case 6:
1403
- return { topology: "triangles", indices: expandTriangleFan(source) };
1404
- case 0:
1405
- return { topology: "points", indices: rawIndices };
1406
- default:
1407
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} has unsupported primitive mode ${mode}`);
1408
- }
1409
- }
1410
-
1411
- function validatePrimitiveIndices(
1412
- indices: readonly number[],
1413
- vertexCount: number,
1414
- meshIndex: number,
1415
- primitiveIndex: number
1416
- ): void {
1417
- for (const index of indices) {
1418
- if (!Number.isInteger(index) || index < 0 || index >= vertexCount) {
1419
- throw new RangeError(`glTF mesh ${meshIndex} primitive ${primitiveIndex} index ${index} is outside vertex count ${vertexCount}`);
1420
- }
1421
- }
1422
- }
1423
-
1424
- function expandLineStrip(indices: readonly number[]): readonly number[] {
1425
- const expanded: number[] = [];
1426
- for (let index = 0; index < indices.length - 1; index += 1) {
1427
- expanded.push(indices[index]!, indices[index + 1]!);
1428
- }
1429
- return expanded;
1430
- }
1431
-
1432
- function expandLineLoop(indices: readonly number[]): readonly number[] {
1433
- if (indices.length < 2) return [];
1434
- const expanded = [...expandLineStrip(indices)];
1435
- expanded.push(indices[indices.length - 1]!, indices[0]!);
1436
- return expanded;
1437
- }
1438
-
1439
- function expandTriangleStrip(indices: readonly number[]): readonly number[] {
1440
- const expanded: number[] = [];
1441
- for (let index = 0; index < indices.length - 2; index += 1) {
1442
- if (index % 2 === 0) {
1443
- expanded.push(indices[index]!, indices[index + 1]!, indices[index + 2]!);
1444
- } else {
1445
- expanded.push(indices[index + 1]!, indices[index]!, indices[index + 2]!);
1446
- }
1447
- }
1448
- return expanded;
1449
- }
1450
-
1451
- function expandTriangleFan(indices: readonly number[]): readonly number[] {
1452
- const expanded: number[] = [];
1453
- for (let index = 1; index < indices.length - 1; index += 1) {
1454
- expanded.push(indices[0]!, indices[index]!, indices[index + 1]!);
1455
- }
1456
- return expanded;
1457
- }
1458
-
1459
- function readMorphTargets(
1460
- json: GLTFJson,
1461
- buffers: readonly ArrayBuffer[],
1462
- primitive: GLTFPrimitive,
1463
- vertexCount: number,
1464
- meshIndex: number,
1465
- primitiveIndex: number,
1466
- accessorCache: GLTFAccessorReadCache,
1467
- targetNames: readonly string[] = []
1468
- ): readonly GLTFMorphTargetAsset[] {
1469
- if (primitive.targets === undefined) {
1470
- return [];
1471
- }
1472
- if (!Array.isArray(primitive.targets)) {
1473
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} targets must be an array`);
1474
- }
1475
- return primitive.targets.map((target, targetIndex) => {
1476
- validateMorphTargetDescriptor(target, meshIndex, primitiveIndex, targetIndex);
1477
- const positions = readOptionalAccessor(json, buffers, target.POSITION, accessorCache);
1478
- const normals = readOptionalAccessor(json, buffers, target.NORMAL, accessorCache);
1479
- const tangents = readOptionalAccessor(json, buffers, target.TANGENT, accessorCache);
1480
- if (positions.length > 0 && positions.length !== vertexCount) {
1481
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} POSITION count mismatch`);
1482
- }
1483
- if (normals.length > 0 && normals.length !== vertexCount) {
1484
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} NORMAL count mismatch`);
1485
- }
1486
- if (tangents.length > 0 && tangents.length !== vertexCount) {
1487
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} TANGENT count mismatch`);
1488
- }
1489
- const name = targetNames[targetIndex];
1490
- return {
1491
- ...(typeof name === "string" && name.length > 0 ? { name } : {}),
1492
- positions: positions.map((position) => [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0] as const),
1493
- normals: normals.map((normal) => [normal[0] ?? 0, normal[1] ?? 0, normal[2] ?? 0] as const),
1494
- tangents: tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0] as const)
1495
- };
1496
- });
1497
- }
1498
-
1499
- // glTF stores morph-target names in `mesh.extras.targetNames` (a parallel array). Read them so the
1500
- // runtime can expose named morph influences instead of bare indices.
1501
- function readMorphTargetNames(mesh: GLTFMesh): readonly string[] {
1502
- const extras = (mesh as { extras?: Record<string, unknown> }).extras;
1503
- const names = extras?.targetNames;
1504
- if (Array.isArray(names) && names.every((n) => typeof n === "string")) {
1505
- return names as readonly string[];
1506
- }
1507
- return [];
1508
- }
1509
-
1510
- function validateMorphTargetDescriptor(
1511
- target: Readonly<Record<string, number>>,
1512
- meshIndex: number,
1513
- primitiveIndex: number,
1514
- targetIndex: number
1515
- ): void {
1516
- if (!isObjectRecord(target) || Array.isArray(target)) {
1517
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} must be an object`);
1518
- }
1519
- for (const key of Object.keys(target)) {
1520
- if (key !== "POSITION" && key !== "NORMAL" && key !== "TANGENT") {
1521
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} attribute ${key} is unsupported`);
1522
- }
1523
- const accessorIndex = target[key];
1524
- if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
1525
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} ${key} accessor must be a non-negative integer`);
1526
- }
1527
- }
1528
- }
1529
-
1530
- function resolveMorphWeights(
1531
- mesh: GLTFMesh,
1532
- meshIndex: number,
1533
- primitiveIndex: number,
1534
- targetCount: number
1535
- ): readonly number[] {
1536
- if (mesh.weights === undefined) {
1537
- return Array.from({ length: targetCount }, () => 0);
1538
- }
1539
- if (!Array.isArray(mesh.weights)) {
1540
- throw new Error(`glTF mesh ${meshIndex} weights must be an array`);
1541
- }
1542
- if (mesh.weights.length !== targetCount) {
1543
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} weights count must match morph target count`);
1544
- }
1545
- if (mesh.weights.some((weight) => !Number.isFinite(weight))) {
1546
- throw new Error(`glTF mesh ${meshIndex} weights must contain finite numbers`);
1547
- }
1548
- return [...mesh.weights];
1549
- }
1550
-
1551
- function createImageAssets(json: GLTFJson, buffers: readonly ArrayBuffer[]): readonly GLTFImageAsset[] {
1552
- const avifEnabled = usesGLTFExtension(json, "EXT_texture_avif");
1553
- const webpEnabled = usesGLTFExtension(json, "EXT_texture_webp");
1554
- const basisuEnabled = usesGLTFExtension(json, "KHR_texture_basisu");
1555
- return (json.images ?? []).map((image, index) => {
1556
- if (image.uri && image.bufferView !== undefined) {
1557
- throw new Error(`glTF image ${index} cannot define both uri and bufferView`);
1558
- }
1559
- if (!image.uri && image.bufferView === undefined) {
1560
- throw new Error(`glTF image ${index} must define uri or bufferView`);
1561
- }
1562
- if (image.bufferView !== undefined && !image.mimeType) {
1563
- throw new Error(`glTF image ${index} with bufferView requires mimeType`);
1564
- }
1565
- validateImageMimeType(image, index, avifEnabled, webpEnabled, basisuEnabled);
1566
- return {
1567
- name: image.name ?? `image-${index}`,
1568
- uri: image.uri,
1569
- mimeType: image.mimeType,
1570
- data: image.bufferView === undefined ? undefined : readBufferViewBytes(json, buffers, image.bufferView)
1571
- };
1572
- });
1573
- }
1574
-
1575
- function validateImageMimeType(image: GLTFImage, index: number, avifEnabled: boolean, webpEnabled: boolean, basisuEnabled: boolean): void {
1576
- if (image.mimeType !== undefined && !isSupportedImageMimeType(image.mimeType, avifEnabled, webpEnabled, basisuEnabled)) {
1577
- throw new Error(`glTF image ${index} mimeType must be ${supportedImageMimeTypeMessage(avifEnabled, webpEnabled, basisuEnabled)}`);
1578
- }
1579
- if (image.uri?.startsWith("data:")) {
1580
- const separator = image.uri.indexOf(",");
1581
- if (separator < 0) {
1582
- throw new Error(`glTF image ${index} data uri must include a comma separator`);
1583
- }
1584
- const mediaType = image.uri.slice(5, separator).split(";")[0]?.toLowerCase();
1585
- if (!isSupportedImageMimeType(mediaType, avifEnabled, webpEnabled, basisuEnabled)) {
1586
- throw new Error(`glTF image ${index} data uri media type must be ${supportedImageMimeTypeMessage(avifEnabled, webpEnabled, basisuEnabled)}`);
1587
- }
1588
- }
1589
- }
1590
-
1591
- function isSupportedImageMimeType(mimeType: string | undefined, avifEnabled: boolean, webpEnabled: boolean, basisuEnabled: boolean): boolean {
1592
- return mimeType === "image/png" || mimeType === "image/jpeg" || (avifEnabled && mimeType === "image/avif") || (webpEnabled && mimeType === "image/webp") || (basisuEnabled && mimeType === "image/ktx2");
1593
- }
1594
-
1595
- function supportedImageMimeTypeMessage(avifEnabled: boolean, webpEnabled: boolean, basisuEnabled: boolean): string {
1596
- return ["image/png", "image/jpeg", ...(avifEnabled ? ["image/avif"] : []), ...(webpEnabled ? ["image/webp"] : []), ...(basisuEnabled ? ["image/ktx2"] : [])].join(", or ");
1597
- }
1598
-
1599
- function createTextureAssets(json: GLTFJson): readonly GLTFTextureAsset[] {
1600
- return (json.textures ?? []).map((texture, index) => {
1601
- const source = validateTextureDescriptor(json, texture, index);
1602
- const sampler = texture.sampler;
1603
- return {
1604
- name: texture.name ?? `texture-${index}`,
1605
- source,
1606
- sampler,
1607
- ...(sampler === undefined ? {} : { samplerDescriptor: createSamplerAsset(json.samplers![sampler]!, sampler) })
1608
- };
1609
- });
1610
- }
1611
-
1612
- function validateTextureDescriptor(json: GLTFJson, texture: GLTFTexture, index: number): number {
1613
- const avifSource = texture.extensions?.EXT_texture_avif?.source;
1614
- if (avifSource !== undefined && !usesGLTFExtension(json, "EXT_texture_avif")) {
1615
- throw new Error(`glTF texture ${index} uses EXT_texture_avif but the extension is not declared`);
1616
- }
1617
- const webpSource = texture.extensions?.EXT_texture_webp?.source;
1618
- if (webpSource !== undefined && !usesGLTFExtension(json, "EXT_texture_webp")) {
1619
- throw new Error(`glTF texture ${index} uses EXT_texture_webp but the extension is not declared`);
1620
- }
1621
- const basisuSource = texture.extensions?.KHR_texture_basisu?.source;
1622
- if (basisuSource !== undefined && !usesGLTFExtension(json, "KHR_texture_basisu")) {
1623
- throw new Error(`glTF texture ${index} uses KHR_texture_basisu but the extension is not declared`);
1624
- }
1625
- const source = basisuSource ?? avifSource ?? webpSource ?? texture.source;
1626
- if (source === undefined) {
1627
- throw new Error(`glTF texture ${index} references missing image source undefined`);
1628
- }
1629
- if (!Number.isInteger(source) || source < 0) {
1630
- throw new Error(`glTF texture ${index} source must be a non-negative integer`);
1631
- }
1632
- if (!json.images?.[source]) {
1633
- throw new Error(`glTF texture ${index} references missing image source ${source}`);
1634
- }
1635
- if (texture.sampler !== undefined) {
1636
- if (!Number.isInteger(texture.sampler) || texture.sampler < 0) {
1637
- throw new Error(`glTF texture ${index} sampler must be a non-negative integer`);
1638
- }
1639
- if (!json.samplers?.[texture.sampler]) {
1640
- throw new Error(`glTF texture ${index} references missing sampler ${texture.sampler}`);
1641
- }
1642
- }
1643
- return source;
1644
- }
1645
-
1646
- function createSamplerAsset(sampler: GLTFSampler, samplerIndex: number): GLTFSamplerAsset {
1647
- return {
1648
- minFilter: minFilterFromGLTF(sampler.minFilter, samplerIndex),
1649
- magFilter: magFilterFromGLTF(sampler.magFilter, samplerIndex),
1650
- addressU: addressModeFromGLTF(sampler.wrapS, samplerIndex, "wrapS"),
1651
- addressV: addressModeFromGLTF(sampler.wrapT, samplerIndex, "wrapT")
1652
- };
1653
- }
1654
-
1655
- function minFilterFromGLTF(value: number | undefined, samplerIndex: number): GLTFSamplerAsset["minFilter"] {
1656
- if (value === undefined) return "linear-mipmap-linear";
1657
- if (value === 9728) return "nearest";
1658
- if (value === 9729) return "linear";
1659
- if (value === 9984) return "nearest-mipmap-nearest";
1660
- if (value === 9985) return "linear-mipmap-nearest";
1661
- if (value === 9986) return "nearest-mipmap-linear";
1662
- if (value === 9987) return "linear-mipmap-linear";
1663
- throw new Error(`glTF sampler ${samplerIndex} minFilter has unsupported value ${value}`);
1664
- }
1665
-
1666
- function magFilterFromGLTF(value: number | undefined, samplerIndex: number): "nearest" | "linear" {
1667
- if (value === undefined) return "linear";
1668
- if (value === 9728) return "nearest";
1669
- if (value === 9729) return "linear";
1670
- throw new Error(`glTF sampler ${samplerIndex} magFilter has unsupported value ${value}`);
1671
- }
1672
-
1673
- function addressModeFromGLTF(value: number | undefined, samplerIndex: number, field: "wrapS" | "wrapT"): "clamp-to-edge" | "repeat" | "mirror-repeat" {
1674
- if (value === undefined || value === 10497) return "repeat";
1675
- if (value === 33071) return "clamp-to-edge";
1676
- if (value === 33648) return "mirror-repeat";
1677
- throw new Error(`glTF sampler ${samplerIndex} ${field} has unsupported value ${value}`);
1678
- }
1679
-
1680
- function createSceneAssets(json: GLTFJson): readonly GLTFSceneAsset[] {
1681
- return (json.scenes ?? []).map((scene, index) => {
1682
- return {
1683
- name: scene.name ?? `scene-${index}`,
1684
- nodeIndices: [...(scene.nodes ?? [])]
1685
- };
1686
- });
1687
- }
1688
-
1689
- function resolveDefaultSceneIndex(json: GLTFJson): number {
1690
- return json.scene ?? 0;
1691
- }
1692
-
1693
- function createMaterialAssets(
1694
- json: GLTFJson,
1695
- textures: readonly GLTFTextureAsset[]
1696
- ): readonly GLTFMaterialAsset[] {
1697
- return (json.materials ?? []).map((material, index) => {
1698
- const pbr = material.pbrMetallicRoughness;
1699
- const pbrSpecularGlossiness = resolvePBRSpecularGlossiness(material, textures, index).pbrSpecularGlossiness;
1700
- const specular = resolveSpecular(material, textures, index).specular ?? createSpecularFromSpecularGlossiness(pbrSpecularGlossiness);
1701
- return {
1702
- name: material.name ?? `material-${index}`,
1703
- unlit: material.extensions?.KHR_materials_unlit !== undefined,
1704
- baseColorFactor: pbrSpecularGlossiness?.diffuseFactor ?? resolveColor4(pbr?.baseColorFactor, [1, 1, 1, 1], `material ${index} baseColorFactor`),
1705
- baseColorTexture: pbrSpecularGlossiness?.diffuseTexture ?? resolveTextureInfo(textures, pbr?.baseColorTexture, `material ${index} baseColorTexture`),
1706
- metallicFactor: pbrSpecularGlossiness ? 0 : resolveUnit(pbr?.metallicFactor, 1, `material ${index} metallicFactor`),
1707
- roughnessFactor: pbrSpecularGlossiness ? 1 - pbrSpecularGlossiness.glossinessFactor : resolveUnit(pbr?.roughnessFactor, 1, `material ${index} roughnessFactor`),
1708
- metallicRoughnessTexture: pbrSpecularGlossiness
1709
- ? undefined
1710
- : resolveTextureInfo(
1711
- textures,
1712
- pbr?.metallicRoughnessTexture,
1713
- `material ${index} metallicRoughnessTexture`
1714
- ),
1715
- normalTexture: withDefaultScale(resolveTextureInfo(textures, material.normalTexture, `material ${index} normalTexture`), material.normalTexture?.scale),
1716
- occlusionTexture: withDefaultStrength(
1717
- resolveTextureInfo(textures, material.occlusionTexture, `material ${index} occlusionTexture`),
1718
- material.occlusionTexture?.strength
1719
- ),
1720
- emissiveTexture: resolveTextureInfo(textures, material.emissiveTexture, `material ${index} emissiveTexture`),
1721
- emissiveFactor: resolveColor3(material.emissiveFactor, [0, 0, 0], `material ${index} emissiveFactor`),
1722
- emissiveStrength: resolveEmissiveStrength(material, index),
1723
- ...(resolveClearcoat(material, textures, index)),
1724
- ...(resolveTransmission(material, textures, index)),
1725
- ...(resolveDiffuseTransmission(material, textures, pbrSpecularGlossiness, index)),
1726
- ...(resolveVolume(material, textures, index)),
1727
- ...(resolveIOR(material, index)),
1728
- ...(specular ? { specular } : {}),
1729
- ...(pbrSpecularGlossiness ? { pbrSpecularGlossiness } : {}),
1730
- ...(resolveSheen(material, textures, index)),
1731
- ...(resolveAnisotropy(material, textures, index)),
1732
- ...(resolveIridescence(material, textures, index)),
1733
- ...(resolveDispersion(material, index)),
1734
- alphaMode: resolveAlphaMode(material.alphaMode, index),
1735
- alphaCutoff: resolveUnit(material.alphaCutoff, 0.5, `material ${index} alphaCutoff`),
1736
- doubleSided: material.doubleSided ?? false
1737
- };
1738
- });
1739
- }
1740
-
1741
- function createMaterialVariantAssets(json: GLTFJson): readonly GLTFMaterialVariantAsset[] {
1742
- if (!usesGLTFExtension(json, "KHR_materials_variants")) return [];
1743
- const variants = json.extensions?.KHR_materials_variants?.variants;
1744
- if (!Array.isArray(variants) || variants.length === 0) {
1745
- throw new Error("glTF KHR_materials_variants.variants must be a non-empty array when the extension is declared");
1746
- }
1747
- return variants.map((variant, index) => {
1748
- if (!isObjectRecord(variant)) {
1749
- throw new Error(`glTF KHR_materials_variants variant ${index} must be an object`);
1750
- }
1751
- return { name: typeof variant.name === "string" && variant.name.length > 0 ? variant.name : `variant-${index}` };
1752
- });
1753
- }
1754
-
1755
- function resolvePrimitiveMaterialVariants(
1756
- primitive: GLTFPrimitive,
1757
- variants: readonly GLTFMaterialVariantAsset[],
1758
- materials: readonly GLTFMaterialAsset[],
1759
- meshIndex: number,
1760
- primitiveIndex: number
1761
- ): readonly GLTFMaterialVariantMappingAsset[] {
1762
- const mappings = primitive.extensions?.KHR_materials_variants?.mappings;
1763
- if (!mappings) return [];
1764
- if (variants.length === 0) {
1765
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} uses KHR_materials_variants but the extension is not declared`);
1766
- }
1767
-
1768
- const resolved: GLTFMaterialVariantMappingAsset[] = [];
1769
- for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex += 1) {
1770
- const mapping = mappings[mappingIndex]!;
1771
- if (!Number.isInteger(mapping.material) || mapping.material < 0) {
1772
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} material must be a non-negative integer`);
1773
- }
1774
- const material = materials[mapping.material];
1775
- if (!material) {
1776
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} references missing material ${mapping.material}`);
1777
- }
1778
- if (!Array.isArray(mapping.variants) || mapping.variants.length === 0) {
1779
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} variants must be a non-empty array`);
1780
- }
1781
- for (const variantIndex of mapping.variants) {
1782
- if (!Number.isInteger(variantIndex) || variantIndex < 0) {
1783
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} variant must be a non-negative integer`);
1784
- }
1785
- const variant = variants[variantIndex];
1786
- if (!variant) {
1787
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} KHR_materials_variants mapping ${mappingIndex} references missing variant ${variantIndex}`);
1788
- }
1789
- resolved.push({
1790
- variantIndex,
1791
- variant: variant.name,
1792
- materialIndex: mapping.material,
1793
- material: material.name
1794
- });
1795
- }
1796
- }
1797
- return resolved;
1798
- }
1799
-
1800
- function resolveAlphaMode(value: string | undefined, materialIndex: number): "OPAQUE" | "MASK" | "BLEND" {
1801
- if (value === undefined) return "OPAQUE";
1802
- if (value === "OPAQUE" || value === "MASK" || value === "BLEND") return value;
1803
- throw new Error(`glTF material ${materialIndex} alphaMode must be OPAQUE, MASK, or BLEND`);
1804
- }
1805
-
1806
- function resolveTextureInfo(
1807
- textures: readonly GLTFTextureAsset[],
1808
- info: GLTFTextureInfo | undefined,
1809
- label: string
1810
- ): GLTFResolvedTextureInfo | undefined {
1811
- if (!info) return undefined;
1812
- if (!Number.isInteger(info.index) || info.index < 0) {
1813
- throw new Error(`glTF ${label} index must be a non-negative integer`);
1814
- }
1815
- const texture = textures[info.index];
1816
- if (!texture) {
1817
- throw new Error(`glTF ${label} references missing texture ${info.index}`);
1818
- }
1819
- const transform = info.extensions?.KHR_texture_transform;
1820
- const texCoord = transform?.texCoord ?? info.texCoord ?? 0;
1821
- if (!Number.isInteger(texCoord) || texCoord < 0) {
1822
- throw new RangeError(`glTF ${label} texCoord must be a non-negative integer`);
1823
- }
1824
- return {
1825
- texture: info.index,
1826
- image: texture.source,
1827
- texCoord,
1828
- ...(transform ? { transform: resolveTextureTransform(transform) } : {})
1829
- };
1830
- }
1831
-
1832
- function usesGLTFExtension(json: GLTFJson, extension: string): boolean {
1833
- return Boolean(json.extensionsRequired?.includes(extension) || json.extensionsUsed?.includes(extension));
1834
- }
1835
-
1836
- function usesGLTFMeshoptExtension(json: GLTFJson): boolean {
1837
- return GLTF_MESHOPT_EXTENSION_NAMES.some((extension) => usesGLTFExtension(json, extension));
1838
- }
1839
-
1840
- const GLTF_MESHOPT_EXTENSION_NAMES = ["EXT_meshopt_compression", "KHR_meshopt_compression"] as const;
1841
-
1842
- function validatePrimitiveAttributeAccessors(
1843
- json: GLTFJson,
1844
- primitive: GLTFPrimitive,
1845
- meshQuantizationEnabled: boolean,
1846
- meshIndex: number,
1847
- primitiveIndex: number
1848
- ): void {
1849
- for (const [semantic, accessorIndex] of Object.entries(primitive.attributes)) {
1850
- if (semantic !== "POSITION" && semantic !== "NORMAL" && semantic !== "TANGENT") {
1851
- continue;
1852
- }
1853
- const accessor = json.accessors?.[accessorIndex];
1854
- if (!accessor) {
1855
- continue;
1856
- }
1857
- if (!isAccessorComponentType(accessor.componentType)) {
1858
- continue;
1859
- }
1860
- validateQuantizedAttributeComponentType(accessor.componentType, semantic, meshQuantizationEnabled, meshIndex, primitiveIndex);
1861
- }
1862
- }
1863
-
1864
- function validateQuantizedAttributeComponentType(
1865
- componentType: GLTFComponentType,
1866
- semantic: string,
1867
- meshQuantizationEnabled: boolean,
1868
- meshIndex: number,
1869
- primitiveIndex: number
1870
- ): void {
1871
- if (componentType === 5126) {
1872
- return;
1873
- }
1874
- if (!meshQuantizationEnabled) {
1875
- throw new Error(
1876
- `glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} accessor uses non-FLOAT componentType ${componentType}; declare KHR_mesh_quantization to import quantized attributes`
1877
- );
1878
- }
1879
- if (componentType === 5120 || componentType === 5121 || componentType === 5122 || componentType === 5123) {
1880
- return;
1881
- }
1882
- throw new Error(
1883
- `glTF mesh ${meshIndex} primitive ${primitiveIndex} ${semantic} accessor componentType ${componentType} is unsupported for KHR_mesh_quantization`
1884
- );
1885
- }
1886
-
1887
- export interface SkinWeightNormalizationStats {
1888
- degenerate: number;
1889
- rescaled: number;
1890
- }
1891
-
1892
- const SKIN_WEIGHT_DEGENERATE_EPSILON = 1e-6;
1893
-
1894
- /**
1895
- * Normalize a single vertex's 4 skin weights so they sum to 1.
1896
- *
1897
- * Real-world catalog GLBs (Objaverse/Sketchfab) frequently ship denormalized,
1898
- * negative, or all-zero WEIGHTS_0 values. Rather than hard-failing the whole
1899
- * asset, sanitize per vertex: negative/non-finite components are clamped to 0,
1900
- * a degenerate (near-zero) sum collapses to a rigid binding `[1, 0, 0, 0]`, and
1901
- * any other sum is rescaled to 1. The optional `stats` accumulator lets callers
1902
- * surface a single diagnostic per primitive instead of one per vertex.
1903
- *
1904
- * Component proportions are preserved regardless of the source accessor
1905
- * componentType (FLOAT 5126, or normalized UNSIGNED_BYTE 5121 / UNSIGNED_SHORT
1906
- * 5123), so integer-encoded weights normalize correctly without extra handling.
1907
- */
1908
- export function normalizeSkinWeights(
1909
- weights: readonly [number, number, number, number],
1910
- stats?: SkinWeightNormalizationStats
1911
- ): readonly [number, number, number, number] {
1912
- const sanitized: [number, number, number, number] = [
1913
- Number.isFinite(weights[0]) && weights[0] > 0 ? weights[0] : 0,
1914
- Number.isFinite(weights[1]) && weights[1] > 0 ? weights[1] : 0,
1915
- Number.isFinite(weights[2]) && weights[2] > 0 ? weights[2] : 0,
1916
- Number.isFinite(weights[3]) && weights[3] > 0 ? weights[3] : 0
1917
- ];
1918
- const sum = sanitized[0] + sanitized[1] + sanitized[2] + sanitized[3];
1919
- if (sum < SKIN_WEIGHT_DEGENERATE_EPSILON) {
1920
- if (stats) stats.degenerate += 1;
1921
- return [1, 0, 0, 0] as const;
1922
- }
1923
- if (Math.abs(sum - 1) <= 1e-5) {
1924
- return sanitized as readonly [number, number, number, number];
1925
- }
1926
- if (stats) stats.rescaled += 1;
1927
- return [
1928
- sanitized[0] / sum,
1929
- sanitized[1] / sum,
1930
- sanitized[2] / sum,
1931
- sanitized[3] / sum
1932
- ] as const;
1933
- }
1934
-
1935
- function reportSkinWeightNormalization(
1936
- meshIndex: number,
1937
- primitiveIndex: number,
1938
- stats: SkinWeightNormalizationStats
1939
- ): void {
1940
- if (stats.degenerate === 0 && stats.rescaled === 0) return;
1941
- if (typeof console === "undefined" || typeof console.warn !== "function") return;
1942
- console.warn(
1943
- `glTF mesh ${meshIndex} primitive ${primitiveIndex} WEIGHTS_0 normalized: ` +
1944
- `${stats.degenerate} degenerate vertex(es) set to [1,0,0,0], ${stats.rescaled} rescaled to sum 1`
1945
- );
1946
- }
1947
-
1948
- function createCameraAssets(json: GLTFJson): readonly GLTFCameraAsset[] {
1949
- return (json.cameras ?? []).map((camera, index) => {
1950
- if (camera.type === "perspective") {
1951
- if (!camera.perspective) {
1952
- throw new Error(`glTF camera ${index} perspective camera requires perspective data`);
1953
- }
1954
- const yfov = resolveCameraPositive(camera.perspective.yfov, `camera ${index} perspective.yfov`);
1955
- if (yfov >= Math.PI) {
1956
- throw new RangeError(`glTF camera ${index} perspective.yfov must be less than PI`);
1957
- }
1958
- const znear = resolveCameraPositive(camera.perspective.znear, `camera ${index} perspective.znear`);
1959
- const zfar = camera.perspective.zfar === undefined
1960
- ? 1000
1961
- : resolveCameraPositive(camera.perspective.zfar, `camera ${index} perspective.zfar`);
1962
- if (zfar <= znear) {
1963
- throw new RangeError(`glTF camera ${index} perspective.zfar must be greater than znear`);
1964
- }
1965
- const aspectRatio = resolveCameraPositive(camera.perspective.aspectRatio ?? 1, `camera ${index} perspective.aspectRatio`);
1966
- return {
1967
- name: camera.name ?? `camera-${index}`,
1968
- type: "perspective",
1969
- perspective: { aspectRatio, yfov, znear, zfar }
1970
- };
1971
- }
1972
- if (camera.type === "orthographic") {
1973
- if (!camera.orthographic) {
1974
- throw new Error(`glTF camera ${index} orthographic camera requires orthographic data`);
1975
- }
1976
- const xmag = resolveCameraPositive(camera.orthographic.xmag, `camera ${index} orthographic.xmag`);
1977
- const ymag = resolveCameraPositive(camera.orthographic.ymag, `camera ${index} orthographic.ymag`);
1978
- const znear = resolveCameraPositive(camera.orthographic.znear, `camera ${index} orthographic.znear`);
1979
- const zfar = resolveCameraPositive(camera.orthographic.zfar, `camera ${index} orthographic.zfar`);
1980
- if (zfar <= znear) {
1981
- throw new RangeError(`glTF camera ${index} orthographic.zfar must be greater than znear`);
1982
- }
1983
- return {
1984
- name: camera.name ?? `camera-${index}`,
1985
- type: "orthographic",
1986
- orthographic: { xmag, ymag, znear, zfar }
1987
- };
1988
- }
1989
- throw new Error(`glTF camera ${index} has unsupported type ${String(camera.type)}`);
1990
- });
1991
- }
1992
-
1993
- function resolveCameraPositive(value: number | undefined, label: string): number {
1994
- const resolved = value;
1995
- if (resolved === undefined || !Number.isFinite(resolved) || resolved <= 0) {
1996
- throw new RangeError(`glTF ${label} must be finite and positive`);
1997
- }
1998
- return resolved;
1999
- }
2000
-
2001
- function createLightAssets(json: GLTFJson): readonly GLTFLightAsset[] {
2002
- return (json.extensions?.KHR_lights_punctual?.lights ?? []).map((light, index) => {
2003
- const color = resolveColor3(light.color, [1, 1, 1], `light ${index} color`);
2004
- const intensity = resolveNonNegative(light.intensity, 1, `light ${index} intensity`);
2005
- if (light.type !== "directional" && light.type !== "point" && light.type !== "spot") {
2006
- throw new Error(`glTF light ${index} has unsupported type ${String(light.type)}`);
2007
- }
2008
- if (light.type === "directional") {
2009
- return { name: light.name ?? `light-${index}`, type: "directional", color, intensity };
2010
- }
2011
-
2012
- const range = resolvePositive(light.range, 10, `light ${index} range`);
2013
- if (light.type === "point") {
2014
- return { name: light.name ?? `light-${index}`, type: "point", color, intensity, range };
2015
- }
2016
-
2017
- const outerConeAngle = resolveConeAngle(light.spot?.outerConeAngle, Math.PI / 4, `light ${index} spot.outerConeAngle`, false);
2018
- const innerConeAngle = resolveConeAngle(light.spot?.innerConeAngle, 0, `light ${index} spot.innerConeAngle`, true);
2019
- if (innerConeAngle > outerConeAngle) {
2020
- throw new RangeError(`glTF light ${index} spot.innerConeAngle must be less than or equal to outerConeAngle`);
2021
- }
2022
- return {
2023
- name: light.name ?? `light-${index}`,
2024
- type: "spot",
2025
- color,
2026
- intensity,
2027
- range,
2028
- spot: { innerConeAngle, outerConeAngle }
2029
- };
2030
- });
2031
- }
2032
-
2033
- function resolveConeAngle(value: number | undefined, fallback: number, label: string, allowZero: boolean): number {
2034
- const resolved = value ?? fallback;
2035
- const valid = allowZero ? resolved >= 0 : resolved > 0;
2036
- if (!Number.isFinite(resolved) || !valid || resolved >= Math.PI / 2) {
2037
- const lower = allowZero ? "[0" : "(0";
2038
- throw new RangeError(`glTF ${label} must be finite and within ${lower}, PI / 2)`);
2039
- }
2040
- return resolved;
2041
- }
2042
-
2043
- function resolveEmissiveStrength(material: GLTFMaterial, materialIndex: number): number {
2044
- const strength = material.extensions?.KHR_materials_emissive_strength?.emissiveStrength ?? 1;
2045
- if (!Number.isFinite(strength) || strength < 0) {
2046
- throw new RangeError(`glTF material ${materialIndex} KHR_materials_emissive_strength.emissiveStrength must be finite and non-negative`);
2047
- }
2048
- return strength;
2049
- }
2050
-
2051
- function resolveClearcoat(
2052
- material: GLTFMaterial,
2053
- textures: readonly GLTFTextureAsset[],
2054
- materialIndex: number
2055
- ): { readonly clearcoat?: GLTFClearcoatMaterialExtension } {
2056
- const extension = material.extensions?.KHR_materials_clearcoat;
2057
- if (!extension) return {};
2058
- return {
2059
- clearcoat: {
2060
- factor: resolveUnit(extension.clearcoatFactor, 0, `material ${materialIndex} KHR_materials_clearcoat.clearcoatFactor`),
2061
- texture: resolveTextureInfo(textures, extension.clearcoatTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatTexture`),
2062
- roughnessFactor: resolveUnit(extension.clearcoatRoughnessFactor, 0, `material ${materialIndex} KHR_materials_clearcoat.clearcoatRoughnessFactor`),
2063
- roughnessTexture: resolveTextureInfo(textures, extension.clearcoatRoughnessTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatRoughnessTexture`),
2064
- normalTexture: withDefaultScale(
2065
- resolveTextureInfo(textures, extension.clearcoatNormalTexture, `material ${materialIndex} KHR_materials_clearcoat.clearcoatNormalTexture`),
2066
- extension.clearcoatNormalTexture?.scale
2067
- )
2068
- }
2069
- };
2070
- }
2071
-
2072
- function resolveTransmission(
2073
- material: GLTFMaterial,
2074
- textures: readonly GLTFTextureAsset[],
2075
- materialIndex: number
2076
- ): { readonly transmission?: GLTFTransmissionMaterialExtension } {
2077
- const extension = material.extensions?.KHR_materials_transmission;
2078
- if (!extension) return {};
2079
- return {
2080
- transmission: {
2081
- factor: resolveUnit(extension.transmissionFactor, 0, `material ${materialIndex} KHR_materials_transmission.transmissionFactor`),
2082
- texture: resolveTextureInfo(textures, extension.transmissionTexture, `material ${materialIndex} KHR_materials_transmission.transmissionTexture`)
2083
- }
2084
- };
2085
- }
2086
-
2087
- function resolveDiffuseTransmission(
2088
- material: GLTFMaterial,
2089
- textures: readonly GLTFTextureAsset[],
2090
- pbrSpecularGlossiness: GLTFPBRSpecularGlossinessMaterialExtension | undefined,
2091
- materialIndex: number
2092
- ): { readonly diffuseTransmission?: GLTFDiffuseTransmissionMaterialExtension } {
2093
- const extension = material.extensions?.KHR_materials_diffuse_transmission;
2094
- if (!extension) return {};
2095
- if (material.extensions?.KHR_materials_unlit !== undefined) {
2096
- throw new Error(`glTF material ${materialIndex} KHR_materials_diffuse_transmission must not be combined with KHR_materials_unlit`);
2097
- }
2098
- if (pbrSpecularGlossiness) {
2099
- throw new Error(`glTF material ${materialIndex} KHR_materials_diffuse_transmission must not be combined with KHR_materials_pbrSpecularGlossiness`);
2100
- }
2101
- return {
2102
- diffuseTransmission: {
2103
- factor: resolveUnit(
2104
- extension.diffuseTransmissionFactor,
2105
- 0,
2106
- `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionFactor`
2107
- ),
2108
- texture: resolveTextureInfo(
2109
- textures,
2110
- extension.diffuseTransmissionTexture,
2111
- `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionTexture`
2112
- ),
2113
- colorFactor: resolveColor3(
2114
- extension.diffuseTransmissionColorFactor,
2115
- [1, 1, 1],
2116
- `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionColorFactor`
2117
- ),
2118
- colorTexture: resolveTextureInfo(
2119
- textures,
2120
- extension.diffuseTransmissionColorTexture,
2121
- `material ${materialIndex} KHR_materials_diffuse_transmission.diffuseTransmissionColorTexture`
2122
- )
2123
- }
2124
- };
2125
- }
2126
-
2127
- function resolveVolume(
2128
- material: GLTFMaterial,
2129
- textures: readonly GLTFTextureAsset[],
2130
- materialIndex: number
2131
- ): { readonly volume?: GLTFVolumeMaterialExtension } {
2132
- const extension = material.extensions?.KHR_materials_volume;
2133
- if (!extension) return {};
2134
- return {
2135
- volume: {
2136
- thicknessFactor: resolveNonNegative(extension.thicknessFactor, 0, `material ${materialIndex} KHR_materials_volume.thicknessFactor`),
2137
- thicknessTexture: resolveTextureInfo(textures, extension.thicknessTexture, `material ${materialIndex} KHR_materials_volume.thicknessTexture`),
2138
- attenuationDistance: resolvePositive(extension.attenuationDistance, Number.POSITIVE_INFINITY, `material ${materialIndex} KHR_materials_volume.attenuationDistance`),
2139
- attenuationColor: resolveColor3(extension.attenuationColor, [1, 1, 1], `material ${materialIndex} KHR_materials_volume.attenuationColor`)
2140
- }
2141
- };
2142
- }
2143
-
2144
- function resolveIOR(material: GLTFMaterial, materialIndex: number): { readonly ior?: number } {
2145
- const extension = material.extensions?.KHR_materials_ior;
2146
- if (!extension) return {};
2147
- const ior = extension.ior ?? 1.5;
2148
- if (!Number.isFinite(ior) || ior < 1) {
2149
- throw new RangeError(`glTF material ${materialIndex} KHR_materials_ior.ior must be finite and at least 1`);
2150
- }
2151
- return { ior };
2152
- }
2153
-
2154
- function resolveSpecular(
2155
- material: GLTFMaterial,
2156
- textures: readonly GLTFTextureAsset[],
2157
- materialIndex: number
2158
- ): { readonly specular?: GLTFSpecularMaterialExtension } {
2159
- const extension = material.extensions?.KHR_materials_specular;
2160
- if (!extension) return {};
2161
- return {
2162
- specular: {
2163
- factor: resolveUnit(extension.specularFactor, 1, `material ${materialIndex} KHR_materials_specular.specularFactor`),
2164
- texture: resolveTextureInfo(textures, extension.specularTexture, `material ${materialIndex} KHR_materials_specular.specularTexture`),
2165
- colorFactor: resolveNonNegativeColor3(extension.specularColorFactor, [1, 1, 1], `material ${materialIndex} KHR_materials_specular.specularColorFactor`),
2166
- colorTexture: resolveTextureInfo(textures, extension.specularColorTexture, `material ${materialIndex} KHR_materials_specular.specularColorTexture`)
2167
- }
2168
- };
2169
- }
2170
-
2171
- function resolvePBRSpecularGlossiness(
2172
- material: GLTFMaterial,
2173
- textures: readonly GLTFTextureAsset[],
2174
- materialIndex: number
2175
- ): { readonly pbrSpecularGlossiness?: GLTFPBRSpecularGlossinessMaterialExtension } {
2176
- const extension = material.extensions?.KHR_materials_pbrSpecularGlossiness;
2177
- if (!extension) return {};
2178
- return {
2179
- pbrSpecularGlossiness: {
2180
- diffuseFactor: resolveColor4(
2181
- extension.diffuseFactor,
2182
- [1, 1, 1, 1],
2183
- `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.diffuseFactor`
2184
- ),
2185
- diffuseTexture: resolveTextureInfo(
2186
- textures,
2187
- extension.diffuseTexture,
2188
- `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.diffuseTexture`
2189
- ),
2190
- specularFactor: resolveColor3(
2191
- extension.specularFactor,
2192
- [1, 1, 1],
2193
- `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.specularFactor`
2194
- ),
2195
- glossinessFactor: resolveUnit(
2196
- extension.glossinessFactor,
2197
- 1,
2198
- `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.glossinessFactor`
2199
- ),
2200
- specularGlossinessTexture: resolveTextureInfo(
2201
- textures,
2202
- extension.specularGlossinessTexture,
2203
- `material ${materialIndex} KHR_materials_pbrSpecularGlossiness.specularGlossinessTexture`
2204
- )
2205
- }
2206
- };
2207
- }
2208
-
2209
- function resolveSheen(
2210
- material: GLTFMaterial,
2211
- textures: readonly GLTFTextureAsset[],
2212
- materialIndex: number
2213
- ): { readonly sheen?: GLTFSheenMaterialExtension } {
2214
- const extension = material.extensions?.KHR_materials_sheen;
2215
- if (!extension) return {};
2216
- return {
2217
- sheen: {
2218
- colorFactor: resolveColor3(extension.sheenColorFactor, [0, 0, 0], `material ${materialIndex} KHR_materials_sheen.sheenColorFactor`),
2219
- colorTexture: resolveTextureInfo(textures, extension.sheenColorTexture, `material ${materialIndex} KHR_materials_sheen.sheenColorTexture`),
2220
- roughnessFactor: resolveUnit(extension.sheenRoughnessFactor, 0, `material ${materialIndex} KHR_materials_sheen.sheenRoughnessFactor`),
2221
- roughnessTexture: resolveTextureInfo(textures, extension.sheenRoughnessTexture, `material ${materialIndex} KHR_materials_sheen.sheenRoughnessTexture`)
2222
- }
2223
- };
2224
- }
2225
-
2226
- function resolveAnisotropy(
2227
- material: GLTFMaterial,
2228
- textures: readonly GLTFTextureAsset[],
2229
- materialIndex: number
2230
- ): { readonly anisotropy?: GLTFAnisotropyMaterialExtension } {
2231
- const extension = material.extensions?.KHR_materials_anisotropy;
2232
- if (!extension) return {};
2233
- return {
2234
- anisotropy: {
2235
- strength: resolveUnit(extension.anisotropyStrength, 0, `material ${materialIndex} KHR_materials_anisotropy.anisotropyStrength`),
2236
- rotation: resolveFiniteNumber(extension.anisotropyRotation, 0, `material ${materialIndex} KHR_materials_anisotropy.anisotropyRotation`),
2237
- texture: resolveTextureInfo(textures, extension.anisotropyTexture, `material ${materialIndex} KHR_materials_anisotropy.anisotropyTexture`)
2238
- }
2239
- };
2240
- }
2241
-
2242
- function resolveIridescence(
2243
- material: GLTFMaterial,
2244
- textures: readonly GLTFTextureAsset[],
2245
- materialIndex: number
2246
- ): { readonly iridescence?: GLTFIridescenceMaterialExtension } {
2247
- const extension = material.extensions?.KHR_materials_iridescence;
2248
- if (!extension) return {};
2249
- const thicknessMinimum = resolveNonNegative(
2250
- extension.iridescenceThicknessMinimum,
2251
- 100,
2252
- `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMinimum`
2253
- );
2254
- const thicknessMaximum = resolveNonNegative(
2255
- extension.iridescenceThicknessMaximum,
2256
- 400,
2257
- `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMaximum`
2258
- );
2259
- if (thicknessMaximum < thicknessMinimum) {
2260
- throw new RangeError(
2261
- `glTF material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessMaximum must be greater than or equal to iridescenceThicknessMinimum`
2262
- );
2263
- }
2264
- return {
2265
- iridescence: {
2266
- factor: resolveUnit(extension.iridescenceFactor, 0, `material ${materialIndex} KHR_materials_iridescence.iridescenceFactor`),
2267
- texture: resolveTextureInfo(textures, extension.iridescenceTexture, `material ${materialIndex} KHR_materials_iridescence.iridescenceTexture`),
2268
- ior: resolveIridescenceIOR(extension.iridescenceIor, materialIndex),
2269
- thicknessMinimum,
2270
- thicknessMaximum,
2271
- thicknessTexture: resolveTextureInfo(
2272
- textures,
2273
- extension.iridescenceThicknessTexture,
2274
- `material ${materialIndex} KHR_materials_iridescence.iridescenceThicknessTexture`
2275
- )
2276
- }
2277
- };
2278
- }
2279
-
2280
- function resolveIridescenceIOR(value: number | undefined, materialIndex: number): number {
2281
- const ior = value ?? 1.3;
2282
- if (!Number.isFinite(ior) || ior < 1 || ior > 3) {
2283
- throw new RangeError(`glTF material ${materialIndex} KHR_materials_iridescence.iridescenceIor must be finite and within [1, 3]`);
2284
- }
2285
- return ior;
2286
- }
2287
-
2288
- function resolveDispersion(material: GLTFMaterial, materialIndex: number): { readonly dispersion?: number } {
2289
- const extension = material.extensions?.KHR_materials_dispersion;
2290
- if (!extension) return {};
2291
- return {
2292
- dispersion: resolveNonNegative(extension.dispersion, 0, `material ${materialIndex} KHR_materials_dispersion.dispersion`)
2293
- };
2294
- }
2295
-
2296
- function resolveTextureTransform(transform: GLTFTextureTransform): GLTFResolvedTextureTransform {
2297
- return {
2298
- offset: resolveFiniteVec2(transform.offset, [0, 0], "KHR_texture_transform.offset"),
2299
- scale: resolveFiniteVec2(transform.scale, [1, 1], "KHR_texture_transform.scale"),
2300
- rotation: resolveFiniteNumber(transform.rotation, 0, "KHR_texture_transform.rotation")
2301
- };
2302
- }
2303
-
2304
- function withDefaultScale<T extends GLTFResolvedTextureInfo | undefined>(
2305
- info: T,
2306
- scale: number | undefined
2307
- ): (GLTFResolvedTextureInfo & { readonly scale: number }) | undefined {
2308
- return info ? { ...info, scale: resolveNonNegative(scale, 1, "normalTexture.scale") } : undefined;
2309
- }
2310
-
2311
- function withDefaultStrength<T extends GLTFResolvedTextureInfo | undefined>(
2312
- info: T,
2313
- strength: number | undefined
2314
- ): (GLTFResolvedTextureInfo & { readonly strength: number }) | undefined {
2315
- return info ? { ...info, strength: resolveUnit(strength, 1, "occlusionTexture.strength") } : undefined;
2316
- }
2317
-
2318
- function resolveColor4(
2319
- value: readonly number[] | undefined,
2320
- fallback: readonly [number, number, number, number],
2321
- label: string
2322
- ): readonly [number, number, number, number] {
2323
- const channels = value ?? fallback;
2324
- if (channels.length !== 4 || channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 1)) {
2325
- throw new RangeError(`glTF ${label} must contain four finite values in [0, 1]`);
2326
- }
2327
- return [channels[0]!, channels[1]!, channels[2]!, channels[3]!];
2328
- }
2329
-
2330
- function resolveColor3(
2331
- value: readonly number[] | undefined,
2332
- fallback: readonly [number, number, number],
2333
- label: string
2334
- ): readonly [number, number, number] {
2335
- const channels = value ?? fallback;
2336
- if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel) || channel < 0 || channel > 1)) {
2337
- throw new RangeError(`glTF ${label} must contain three finite values in [0, 1]`);
2338
- }
2339
- return [channels[0]!, channels[1]!, channels[2]!];
2340
- }
2341
-
2342
- function resolveNonNegativeColor3(
2343
- value: readonly number[] | undefined,
2344
- fallback: readonly [number, number, number],
2345
- label: string
2346
- ): readonly [number, number, number] {
2347
- const channels = value ?? fallback;
2348
- if (channels.length !== 3 || channels.some((channel) => !Number.isFinite(channel) || channel < 0)) {
2349
- throw new RangeError(`glTF ${label} must contain three finite non-negative values`);
2350
- }
2351
- return [channels[0]!, channels[1]!, channels[2]!];
2352
- }
2353
-
2354
- function resolveUnit(value: number | undefined, fallback: number, label: string): number {
2355
- const resolved = value ?? fallback;
2356
- if (!Number.isFinite(resolved) || resolved < 0 || resolved > 1) {
2357
- throw new RangeError(`glTF ${label} must be finite and within [0, 1]`);
2358
- }
2359
- return resolved;
2360
- }
2361
-
2362
- function resolveNonNegative(value: number | undefined, fallback: number, label: string): number {
2363
- const resolved = value ?? fallback;
2364
- if (!Number.isFinite(resolved) || resolved < 0) {
2365
- throw new RangeError(`glTF ${label} must be finite and non-negative`);
2366
- }
2367
- return resolved;
2368
- }
2369
-
2370
- function resolvePositive(value: number | undefined, fallback: number, label: string): number {
2371
- const resolved = value ?? fallback;
2372
- if (resolved === Number.POSITIVE_INFINITY) return resolved;
2373
- if (!Number.isFinite(resolved) || resolved <= 0) {
2374
- throw new RangeError(`glTF ${label} must be finite and positive`);
2375
- }
2376
- return resolved;
2377
- }
2378
-
2379
- function resolveFiniteNumber(value: number | undefined, fallback: number, label: string): number {
2380
- const resolved = value ?? fallback;
2381
- if (!Number.isFinite(resolved)) {
2382
- throw new RangeError(`glTF ${label} must be finite`);
2383
- }
2384
- return resolved;
2385
- }
2386
-
2387
- function resolveFiniteVec2(
2388
- value: readonly number[] | undefined,
2389
- fallback: readonly [number, number],
2390
- label: string
2391
- ): readonly [number, number] {
2392
- const resolved = value ?? fallback;
2393
- if (resolved.length !== 2 || resolved.some((channel) => !Number.isFinite(channel))) {
2394
- throw new RangeError(`glTF ${label} must contain two finite values`);
2395
- }
2396
- return [resolved[0]!, resolved[1]!];
2397
- }
2398
-
2399
- function createSkinAssets(json: GLTFJson, buffers: readonly ArrayBuffer[], accessorCache: GLTFAccessorReadCache): readonly GLTFSkinAsset[] {
2400
- if (!json.skins || json.skins.length === 0) return [];
2401
- const nodeWorldMatrices = computeGLTFNodeWorldMatrices(json);
2402
- const nodeNameForIndex = createGLTFNodeNameResolver(json);
2403
- return (json.skins ?? []).map((skin, skinIndex) => {
2404
- if (skin.joints.length === 0) {
2405
- throw new Error(`glTF skin ${skinIndex} requires at least one joint`);
2406
- }
2407
- validateSkinDescriptor(json, skin, skinIndex);
2408
- const parentByNode = parentIndexByNode(json);
2409
- const inverseBindMatrices = skin.inverseBindMatrices === undefined
2410
- ? []
2411
- : readAccessor(json, buffers, skin.inverseBindMatrices, accessorCache).map(toMat4);
2412
- if (inverseBindMatrices.length > 0 && inverseBindMatrices.length !== skin.joints.length) {
2413
- throw new Error(`glTF skin ${skinIndex} inverseBindMatrices count must match joints count`);
2414
- }
2415
- const jointSet = new Set(skin.joints);
2416
- const orderedJoints = orderSkinJoints(json, skin.joints);
2417
- const boneIndexByNode = new Map<number, number>();
2418
- for (let index = 0; index < orderedJoints.length; index += 1) {
2419
- boneIndexByNode.set(orderedJoints[index]!, index);
2420
- }
2421
- const skeleton = new Skeleton(orderedJoints.map((nodeIndex) => {
2422
- const node = json.nodes?.[nodeIndex];
2423
- if (!node) {
2424
- throw new Error(`glTF skin ${skinIndex} references missing joint node ${nodeIndex}`);
2425
- }
2426
- const parentNode = parentByNode.get(nodeIndex);
2427
- const parentIndex = parentNode !== undefined && jointSet.has(parentNode) ? boneIndexByNode.get(parentNode) ?? -1 : -1;
2428
- const transform = resolveNodeTransform(node, nodeIndex);
2429
- return {
2430
- name: nodeNameForIndex(nodeIndex),
2431
- parentIndex,
2432
- translation: transform.translation,
2433
- rotation: transform.rotation,
2434
- scale: transform.scale,
2435
- inverseBindMatrix: inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4()
2436
- };
2437
- }));
2438
- return {
2439
- name: skin.name ?? `skin-${skinIndex}`,
2440
- joints: skin.joints,
2441
- jointNames: skin.joints.map((nodeIndex) => nodeNameForIndex(nodeIndex)),
2442
- inverseBindMatrices: skin.joints.map((nodeIndex) => inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4()),
2443
- skeletonRoot: skin.skeleton,
2444
- skeleton,
2445
- jointBindMatrices: skin.joints.map((nodeIndex) => multiplyLoaderMat4(
2446
- nodeWorldMatrices.get(nodeIndex) ?? identityMat4(),
2447
- inverseBindMatrices[skin.joints.indexOf(nodeIndex)] ?? identityMat4()
2448
- )),
2449
- ...(skin.joints.length <= MAX_RENDERABLE_SKIN_JOINTS ? { skinningPalette: buildGLTFSkinningPalette(skeleton, orderedJoints, skin.joints) } : {})
2450
- };
2451
- });
2452
- }
2453
-
2454
- function computeGLTFNodeWorldMatrices(json: GLTFJson): ReadonlyMap<number, Mat4> {
2455
- const nodes = json.nodes ?? [];
2456
- const parents = parentIndexByNode(json);
2457
- const matrices = new Map<number, Mat4>();
2458
- const visiting = new Set<number>();
2459
- const resolve = (nodeIndex: number): Mat4 => {
2460
- const existing = matrices.get(nodeIndex);
2461
- if (existing) return existing;
2462
- if (visiting.has(nodeIndex)) {
2463
- throw new Error(`glTF node graph contains a cycle at node ${nodeIndex}`);
2464
- }
2465
- const node = nodes[nodeIndex];
2466
- if (!node) return identityMat4();
2467
- visiting.add(nodeIndex);
2468
- const transform = resolveNodeTransform(node, nodeIndex);
2469
- const local = composeLoaderMat4(transform.translation, transform.rotation, transform.scale);
2470
- const parent = parents.get(nodeIndex);
2471
- const world = parent === undefined ? local : multiplyLoaderMat4(resolve(parent), local);
2472
- matrices.set(nodeIndex, world);
2473
- visiting.delete(nodeIndex);
2474
- return world;
2475
- };
2476
- for (let index = 0; index < nodes.length; index += 1) {
2477
- resolve(index);
2478
- }
2479
- return matrices;
2480
- }
2481
-
2482
- function buildGLTFSkinningPalette(skeleton: Skeleton, orderedJoints: readonly number[], gltfJoints: readonly number[]): SkinningPalette {
2483
- const orderedPalette = skeleton.matrixPalette();
2484
- const paletteByNode = new Map<number, Mat4>();
2485
- for (let index = 0; index < orderedJoints.length; index += 1) {
2486
- paletteByNode.set(orderedJoints[index]!, orderedPalette[index]!);
2487
- }
2488
- const matrices = new Float32Array(gltfJoints.length * 16);
2489
- for (let index = 0; index < gltfJoints.length; index += 1) {
2490
- const matrix = paletteByNode.get(gltfJoints[index]!);
2491
- if (!matrix) {
2492
- throw new Error(`glTF skin references missing ordered joint ${gltfJoints[index]}`);
2493
- }
2494
- matrices.set(matrix, index * 16);
2495
- }
2496
- return { jointCount: gltfJoints.length, matrices };
2497
- }
2498
-
2499
- function validateSkinDescriptor(json: GLTFJson, skin: GLTFSkin, skinIndex: number): void {
2500
- const seen = new Set<number>();
2501
- for (const joint of skin.joints) {
2502
- if (!Number.isInteger(joint) || joint < 0) {
2503
- throw new Error(`glTF skin ${skinIndex} joint indices must be non-negative integers`);
2504
- }
2505
- if (seen.has(joint)) {
2506
- throw new Error(`glTF skin ${skinIndex} contains duplicate joint ${joint}`);
2507
- }
2508
- seen.add(joint);
2509
- }
2510
- if (skin.skeleton !== undefined && (!Number.isInteger(skin.skeleton) || skin.skeleton < 0 || !json.nodes?.[skin.skeleton])) {
2511
- throw new Error(`glTF skin ${skinIndex} references missing skeleton node ${skin.skeleton}`);
2512
- }
2513
- if (skin.inverseBindMatrices !== undefined && (!Number.isInteger(skin.inverseBindMatrices) || skin.inverseBindMatrices < 0)) {
2514
- throw new Error(`glTF skin ${skinIndex} inverseBindMatrices must reference a non-negative accessor index`);
2515
- }
2516
- }
2517
-
2518
- function createNodeInstanceTransforms(json: GLTFJson, buffers: readonly ArrayBuffer[], accessorCache: GLTFAccessorReadCache): ReadonlyMap<number, Float32Array> {
2519
- const transforms = new Map<number, Float32Array>();
2520
- for (const [nodeIndex, node] of (json.nodes ?? []).entries()) {
2521
- const extension = node.extensions?.EXT_mesh_gpu_instancing;
2522
- if (!extension) continue;
2523
- if (!isObjectRecord(extension.attributes) || Array.isArray(extension.attributes)) {
2524
- throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing.attributes must be an object`);
2525
- }
2526
- const allowed = new Set(["TRANSLATION", "ROTATION", "SCALE"]);
2527
- for (const semantic of Object.keys(extension.attributes)) {
2528
- if (!allowed.has(semantic)) {
2529
- throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing attribute ${semantic} is unsupported`);
2530
- }
2531
- const accessorIndex = extension.attributes[semantic];
2532
- if (!Number.isInteger(accessorIndex) || accessorIndex < 0) {
2533
- throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing ${semantic} accessor must be a non-negative integer`);
2534
- }
2535
- }
2536
- const translations = readOptionalAccessor(json, buffers, extension.attributes.TRANSLATION, accessorCache);
2537
- const rotations = readOptionalAccessor(json, buffers, extension.attributes.ROTATION, accessorCache);
2538
- const scales = readOptionalAccessor(json, buffers, extension.attributes.SCALE, accessorCache);
2539
- const instanceCount = firstPositiveLength([translations.length, rotations.length, scales.length]);
2540
- if (instanceCount === 0) {
2541
- throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing requires TRANSLATION, ROTATION, or SCALE attributes`);
2542
- }
2543
- validateInstanceAttributeCount("TRANSLATION", translations.length, instanceCount, nodeIndex);
2544
- validateInstanceAttributeCount("ROTATION", rotations.length, instanceCount, nodeIndex);
2545
- validateInstanceAttributeCount("SCALE", scales.length, instanceCount, nodeIndex);
2546
- const packed = new Float32Array(instanceCount * 16);
2547
- for (let instanceIndex = 0; instanceIndex < instanceCount; instanceIndex += 1) {
2548
- const translation = toInstanceVec3(translations[instanceIndex], [0, 0, 0], `node ${nodeIndex} EXT_mesh_gpu_instancing TRANSLATION`);
2549
- const rotation = toInstanceQuat(rotations[instanceIndex], [0, 0, 0, 1], `node ${nodeIndex} EXT_mesh_gpu_instancing ROTATION`);
2550
- const scale = toInstanceVec3(scales[instanceIndex], [1, 1, 1], `node ${nodeIndex} EXT_mesh_gpu_instancing SCALE`);
2551
- packed.set(composeMat4(translation, rotation, scale), instanceIndex * 16);
2552
- }
2553
- transforms.set(nodeIndex, packed);
2554
- }
2555
- return transforms;
2556
- }
2557
-
2558
- function firstPositiveLength(lengths: readonly number[]): number {
2559
- return lengths.find((length) => length > 0) ?? 0;
2560
- }
2561
-
2562
- function validateInstanceAttributeCount(semantic: string, count: number, instanceCount: number, nodeIndex: number): void {
2563
- if (count > 0 && count !== instanceCount) {
2564
- throw new Error(`glTF node ${nodeIndex} EXT_mesh_gpu_instancing ${semantic} count must match instance count`);
2565
- }
2566
- }
2567
-
2568
- function toInstanceVec3(row: readonly number[] | undefined, fallback: SceneVec3, label: string): SceneVec3 {
2569
- if (!row) return fallback;
2570
- if (row.length < 3 || row.slice(0, 3).some((value) => !Number.isFinite(value))) {
2571
- throw new Error(`glTF ${label} must contain three finite values`);
2572
- }
2573
- return [row[0]!, row[1]!, row[2]!];
2574
- }
2575
-
2576
- function toInstanceQuat(row: readonly number[] | undefined, fallback: SceneQuat, label: string): SceneQuat {
2577
- if (!row) return fallback;
2578
- if (row.length < 4 || row.slice(0, 4).some((value) => !Number.isFinite(value))) {
2579
- throw new Error(`glTF ${label} must contain four finite values`);
2580
- }
2581
- return [row[0]!, row[1]!, row[2]!, row[3]!];
2582
- }
2583
-
2584
- function createAnimationClips(json: GLTFJson, buffers: readonly ArrayBuffer[], accessorCache: GLTFAccessorReadCache): readonly AnimationClip[] {
2585
- const nodeNameForIndex = createGLTFNodeNameResolver(json);
2586
- return (json.animations ?? []).map((animation, animationIndex) => {
2587
- const tracks = animation.channels.flatMap((channel, channelIndex) => {
2588
- const sampler = animation.samplers[channel.sampler];
2589
- if (!sampler) {
2590
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} references missing sampler ${channel.sampler}`);
2591
- }
2592
- if (isOptionalAnimationPointerChannel(json, channel)) {
2593
- return [];
2594
- }
2595
- if (channel.target.node === undefined || !json.nodes?.[channel.target.node]) {
2596
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} references missing target node ${channel.target.node ?? "undefined"}`);
2597
- }
2598
- const interpolation = resolveAnimationInterpolation(sampler.interpolation, animationIndex, channelIndex);
2599
- const path = resolveAnimationTargetPath(channel.target.path, animationIndex, channelIndex);
2600
- const times = readAccessor(json, buffers, sampler.input, accessorCache).map((row) => row[0] ?? 0);
2601
- const output = readAccessor(json, buffers, sampler.output, accessorCache);
2602
- const values = animationKeyframesForSampler(path, times, output, interpolation, animationIndex, channelIndex, morphWeightCountForAnimationTarget(json, channel.target.node));
2603
- const valueType = valueTypeForAnimationPath(path);
2604
- return [new AnimationTrack({
2605
- target: `${nodeNameForIndex(channel.target.node)}.${path}`,
2606
- valueType,
2607
- keyframes: values
2608
- })];
2609
- });
2610
- return new AnimationClip({ name: animation.name ?? `animation-${animationIndex}`, tracks });
2611
- });
2612
- }
2613
-
2614
- function isOptionalAnimationPointerChannel(json: GLTFJson, channel: GLTFAnimationChannel): boolean {
2615
- return channel.target.path === "pointer" &&
2616
- json.extensionsUsed?.includes("KHR_animation_pointer") === true &&
2617
- json.extensionsRequired?.includes("KHR_animation_pointer") !== true &&
2618
- typeof channel.target.extensions?.KHR_animation_pointer?.pointer === "string";
2619
- }
2620
-
2621
- function resolveAnimationInterpolation(
2622
- value: string | undefined,
2623
- animationIndex: number,
2624
- channelIndex: number
2625
- ): "LINEAR" | "STEP" | "CUBICSPLINE" {
2626
- if (value === undefined || value === "LINEAR" || value === "STEP" || value === "CUBICSPLINE") {
2627
- return value ?? "LINEAR";
2628
- }
2629
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} interpolation ${value} is unsupported`);
2630
- }
2631
-
2632
- function resolveAnimationTargetPath(
2633
- path: string,
2634
- animationIndex: number,
2635
- channelIndex: number
2636
- ): GLTFAnimationChannel["target"]["path"] {
2637
- if (path === "translation" || path === "rotation" || path === "scale" || path === "weights") {
2638
- return path;
2639
- }
2640
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} target path ${path} is unsupported`);
2641
- }
2642
-
2643
- function animationKeyframesForSampler(
2644
- path: GLTFAnimationChannel["target"]["path"],
2645
- times: readonly number[],
2646
- output: readonly (readonly number[])[],
2647
- interpolation: "LINEAR" | "STEP" | "CUBICSPLINE",
2648
- animationIndex: number,
2649
- channelIndex: number,
2650
- morphWeightCount?: number
2651
- ): readonly {
2652
- readonly time: number;
2653
- readonly value: number | Vec3 | Quat | readonly number[];
2654
- readonly interpolation: "step" | "linear" | "cubicspline";
2655
- readonly inTangent?: number | Vec3 | Quat | readonly number[];
2656
- readonly outTangent?: number | Vec3 | Quat | readonly number[];
2657
- }[] {
2658
- const normalizedOutput = path === "weights" ? normalizeWeightAnimationOutput(times, output, interpolation, animationIndex, channelIndex, morphWeightCount) : output;
2659
- if (interpolation !== "CUBICSPLINE") {
2660
- if (times.length !== normalizedOutput.length) {
2661
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} input/output count mismatch`);
2662
- }
2663
- const samples = sanitizeAnimationSamples(times, normalizedOutput, 1);
2664
- const mode = interpolation === "STEP" ? "step" : "linear";
2665
- return samples.times.map((time, index) => ({
2666
- time,
2667
- value: animationValueForPath(path, samples.output[index]!),
2668
- interpolation: mode
2669
- }));
2670
- }
2671
-
2672
- if (normalizedOutput.length !== times.length * 3) {
2673
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} CUBICSPLINE output count must be three times input count`);
2674
- }
2675
- const samples = sanitizeAnimationSamples(times, normalizedOutput, 3);
2676
- return samples.times.map((time, index) => {
2677
- const offset = index * 3;
2678
- return {
2679
- time,
2680
- inTangent: animationValueForPath(path, samples.output[offset]!),
2681
- value: animationValueForPath(path, samples.output[offset + 1]!),
2682
- outTangent: animationValueForPath(path, samples.output[offset + 2]!),
2683
- interpolation: "cubicspline"
2684
- };
2685
- });
2686
- }
2687
-
2688
- function sanitizeAnimationSamples<T>(
2689
- times: readonly number[],
2690
- output: readonly T[],
2691
- rowsPerSample: 1 | 3
2692
- ): { readonly times: readonly number[]; readonly output: readonly T[] } {
2693
- const cleanTimes: number[] = [];
2694
- const cleanOutput: T[] = [];
2695
- let previous = -Number.MAX_VALUE;
2696
- for (let index = 0; index < times.length; index += 1) {
2697
- const time = times[index] ?? 0;
2698
- if (!Number.isFinite(time) || time < 0 || time <= previous) {
2699
- continue;
2700
- }
2701
- cleanTimes.push(time);
2702
- for (let row = 0; row < rowsPerSample; row += 1) {
2703
- const value = output[index * rowsPerSample + row];
2704
- if (value !== undefined) cleanOutput.push(value);
2705
- }
2706
- previous = time;
2707
- }
2708
- return cleanTimes.length > 0 ? { times: cleanTimes, output: cleanOutput } : { times, output };
2709
- }
2710
-
2711
- function morphWeightCountForAnimationTarget(json: GLTFJson, nodeIndex: number | undefined): number | undefined {
2712
- const node = nodeIndex === undefined ? undefined : json.nodes?.[nodeIndex];
2713
- const mesh = node?.mesh === undefined ? undefined : json.meshes?.[node.mesh];
2714
- if (!mesh) return undefined;
2715
- if (mesh.weights && mesh.weights.length > 0) return mesh.weights.length;
2716
- return Math.max(0, ...mesh.primitives.map((primitive) => primitive.targets?.length ?? 0)) || undefined;
2717
- }
2718
-
2719
- function normalizeWeightAnimationOutput(
2720
- times: readonly number[],
2721
- output: readonly (readonly number[])[],
2722
- interpolation: "LINEAR" | "STEP" | "CUBICSPLINE",
2723
- animationIndex: number,
2724
- channelIndex: number,
2725
- morphWeightCount: number | undefined
2726
- ): readonly (readonly number[])[] {
2727
- const samplesPerTime = interpolation === "CUBICSPLINE" ? 3 : 1;
2728
- const expectedRows = times.length * samplesPerTime;
2729
- if (output.length === expectedRows) return output;
2730
- if (times.length === 0 || output.length % expectedRows !== 0) {
2731
- return output;
2732
- }
2733
- const width = morphWeightCount ?? (output.length / expectedRows);
2734
- if (!Number.isInteger(width) || width <= 0 || output.length !== expectedRows * width) {
2735
- throw new Error(`glTF animation ${animationIndex} channel ${channelIndex} weights output count does not match morph target count`);
2736
- }
2737
- const grouped: number[][] = [];
2738
- for (let sampleIndex = 0; sampleIndex < expectedRows; sampleIndex += 1) {
2739
- const values: number[] = [];
2740
- for (let component = 0; component < width; component += 1) {
2741
- values.push(output[sampleIndex * width + component]?.[0] ?? 0);
2742
- }
2743
- grouped.push(values);
2744
- }
2745
- return grouped;
2746
- }
2747
-
2748
- interface GLTFDocument {
2749
- readonly json: GLTFJson;
2750
- readonly url: string;
2751
- readonly byteLength: number;
2752
- readonly binaryChunk?: ArrayBuffer;
2753
- }
2754
-
2755
- async function loadDocument(request: AssetLoadRequest): Promise<GLTFDocument> {
2756
- const binary = request.url.startsWith("data:model/gltf-binary") || /\.glb(?:\?.*)?$/i.test(request.url);
2757
-
2758
- if (request.url.startsWith("data:")) {
2759
- const data = decodeDataUri(request.url);
2760
- if (binary) {
2761
- return parseGLB(data, request.url);
2762
- }
2763
- const text = new TextDecoder().decode(data);
2764
- return { json: JSON.parse(text) as GLTFJson, url: request.url, byteLength: data.byteLength };
2765
- }
2766
-
2767
- if (typeof fetch !== "function") {
2768
- throw new Error("GLTFLoader requires fetch for non-data URLs");
2769
- }
2770
-
2771
- const response = await fetch(request.url, { signal: request.signal });
2772
- if (!response.ok) {
2773
- throw new Error(`glTF request failed with ${response.status}`);
2774
- }
2775
-
2776
- if (binary) {
2777
- return parseGLB(await readResponseBytes(response, request.url, "document", request), request.url);
2778
- }
2779
-
2780
- const bytes = await readResponseBytes(response, request.url, "document", request);
2781
- return { json: JSON.parse(new TextDecoder().decode(bytes)) as GLTFJson, url: request.url, byteLength: bytes.byteLength };
2782
- }
2783
-
2784
- async function loadBuffer(
2785
- buffer: GLTFBuffer,
2786
- index: number,
2787
- document: GLTFDocument,
2788
- request: AssetLoadRequest
2789
- ): Promise<ArrayBuffer> {
2790
- validateBufferDescriptor(index, buffer);
2791
- if (!buffer.uri) {
2792
- if (index === 0 && document.binaryChunk) {
2793
- if (buffer.byteLength > document.binaryChunk.byteLength) {
2794
- throw new Error(`glTF buffer 0 declares ${buffer.byteLength} bytes but GLB BIN chunk has ${document.binaryChunk.byteLength}`);
2795
- }
2796
- return buffer.byteLength === document.binaryChunk.byteLength
2797
- ? document.binaryChunk
2798
- : document.binaryChunk.slice(0, buffer.byteLength);
2799
- }
2800
- throw new Error(`glTF buffer ${index} is missing a uri and no GLB BIN chunk is available`);
2801
- }
2802
-
2803
- if (buffer.uri.startsWith("data:")) {
2804
- return validateLoadedBufferLength(index, buffer, decodeBufferDataUri(index, buffer.uri));
2805
- }
2806
-
2807
- if (typeof fetch !== "function") {
2808
- throw new Error("GLTFLoader requires fetch for external buffers");
2809
- }
2810
-
2811
- const url = new URL(buffer.uri, document.url).toString();
2812
- const response = await fetch(url, { signal: request.signal });
2813
- if (!response.ok) {
2814
- throw new Error(`glTF buffer request failed with ${response.status}`);
2815
- }
2816
- return validateLoadedBufferLength(index, buffer, await readResponseBytes(response, url, "buffer", request));
2817
- }
2818
-
2819
- function validateBufferDescriptor(index: number, buffer: GLTFBuffer): void {
2820
- if (!Number.isInteger(buffer.byteLength) || buffer.byteLength < 0) {
2821
- throw new Error(`glTF buffer ${index} byteLength must be a non-negative integer`);
2822
- }
2823
- }
2824
-
2825
- function decodeBufferDataUri(index: number, uri: string): ArrayBuffer {
2826
- const separator = uri.indexOf(",");
2827
- if (separator < 0) {
2828
- throw new Error(`glTF buffer ${index} data uri must include a comma separator`);
2829
- }
2830
- const header = uri.slice(5, separator).toLowerCase();
2831
- const mediaType = header.split(";")[0] ?? "";
2832
- if (mediaType && mediaType !== "application/octet-stream" && mediaType !== "application/gltf-buffer") {
2833
- throw new Error(`glTF buffer ${index} data uri media type must be application/octet-stream or application/gltf-buffer`);
2834
- }
2835
- const payload = uri.slice(separator + 1);
2836
- if (header.endsWith(";base64")) {
2837
- return decodeBufferBase64DataUri(index, payload);
2838
- }
2839
- try {
2840
- return new TextEncoder().encode(decodeURIComponent(payload)).buffer;
2841
- } catch (error) {
2842
- throw new Error(`glTF buffer ${index} data uri payload must be valid URI encoding: ${error instanceof Error ? error.message : String(error)}`);
2843
- }
2844
- }
2845
-
2846
- function decodeBufferBase64DataUri(index: number, payload: string): ArrayBuffer {
2847
- let decodedPayload: string;
2848
- try {
2849
- decodedPayload = decodeURIComponent(payload);
2850
- } catch (error) {
2851
- throw new Error(`glTF buffer ${index} data uri base64 payload must be valid URI encoding: ${error instanceof Error ? error.message : String(error)}`);
2852
- }
2853
- const compactPayload = decodedPayload.replace(/\s+/g, "");
2854
- const paddingIndex = compactPayload.indexOf("=");
2855
- const paddingIsTerminal = paddingIndex < 0 || /^={1,2}$/.test(compactPayload.slice(paddingIndex));
2856
- if (
2857
- compactPayload.length % 4 === 1 ||
2858
- !paddingIsTerminal ||
2859
- !/^[A-Za-z0-9+/]*={0,2}$/.test(compactPayload)
2860
- ) {
2861
- throw new Error(`glTF buffer ${index} data uri base64 payload is malformed`);
2862
- }
2863
-
2864
- let binary: string;
2865
- try {
2866
- binary = atob(compactPayload);
2867
- } catch (error) {
2868
- throw new Error(`glTF buffer ${index} data uri base64 payload is malformed: ${error instanceof Error ? error.message : String(error)}`);
2869
- }
2870
- const bytes = new Uint8Array(binary.length);
2871
- for (let i = 0; i < binary.length; i += 1) {
2872
- bytes[i] = binary.charCodeAt(i);
2873
- }
2874
- return bytes.buffer;
2875
- }
2876
-
2877
- function validateLoadedBufferLength(index: number, buffer: GLTFBuffer, data: ArrayBuffer): ArrayBuffer {
2878
- if (data.byteLength !== buffer.byteLength) {
2879
- throw new Error(`glTF buffer ${index} declares ${buffer.byteLength} bytes but loaded ${data.byteLength}`);
2880
- }
2881
- return data;
2882
- }
2883
-
2884
- async function prepareBufferViews(
2885
- json: GLTFJson,
2886
- buffers: readonly ArrayBuffer[],
2887
- meshoptDecoder: GLTFMeshoptDecoder | undefined
2888
- ): Promise<{ readonly json: GLTFJson; readonly buffers: readonly ArrayBuffer[] }> {
2889
- const bufferViews = json.bufferViews ?? [];
2890
- const compressedViewEntries = bufferViews
2891
- .map((view, index) => ({ view, index, meshopt: resolveMeshoptCompressionExtension(view) }))
2892
- .filter((entry): entry is { readonly view: GLTFBufferView; readonly index: number; readonly meshopt: { readonly name: typeof GLTF_MESHOPT_EXTENSION_NAMES[number]; readonly extension: GLTFMeshoptCompressionExtension } } => entry.meshopt !== undefined);
2893
- if (compressedViewEntries.length === 0) {
2894
- return { json, buffers };
2895
- }
2896
- if (!usesGLTFMeshoptExtension(json)) {
2897
- throw new Error("glTF uses meshopt-compressed bufferViews but meshopt extension is not declared");
2898
- }
2899
- if (!meshoptDecoder) {
2900
- throw new Error("glTF meshopt compression requires a meshoptDecoder");
2901
- }
2902
-
2903
- const preparedBuffers = [...buffers];
2904
- const preparedBufferViews = [...bufferViews];
2905
- for (const { index, meshopt } of compressedViewEntries) {
2906
- const { name, extension } = meshopt;
2907
- validateMeshoptDescriptor(json, buffers, index, name, extension);
2908
- const sourceBuffer = buffers[extension.buffer]!;
2909
- const byteOffset = extension.byteOffset ?? 0;
2910
- const source = new Uint8Array(sourceBuffer, byteOffset, extension.byteLength);
2911
- const descriptor: GLTFMeshoptDecodeDescriptor = {
2912
- bufferViewIndex: index,
2913
- byteStride: extension.byteStride,
2914
- count: extension.count,
2915
- mode: extension.mode,
2916
- filter: extension.filter ?? "NONE"
2917
- };
2918
- const decoded = toOwnedArrayBuffer(await meshoptDecoder(source, descriptor));
2919
- const expectedByteLength = extension.count * extension.byteStride;
2920
- if (decoded.byteLength !== expectedByteLength) {
2921
- throw new Error(`glTF bufferView ${index} EXT_meshopt_compression decoded ${decoded.byteLength} bytes but expected ${expectedByteLength}`);
2922
- }
2923
- const decodedBufferIndex = preparedBuffers.length;
2924
- preparedBuffers.push(decoded);
2925
- preparedBufferViews[index] = {
2926
- buffer: decodedBufferIndex,
2927
- byteOffset: 0,
2928
- byteLength: decoded.byteLength,
2929
- ...(extension.mode === "ATTRIBUTES" ? { byteStride: extension.byteStride } : {})
2930
- };
2931
- }
2932
-
2933
- return {
2934
- json: { ...json, bufferViews: preparedBufferViews },
2935
- buffers: preparedBuffers
2936
- };
2937
- }
2938
-
2939
- function resolveMeshoptCompressionExtension(view: GLTFBufferView): { readonly name: typeof GLTF_MESHOPT_EXTENSION_NAMES[number]; readonly extension: GLTFMeshoptCompressionExtension } | undefined {
2940
- for (const name of GLTF_MESHOPT_EXTENSION_NAMES) {
2941
- const extension = view.extensions?.[name];
2942
- if (extension) return { name, extension };
2943
- }
2944
- return undefined;
2945
- }
2946
-
2947
- function validateMeshoptDescriptor(
2948
- json: GLTFJson,
2949
- buffers: readonly ArrayBuffer[],
2950
- bufferViewIndex: number,
2951
- extensionName: string,
2952
- extension: GLTFMeshoptCompressionExtension
2953
- ): void {
2954
- if (!Number.isInteger(extension.buffer) || extension.buffer < 0 || !json.buffers?.[extension.buffer] || !buffers[extension.buffer]) {
2955
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} buffer must reference an existing buffer`);
2956
- }
2957
- if (extension.byteOffset !== undefined && (!Number.isInteger(extension.byteOffset) || extension.byteOffset < 0)) {
2958
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteOffset must be a non-negative integer`);
2959
- }
2960
- if (!Number.isInteger(extension.byteLength) || extension.byteLength < 0) {
2961
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteLength must be a non-negative integer`);
2962
- }
2963
- if (!Number.isInteger(extension.byteStride) || extension.byteStride <= 0) {
2964
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} byteStride must be a positive integer`);
2965
- }
2966
- if (!Number.isInteger(extension.count) || extension.count < 0) {
2967
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} count must be a non-negative integer`);
2968
- }
2969
- if (extension.mode !== "ATTRIBUTES" && extension.mode !== "TRIANGLES" && extension.mode !== "INDICES") {
2970
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} mode ${String(extension.mode)} is unsupported`);
2971
- }
2972
- if (
2973
- extension.filter !== undefined &&
2974
- extension.filter !== "NONE" &&
2975
- extension.filter !== "OCTAHEDRAL" &&
2976
- extension.filter !== "QUATERNION" &&
2977
- extension.filter !== "EXPONENTIAL" &&
2978
- extension.filter !== "COLOR"
2979
- ) {
2980
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} filter ${String(extension.filter)} is unsupported`);
2981
- }
2982
- const sourceBuffer = buffers[extension.buffer]!;
2983
- const sourceStart = extension.byteOffset ?? 0;
2984
- const sourceEnd = sourceStart + extension.byteLength;
2985
- if (sourceEnd > sourceBuffer.byteLength) {
2986
- throw new Error(`glTF bufferView ${bufferViewIndex} ${extensionName} source range exceeds buffer ${extension.buffer}`);
2987
- }
2988
- }
2989
-
2990
- function toOwnedArrayBuffer(value: ArrayBuffer | Uint8Array): ArrayBuffer {
2991
- if (value instanceof ArrayBuffer) {
2992
- return value.slice(0);
2993
- }
2994
- const copy = new Uint8Array(value.byteLength);
2995
- copy.set(value);
2996
- return copy.buffer;
2997
- }
2998
-
2999
- async function readResponseBytes(
3000
- response: Response,
3001
- url: string,
3002
- phase: AssetLoadProgress["phase"],
3003
- request: AssetLoadRequest
3004
- ): Promise<ArrayBuffer> {
3005
- const totalBytes = parseContentLength(response.headers?.get("content-length"));
3006
- const reader = response.body?.getReader();
3007
-
3008
- if (!reader) {
3009
- const buffer = await response.arrayBuffer();
3010
- request.onProgress?.({ url, phase: "complete", loadedBytes: buffer.byteLength, ...(totalBytes === undefined ? {} : { totalBytes }) });
3011
- return buffer;
3012
- }
3013
-
3014
- const chunks: Uint8Array[] = [];
3015
- let loadedBytes = 0;
3016
-
3017
- while (true) {
3018
- if (request.signal?.aborted) {
3019
- throw request.signal.reason instanceof Error ? request.signal.reason : new Error(`glTF request aborted for ${url}`);
3020
- }
3021
- const { done, value } = await reader.read();
3022
- if (done) break;
3023
- chunks.push(value);
3024
- loadedBytes += value.byteLength;
3025
- request.onProgress?.({ url, phase, loadedBytes, ...(totalBytes === undefined ? {} : { totalBytes }) });
3026
- }
3027
-
3028
- const buffer = new Uint8Array(loadedBytes);
3029
- let offset = 0;
3030
- for (const chunk of chunks) {
3031
- buffer.set(chunk, offset);
3032
- offset += chunk.byteLength;
3033
- }
3034
- request.onProgress?.({ url, phase: "complete", loadedBytes, ...(totalBytes === undefined ? {} : { totalBytes }) });
3035
- return buffer.buffer;
3036
- }
3037
-
3038
- function parseContentLength(value: string | null | undefined): number | undefined {
3039
- if (!value) return undefined;
3040
- const parsed = Number(value);
3041
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
3042
- }
3043
-
3044
- function decodeDataUri(uri: string): ArrayBuffer {
3045
- const [header = "", payload = ""] = uri.split(",", 2);
3046
- if (header.endsWith(";base64")) {
3047
- let binary: string;
3048
- try {
3049
- binary = atob(payload);
3050
- } catch (error) {
3051
- throw new Error(`Invalid base64 data URI payload: ${error instanceof Error ? error.message : String(error)}`);
3052
- }
3053
- const bytes = new Uint8Array(binary.length);
3054
- for (let i = 0; i < binary.length; i += 1) {
3055
- bytes[i] = binary.charCodeAt(i);
3056
- }
3057
- return bytes.buffer;
3058
- }
3059
- return new TextEncoder().encode(decodeURIComponent(payload)).buffer;
3060
- }
3061
-
3062
- const GLB_MAGIC = 0x46546c67;
3063
- const GLB_VERSION = 2;
3064
- const GLB_CHUNK_JSON = 0x4e4f534a;
3065
- const GLB_CHUNK_BIN = 0x004e4942;
3066
-
3067
- function parseGLB(data: ArrayBuffer, url: string): GLTFDocument {
3068
- const view = new DataView(data);
3069
- if (data.byteLength < 20) {
3070
- throw new Error("GLB file is too small");
3071
- }
3072
-
3073
- const magic = view.getUint32(0, true);
3074
- const version = view.getUint32(4, true);
3075
- const declaredLength = view.getUint32(8, true);
3076
- if (magic !== GLB_MAGIC) {
3077
- throw new Error("Invalid GLB magic");
3078
- }
3079
- if (version !== GLB_VERSION) {
3080
- throw new Error(`Unsupported GLB version: ${version}`);
3081
- }
3082
- if (declaredLength !== data.byteLength) {
3083
- throw new Error(`GLB length mismatch: header ${declaredLength}, actual ${data.byteLength}`);
3084
- }
3085
-
3086
- let offset = 12;
3087
- let json: GLTFJson | undefined;
3088
- let binaryChunk: ArrayBuffer | undefined;
3089
-
3090
- while (offset < data.byteLength) {
3091
- if (offset + 8 > data.byteLength) {
3092
- throw new Error("Malformed GLB chunk header");
3093
- }
3094
- const chunkLength = view.getUint32(offset, true);
3095
- const chunkType = view.getUint32(offset + 4, true);
3096
- const chunkStart = offset + 8;
3097
- const chunkEnd = chunkStart + chunkLength;
3098
- if (chunkEnd > data.byteLength) {
3099
- throw new Error("Malformed GLB chunk length");
3100
- }
3101
-
3102
- const chunk = data.slice(chunkStart, chunkEnd);
3103
- if (chunkType === GLB_CHUNK_JSON) {
3104
- const text = new TextDecoder().decode(chunk).replace(/[\u0000\s]+$/u, "");
3105
- json = JSON.parse(text) as GLTFJson;
3106
- } else if (chunkType === GLB_CHUNK_BIN) {
3107
- binaryChunk = chunk;
3108
- }
3109
-
3110
- offset = chunkEnd;
3111
- }
3112
-
3113
- if (!json) {
3114
- throw new Error("GLB is missing a JSON chunk");
3115
- }
3116
-
3117
- return { json, binaryChunk, url, byteLength: data.byteLength };
3118
- }
3119
-
3120
- function readAccessor(json: GLTFJson, buffers: readonly ArrayBuffer[], accessorIndex: number, accessorCache?: GLTFAccessorReadCache): number[][] {
3121
- if (accessorCache) accessorCache.profile.accessorReadCount += 1;
3122
- const cached = accessorCache?.rowsByAccessor.get(accessorIndex);
3123
- if (cached && accessorCache) {
3124
- accessorCache.profile.accessorCacheHitCount++;
3125
- return cached;
3126
- }
3127
- const startedAt = nowMs();
3128
- const accessor = json.accessors?.[accessorIndex];
3129
- if (!accessor) {
3130
- throw new Error(`Missing glTF accessor ${accessorIndex}`);
3131
- }
3132
- validateAccessorShape(accessorIndex, accessor);
3133
- const componentCount = componentCountForType(accessor.type);
3134
- const componentBytes = componentByteSize(accessor.componentType);
3135
- const rowByteLength = componentCount * componentBytes;
3136
- validateAccessorDescriptor(accessorIndex, accessor, rowByteLength);
3137
- const output = Array.from({ length: accessor.count }, () => new Array<number>(componentCount).fill(0));
3138
-
3139
- if (accessor.bufferView !== undefined) {
3140
- const { view, buffer } = getBufferView(json, buffers, accessor.bufferView);
3141
- const dataView = dataViewForBuffer(buffer, accessorCache);
3142
- const stride = view.byteStride ?? rowByteLength;
3143
- if (stride < rowByteLength) {
3144
- throw new Error(`glTF accessor ${accessorIndex} byteStride ${stride} is smaller than element size ${rowByteLength}`);
3145
- }
3146
- const offset = (view.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
3147
- validateAccessorBounds(accessorIndex, buffer, view, offset, accessor.count, stride, rowByteLength);
3148
-
3149
- for (let item = 0; item < accessor.count; item += 1) {
3150
- const baseOffset = offset + item * stride;
3151
- for (let component = 0; component < componentCount; component += 1) {
3152
- output[item]![component] = readComponent(dataView, baseOffset + component * componentBytes, accessor.componentType, accessor.normalized ?? false);
3153
- }
3154
- }
3155
- }
3156
-
3157
- if (accessor.sparse) {
3158
- applySparseAccessor(json, buffers, accessorIndex, accessor, output, componentCount, componentBytes, accessorCache);
3159
- }
3160
-
3161
- if (accessorCache) {
3162
- recordAccessorRead(accessorCache, accessorIndex, accessor, componentCount, output.length, nowMs() - startedAt);
3163
- accessorCache.rowsByAccessor.set(accessorIndex, output);
3164
- }
3165
- return output;
3166
- }
3167
-
3168
- function dataViewForBuffer(buffer: ArrayBuffer, accessorCache?: GLTFAccessorReadCache): DataView {
3169
- if (!accessorCache) return new DataView(buffer);
3170
- const existing = accessorCache.dataViews.get(buffer);
3171
- if (existing) return existing;
3172
- const view = new DataView(buffer);
3173
- accessorCache.dataViews.set(buffer, view);
3174
- accessorCache.profile.dataViewCacheEntries = accessorCache.dataViews.size;
3175
- return view;
3176
- }
3177
-
3178
- function recordAccessorRead(
3179
- accessorCache: GLTFAccessorReadCache,
3180
- accessorIndex: number,
3181
- accessor: GLTFAccessor,
3182
- componentCount: number,
3183
- rowCount: number,
3184
- elapsedMs: number
3185
- ): void {
3186
- const profile = accessorCache.profile;
3187
- const byteLength = rowCount * componentCount * componentByteSize(accessor.componentType);
3188
- profile.uniqueAccessorReadCount++;
3189
- profile.accessorRowsRead += rowCount;
3190
- profile.accessorValuesRead += rowCount * componentCount;
3191
- profile.accessorReadMs += elapsedMs;
3192
- profile.largestAccessors.push({
3193
- accessorIndex,
3194
- count: accessor.count,
3195
- type: accessor.type,
3196
- componentType: accessor.componentType,
3197
- componentCount,
3198
- byteLength,
3199
- ...(accessor.bufferView !== undefined ? { bufferView: accessor.bufferView } : {})
3200
- });
3201
- profile.largestAccessors.sort((left, right) => right.byteLength - left.byteLength);
3202
- if (profile.largestAccessors.length > 8) {
3203
- profile.largestAccessors.length = 8;
3204
- }
3205
- }
3206
-
3207
- function validateAccessorShape(accessorIndex: number, accessor: GLTFAccessor): void {
3208
- if (!isAccessorType(accessor.type)) {
3209
- throw new Error(`glTF accessor ${accessorIndex} type ${String(accessor.type)} is unsupported`);
3210
- }
3211
- if (!isAccessorComponentType(accessor.componentType)) {
3212
- throw new Error(`glTF accessor ${accessorIndex} componentType ${String(accessor.componentType)} is unsupported`);
3213
- }
3214
- if (accessor.bufferView !== undefined && (!Number.isInteger(accessor.bufferView) || accessor.bufferView < 0)) {
3215
- throw new Error(`glTF accessor ${accessorIndex} bufferView must be a non-negative integer`);
3216
- }
3217
- if (accessor.sparse) {
3218
- validateSparseAccessorShape(accessorIndex, accessor.sparse);
3219
- }
3220
- }
3221
-
3222
- function validateAccessorDescriptor(accessorIndex: number, accessor: GLTFAccessor, rowByteLength: number): void {
3223
- if (!Number.isInteger(accessor.count) || accessor.count < 0) {
3224
- throw new Error(`glTF accessor ${accessorIndex} count must be a non-negative integer`);
3225
- }
3226
- if (accessor.byteOffset !== undefined && (!Number.isInteger(accessor.byteOffset) || accessor.byteOffset < 0)) {
3227
- throw new Error(`glTF accessor ${accessorIndex} byteOffset must be a non-negative integer`);
3228
- }
3229
- if (accessor.sparse) {
3230
- if (!Number.isInteger(accessor.sparse.count) || accessor.sparse.count < 0) {
3231
- throw new Error(`glTF accessor ${accessorIndex} sparse count must be a non-negative integer`);
3232
- }
3233
- if (accessor.sparse.count > accessor.count) {
3234
- throw new Error(`glTF accessor ${accessorIndex} sparse count exceeds accessor count`);
3235
- }
3236
- if (accessor.sparse.indices.byteOffset !== undefined && (!Number.isInteger(accessor.sparse.indices.byteOffset) || accessor.sparse.indices.byteOffset < 0)) {
3237
- throw new Error(`glTF accessor ${accessorIndex} sparse indices byteOffset must be a non-negative integer`);
3238
- }
3239
- if (accessor.sparse.values.byteOffset !== undefined && (!Number.isInteger(accessor.sparse.values.byteOffset) || accessor.sparse.values.byteOffset < 0)) {
3240
- throw new Error(`glTF accessor ${accessorIndex} sparse values byteOffset must be a non-negative integer`);
3241
- }
3242
- }
3243
- if (rowByteLength <= 0) {
3244
- throw new Error(`glTF accessor ${accessorIndex} has invalid element size`);
3245
- }
3246
- }
3247
-
3248
- function validateSparseAccessorShape(accessorIndex: number, sparse: GLTFSparseAccessor): void {
3249
- const record = sparse as unknown as Record<string, unknown>;
3250
- if (!isObjectRecord(record.indices)) {
3251
- throw new Error(`glTF accessor ${accessorIndex} sparse indices must be an object`);
3252
- }
3253
- if (!isObjectRecord(record.values)) {
3254
- throw new Error(`glTF accessor ${accessorIndex} sparse values must be an object`);
3255
- }
3256
- if (!isSparseIndexComponentType(sparse.indices.componentType)) {
3257
- throw new Error(`glTF accessor ${accessorIndex} sparse indices componentType ${String(sparse.indices.componentType)} is unsupported`);
3258
- }
3259
- if (!Number.isInteger(sparse.indices.bufferView) || sparse.indices.bufferView < 0) {
3260
- throw new Error(`glTF accessor ${accessorIndex} sparse indices bufferView must be a non-negative integer`);
3261
- }
3262
- if (!Number.isInteger(sparse.values.bufferView) || sparse.values.bufferView < 0) {
3263
- throw new Error(`glTF accessor ${accessorIndex} sparse values bufferView must be a non-negative integer`);
3264
- }
3265
- }
3266
-
3267
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
3268
- return typeof value === "object" && value !== null;
3269
- }
3270
-
3271
- function applySparseAccessor(
3272
- json: GLTFJson,
3273
- buffers: readonly ArrayBuffer[],
3274
- accessorIndex: number,
3275
- accessor: GLTFAccessor,
3276
- output: number[][],
3277
- componentCount: number,
3278
- componentBytes: number,
3279
- accessorCache?: GLTFAccessorReadCache
3280
- ): void {
3281
- const sparse = accessor.sparse;
3282
- if (!sparse || sparse.count === 0) {
3283
- return;
3284
- }
3285
- const { view: indexView, buffer: indexBuffer } = getBufferView(json, buffers, sparse.indices.bufferView);
3286
- const indexDataView = dataViewForBuffer(indexBuffer, accessorCache);
3287
- const indexComponentBytes = componentByteSize(sparse.indices.componentType);
3288
- const indexStart = (indexView.byteOffset ?? 0) + (sparse.indices.byteOffset ?? 0);
3289
- validateRawRange(`glTF accessor ${accessorIndex} sparse indices`, indexBuffer, indexView, indexStart, sparse.count * indexComponentBytes);
3290
-
3291
- const { view: valueView, buffer: valueBuffer } = getBufferView(json, buffers, sparse.values.bufferView);
3292
- const valueDataView = dataViewForBuffer(valueBuffer, accessorCache);
3293
- const valueStart = (valueView.byteOffset ?? 0) + (sparse.values.byteOffset ?? 0);
3294
- const rowByteLength = componentCount * componentBytes;
3295
- validateRawRange(`glTF accessor ${accessorIndex} sparse values`, valueBuffer, valueView, valueStart, sparse.count * rowByteLength);
3296
-
3297
- for (let item = 0; item < sparse.count; item += 1) {
3298
- const targetIndex = readComponent(indexDataView, indexStart + item * indexComponentBytes, sparse.indices.componentType, false);
3299
- if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= accessor.count) {
3300
- throw new Error(`glTF accessor ${accessorIndex} sparse index ${targetIndex} is out of range`);
3301
- }
3302
- const valueOffset = valueStart + item * rowByteLength;
3303
- for (let component = 0; component < componentCount; component += 1) {
3304
- output[targetIndex]![component] = readComponent(valueDataView, valueOffset + component * componentBytes, accessor.componentType, accessor.normalized ?? false);
3305
- }
3306
- }
3307
- }
3308
-
3309
- function getBufferView(json: GLTFJson, buffers: readonly ArrayBuffer[], bufferViewIndex: number): { readonly view: GLTFBufferView; readonly buffer: ArrayBuffer } {
3310
- if (!Number.isInteger(bufferViewIndex) || bufferViewIndex < 0) {
3311
- throw new Error(`glTF bufferView ${String(bufferViewIndex)} must be a non-negative integer`);
3312
- }
3313
- const view = json.bufferViews?.[bufferViewIndex];
3314
- if (!view) {
3315
- throw new Error(`Missing glTF bufferView ${bufferViewIndex}`);
3316
- }
3317
- validateBufferViewDescriptor(bufferViewIndex, view);
3318
- const buffer = buffers[view.buffer];
3319
- if (!buffer) {
3320
- throw new Error(`Missing glTF buffer ${view.buffer}`);
3321
- }
3322
- const start = view.byteOffset ?? 0;
3323
- const end = start + view.byteLength;
3324
- if (end > buffer.byteLength) {
3325
- throw new Error(`glTF bufferView ${bufferViewIndex} exceeds buffer ${view.buffer}`);
3326
- }
3327
- return { view, buffer };
3328
- }
3329
-
3330
- function validateBufferViewDescriptor(bufferViewIndex: number, view: GLTFBufferView): void {
3331
- if (!Number.isInteger(view.buffer) || view.buffer < 0) {
3332
- throw new Error(`glTF bufferView ${bufferViewIndex} buffer must be a non-negative integer`);
3333
- }
3334
- if (view.byteOffset !== undefined && (!Number.isInteger(view.byteOffset) || view.byteOffset < 0)) {
3335
- throw new Error(`glTF bufferView ${bufferViewIndex} byteOffset must be a non-negative integer`);
3336
- }
3337
- if (!Number.isInteger(view.byteLength) || view.byteLength < 0) {
3338
- throw new Error(`glTF bufferView ${bufferViewIndex} byteLength must be a non-negative integer`);
3339
- }
3340
- if (
3341
- view.byteStride !== undefined &&
3342
- (!Number.isInteger(view.byteStride) || view.byteStride < 4 || view.byteStride > 252 || view.byteStride % 4 !== 0)
3343
- ) {
3344
- throw new Error(`glTF bufferView ${bufferViewIndex} byteStride must be a 4-byte aligned integer between 4 and 252`);
3345
- }
3346
- }
3347
-
3348
- function validateAccessorBounds(
3349
- accessorIndex: number,
3350
- buffer: ArrayBuffer,
3351
- view: GLTFBufferView,
3352
- start: number,
3353
- count: number,
3354
- stride: number,
3355
- rowByteLength: number
3356
- ): void {
3357
- const byteLength = count === 0 ? 0 : (count - 1) * stride + rowByteLength;
3358
- validateRawRange(`glTF accessor ${accessorIndex}`, buffer, view, start, byteLength);
3359
- }
3360
-
3361
- function validateRawRange(label: string, buffer: ArrayBuffer, view: GLTFBufferView, start: number, byteLength: number): void {
3362
- if (!Number.isInteger(start) || start < 0 || !Number.isInteger(byteLength) || byteLength < 0) {
3363
- throw new Error(`${label} byte range must be non-negative integers`);
3364
- }
3365
- const viewStart = view.byteOffset ?? 0;
3366
- const viewEnd = viewStart + view.byteLength;
3367
- const end = start + byteLength;
3368
- if (end > viewEnd || end > buffer.byteLength) {
3369
- throw new Error(`${label} exceeds its glTF bufferView`);
3370
- }
3371
- }
3372
-
3373
- function readOptionalAccessor(json: GLTFJson, buffers: readonly ArrayBuffer[], accessorIndex: number | undefined, accessorCache?: GLTFAccessorReadCache): number[][] {
3374
- return accessorIndex === undefined ? [] : readAccessor(json, buffers, accessorIndex, accessorCache);
3375
- }
3376
-
3377
- function readBufferViewBytes(json: GLTFJson, buffers: readonly ArrayBuffer[], bufferViewIndex: number): ArrayBuffer {
3378
- const { view, buffer } = getBufferView(json, buffers, bufferViewIndex);
3379
- const start = view.byteOffset ?? 0;
3380
- const end = start + view.byteLength;
3381
- return buffer.slice(start, end);
3382
- }
3383
-
3384
- function isAccessorType(type: unknown): type is GLTFAccessorType {
3385
- return type === "SCALAR" || type === "VEC2" || type === "VEC3" || type === "VEC4" || type === "MAT4";
3386
- }
3387
-
3388
- function isAccessorComponentType(type: unknown): type is GLTFComponentType {
3389
- return type === 5120 || type === 5121 || type === 5122 || type === 5123 || type === 5125 || type === 5126;
3390
- }
3391
-
3392
- function isSparseIndexComponentType(type: unknown): type is GLTFSparseIndexComponentType {
3393
- return type === 5121 || type === 5123 || type === 5125;
3394
- }
3395
-
3396
- function componentCountForType(type: GLTFAccessorType): number {
3397
- return type === "SCALAR" ? 1 : type === "VEC2" ? 2 : type === "VEC3" ? 3 : type === "VEC4" ? 4 : 16;
3398
- }
3399
-
3400
- function componentByteSize(type: GLTFComponentType): number {
3401
- if (type === 5120 || type === 5121) return 1;
3402
- if (type === 5122 || type === 5123) return 2;
3403
- return 4;
3404
- }
3405
-
3406
- function readComponent(view: DataView, byteOffset: number, type: GLTFComponentType, normalized: boolean): number {
3407
- if (type === 5120) {
3408
- const value = view.getInt8(byteOffset);
3409
- return normalized ? Math.max(value / 127, -1) : value;
3410
- }
3411
- if (type === 5121) {
3412
- const value = view.getUint8(byteOffset);
3413
- return normalized ? value / 255 : value;
3414
- }
3415
- if (type === 5122) {
3416
- const value = view.getInt16(byteOffset, true);
3417
- return normalized ? Math.max(value / 32767, -1) : value;
3418
- }
3419
- if (type === 5126) return view.getFloat32(byteOffset, true);
3420
- if (type === 5125) return view.getUint32(byteOffset, true);
3421
- const value = view.getUint16(byteOffset, true);
3422
- return normalized ? value / 65535 : value;
3423
- }
3424
-
3425
- function resolvePrimitiveMaterialIndex(
3426
- primitive: GLTFPrimitive,
3427
- materials: readonly GLTFMaterialAsset[],
3428
- meshIndex: number,
3429
- primitiveIndex: number
3430
- ): number | undefined {
3431
- if (primitive.material === undefined) {
3432
- return undefined;
3433
- }
3434
- if (!Number.isInteger(primitive.material) || primitive.material < 0) {
3435
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} material must be a non-negative integer`);
3436
- }
3437
- if (!materials[primitive.material]) {
3438
- throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} references missing material ${primitive.material}`);
3439
- }
3440
- return primitive.material;
3441
- }
3442
-
3443
- function materialName(material: GLTFMaterialAsset | undefined, index: number | undefined): string {
3444
- return index === undefined ? DEFAULT_GLTF_MATERIAL_NAME : material?.name ?? `material-${index}`;
3445
- }
3446
-
3447
- function meshPrimitiveName(mesh: GLTFMesh, meshIndex: number, primitiveIndex: number): string {
3448
- const base = mesh.name ?? `mesh-${meshIndex}`;
3449
- return mesh.primitives.length === 1 ? base : `${base}-primitive-${primitiveIndex}`;
3450
- }
3451
-
3452
- function resolveNodeTransform(
3453
- node: GLTFNode,
3454
- nodeIndex: number
3455
- ): { readonly translation: Vec3; readonly rotation: Quat; readonly scale: Vec3 } {
3456
- if (node.matrix !== undefined) {
3457
- if (node.translation !== undefined || node.rotation !== undefined || node.scale !== undefined) {
3458
- throw new Error(`glTF node ${nodeIndex} cannot combine matrix with translation, rotation, or scale`);
3459
- }
3460
- const decomposed = decomposeMat4(resolveNodeMatrix(node.matrix, nodeIndex));
3461
- return {
3462
- translation: decomposed.position,
3463
- rotation: decomposed.rotation,
3464
- scale: decomposed.scale
3465
- };
3466
- }
3467
- return {
3468
- translation: resolveVec3(node.translation, [0, 0, 0], `node ${nodeIndex} translation`),
3469
- rotation: resolveQuat(node.rotation, [0, 0, 0, 1], `node ${nodeIndex} rotation`),
3470
- scale: resolveVec3(node.scale, [1, 1, 1], `node ${nodeIndex} scale`)
3471
- };
3472
- }
3473
-
3474
- function resolveNodeMatrix(matrix: readonly number[], nodeIndex: number): SceneMat4 {
3475
- if (matrix.length !== 16 || matrix.some((value) => !Number.isFinite(value))) {
3476
- throw new RangeError(`glTF node ${nodeIndex} matrix must contain 16 finite values`);
3477
- }
3478
- return [
3479
- matrix[0]!, matrix[1]!, matrix[2]!, matrix[3]!,
3480
- matrix[4]!, matrix[5]!, matrix[6]!, matrix[7]!,
3481
- matrix[8]!, matrix[9]!, matrix[10]!, matrix[11]!,
3482
- matrix[12]!, matrix[13]!, matrix[14]!, matrix[15]!
3483
- ];
3484
- }
3485
-
3486
- function resolveVec3(value: readonly number[] | undefined, fallback: Vec3, label: string): Vec3 {
3487
- const resolved = value ?? fallback;
3488
- if (resolved.length !== 3 || resolved.some((component) => !Number.isFinite(component))) {
3489
- throw new RangeError(`glTF ${label} must contain three finite values`);
3490
- }
3491
- return [resolved[0]!, resolved[1]!, resolved[2]!];
3492
- }
3493
-
3494
- function resolveQuat(value: readonly number[] | undefined, fallback: Quat, label: string): Quat {
3495
- const resolved = value ?? fallback;
3496
- if (resolved.length !== 4 || resolved.some((component) => !Number.isFinite(component))) {
3497
- throw new RangeError(`glTF ${label} must contain four finite values`);
3498
- }
3499
- return [resolved[0]!, resolved[1]!, resolved[2]!, resolved[3]!];
3500
- }
3501
-
3502
- function createScene(
3503
- json: GLTFJson,
3504
- meshes: readonly GLTFMeshAsset[],
3505
- cameras: readonly GLTFCameraAsset[],
3506
- lights: readonly GLTFLightAsset[],
3507
- skins: readonly GLTFSkinAsset[],
3508
- nodeInstanceTransforms: ReadonlyMap<number, Float32Array>,
3509
- materialVariants: readonly GLTFMaterialVariantAsset[],
3510
- options: GLTFSceneCreateOptions = {}
3511
- ): Scene {
3512
- const sceneIndex = resolveSelectedSceneIndex(json, options);
3513
- validateSceneGraph(json, sceneIndex);
3514
- validateSceneMaterialVariant(materialVariants, options.materialVariant);
3515
- const materialKeyForRenderable = createRuntimeMaterialKeyResolver(json, sceneIndex, meshes, skins, nodeInstanceTransforms, options.materialVariant);
3516
- const nodeWorldMatrices = computeGLTFNodeWorldMatrices(json);
3517
- const nodeNameForIndex = createGLTFNodeNameResolver(json);
3518
- const scene = new Scene();
3519
- const nodes = new Map<number, SceneNode>();
3520
- const createNode = (index: number): SceneNode => {
3521
- const source = json.nodes?.[index];
3522
- if (!source) {
3523
- throw new Error(`Missing glTF node ${index}`);
3524
- }
3525
-
3526
- const existing = nodes.get(index);
3527
- if (existing) {
3528
- return existing;
3529
- }
3530
-
3531
- const node = createSceneNodeForGLTFNode(scene, source, index, nodeNameForIndex(index), cameras, lights);
3532
- const transform = resolveNodeTransform(source, index);
3533
- node.transform.setPosition(transform.translation[0], transform.translation[1], transform.translation[2]);
3534
- node.transform.setRotation(transform.rotation[0], transform.rotation[1], transform.rotation[2], transform.rotation[3]);
3535
- node.transform.setScale(transform.scale[0], transform.scale[1], transform.scale[2]);
3536
- if (source.mesh !== undefined) {
3537
- attachMeshPrimitives(scene, node, source.mesh, index, meshes, skins, source.skin, nodeWorldMatrices.get(index), nodeInstanceTransforms.get(index), materialKeyForRenderable);
3538
- }
3539
- nodes.set(index, node);
3540
- for (const childIndex of source.children ?? []) {
3541
- node.addChild(createNode(childIndex));
3542
- }
3543
- return node;
3544
- };
3545
-
3546
- const sceneDefinition = json.scenes?.[sceneIndex];
3547
- for (const nodeIndex of sceneDefinition?.nodes ?? []) {
3548
- scene.root.addChild(createNode(nodeIndex));
3549
- }
3550
- return scene;
3551
- }
3552
-
3553
- function resolveSelectedSceneIndex(json: GLTFJson, options: GLTFSceneCreateOptions): number {
3554
- if (options.sceneIndex !== undefined && options.sceneName !== undefined) {
3555
- throw new Error("glTF scene selection cannot specify both sceneIndex and sceneName");
3556
- }
3557
- if (options.sceneName !== undefined) {
3558
- const sceneIndex = (json.scenes ?? []).findIndex((scene, index) => (scene.name ?? `scene-${index}`) === options.sceneName);
3559
- if (sceneIndex < 0) {
3560
- throw new Error(`glTF scene named ${options.sceneName} is not defined`);
3561
- }
3562
- return sceneIndex;
3563
- }
3564
- if (options.sceneIndex !== undefined) {
3565
- if (!Number.isInteger(options.sceneIndex) || options.sceneIndex < 0 || !json.scenes?.[options.sceneIndex]) {
3566
- throw new Error(`glTF scene ${options.sceneIndex} is missing`);
3567
- }
3568
- return options.sceneIndex;
3569
- }
3570
- return resolveDefaultSceneIndex(json);
3571
- }
3572
-
3573
- function validateSceneMaterialVariant(
3574
- materialVariants: readonly GLTFMaterialVariantAsset[],
3575
- selectedVariant: string | undefined
3576
- ): void {
3577
- if (selectedVariant === undefined) return;
3578
- if (!materialVariants.some((variant) => variant.name === selectedVariant)) {
3579
- throw new Error(`glTF material variant ${selectedVariant} is not defined`);
3580
- }
3581
- }
3582
-
3583
- function validateSceneGraph(json: GLTFJson, sceneIndex = resolveDefaultSceneIndex(json)): void {
3584
- const nodeDefinitions = json.nodes ?? [];
3585
- if (json.scenes && (!Number.isInteger(sceneIndex) || sceneIndex < 0 || !json.scenes[sceneIndex])) throw new Error(`glTF scene ${sceneIndex} is missing`);
3586
-
3587
- const parentByNode = new Map<number, number>();
3588
- for (const [nodeIndex, node] of nodeDefinitions.entries()) {
3589
- for (const childIndex of node.children ?? []) {
3590
- if (!Number.isInteger(childIndex) || childIndex < 0 || !nodeDefinitions[childIndex]) {
3591
- throw new Error(`glTF node ${nodeIndex} references missing child node ${childIndex}`);
3592
- }
3593
- const existingParent = parentByNode.get(childIndex);
3594
- if (existingParent !== undefined) {
3595
- throw new Error(`glTF node ${childIndex} has multiple parents: ${existingParent} and ${nodeIndex}`);
3596
- }
3597
- parentByNode.set(childIndex, nodeIndex);
3598
- }
3599
- }
3600
-
3601
- const visiting = new Set<number>();
3602
- const visited = new Set<number>();
3603
- const visit = (nodeIndex: number): void => {
3604
- if (visiting.has(nodeIndex)) {
3605
- throw new Error(`glTF node graph contains a cycle at node ${nodeIndex}`);
3606
- }
3607
- if (visited.has(nodeIndex)) {
3608
- return;
3609
- }
3610
- visiting.add(nodeIndex);
3611
- for (const childIndex of nodeDefinitions[nodeIndex]?.children ?? []) {
3612
- visit(childIndex);
3613
- }
3614
- visiting.delete(nodeIndex);
3615
- visited.add(nodeIndex);
3616
- };
3617
- for (let nodeIndex = 0; nodeIndex < nodeDefinitions.length; nodeIndex += 1) {
3618
- visit(nodeIndex);
3619
- }
3620
-
3621
- for (const [rootListIndex, rootNodeIndex] of (json.scenes?.[sceneIndex]?.nodes ?? []).entries()) {
3622
- if (!Number.isInteger(rootNodeIndex) || rootNodeIndex < 0 || !nodeDefinitions[rootNodeIndex]) {
3623
- throw new Error(`glTF scene ${sceneIndex} root ${rootListIndex} references missing node ${rootNodeIndex}`);
3624
- }
3625
- if (parentByNode.has(rootNodeIndex)) {
3626
- throw new Error(`glTF scene ${sceneIndex} root ${rootNodeIndex} is also a child node`);
3627
- }
3628
- }
3629
- }
3630
-
3631
- function attachMeshPrimitives(
3632
- scene: Scene,
3633
- node: SceneNode,
3634
- meshIndex: number,
3635
- nodeIndex: number,
3636
- meshes: readonly GLTFMeshAsset[],
3637
- skins: readonly GLTFSkinAsset[],
3638
- nodeSkinIndex?: number,
3639
- nodeWorldMatrix?: Mat4,
3640
- instanceTransforms?: Float32Array,
3641
- materialKeyForRenderable?: (mesh: GLTFMeshAsset, contract: GLTFRuntimeMaterialContract) => string
3642
- ): void {
3643
- if (!Number.isInteger(meshIndex) || meshIndex < 0) {
3644
- throw new RangeError(`glTF node ${nodeIndex} mesh must be a non-negative integer`);
3645
- }
3646
- const primitives = meshes.filter((mesh) => mesh.sourceMeshIndex === meshIndex);
3647
- if (primitives.length === 0) {
3648
- throw new Error(`glTF node ${nodeIndex} references missing mesh ${meshIndex}`);
3649
- }
3650
- if (primitives.length === 1) {
3651
- const mesh = primitives[0]!;
3652
- const skinning = skinningForMesh(mesh, skins, nodeSkinIndex, nodeWorldMatrix);
3653
- scene.addRenderable(node, new Renderable({
3654
- geometry: mesh.name,
3655
- material: materialKeyForRenderable?.(mesh, runtimeMaterialContract(skinning, instanceTransforms)) ?? mesh.material,
3656
- ...(skinning ? { skinning } : {}),
3657
- morphWeights: mesh.morphWeights,
3658
- ...(instanceTransforms ? { instanceTransforms } : {})
3659
- }));
3660
- return;
3661
- }
3662
- for (const mesh of primitives) {
3663
- const skinning = skinningForMesh(mesh, skins, nodeSkinIndex, nodeWorldMatrix);
3664
- const primitiveNode = scene.createNode(mesh.name);
3665
- primitiveNode.transform.setPosition(0, 0, 0);
3666
- primitiveNode.transform.setRotation(0, 0, 0, 1);
3667
- primitiveNode.transform.setScale(1, 1, 1);
3668
- scene.addRenderable(primitiveNode, new Renderable({
3669
- geometry: mesh.name,
3670
- material: materialKeyForRenderable?.(mesh, runtimeMaterialContract(skinning, instanceTransforms)) ?? mesh.material,
3671
- ...(skinning ? { skinning } : {}),
3672
- morphWeights: mesh.morphWeights,
3673
- ...(instanceTransforms ? { instanceTransforms } : {})
3674
- }));
3675
- node.addChild(primitiveNode);
3676
- }
3677
- }
3678
-
3679
- function skinningForMesh(mesh: GLTFMeshAsset, skins: readonly GLTFSkinAsset[], nodeSkinIndex?: number, nodeWorldMatrix?: Mat4): { readonly jointCount: number; readonly matrices: Float32Array } | undefined {
3680
- const skinIndex = nodeSkinIndex;
3681
- if (skinIndex === undefined) return undefined;
3682
- if (mesh.joints.length === 0 || mesh.weights.length === 0) {
3683
- throw new Error(`glTF mesh ${mesh.name} references skin ${skinIndex} but is missing JOINTS_0 or WEIGHTS_0 attributes`);
3684
- }
3685
- const skin = skins[skinIndex];
3686
- if (!skin) {
3687
- throw new Error(`glTF mesh ${mesh.name} references missing skin ${skinIndex}`);
3688
- }
3689
- if (skin.joints.length > MAX_RENDERABLE_SKIN_JOINTS) {
3690
- return undefined;
3691
- }
3692
- if (!nodeWorldMatrix) return skin.skinningPalette;
3693
- const inverseMeshWorld = invertLoaderMat4(nodeWorldMatrix);
3694
- const matrices = new Float32Array(skin.jointBindMatrices.length * 16);
3695
- for (let index = 0; index < skin.jointBindMatrices.length; index += 1) {
3696
- matrices.set(multiplyLoaderMat4(inverseMeshWorld, skin.jointBindMatrices[index]!), index * 16);
3697
- }
3698
- return { jointCount: skin.jointBindMatrices.length, matrices };
3699
- }
3700
-
3701
- function composeLoaderMat4(translation: Vec3, rotation: Quat, scale: Vec3): Mat4 {
3702
- return composeMat4(
3703
- [...translation] as SceneVec3,
3704
- [...rotation] as SceneQuat,
3705
- [...scale] as SceneVec3
3706
- ) as unknown as Mat4;
3707
- }
3708
-
3709
- function multiplyLoaderMat4(left: Mat4, right: Mat4): Mat4 {
3710
- return multiplyMat4([...left] as SceneMat4, [...right] as SceneMat4) as unknown as Mat4;
3711
- }
3712
-
3713
- function invertLoaderMat4(matrix: Mat4): Mat4 {
3714
- return invertMat4([...matrix] as SceneMat4) as unknown as Mat4;
3715
- }
3716
-
3717
- export function gltfRuntimeMaterialKey(material: string, contract: GLTFRuntimeMaterialContract): string {
3718
- const signature = runtimeMaterialSignature(contract);
3719
- return signature === "base" ? material : `${material}${RUNTIME_MATERIAL_KEY_MARKER}${signature}`;
3720
- }
3721
-
3722
- export function parseGLTFRuntimeMaterialKey(key: string): GLTFRuntimeMaterialKey {
3723
- const markerIndex = key.lastIndexOf(RUNTIME_MATERIAL_KEY_MARKER);
3724
- if (markerIndex < 0) return { material: key, contract: {} };
3725
- const signature = key.slice(markerIndex + RUNTIME_MATERIAL_KEY_MARKER.length);
3726
- const contract = runtimeMaterialContractFromSignature(signature);
3727
- if (!contract) return { material: key, contract: {} };
3728
- return {
3729
- material: key.slice(0, markerIndex),
3730
- contract
3731
- };
3732
- }
3733
-
3734
- function createRuntimeMaterialKeyResolver(
3735
- json: GLTFJson,
3736
- sceneIndex: number,
3737
- meshes: readonly GLTFMeshAsset[],
3738
- skins: readonly GLTFSkinAsset[],
3739
- nodeInstanceTransforms: ReadonlyMap<number, Float32Array>,
3740
- materialVariant: string | undefined
3741
- ): (mesh: GLTFMeshAsset, contract: GLTFRuntimeMaterialContract) => string {
3742
- const signaturesByMaterial = new Map<string, Set<string>>();
3743
- const register = (material: string, contract: GLTFRuntimeMaterialContract): void => {
3744
- const signatures = signaturesByMaterial.get(material) ?? new Set<string>();
3745
- signatures.add(runtimeMaterialSignature(contract));
3746
- signaturesByMaterial.set(material, signatures);
3747
- };
3748
- const visit = (nodeIndex: number): void => {
3749
- const node = json.nodes?.[nodeIndex];
3750
- if (!node) return;
3751
- if (node.mesh !== undefined) {
3752
- const instanceTransforms = nodeInstanceTransforms.get(nodeIndex);
3753
- for (const mesh of meshes.filter((entry) => entry.sourceMeshIndex === node.mesh)) {
3754
- const skinning = skinningForMesh(mesh, skins, node.skin);
3755
- register(materialForVariant(mesh, materialVariant), runtimeMaterialContract(skinning, instanceTransforms));
3756
- }
3757
- }
3758
- for (const childIndex of node.children ?? []) visit(childIndex);
3759
- };
3760
- for (const nodeIndex of json.scenes?.[sceneIndex]?.nodes ?? []) visit(nodeIndex);
3761
- return (mesh, contract) => {
3762
- const material = materialForVariant(mesh, materialVariant);
3763
- const signatures = signaturesByMaterial.get(material);
3764
- return signatures && signatures.size > 1 ? gltfRuntimeMaterialKey(material, contract) : material;
3765
- };
3766
- }
3767
-
3768
- function runtimeMaterialContract(
3769
- skinning: { readonly jointCount: number; readonly matrices: Float32Array } | undefined,
3770
- instanceTransforms: Float32Array | undefined
3771
- ): GLTFRuntimeMaterialContract {
3772
- return {
3773
- ...(skinning ? { skinned: true } : {}),
3774
- ...(instanceTransforms ? { instanced: true } : {})
3775
- };
3776
- }
3777
-
3778
- function runtimeMaterialSignature(contract: GLTFRuntimeMaterialContract): string {
3779
- if (contract.skinned && contract.instanced) return "skinned+instanced";
3780
- if (contract.skinned) return "skinned";
3781
- if (contract.instanced) return "instanced";
3782
- return "base";
3783
- }
3784
-
3785
- function runtimeMaterialContractFromSignature(signature: string): GLTFRuntimeMaterialContract | undefined {
3786
- if (signature === "skinned+instanced") return { skinned: true, instanced: true };
3787
- if (signature === "skinned") return { skinned: true };
3788
- if (signature === "instanced") return { instanced: true };
3789
- if (signature === "base") return {};
3790
- return undefined;
3791
- }
3792
-
3793
- function materialForVariant(mesh: GLTFMeshAsset, materialVariant: string | undefined): string {
3794
- if (materialVariant === undefined) return mesh.material;
3795
- return mesh.materialVariants.find((mapping) => mapping.variant === materialVariant)?.material ?? mesh.material;
3796
- }
3797
-
3798
- function createSceneNodeForGLTFNode(
3799
- scene: Scene,
3800
- source: GLTFNode,
3801
- nodeIndex: number,
3802
- nodeName: string,
3803
- cameras: readonly GLTFCameraAsset[],
3804
- lights: readonly GLTFLightAsset[]
3805
- ): SceneNode {
3806
- if (source.camera !== undefined && source.extensions?.KHR_lights_punctual?.light !== undefined) {
3807
- throw new Error(`glTF node ${nodeIndex} cannot combine camera and KHR_lights_punctual light`);
3808
- }
3809
- let node: SceneNode;
3810
- if (source.camera !== undefined) {
3811
- node = createCameraNodeForGLTFNode(scene, source, nodeIndex, nodeName, cameras);
3812
- applyGLTFNodeMetadata(node, source, nodeIndex);
3813
- return node;
3814
- }
3815
- const lightIndex = source.extensions?.KHR_lights_punctual?.light;
3816
- if (lightIndex === undefined) {
3817
- node = scene.createNode(nodeName);
3818
- applyGLTFNodeMetadata(node, source, nodeIndex);
3819
- return node;
3820
- }
3821
- if (!Number.isInteger(lightIndex) || lightIndex < 0) {
3822
- throw new RangeError(`glTF node ${nodeIndex} KHR_lights_punctual.light must be a non-negative integer`);
3823
- }
3824
- const light = lights[lightIndex];
3825
- if (!light) {
3826
- throw new Error(`glTF node ${nodeIndex} references missing punctual light ${lightIndex}`);
3827
- }
3828
- const lightNode = scene.createLight(light.type, nodeName);
3829
- lightNode.color = [...light.color];
3830
- lightNode.intensity = light.intensity;
3831
- if (lightNode instanceof PointLight) {
3832
- lightNode.range = light.range ?? 10;
3833
- } else if (lightNode instanceof SpotLight) {
3834
- lightNode.range = light.range ?? 10;
3835
- lightNode.angle = light.spot?.outerConeAngle ?? Math.PI / 4;
3836
- lightNode.penumbra = spotPenumbra(light.spot?.innerConeAngle ?? 0, lightNode.angle);
3837
- }
3838
- applyGLTFNodeMetadata(lightNode, source, nodeIndex);
3839
- return lightNode;
3840
- }
3841
-
3842
- function applyGLTFNodeMetadata(node: SceneNode, source: GLTFNode, nodeIndex: number): void {
3843
- node.userData.gltfNodeIndex = nodeIndex;
3844
- if (!source.extras) return;
3845
- node.userData.gltfExtras = { ...source.extras };
3846
- for (const [key, value] of Object.entries(source.extras)) {
3847
- if (key.startsWith("a3d_")) node.userData[key] = value;
3848
- }
3849
- }
3850
-
3851
- function createCameraNodeForGLTFNode(
3852
- scene: Scene,
3853
- source: GLTFNode,
3854
- nodeIndex: number,
3855
- nodeName: string,
3856
- cameras: readonly GLTFCameraAsset[]
3857
- ): SceneNode {
3858
- const cameraIndex = source.camera;
3859
- if (!Number.isInteger(cameraIndex) || cameraIndex === undefined || cameraIndex < 0) {
3860
- throw new RangeError(`glTF node ${nodeIndex} camera must be a non-negative integer`);
3861
- }
3862
- const camera = cameras[cameraIndex];
3863
- if (!camera) {
3864
- throw new Error(`glTF node ${nodeIndex} references missing camera ${cameraIndex}`);
3865
- }
3866
- if (camera.type === "perspective") {
3867
- const perspective = camera.perspective!;
3868
- return scene.createPerspectiveCamera({
3869
- name: nodeName,
3870
- fovYRadians: perspective.yfov,
3871
- aspect: perspective.aspectRatio,
3872
- near: perspective.znear,
3873
- far: perspective.zfar
3874
- });
3875
- }
3876
- const orthographic = camera.orthographic!;
3877
- return scene.createOrthographicCamera({
3878
- name: nodeName,
3879
- left: -orthographic.xmag / 2,
3880
- right: orthographic.xmag / 2,
3881
- bottom: -orthographic.ymag / 2,
3882
- top: orthographic.ymag / 2,
3883
- near: orthographic.znear,
3884
- far: orthographic.zfar
3885
- });
3886
- }
3887
-
3888
- function spotPenumbra(innerConeAngle: number, outerConeAngle: number): number {
3889
- if (outerConeAngle <= 0) return 0;
3890
- return Math.max(0, Math.min(1, 1 - innerConeAngle / outerConeAngle));
3891
- }
3892
-
3893
- function createGLTFNodeNameResolver(json: GLTFJson): (nodeIndex: number) => string {
3894
- const bases = (json.nodes ?? []).map((node, index) => typeof node.name === "string" && node.name.trim().length > 0 ? node.name : `node-${index}`);
3895
- const totals = new Map<string, number>();
3896
- for (const base of bases) {
3897
- totals.set(base, (totals.get(base) ?? 0) + 1);
3898
- }
3899
- const seen = new Map<string, number>();
3900
- const names = bases.map((base) => {
3901
- const count = seen.get(base) ?? 0;
3902
- seen.set(base, count + 1);
3903
- return (totals.get(base) ?? 0) > 1 && count > 0 ? `${base}_${count}` : base;
3904
- });
3905
- return (nodeIndex) => {
3906
- const name = names[nodeIndex];
3907
- if (name === undefined) {
3908
- throw new Error(`glTF node ${nodeIndex} is missing`);
3909
- }
3910
- return name;
3911
- };
3912
- }
3913
-
3914
- function findSkinForMesh(json: GLTFJson, meshIndex: number): number | undefined {
3915
- const nodeIndex = (json.nodes ?? []).findIndex((node) => node.mesh === meshIndex && node.skin !== undefined);
3916
- if (nodeIndex < 0) return undefined;
3917
- return json.nodes?.[nodeIndex]?.skin;
3918
- }
3919
-
3920
- function parentIndexByNode(json: GLTFJson): Map<number, number> {
3921
- const parents = new Map<number, number>();
3922
- for (const [parentIndex, node] of (json.nodes ?? []).entries()) {
3923
- for (const childIndex of node.children ?? []) {
3924
- parents.set(childIndex, parentIndex);
3925
- }
3926
- }
3927
- return parents;
3928
- }
3929
-
3930
- function orderSkinJoints(json: GLTFJson, joints: readonly number[]): number[] {
3931
- const jointSet = new Set(joints);
3932
- const ordered: number[] = [];
3933
- const visited = new Set<number>();
3934
- const parents = parentIndexByNode(json);
3935
- const visit = (joint: number): void => {
3936
- if (visited.has(joint)) return;
3937
- const parent = parents.get(joint);
3938
- if (parent !== undefined && jointSet.has(parent)) visit(parent);
3939
- if (!json.nodes?.[joint]) {
3940
- throw new Error(`glTF skin references missing joint node ${joint}`);
3941
- }
3942
- visited.add(joint);
3943
- ordered.push(joint);
3944
- };
3945
- for (const joint of joints) visit(joint);
3946
- return ordered;
3947
- }
3948
-
3949
- function valueTypeForAnimationPath(path: GLTFAnimationChannel["target"]["path"]): TrackValueType {
3950
- if (path === "rotation") return "quaternion";
3951
- if (path === "translation" || path === "scale") return "vector3";
3952
- return "number-array";
3953
- }
3954
-
3955
- function animationValueForPath(path: GLTFAnimationChannel["target"]["path"], row: readonly number[]): number | Vec3 | Quat | readonly number[] {
3956
- if (path === "rotation") return toQuat(row);
3957
- if (path === "translation" || path === "scale") return toVec3(row);
3958
- return [...row];
3959
- }
3960
-
3961
- function toVec3(row: readonly number[]): Vec3 {
3962
- return [row[0] ?? 0, row[1] ?? 0, row[2] ?? 0];
3963
- }
3964
-
3965
- function toQuat(row: readonly number[]): Quat {
3966
- return [row[0] ?? 0, row[1] ?? 0, row[2] ?? 0, row[3] ?? 1];
3967
- }
3968
-
3969
- function toMat4(row: readonly number[]): Mat4 {
3970
- if (row.length !== 16) {
3971
- throw new Error(`glTF MAT4 accessor row has ${row.length} components`);
3972
- }
3973
- return [
3974
- row[0]!, row[1]!, row[2]!, row[3]!,
3975
- row[4]!, row[5]!, row[6]!, row[7]!,
3976
- row[8]!, row[9]!, row[10]!, row[11]!,
3977
- row[12]!, row[13]!, row[14]!, row[15]!
3978
- ];
3979
- }
3980
-
3981
- function identityMat4(): Mat4 {
3982
- return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
3983
- }
3984
-
3985
- function serializeGLTFAsset(
3986
- url: string,
3987
- loaderDiagnostics: GLTFLoaderDiagnostics,
3988
- images: readonly GLTFImageAsset[],
3989
- textures: readonly GLTFTextureAsset[],
3990
- materials: readonly GLTFMaterialAsset[],
3991
- materialVariants: readonly GLTFMaterialVariantAsset[],
3992
- scenes: readonly GLTFSceneAsset[],
3993
- defaultScene: number,
3994
- meshes: readonly GLTFMeshAsset[],
3995
- cameras: readonly GLTFCameraAsset[],
3996
- lights: readonly GLTFLightAsset[],
3997
- skins: readonly GLTFSkinAsset[],
3998
- animations: readonly AnimationClip[]
3999
- ): SerializedGLTFAsset {
4000
- return {
4001
- url,
4002
- loaderDiagnostics,
4003
- images: images.map((image) => ({
4004
- name: image.name,
4005
- ...(image.uri ? { uri: image.uri } : {}),
4006
- ...(image.mimeType ? { mimeType: image.mimeType } : {}),
4007
- ...(image.data ? { byteLength: image.data.byteLength } : {})
4008
- })),
4009
- textures: textures.map((texture) => ({ ...texture })),
4010
- materials: materials.map((material) => ({ ...material })),
4011
- materialVariants: materialVariants.map((variant) => ({ ...variant })),
4012
- scenes: scenes.map((scene) => ({ name: scene.name, nodeIndices: [...scene.nodeIndices] })),
4013
- defaultScene,
4014
- meshes: meshes.map((mesh) => ({
4015
- ...mesh,
4016
- positions: mesh.positions.map((position) => [...position] as const),
4017
- normals: mesh.normals.map((normal) => [...normal] as const),
4018
- texcoords: mesh.texcoords.map((texcoord) => [...texcoord] as const),
4019
- texcoordSets: mesh.texcoordSets.map((set) => set.map((texcoord) => [...texcoord] as const)),
4020
- tangents: mesh.tangents.map((tangent) => [...tangent] as const),
4021
- colors: mesh.colors.map((color) => [...color] as const),
4022
- joints: mesh.joints.map((joint) => [...joint] as const),
4023
- weights: mesh.weights.map((weight) => [...weight] as const),
4024
- morphTargets: mesh.morphTargets.map((target) => ({
4025
- ...(target.name ? { name: target.name } : {}),
4026
- positions: target.positions.map((position) => [...position] as const),
4027
- normals: target.normals.map((normal) => [...normal] as const),
4028
- tangents: target.tangents.map((tangent) => [...tangent] as const)
4029
- })),
4030
- morphWeights: [...mesh.morphWeights],
4031
- indices: mesh.indices ? [...mesh.indices] : undefined
4032
- })),
4033
- cameras: cameras.map((camera) => ({
4034
- ...camera,
4035
- ...(camera.perspective ? { perspective: { ...camera.perspective } } : {}),
4036
- ...(camera.orthographic ? { orthographic: { ...camera.orthographic } } : {})
4037
- })),
4038
- lights: lights.map((light) => ({
4039
- ...light,
4040
- color: [...light.color] as [number, number, number],
4041
- ...(light.spot ? { spot: { ...light.spot } } : {})
4042
- })),
4043
- skins: skins.map((skin) => ({
4044
- name: skin.name,
4045
- joints: [...skin.joints],
4046
- jointNames: [...skin.jointNames],
4047
- inverseBindMatrices: skin.inverseBindMatrices.map((matrix) => [...matrix] as Mat4),
4048
- ...(skin.skeletonRoot !== undefined ? { skeletonRoot: skin.skeletonRoot } : {}),
4049
- bones: skin.skeleton.bones.map((bone) => ({
4050
- name: bone.name,
4051
- parentIndex: bone.parentIndex,
4052
- translation: [...bone.translation] as Vec3,
4053
- rotation: [...bone.rotation] as Quat,
4054
- scale: [...bone.scale] as Vec3,
4055
- inverseBindMatrix: [...bone.inverseBindMatrix] as Mat4
4056
- }))
4057
- })),
4058
- animations: animations.map((animation) => animation.toJSON())
4059
- };
4060
- }