@luma.gl/gltf 9.2.6 → 9.3.0-alpha.11

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 (96) hide show
  1. package/dist/dist.dev.js +4064 -1967
  2. package/dist/dist.min.js +117 -46
  3. package/dist/gltf/animations/animations.d.ts +57 -5
  4. package/dist/gltf/animations/animations.d.ts.map +1 -1
  5. package/dist/gltf/animations/interpolate.d.ts +6 -3
  6. package/dist/gltf/animations/interpolate.d.ts.map +1 -1
  7. package/dist/gltf/animations/interpolate.js +47 -51
  8. package/dist/gltf/animations/interpolate.js.map +1 -1
  9. package/dist/gltf/create-gltf-model.d.ts +15 -1
  10. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  11. package/dist/gltf/create-gltf-model.js +168 -43
  12. package/dist/gltf/create-gltf-model.js.map +1 -1
  13. package/dist/gltf/create-scenegraph-from-gltf.d.ts +39 -2
  14. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  15. package/dist/gltf/create-scenegraph-from-gltf.js +76 -6
  16. package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
  17. package/dist/gltf/gltf-animator.d.ts +37 -0
  18. package/dist/gltf/gltf-animator.d.ts.map +1 -1
  19. package/dist/gltf/gltf-animator.js +112 -17
  20. package/dist/gltf/gltf-animator.js.map +1 -1
  21. package/dist/gltf/gltf-extension-support.d.ts +13 -0
  22. package/dist/gltf/gltf-extension-support.d.ts.map +1 -0
  23. package/dist/gltf/gltf-extension-support.js +178 -0
  24. package/dist/gltf/gltf-extension-support.js.map +1 -0
  25. package/dist/index.cjs +1806 -298
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/parsers/parse-gltf-animations.d.ts +1 -0
  32. package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
  33. package/dist/parsers/parse-gltf-animations.js +373 -27
  34. package/dist/parsers/parse-gltf-animations.js.map +1 -1
  35. package/dist/parsers/parse-gltf-lights.d.ts +5 -0
  36. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -0
  37. package/dist/parsers/parse-gltf-lights.js +163 -0
  38. package/dist/parsers/parse-gltf-lights.js.map +1 -0
  39. package/dist/parsers/parse-gltf.d.ts +19 -2
  40. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  41. package/dist/parsers/parse-gltf.js +120 -67
  42. package/dist/parsers/parse-gltf.js.map +1 -1
  43. package/dist/parsers/parse-pbr-material.d.ts +115 -2
  44. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  45. package/dist/parsers/parse-pbr-material.js +602 -53
  46. package/dist/parsers/parse-pbr-material.js.map +1 -1
  47. package/dist/pbr/pbr-environment.d.ts +10 -4
  48. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  49. package/dist/pbr/pbr-environment.js +18 -15
  50. package/dist/pbr/pbr-environment.js.map +1 -1
  51. package/dist/pbr/pbr-material.d.ts +13 -3
  52. package/dist/pbr/pbr-material.d.ts.map +1 -1
  53. package/dist/pbr/texture-transform.d.ts +24 -0
  54. package/dist/pbr/texture-transform.d.ts.map +1 -0
  55. package/dist/pbr/texture-transform.js +98 -0
  56. package/dist/pbr/texture-transform.js.map +1 -0
  57. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  58. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  59. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  60. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  61. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +11 -5
  62. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  63. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +16 -12
  64. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  65. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -9
  66. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  67. package/dist/webgl-to-webgpu/convert-webgl-topology.js +3 -15
  68. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  69. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  70. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  71. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  72. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  73. package/package.json +8 -9
  74. package/src/gltf/animations/animations.ts +88 -6
  75. package/src/gltf/animations/interpolate.ts +84 -96
  76. package/src/gltf/create-gltf-model.ts +233 -48
  77. package/src/gltf/create-scenegraph-from-gltf.ts +134 -11
  78. package/src/gltf/gltf-animator.ts +198 -20
  79. package/src/gltf/gltf-extension-support.ts +226 -0
  80. package/src/index.ts +11 -2
  81. package/src/parsers/parse-gltf-animations.ts +533 -32
  82. package/src/parsers/parse-gltf-lights.ts +218 -0
  83. package/src/parsers/parse-gltf.ts +189 -96
  84. package/src/parsers/parse-pbr-material.ts +974 -79
  85. package/src/pbr/pbr-environment.ts +44 -21
  86. package/src/pbr/pbr-material.ts +18 -3
  87. package/src/pbr/texture-transform.ts +263 -0
  88. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  89. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +38 -29
  90. package/src/webgl-to-webgpu/convert-webgl-topology.ts +3 -15
  91. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
  92. package/dist/utils/deep-copy.d.ts +0 -3
  93. package/dist/utils/deep-copy.d.ts.map +0 -1
  94. package/dist/utils/deep-copy.js +0 -21
  95. package/dist/utils/deep-copy.js.map +0 -1
  96. package/src/utils/deep-copy.ts +0 -22
@@ -2,54 +2,555 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ import {log} from '@luma.gl/core';
5
6
  import {type GLTFAccessorPostprocessed, type GLTFPostprocessed} from '@loaders.gl/gltf';
6
7
  import {
8
+ GLTFAnimationPath,
7
9
  type GLTFAnimation,
8
10
  type GLTFAnimationChannel,
9
- type GLTFAnimationSampler
11
+ type GLTFMaterialAnimationChannel,
12
+ type GLTFMaterialAnimationProperty,
13
+ type GLTFNodeAnimationChannel,
14
+ type GLTFAnimationSampler,
15
+ type GLTFTextureTransformAnimationChannel
10
16
  } from '../gltf/animations/animations';
17
+ import {
18
+ resolveTextureTransform,
19
+ resolveTextureTransformSlot,
20
+ type PBRTextureTransformPath
21
+ } from '../pbr/texture-transform';
22
+ import {getRegisteredGLTFExtensionSupport} from '../gltf/gltf-extension-support';
23
+
24
+ import {accessorToTypedArray} from '../webgl-to-webgpu/convert-webgl-attribute';
11
25
 
12
- import {accessorToTypedArray} from '..//webgl-to-webgpu/convert-webgl-attribute';
26
+ type UnsupportedAnimationPointerResolution = {
27
+ reason: string;
28
+ };
13
29
 
30
+ /** Parses glTF animation records into the runtime animation model used by `GLTFAnimator`. */
14
31
  export function parseGLTFAnimations(gltf: GLTFPostprocessed): GLTFAnimation[] {
15
32
  const gltfAnimations = gltf.animations || [];
16
- return gltfAnimations.map((animation, index) => {
33
+ const accessorCache1D = new Map<GLTFAccessorPostprocessed, number[]>();
34
+ const accessorCache2D = new Map<GLTFAccessorPostprocessed, number[][]>();
35
+
36
+ return gltfAnimations.flatMap((animation, index) => {
17
37
  const name = animation.name || `Animation-${index}`;
18
- const samplers: GLTFAnimationSampler[] = animation.samplers.map(
19
- ({input, interpolation = 'LINEAR', output}) => ({
20
- input: accessorToJsArray(gltf.accessors[input]) as number[],
21
- interpolation,
22
- output: accessorToJsArray(gltf.accessors[output])
23
- })
24
- );
25
- const channels: GLTFAnimationChannel[] = animation.channels.map(({sampler, target}) => ({
26
- sampler: samplers[sampler],
27
- target: gltf.nodes[target.node ?? 0],
28
- path: target.path as GLTFAnimationChannel['path']
29
- }));
30
- return {name, channels};
38
+ const samplerCache = new Map<number, GLTFAnimationSampler>();
39
+ const channels: GLTFAnimationChannel[] = animation.channels.flatMap(({sampler, target}) => {
40
+ let parsedSampler = samplerCache.get(sampler);
41
+ if (!parsedSampler) {
42
+ const gltfSampler = animation.samplers[sampler];
43
+ if (!gltfSampler) {
44
+ throw new Error(`Cannot find animation sampler ${sampler}`);
45
+ }
46
+ const {input, interpolation = 'LINEAR', output} = gltfSampler;
47
+ parsedSampler = {
48
+ input: accessorToJsArray1D(gltf.accessors[input], accessorCache1D),
49
+ interpolation,
50
+ output: accessorToJsArray2D(gltf.accessors[output], accessorCache2D)
51
+ };
52
+ samplerCache.set(sampler, parsedSampler);
53
+ }
54
+
55
+ const parsedChannel = parseAnimationChannel(gltf, target, parsedSampler);
56
+ return parsedChannel ? [parsedChannel] : [];
57
+ });
58
+
59
+ return channels.length ? [{name, channels}] : [];
31
60
  });
32
61
  }
33
62
 
34
- //
63
+ function parseAnimationChannel(
64
+ gltf: GLTFPostprocessed,
65
+ target: {node?: number; path: string; extensions?: Record<string, any>},
66
+ sampler: GLTFAnimationSampler
67
+ ): GLTFAnimationChannel | null {
68
+ if (target.path === 'pointer') {
69
+ return parseAnimationPointerChannel(gltf, target, sampler);
70
+ }
71
+
72
+ const path = getNodeAnimationPath(target.path);
73
+ if (!path) {
74
+ return null;
75
+ }
35
76
 
36
- function accessorToJsArray(
37
- accessor: GLTFAccessorPostprocessed & {_animation?: number[] | number[][]}
38
- ): number[] | number[][] {
39
- if (!accessor._animation) {
40
- const {typedArray: array, components} = accessorToTypedArray(accessor);
77
+ const targetNode = gltf.nodes[target.node ?? 0];
78
+ if (!targetNode) {
79
+ throw new Error(`Cannot find animation target ${target.node}`);
80
+ }
41
81
 
42
- if (components === 1) {
43
- accessor._animation = Array.from(array);
44
- } else {
45
- // Slice array
46
- const slicedArray: number[][] = [];
47
- for (let i = 0; i < array.length; i += components) {
48
- slicedArray.push(Array.from(array.slice(i, i + components)));
49
- }
50
- accessor._animation = slicedArray;
82
+ return {
83
+ type: 'node',
84
+ sampler,
85
+ targetNodeId: targetNode.id,
86
+ path
87
+ };
88
+ }
89
+
90
+ function parseAnimationPointerChannel(
91
+ gltf: GLTFPostprocessed,
92
+ target: {extensions?: Record<string, any>},
93
+ sampler: GLTFAnimationSampler
94
+ ): GLTFAnimationChannel | null {
95
+ const pointer = target.extensions?.['KHR_animation_pointer']?.pointer;
96
+ if (typeof pointer !== 'string' || !pointer.startsWith('/')) {
97
+ log.warn('KHR_animation_pointer channel is missing a valid JSON pointer and will be skipped')();
98
+ return null;
99
+ }
100
+
101
+ const pointerSegments = splitJsonPointer(pointer);
102
+ switch (pointerSegments[0]) {
103
+ case 'nodes':
104
+ return parseNodePointerAnimationChannel(gltf, pointerSegments, sampler, pointer);
105
+
106
+ case 'materials':
107
+ return parseMaterialPointerAnimationChannel(gltf, pointerSegments, sampler, pointer);
108
+
109
+ default:
110
+ warnUnsupportedAnimationPointer(
111
+ pointer,
112
+ `top-level target "${pointerSegments[0]}" has no runtime animation mapping`
113
+ );
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function parseNodePointerAnimationChannel(
119
+ gltf: GLTFPostprocessed,
120
+ pointerSegments: string[],
121
+ sampler: GLTFAnimationSampler,
122
+ pointer: string
123
+ ): GLTFNodeAnimationChannel | null {
124
+ if (pointerSegments.length !== 3) {
125
+ warnUnsupportedAnimationPointer(
126
+ pointer,
127
+ 'node pointers must use /nodes/{index}/{translation|rotation|scale|weights}'
128
+ );
129
+ return null;
130
+ }
131
+
132
+ const nodeIndex = Number(pointerSegments[1]);
133
+ const targetNode = gltf.nodes[nodeIndex];
134
+ if (!Number.isInteger(nodeIndex) || !targetNode) {
135
+ log.warn(
136
+ `KHR_animation_pointer target ${pointer} references a missing node and will be skipped`
137
+ )();
138
+ return null;
139
+ }
140
+
141
+ const path = getNodeAnimationPath(pointerSegments[2]);
142
+ if (!path) {
143
+ warnUnsupportedAnimationPointer(
144
+ pointer,
145
+ `node property "${pointerSegments[2]}" has no runtime animation mapping`
146
+ );
147
+ return null;
148
+ }
149
+ if (path === 'weights') {
150
+ log.warn(
151
+ `KHR_animation_pointer target ${pointer} will be skipped because morph weights are not implemented in GLTFAnimator`
152
+ )();
153
+ return null;
154
+ }
155
+
156
+ return {
157
+ type: 'node',
158
+ sampler,
159
+ targetNodeId: targetNode.id,
160
+ path
161
+ };
162
+ }
163
+
164
+ function parseMaterialPointerAnimationChannel(
165
+ gltf: GLTFPostprocessed,
166
+ pointerSegments: string[],
167
+ sampler: GLTFAnimationSampler,
168
+ pointer: string
169
+ ): GLTFMaterialAnimationChannel | GLTFTextureTransformAnimationChannel | null {
170
+ if (pointerSegments.length < 3) {
171
+ warnUnsupportedAnimationPointer(
172
+ pointer,
173
+ 'material pointers must include a material index and target property path'
174
+ );
175
+ return null;
176
+ }
177
+
178
+ const materialIndex = Number(pointerSegments[1]);
179
+ const material = gltf.materials[materialIndex] as Record<string, any> | undefined;
180
+ if (!Number.isInteger(materialIndex) || !material) {
181
+ log.warn(
182
+ `KHR_animation_pointer target ${pointer} references a missing material and will be skipped`
183
+ )();
184
+ return null;
185
+ }
186
+
187
+ const materialTarget = resolveMaterialAnimationTarget(material, pointerSegments.slice(2));
188
+ if ('reason' in materialTarget) {
189
+ warnUnsupportedAnimationPointer(pointer, materialTarget.reason);
190
+ return null;
191
+ }
192
+
193
+ return {
194
+ sampler,
195
+ pointer,
196
+ targetMaterialIndex: materialIndex,
197
+ ...materialTarget
198
+ };
199
+ }
200
+
201
+ function getNodeAnimationPath(path: string): GLTFAnimationPath | null {
202
+ switch (path) {
203
+ case 'translation':
204
+ case 'rotation':
205
+ case 'scale':
206
+ case 'weights':
207
+ return path;
208
+
209
+ default:
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function resolveMaterialAnimationTarget(
215
+ material: Record<string, any>,
216
+ pointerSegments: string[]
217
+ ):
218
+ | {type: 'material'; property: GLTFMaterialAnimationProperty; component?: number}
219
+ | {
220
+ type: 'textureTransform';
221
+ textureSlot: import('../pbr/texture-transform').PBRTextureTransformSlot;
222
+ path: PBRTextureTransformPath;
223
+ component?: number;
224
+ baseTransform: import('../pbr/texture-transform').PBRTextureTransform;
225
+ }
226
+ | UnsupportedAnimationPointerResolution {
227
+ const textureTransformTarget = resolveTextureTransformAnimationTarget(material, pointerSegments);
228
+ if (!('reason' in textureTransformTarget)) {
229
+ return textureTransformTarget;
230
+ }
231
+ if (textureTransformTarget.reason !== 'not-a-texture-transform-target') {
232
+ return textureTransformTarget;
233
+ }
234
+
235
+ const pointerPath = pointerSegments.join('/');
236
+
237
+ switch (pointerPath) {
238
+ case 'pbrMetallicRoughness/baseColorFactor':
239
+ return material['pbrMetallicRoughness']
240
+ ? {type: 'material', property: 'baseColorFactor'}
241
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
242
+
243
+ case 'pbrMetallicRoughness/metallicFactor':
244
+ return material['pbrMetallicRoughness']
245
+ ? {type: 'material', property: 'metallicRoughnessValues', component: 0}
246
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
247
+
248
+ case 'pbrMetallicRoughness/roughnessFactor':
249
+ return material['pbrMetallicRoughness']
250
+ ? {type: 'material', property: 'metallicRoughnessValues', component: 1}
251
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
252
+
253
+ case 'normalTexture/scale':
254
+ return material['normalTexture']
255
+ ? {type: 'material', property: 'normalScale'}
256
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
257
+
258
+ case 'occlusionTexture/strength':
259
+ return material['occlusionTexture']
260
+ ? {type: 'material', property: 'occlusionStrength'}
261
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
262
+
263
+ case 'emissiveFactor':
264
+ return {type: 'material', property: 'emissiveFactor'};
265
+
266
+ case 'alphaCutoff':
267
+ return {type: 'material', property: 'alphaCutoff'};
268
+
269
+ case 'extensions/KHR_materials_specular/specularFactor':
270
+ return material['extensions']?.['KHR_materials_specular']
271
+ ? {type: 'material', property: 'specularIntensityFactor'}
272
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
273
+
274
+ case 'extensions/KHR_materials_specular/specularColorFactor':
275
+ return material['extensions']?.['KHR_materials_specular']
276
+ ? {type: 'material', property: 'specularColorFactor'}
277
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
278
+
279
+ case 'extensions/KHR_materials_ior/ior':
280
+ return material['extensions']?.['KHR_materials_ior']
281
+ ? {type: 'material', property: 'ior'}
282
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
283
+
284
+ case 'extensions/KHR_materials_transmission/transmissionFactor':
285
+ return material['extensions']?.['KHR_materials_transmission']
286
+ ? {type: 'material', property: 'transmissionFactor'}
287
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
288
+
289
+ case 'extensions/KHR_materials_volume/thicknessFactor':
290
+ return material['extensions']?.['KHR_materials_volume']
291
+ ? {type: 'material', property: 'thicknessFactor'}
292
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
293
+
294
+ case 'extensions/KHR_materials_volume/attenuationDistance':
295
+ return material['extensions']?.['KHR_materials_volume']
296
+ ? {type: 'material', property: 'attenuationDistance'}
297
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
298
+
299
+ case 'extensions/KHR_materials_volume/attenuationColor':
300
+ return material['extensions']?.['KHR_materials_volume']
301
+ ? {type: 'material', property: 'attenuationColor'}
302
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
303
+
304
+ case 'extensions/KHR_materials_clearcoat/clearcoatFactor':
305
+ return material['extensions']?.['KHR_materials_clearcoat']
306
+ ? {type: 'material', property: 'clearcoatFactor'}
307
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
308
+
309
+ case 'extensions/KHR_materials_clearcoat/clearcoatRoughnessFactor':
310
+ return material['extensions']?.['KHR_materials_clearcoat']
311
+ ? {type: 'material', property: 'clearcoatRoughnessFactor'}
312
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
313
+
314
+ case 'extensions/KHR_materials_sheen/sheenColorFactor':
315
+ return material['extensions']?.['KHR_materials_sheen']
316
+ ? {type: 'material', property: 'sheenColorFactor'}
317
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
318
+
319
+ case 'extensions/KHR_materials_sheen/sheenRoughnessFactor':
320
+ return material['extensions']?.['KHR_materials_sheen']
321
+ ? {type: 'material', property: 'sheenRoughnessFactor'}
322
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
323
+
324
+ case 'extensions/KHR_materials_iridescence/iridescenceFactor':
325
+ return material['extensions']?.['KHR_materials_iridescence']
326
+ ? {type: 'material', property: 'iridescenceFactor'}
327
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
328
+
329
+ case 'extensions/KHR_materials_iridescence/iridescenceIor':
330
+ return material['extensions']?.['KHR_materials_iridescence']
331
+ ? {type: 'material', property: 'iridescenceIor'}
332
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
333
+
334
+ case 'extensions/KHR_materials_iridescence/iridescenceThicknessMinimum':
335
+ return material['extensions']?.['KHR_materials_iridescence']
336
+ ? {type: 'material', property: 'iridescenceThicknessRange', component: 0}
337
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
338
+
339
+ case 'extensions/KHR_materials_iridescence/iridescenceThicknessMaximum':
340
+ return material['extensions']?.['KHR_materials_iridescence']
341
+ ? {type: 'material', property: 'iridescenceThicknessRange', component: 1}
342
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
343
+
344
+ case 'extensions/KHR_materials_anisotropy/anisotropyStrength':
345
+ return material['extensions']?.['KHR_materials_anisotropy']
346
+ ? {type: 'material', property: 'anisotropyStrength'}
347
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
348
+
349
+ case 'extensions/KHR_materials_anisotropy/anisotropyRotation':
350
+ return material['extensions']?.['KHR_materials_anisotropy']
351
+ ? {type: 'material', property: 'anisotropyRotation'}
352
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
353
+
354
+ case 'extensions/KHR_materials_emissive_strength/emissiveStrength':
355
+ return material['extensions']?.['KHR_materials_emissive_strength']
356
+ ? {type: 'material', property: 'emissiveStrength'}
357
+ : {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
358
+
359
+ default:
360
+ return {reason: getUnsupportedMaterialPointerReason(pointerSegments)};
361
+ }
362
+ }
363
+
364
+ function resolveTextureTransformAnimationTarget(
365
+ material: Record<string, any>,
366
+ pointerSegments: string[]
367
+ ):
368
+ | {
369
+ type: 'textureTransform';
370
+ textureSlot: import('../pbr/texture-transform').PBRTextureTransformSlot;
371
+ path: PBRTextureTransformPath;
372
+ component?: number;
373
+ baseTransform: import('../pbr/texture-transform').PBRTextureTransform;
374
+ }
375
+ | UnsupportedAnimationPointerResolution {
376
+ const extensionIndex = pointerSegments.lastIndexOf('extensions');
377
+ if (
378
+ extensionIndex < 0 ||
379
+ pointerSegments[extensionIndex + 1] !== 'KHR_texture_transform' ||
380
+ extensionIndex < 1
381
+ ) {
382
+ return {reason: 'not-a-texture-transform-target'};
383
+ }
384
+
385
+ const textureSlotDefinition = resolveTextureTransformSlot(
386
+ pointerSegments.slice(0, extensionIndex)
387
+ );
388
+ if (!textureSlotDefinition) {
389
+ return {
390
+ reason: getUnsupportedTextureTransformSlotReason(pointerSegments.slice(0, extensionIndex))
391
+ };
392
+ }
393
+
394
+ const textureInfo = getNestedMaterialValue(material, textureSlotDefinition.pathSegments);
395
+ if (!textureInfo) {
396
+ return {
397
+ reason: `texture-transform target "${pointerSegments
398
+ .slice(0, extensionIndex)
399
+ .join('/')}" does not exist on the referenced material`
400
+ };
401
+ }
402
+
403
+ const textureTransformPath = pointerSegments[extensionIndex + 2];
404
+ if (textureTransformPath === 'texCoord') {
405
+ return {
406
+ reason:
407
+ 'animated KHR_texture_transform.texCoord is unsupported because texCoord selection is structural, not a runtime float/vector update'
408
+ };
409
+ }
410
+ if (
411
+ textureTransformPath !== 'offset' &&
412
+ textureTransformPath !== 'rotation' &&
413
+ textureTransformPath !== 'scale'
414
+ ) {
415
+ return {
416
+ reason: `KHR_texture_transform property "${textureTransformPath}" is not animatable; supported properties are offset, rotation, and scale`
417
+ };
418
+ }
419
+
420
+ const componentSegment = pointerSegments[extensionIndex + 3];
421
+ if (pointerSegments.length > extensionIndex + 4) {
422
+ return {
423
+ reason: `KHR_texture_transform.${textureTransformPath} does not support nested property paths`
424
+ };
425
+ }
426
+
427
+ let component: number | undefined;
428
+ if (componentSegment !== undefined) {
429
+ component = Number(componentSegment);
430
+ if (textureTransformPath === 'rotation') {
431
+ return {
432
+ reason: 'KHR_texture_transform.rotation does not support component indices'
433
+ };
434
+ }
435
+ if (!Number.isInteger(component) || component < 0 || component > 1) {
436
+ return {
437
+ reason: `KHR_texture_transform.${textureTransformPath} component index "${componentSegment}" is invalid; only 0 and 1 are supported`
438
+ };
439
+ }
440
+ }
441
+
442
+ return {
443
+ type: 'textureTransform',
444
+ textureSlot: textureSlotDefinition.slot,
445
+ path: textureTransformPath,
446
+ component,
447
+ baseTransform: resolveTextureTransform(textureInfo)
448
+ };
449
+ }
450
+
451
+ function getNestedMaterialValue(
452
+ material: Record<string, any>,
453
+ pathSegments: string[]
454
+ ): Record<string, any> | null {
455
+ let value: any = material;
456
+ for (const pathSegment of pathSegments) {
457
+ value = value?.[pathSegment];
458
+ if (!value) {
459
+ return null;
51
460
  }
52
461
  }
53
462
 
54
- return accessor._animation;
463
+ return value;
464
+ }
465
+
466
+ function splitJsonPointer(pointer: string): string[] {
467
+ return pointer
468
+ .slice(1)
469
+ .split('/')
470
+ .map(segment => segment.replace(/~1/g, '/').replace(/~0/g, '~'));
471
+ }
472
+
473
+ function getUnsupportedMaterialPointerReason(pointerSegments: string[]): string {
474
+ const extensionName = getPointerExtensionName(pointerSegments);
475
+ if (extensionName) {
476
+ const extensionSupport = getRegisteredGLTFExtensionSupport(extensionName);
477
+ if (extensionSupport?.supportLevel === 'none') {
478
+ return `${extensionName} is referenced by this pointer, but ${extensionSupport.comment
479
+ .charAt(0)
480
+ .toLowerCase()}${extensionSupport.comment.slice(1)}`;
481
+ }
482
+ }
483
+
484
+ return `no runtime target exists for material property "${pointerSegments.join('/')}"`;
485
+ }
486
+
487
+ function getUnsupportedTextureTransformSlotReason(pointerSegments: string[]): string {
488
+ const extensionName = getPointerExtensionName(pointerSegments);
489
+ if (extensionName) {
490
+ const extensionSupport = getRegisteredGLTFExtensionSupport(extensionName);
491
+ if (extensionSupport?.supportLevel === 'none') {
492
+ return `${extensionName} is referenced by this pointer, but ${extensionSupport.comment
493
+ .charAt(0)
494
+ .toLowerCase()}${extensionSupport.comment.slice(1)}`;
495
+ }
496
+ }
497
+
498
+ return `texture-transform target "${pointerSegments.join('/')}" has no runtime texture-slot mapping`;
499
+ }
500
+
501
+ function getPointerExtensionName(pointerSegments: string[]): string | null {
502
+ const extensionIndex = pointerSegments.indexOf('extensions');
503
+ const extensionName = pointerSegments[extensionIndex + 1];
504
+ return extensionIndex >= 0 && extensionName ? extensionName : null;
505
+ }
506
+
507
+ function warnUnsupportedAnimationPointer(pointer: string, reason: string): void {
508
+ log.warn(`KHR_animation_pointer target ${pointer} will be skipped because ${reason}`)();
509
+ }
510
+
511
+ /** Converts a scalar accessor into a cached JavaScript number array. */
512
+ function accessorToJsArray1D(
513
+ accessor: GLTFAccessorPostprocessed,
514
+ accessorCache: Map<GLTFAccessorPostprocessed, number[]>
515
+ ): number[] {
516
+ if (accessorCache.has(accessor)) {
517
+ return accessorCache.get(accessor)!;
518
+ }
519
+
520
+ const {typedArray: array, components} = accessorToTypedArray(accessor);
521
+ assert(components === 1, 'accessorToJsArray1D must have exactly 1 component');
522
+ const result = Array.from(array);
523
+
524
+ accessorCache.set(accessor, result);
525
+ return result;
526
+ }
527
+
528
+ /** Converts a scalar, vector, or matrix accessor into a cached JavaScript array-of-arrays. */
529
+ function accessorToJsArray2D(
530
+ accessor: GLTFAccessorPostprocessed,
531
+ accessorCache: Map<GLTFAccessorPostprocessed, number[][]>
532
+ ): number[][] {
533
+ if (accessorCache.has(accessor)) {
534
+ return accessorCache.get(accessor)!;
535
+ }
536
+
537
+ const {typedArray: array, components} = accessorToTypedArray(accessor);
538
+ assert(components >= 1, 'accessorToJsArray2D must have at least 1 component');
539
+
540
+ const result = [];
541
+
542
+ // Slice array
543
+ for (let i = 0; i < array.length; i += components) {
544
+ result.push(Array.from(array.slice(i, i + components)));
545
+ }
546
+
547
+ accessorCache.set(accessor, result);
548
+ return result;
549
+ }
550
+
551
+ /** Throws when the supplied condition is false. */
552
+ function assert(condition: boolean, message?: string): asserts condition {
553
+ if (!condition) {
554
+ throw new Error(message);
555
+ }
55
556
  }