@needle-tools/three-animation-pointer 1.0.0-alpha → 1.0.0-next.3b7dc2f

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.
@@ -1,746 +1,746 @@
1
- import {
2
- AnimationClip,
3
- ColorKeyframeTrack,
4
- InterpolateDiscrete,
5
- InterpolateLinear,
6
- NumberKeyframeTrack,
7
- PropertyBinding,
8
- QuaternionKeyframeTrack,
9
- VectorKeyframeTrack,
10
- SkinnedMesh
11
- } from 'three';
12
-
13
- /**
14
- * @typedef {import("three/examples/jsm/loaders/GLTFLoader").GLTFLoaderPlugin} GLTFLoaderPlugin
15
- */
16
-
17
- // DUPLICATED from GLTFLoader.js
18
- const ANIMATION_TARGET_TYPE = {
19
- node: 'node',
20
- material: 'material',
21
- camera: 'camera',
22
- light: 'light',
23
- };
24
-
25
- const KHR_ANIMATION_POINTER = 'KHR_animation_pointer';
26
-
27
- // DUPLICATED from GLTFLoader.js
28
- const INTERPOLATION = {
29
- // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
30
- // keyframe track will be initialized with a default interpolation type, then modified.
31
- CUBICSPLINE: undefined,
32
- LINEAR: InterpolateLinear,
33
- STEP: InterpolateDiscrete
34
- };
35
-
36
- const _animationPointerDebug = false;
37
-
38
-
39
- /**
40
- * Animation Pointer Extension
41
- *
42
- * Draft Specification: https://github.com/ux3d/glTF/tree/extensions/KHR_animation_pointer/extensions/2.0/Khronos/KHR_animation_pointer
43
- *
44
- * @implements {GLTFLoaderPlugin}
45
- */
46
- export class GLTFAnimationPointerExtension {
47
-
48
- /** @type {import("three/examples/jsm/loaders/GLTFLoader").GLTFParser} */
49
- constructor( parser ) {
50
-
51
- this.name = KHR_ANIMATION_POINTER;
52
- this.parser = parser;
53
-
54
- /** @type {import("..").AnimationPointerResolver | null} */
55
- this.animationPointerResolver = null;
56
-
57
- }
58
-
59
- /**
60
- * @param {import("..").AnimationPointerResolver | null} animationPointerResolver
61
- */
62
- setAnimationPointerResolver( animationPointerResolver ) {
63
-
64
- this.animationPointerResolver = animationPointerResolver;
65
- return this;
66
-
67
- }
68
-
69
- /* DUPLICATE of functionality in GLTFLoader */
70
- loadAnimationTargetFromChannel( animationChannel ) {
71
-
72
- const target = animationChannel.target;
73
- const name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
74
- return this.parser.getDependency( 'node', name );
75
-
76
- }
77
-
78
- loadAnimationTargetFromChannelWithAnimationPointer( animationChannel ) {
79
-
80
- _ensurePropertyBindingPatch();
81
-
82
- const target = animationChannel.target;
83
- const useExtension = target.extensions && target.extensions[ KHR_ANIMATION_POINTER ] && target.path && target.path === 'pointer';
84
- if ( ! useExtension ) return null;
85
-
86
- let targetProperty = undefined;
87
-
88
- // check if this is a extension animation
89
- let type = ANIMATION_TARGET_TYPE.node;
90
- let targetId = undefined;
91
-
92
- if ( useExtension ) {
93
-
94
- const ext = target.extensions[ KHR_ANIMATION_POINTER ];
95
- let path = ext.pointer;
96
- if ( _animationPointerDebug )
97
- console.log( 'Original path: ' + path, target );
98
-
99
- if ( ! path ) {
100
-
101
- console.warn( 'Invalid path', ext, target );
102
- return;
103
-
104
- }
105
-
106
- if ( path.startsWith( '/materials/' ) )
107
- type = ANIMATION_TARGET_TYPE.material;
108
- else if ( path.startsWith( '/extensions/KHR_lights_punctual/lights/' ) )
109
- type = ANIMATION_TARGET_TYPE.light;
110
- else if ( path.startsWith( '/cameras/' ) )
111
- type = ANIMATION_TARGET_TYPE.camera;
112
-
113
- targetId = this._tryResolveTargetId( path, type );
114
- if ( targetId === null || isNaN( targetId ) ) {
115
-
116
- console.warn( 'Failed resolving animation node id: ' + targetId, path );
117
- return;
118
-
119
- } else {
120
-
121
- if ( _animationPointerDebug ) console.log( 'Resolved node ID for ' + type, targetId );
122
-
123
- }
124
-
125
- // TODO could be parsed better
126
- switch ( type ) {
127
-
128
- case ANIMATION_TARGET_TYPE.material:
129
- const pathIndex = ( '/materials/' + targetId.toString() + '/' ).length;
130
- const pathStart = path.substring( 0, pathIndex );
131
- targetProperty = path.substring( pathIndex );
132
-
133
- switch ( targetProperty ) {
134
-
135
- // Core Spec PBR Properties
136
- case 'pbrMetallicRoughness/baseColorFactor':
137
- targetProperty = 'color';
138
- break;
139
- case 'pbrMetallicRoughness/roughnessFactor':
140
- targetProperty = 'roughness';
141
- break;
142
- case 'pbrMetallicRoughness/metallicFactor':
143
- targetProperty = 'metalness';
144
- break;
145
- case 'emissiveFactor':
146
- targetProperty = 'emissive';
147
- break;
148
- case 'alphaCutoff':
149
- targetProperty = 'alphaTest';
150
- break;
151
- case 'occlusionTexture/strength':
152
- targetProperty = 'aoMapIntensity';
153
- break;
154
- case 'normalTexture/scale':
155
- targetProperty = 'normalScale';
156
- break;
157
-
158
- // Core Spec + KHR_texture_transform
159
- case 'pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/scale':
160
- targetProperty = 'map/repeat';
161
- break;
162
- case 'pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/offset':
163
- targetProperty = 'map/offset';
164
- break;
165
-
166
- // UV transforms for anything but map doesn't seem to currently be supported in three.js
167
- case 'emissiveTexture/extensions/KHR_texture_transform/scale':
168
- targetProperty = 'emissiveMap/repeat';
169
- break;
170
- case 'emissiveTexture/extensions/KHR_texture_transform/offset':
171
- targetProperty = 'emissiveMap/offset';
172
- break;
173
-
174
- // KHR_materials_emissive_strength
175
- case 'extensions/KHR_materials_emissive_strength/emissiveStrength':
176
- targetProperty = 'emissiveIntensity';
177
- break;
178
-
179
- // KHR_materials_transmission
180
- case 'extensions/KHR_materials_transmission/transmissionFactor':
181
- targetProperty = 'transmission';
182
- break;
183
-
184
- // KHR_materials_ior
185
- case 'extensions/KHR_materials_ior/ior':
186
- targetProperty = 'ior';
187
- break;
188
-
189
- // KHR_materials_volume
190
- case 'extensions/KHR_materials_volume/thicknessFactor':
191
- targetProperty = 'thickness';
192
- break;
193
- case 'extensions/KHR_materials_volume/attenuationColor':
194
- targetProperty = 'attenuationColor';
195
- break;
196
- case 'extensions/KHR_materials_volume/attenuationDistance':
197
- targetProperty = 'attenuationDistance';
198
- break;
199
-
200
- // KHR_materials_iridescence
201
- case 'extensions/KHR_materials_iridescence/iridescenceFactor':
202
- targetProperty = 'iridescence';
203
- break;
204
- case 'extensions/KHR_materials_iridescence/iridescenceIor':
205
- targetProperty = 'iridescenceIOR';
206
- break;
207
- case 'extensions/KHR_materials_iridescence/iridescenceThicknessMinimum':
208
- targetProperty = 'iridescenceThicknessRange[0]';
209
- break;
210
- case 'extensions/KHR_materials_iridescence/iridescenceThicknessMaximum':
211
- targetProperty = 'iridescenceThicknessRange[1]';
212
- break;
213
-
214
- // KHR_materials_clearcoat
215
- case 'extensions/KHR_materials_clearcoat/clearcoatFactor':
216
- targetProperty = 'clearcoat';
217
- break;
218
- case 'extensions/KHR_materials_clearcoat/clearcoatRoughnessFactor':
219
- targetProperty = 'clearcoatRoughness';
220
- break;
221
-
222
- // KHR_materials_sheen
223
- case 'extensions/KHR_materials_sheen/sheenColorFactor':
224
- targetProperty = 'sheenColor';
225
- break;
226
- case 'extensions/KHR_materials_sheen/sheenRoughnessFactor':
227
- targetProperty = 'sheenRoughness';
228
- break;
229
-
230
- // KHR_materials_specular
231
- case 'extensions/KHR_materials_specular/specularFactor':
232
- targetProperty = 'specularIntensity';
233
- break;
234
- case 'extensions/KHR_materials_specular/specularColorFactor':
235
- targetProperty = 'specularColor';
236
- break;
237
-
238
- }
239
-
240
- path = pathStart + targetProperty;
241
- if ( _animationPointerDebug ) console.log( 'PROPERTY PATH', pathStart, targetProperty, path );
242
- break;
243
-
244
- case ANIMATION_TARGET_TYPE.node:
245
- const pathIndexNode = ( '/nodes/' + targetId.toString() + '/' ).length;
246
- const pathStartNode = path.substring( 0, pathIndexNode );
247
- targetProperty = path.substring( pathIndexNode );
248
-
249
- switch ( targetProperty ) {
250
-
251
- case 'translation':
252
- targetProperty = 'position';
253
- break;
254
- case 'rotation':
255
- targetProperty = 'quaternion';
256
- break;
257
- case 'scale':
258
- targetProperty = 'scale';
259
- break;
260
- case 'weights':
261
- targetProperty = 'morphTargetInfluences';
262
- break;
263
-
264
- }
265
-
266
- path = pathStartNode + targetProperty;
267
- break;
268
-
269
- case ANIMATION_TARGET_TYPE.light:
270
- const pathIndexLight = ( '/extensions/KHR_lights_punctual/lights/' + targetId.toString() + '/' ).length;
271
- targetProperty = path.substring( pathIndexLight );
272
-
273
- switch ( targetProperty ) {
274
-
275
- case 'color':
276
- break;
277
- case 'intensity':
278
- break;
279
- case 'spot/innerConeAngle':
280
- // TODO would need to set .penumbra, but requires calculations on every animation change (?)
281
- targetProperty = 'penumbra';
282
- break;
283
- case 'spot/outerConeAngle':
284
- targetProperty = 'angle';
285
- break;
286
- case 'range':
287
- targetProperty = 'distance';
288
- break;
289
-
290
- }
291
-
292
- path = '/lights/' + targetId.toString() + '/' + targetProperty;
293
- break;
294
-
295
- case ANIMATION_TARGET_TYPE.camera:
296
- const pathIndexCamera = ( '/cameras/' + targetId.toString() + '/' ).length;
297
- const pathStartCamera = path.substring( 0, pathIndexCamera );
298
- targetProperty = path.substring( pathIndexCamera );
299
-
300
- switch ( targetProperty ) {
301
-
302
- case 'perspective/yfov':
303
- targetProperty = 'fov';
304
- break;
305
- case 'perspective/znear':
306
- case 'orthographic/znear':
307
- targetProperty = 'near';
308
- break;
309
- case 'perspective/zfar':
310
- case 'orthographic/zfar':
311
- targetProperty = 'far';
312
- break;
313
- case 'perspective/aspect':
314
- targetProperty = 'aspect';
315
- break;
316
- // these two write to the same target property since three.js orthographic camera only supports 'zoom'.
317
- // TODO should there be a warning for either of them? E.g. a warning for "xmag" so that "yfov" + "ymag" work by default?
318
- case 'orthographic/xmag':
319
- targetProperty = 'zoom';
320
- break;
321
- case 'orthographic/ymag':
322
- targetProperty = 'zoom';
323
- break;
324
-
325
- }
326
-
327
- path = pathStartCamera + targetProperty;
328
- break;
329
-
330
- }
331
-
332
- if ( this.animationPointerResolver?.resolvePath ) {
333
-
334
- path = this.animationPointerResolver.resolvePath( path );
335
-
336
- }
337
-
338
- target.extensions[ KHR_ANIMATION_POINTER ].pointer = path;
339
-
340
- }
341
-
342
- if ( targetId === null || isNaN( targetId ) ) {
343
-
344
- console.warn( 'Failed resolving animation node id: ' + targetId, target );
345
- return;
346
-
347
- }
348
-
349
- let depPromise;
350
-
351
- if ( type === ANIMATION_TARGET_TYPE.node )
352
- depPromise = this.parser.getDependency( 'node', targetId );
353
- else if ( type === ANIMATION_TARGET_TYPE.material )
354
- depPromise = this.parser.getDependency( 'material', targetId );
355
- else if ( type === ANIMATION_TARGET_TYPE.light )
356
- depPromise = this.parser.getDependency( 'light', targetId );
357
- else if ( type === ANIMATION_TARGET_TYPE.camera )
358
- depPromise = this.parser.getDependency( 'camera', targetId );
359
- else
360
- console.error( 'Unhandled type', type );
361
-
362
- return depPromise;
363
-
364
- }
365
-
366
- createAnimationTracksWithAnimationPointer( node, inputAccessor, outputAccessor, sampler, target ) {
367
-
368
- const useExtension = target.extensions && target.extensions[ KHR_ANIMATION_POINTER ] && target.path && target.path === 'pointer';
369
- if ( ! useExtension ) return null;
370
-
371
- let animationPointerPropertyPath = target.extensions[ KHR_ANIMATION_POINTER ].pointer;
372
- if ( ! animationPointerPropertyPath ) return null;
373
-
374
- const tracks = [];
375
-
376
- animationPointerPropertyPath = animationPointerPropertyPath.replaceAll( '/', '.' );
377
- // replace node/material/camera/light ID by UUID
378
- const parts = animationPointerPropertyPath.split( '.' );
379
- const hasName = node.name !== undefined && node.name !== null;
380
- var nodeTargetName = hasName ? node.name : node.uuid;
381
- parts[ 2 ] = nodeTargetName;
382
-
383
- // specially handle the morphTargetInfluences property for multi-material meshes
384
- // in which case the target object is a Group and the children are the actual targets
385
- // see NE-3311
386
- if ( parts[ 3 ] === 'morphTargetInfluences' ) {
387
-
388
- if ( node.type === 'Group' ) {
389
-
390
- if ( _animationPointerDebug )
391
- console.log( 'Detected multi-material skinnedMesh export', animationPointerPropertyPath, node );
392
-
393
- // We assume the children are skinned meshes
394
- for ( const ch of node.children ) {
395
-
396
- if ( ch instanceof SkinnedMesh && ch.morphTargetInfluences ) {
397
-
398
- parts[ 3 ] = ch.name;
399
- parts[ 4 ] = 'morphTargetInfluences';
400
- __createTrack( this.parser );
401
-
402
- }
403
-
404
- }
405
-
406
- return tracks;
407
-
408
- }
409
-
410
- }
411
-
412
- // default
413
- __createTrack( this.parser );
414
-
415
- /** Create a new track using the current parts array */
416
- function __createTrack( parser ) {
417
-
418
- animationPointerPropertyPath = parts.join( '.' );
419
-
420
- if ( _animationPointerDebug )
421
- console.log( node, inputAccessor, outputAccessor, target, animationPointerPropertyPath );
422
-
423
- let TypedKeyframeTrack;
424
-
425
- switch ( outputAccessor.itemSize ) {
426
-
427
- case 1:
428
- TypedKeyframeTrack = NumberKeyframeTrack;
429
- break;
430
- case 2:
431
- case 3:
432
- TypedKeyframeTrack = VectorKeyframeTrack;
433
- break;
434
- case 4:
435
-
436
- if ( animationPointerPropertyPath.endsWith( '.quaternion' ) )
437
- TypedKeyframeTrack = QuaternionKeyframeTrack;
438
- else
439
- TypedKeyframeTrack = ColorKeyframeTrack;
440
-
441
- break;
442
-
443
- }
444
-
445
- const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear;
446
-
447
- let outputArray = parser._getArrayFromAccessor( outputAccessor );
448
-
449
- // convert fov values from radians to degrees
450
- if ( animationPointerPropertyPath.endsWith( '.fov' ) ) {
451
-
452
- outputArray = outputArray.map( value => value / Math.PI * 180 );
453
-
454
- }
455
-
456
- const track = new TypedKeyframeTrack(
457
- animationPointerPropertyPath,
458
- inputAccessor.array,
459
- outputArray,
460
- interpolation
461
- );
462
-
463
- // Override interpolation with custom factory method.
464
- if ( interpolation === 'CUBICSPLINE' ) {
465
-
466
- parser._createCubicSplineTrackInterpolant( track );
467
-
468
- }
469
-
470
- tracks.push( track );
471
-
472
- // glTF has opacity animation as last component of baseColorFactor,
473
- // so we need to split that up here and create a separate opacity track if that is animated.
474
- if ( animationPointerPropertyPath && outputAccessor.itemSize === 4 &&
475
- animationPointerPropertyPath.startsWith( '.materials.' ) && animationPointerPropertyPath.endsWith( '.color' ) ) {
476
-
477
- const opacityArray = new Float32Array( outputArray.length / 4 );
478
-
479
- for ( let j = 0, jl = outputArray.length / 4; j < jl; j += 1 ) {
480
-
481
- opacityArray[ j ] = outputArray[ j * 4 + 3 ];
482
-
483
- }
484
-
485
- const opacityTrack = new TypedKeyframeTrack(
486
- animationPointerPropertyPath.replace( '.color', '.opacity' ),
487
- inputAccessor.array,
488
- opacityArray,
489
- interpolation
490
- );
491
-
492
- // Override interpolation with custom factory method.
493
- if ( interpolation === 'CUBICSPLINE' ) {
494
-
495
- parser._createCubicSplineTrackInterpolant( track );
496
-
497
- }
498
-
499
- tracks.push( opacityTrack );
500
-
501
- }
502
-
503
- }
504
-
505
- return tracks;
506
-
507
- }
508
-
509
- _tryResolveTargetId( path, type ) {
510
-
511
- let name = '';
512
- if ( type === 'node' ) {
513
-
514
- name = path.substring( '/nodes/'.length );
515
-
516
- } else if ( type === 'material' ) {
517
-
518
- name = path.substring( '/materials/'.length );
519
-
520
- } else if ( type === 'light' ) {
521
-
522
- name = path.substring( '/extensions/KHR_lights_punctual/lights/'.length );
523
-
524
- } else if ( type === 'camera' ) {
525
-
526
- name = path.substring( '/cameras/'.length );
527
-
528
- }
529
-
530
- name = name.substring( 0, name.indexOf( '/' ) );
531
- const index = Number.parseInt( name );
532
- return index;
533
-
534
- }
535
-
536
- /* MOSTLY DUPLICATE of GLTFLoader.loadAnimation, but also tries to resolve KHR_animation_pointer. */
537
- /**
538
- * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
539
- * @param {number} animationIndex
540
- * @return {Promise<AnimationClip>}
541
- */
542
- loadAnimation( animationIndex ) {
543
-
544
- const me = this;
545
- const json = this.parser.json;
546
- const parser = this.parser;
547
-
548
- const animationDef = json.animations[ animationIndex ];
549
- const animationName = animationDef.name ? animationDef.name : 'animation_' + animationIndex;
550
-
551
- const pendingNodes = [];
552
- const pendingInputAccessors = [];
553
- const pendingOutputAccessors = [];
554
- const pendingSamplers = [];
555
- const pendingTargets = [];
556
-
557
- for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) {
558
-
559
- const channel = animationDef.channels[ i ];
560
- const sampler = animationDef.samplers[ channel.sampler ];
561
- const target = channel.target;
562
- const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
563
- const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;
564
-
565
- let nodeDependency = me.loadAnimationTargetFromChannelWithAnimationPointer( channel );
566
- if ( ! nodeDependency )
567
- nodeDependency = me.loadAnimationTargetFromChannel( channel );
568
-
569
- pendingNodes.push( nodeDependency );
570
- pendingInputAccessors.push( parser.getDependency( 'accessor', input ) );
571
- pendingOutputAccessors.push( parser.getDependency( 'accessor', output ) );
572
- pendingSamplers.push( sampler );
573
- pendingTargets.push( target );
574
-
575
- }
576
-
577
- return Promise.all( [
578
-
579
- Promise.all( pendingNodes ),
580
- Promise.all( pendingInputAccessors ),
581
- Promise.all( pendingOutputAccessors ),
582
- Promise.all( pendingSamplers ),
583
- Promise.all( pendingTargets )
584
-
585
- ] ).then( function ( dependencies ) {
586
-
587
- const nodes = dependencies[ 0 ];
588
- const inputAccessors = dependencies[ 1 ];
589
- const outputAccessors = dependencies[ 2 ];
590
- const samplers = dependencies[ 3 ];
591
- const targets = dependencies[ 4 ];
592
-
593
- const tracks = [];
594
-
595
- for ( let i = 0, il = nodes.length; i < il; i ++ ) {
596
-
597
- const node = nodes[ i ];
598
- const inputAccessor = inputAccessors[ i ];
599
- const outputAccessor = outputAccessors[ i ];
600
- const sampler = samplers[ i ];
601
- const target = targets[ i ];
602
-
603
- if ( node === undefined ) continue;
604
-
605
- if ( node.updateMatrix ) {
606
-
607
- node.updateMatrix();
608
- node.matrixAutoUpdate = true;
609
-
610
- }
611
-
612
- let createdTracks = me.createAnimationTracksWithAnimationPointer( node, inputAccessor, outputAccessor, sampler, target );
613
- if ( ! createdTracks )
614
- createdTracks = parser._createAnimationTracks( node, inputAccessor, outputAccessor, sampler, target );
615
-
616
- if ( createdTracks ) {
617
-
618
- for ( let k = 0; k < createdTracks.length; k ++ ) {
619
-
620
- tracks.push( createdTracks[ k ] );
621
-
622
- }
623
-
624
- }
625
-
626
- }
627
-
628
- return new AnimationClip( animationName, undefined, tracks );
629
-
630
- } );
631
-
632
- }
633
-
634
- }
635
-
636
-
637
-
638
- let _havePatchedPropertyBindings = false;
639
-
640
- // HACK monkey patching findNode to ensure we can map to other types required by KHR_animation_pointer.
641
- /** @type {PropertyBinding.findNode | null} */
642
- let findNodeFn = null;
643
-
644
- function _ensurePropertyBindingPatch() {
645
-
646
- if (_havePatchedPropertyBindings) return;
647
- _havePatchedPropertyBindings = true;
648
-
649
- const findNode = (findNodeFn ||= PropertyBinding.findNode);
650
-
651
- // "node" is the Animator component in our case
652
- // "path" is the animated property path, just with translated material names.
653
- PropertyBinding.findNode = function (node, path) {
654
-
655
- if (path.startsWith('.materials.')) {
656
-
657
- if (_animationPointerDebug) console.log('FIND', path);
658
-
659
- const remainingPath = path.substring('.materials.'.length).substring(path.indexOf('.'));
660
- const nextIndex = remainingPath.indexOf('.');
661
- const uuid = nextIndex < 0 ? remainingPath : remainingPath.substring(0, nextIndex);
662
- let res = null;
663
- node.traverse(x => {
664
-
665
- if (res !== null || (x.type !== 'Mesh' && x.type !== 'SkinnedMesh')) return;
666
- if (x['material'] && (x['material'].uuid === uuid || x['material'].name === uuid)) {
667
-
668
- res = x['material'];
669
- if (_animationPointerDebug) console.log(res, remainingPath);
670
- if (res !== null) {
671
-
672
- if (remainingPath.endsWith('.map'))
673
- res = res['map'];
674
- else if (remainingPath.endsWith('.emissiveMap'))
675
- res = res['emissiveMap'];
676
-
677
- // TODO other texture slots only make sense if three.js actually supports them
678
- // (currently only .map can have repeat/offset)
679
-
680
- }
681
-
682
- }
683
-
684
- });
685
-
686
- return res;
687
-
688
- } else if (path.startsWith('.nodes.') || path.startsWith('.lights.') || path.startsWith('.cameras.')) {
689
-
690
- const sections = path.split('.');
691
- let currentTarget = undefined;
692
- for (let i = 1; i < sections.length; i++) {
693
-
694
- const val = sections[i];
695
- const isUUID = val.length == 36;
696
- if (isUUID) {
697
-
698
- // access by UUID
699
- currentTarget = node.getObjectByProperty('uuid', val);
700
-
701
- } else if (currentTarget && currentTarget[val]) {
702
-
703
- // access by index
704
- const index = Number.parseInt(val);
705
- /** @type {string|number} */
706
- let key = val;
707
- if (index >= 0) key = index;
708
- currentTarget = currentTarget[key];
709
- if (_animationPointerDebug)
710
- console.log(currentTarget);
711
-
712
- } else {
713
-
714
- // access by node name
715
- const foundNode = node.getObjectByName(val);
716
-
717
- if (foundNode)
718
- currentTarget = foundNode;
719
-
720
- }
721
-
722
- }
723
-
724
- if (!currentTarget) {
725
-
726
- const originalFindResult = findNode(node, sections[2]);
727
-
728
- if (!originalFindResult)
729
- console.warn(KHR_ANIMATION_POINTER + ': Property binding not found', path, node, node.name, sections);
730
-
731
- return originalFindResult;
732
-
733
- }
734
-
735
- if (_animationPointerDebug)
736
- console.log('NODE', path, currentTarget);
737
-
738
- return currentTarget;
739
-
740
- }
741
-
742
- return findNode(node, path);
743
-
744
- };
745
-
1
+ import {
2
+ AnimationClip,
3
+ ColorKeyframeTrack,
4
+ InterpolateDiscrete,
5
+ InterpolateLinear,
6
+ NumberKeyframeTrack,
7
+ PropertyBinding,
8
+ QuaternionKeyframeTrack,
9
+ VectorKeyframeTrack,
10
+ SkinnedMesh
11
+ } from 'three';
12
+
13
+ /**
14
+ * @typedef {import("three/examples/jsm/loaders/GLTFLoader").GLTFLoaderPlugin} GLTFLoaderPlugin
15
+ */
16
+
17
+ // DUPLICATED from GLTFLoader.js
18
+ const ANIMATION_TARGET_TYPE = {
19
+ node: 'node',
20
+ material: 'material',
21
+ camera: 'camera',
22
+ light: 'light',
23
+ };
24
+
25
+ const KHR_ANIMATION_POINTER = 'KHR_animation_pointer';
26
+
27
+ // DUPLICATED from GLTFLoader.js
28
+ const INTERPOLATION = {
29
+ // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
30
+ // keyframe track will be initialized with a default interpolation type, then modified.
31
+ CUBICSPLINE: undefined,
32
+ LINEAR: InterpolateLinear,
33
+ STEP: InterpolateDiscrete
34
+ };
35
+
36
+ const _animationPointerDebug = false;
37
+
38
+
39
+ /**
40
+ * Animation Pointer Extension
41
+ *
42
+ * Draft Specification: https://github.com/ux3d/glTF/tree/extensions/KHR_animation_pointer/extensions/2.0/Khronos/KHR_animation_pointer
43
+ *
44
+ * @implements {GLTFLoaderPlugin}
45
+ */
46
+ export class GLTFAnimationPointerExtension {
47
+
48
+ /** @type {import("three/examples/jsm/loaders/GLTFLoader").GLTFParser} */
49
+ constructor( parser ) {
50
+
51
+ this.name = KHR_ANIMATION_POINTER;
52
+ this.parser = parser;
53
+
54
+ /** @type {import("..").AnimationPointerResolver | null} */
55
+ this.animationPointerResolver = null;
56
+
57
+ }
58
+
59
+ /**
60
+ * @param {import("..").AnimationPointerResolver | null} animationPointerResolver
61
+ */
62
+ setAnimationPointerResolver( animationPointerResolver ) {
63
+
64
+ this.animationPointerResolver = animationPointerResolver;
65
+ return this;
66
+
67
+ }
68
+
69
+ /* DUPLICATE of functionality in GLTFLoader */
70
+ loadAnimationTargetFromChannel( animationChannel ) {
71
+
72
+ const target = animationChannel.target;
73
+ const name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
74
+ return this.parser.getDependency( 'node', name );
75
+
76
+ }
77
+
78
+ loadAnimationTargetFromChannelWithAnimationPointer( animationChannel ) {
79
+
80
+ _ensurePropertyBindingPatch();
81
+
82
+ const target = animationChannel.target;
83
+ const useExtension = target.extensions && target.extensions[ KHR_ANIMATION_POINTER ] && target.path && target.path === 'pointer';
84
+ if ( ! useExtension ) return null;
85
+
86
+ let targetProperty = undefined;
87
+
88
+ // check if this is a extension animation
89
+ let type = ANIMATION_TARGET_TYPE.node;
90
+ let targetId = undefined;
91
+
92
+ if ( useExtension ) {
93
+
94
+ const ext = target.extensions[ KHR_ANIMATION_POINTER ];
95
+ let path = ext.pointer;
96
+ if ( _animationPointerDebug )
97
+ console.log( 'Original path: ' + path, target );
98
+
99
+ if ( ! path ) {
100
+
101
+ console.warn( 'Invalid path', ext, target );
102
+ return;
103
+
104
+ }
105
+
106
+ if ( path.startsWith( '/materials/' ) )
107
+ type = ANIMATION_TARGET_TYPE.material;
108
+ else if ( path.startsWith( '/extensions/KHR_lights_punctual/lights/' ) )
109
+ type = ANIMATION_TARGET_TYPE.light;
110
+ else if ( path.startsWith( '/cameras/' ) )
111
+ type = ANIMATION_TARGET_TYPE.camera;
112
+
113
+ targetId = this._tryResolveTargetId( path, type );
114
+ if ( targetId === null || isNaN( targetId ) ) {
115
+
116
+ console.warn( 'Failed resolving animation node id: ' + targetId, path );
117
+ return;
118
+
119
+ } else {
120
+
121
+ if ( _animationPointerDebug ) console.log( 'Resolved node ID for ' + type, targetId );
122
+
123
+ }
124
+
125
+ // TODO could be parsed better
126
+ switch ( type ) {
127
+
128
+ case ANIMATION_TARGET_TYPE.material:
129
+ const pathIndex = ( '/materials/' + targetId.toString() + '/' ).length;
130
+ const pathStart = path.substring( 0, pathIndex );
131
+ targetProperty = path.substring( pathIndex );
132
+
133
+ switch ( targetProperty ) {
134
+
135
+ // Core Spec PBR Properties
136
+ case 'pbrMetallicRoughness/baseColorFactor':
137
+ targetProperty = 'color';
138
+ break;
139
+ case 'pbrMetallicRoughness/roughnessFactor':
140
+ targetProperty = 'roughness';
141
+ break;
142
+ case 'pbrMetallicRoughness/metallicFactor':
143
+ targetProperty = 'metalness';
144
+ break;
145
+ case 'emissiveFactor':
146
+ targetProperty = 'emissive';
147
+ break;
148
+ case 'alphaCutoff':
149
+ targetProperty = 'alphaTest';
150
+ break;
151
+ case 'occlusionTexture/strength':
152
+ targetProperty = 'aoMapIntensity';
153
+ break;
154
+ case 'normalTexture/scale':
155
+ targetProperty = 'normalScale';
156
+ break;
157
+
158
+ // Core Spec + KHR_texture_transform
159
+ case 'pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/scale':
160
+ targetProperty = 'map/repeat';
161
+ break;
162
+ case 'pbrMetallicRoughness/baseColorTexture/extensions/KHR_texture_transform/offset':
163
+ targetProperty = 'map/offset';
164
+ break;
165
+
166
+ // UV transforms for anything but map doesn't seem to currently be supported in three.js
167
+ case 'emissiveTexture/extensions/KHR_texture_transform/scale':
168
+ targetProperty = 'emissiveMap/repeat';
169
+ break;
170
+ case 'emissiveTexture/extensions/KHR_texture_transform/offset':
171
+ targetProperty = 'emissiveMap/offset';
172
+ break;
173
+
174
+ // KHR_materials_emissive_strength
175
+ case 'extensions/KHR_materials_emissive_strength/emissiveStrength':
176
+ targetProperty = 'emissiveIntensity';
177
+ break;
178
+
179
+ // KHR_materials_transmission
180
+ case 'extensions/KHR_materials_transmission/transmissionFactor':
181
+ targetProperty = 'transmission';
182
+ break;
183
+
184
+ // KHR_materials_ior
185
+ case 'extensions/KHR_materials_ior/ior':
186
+ targetProperty = 'ior';
187
+ break;
188
+
189
+ // KHR_materials_volume
190
+ case 'extensions/KHR_materials_volume/thicknessFactor':
191
+ targetProperty = 'thickness';
192
+ break;
193
+ case 'extensions/KHR_materials_volume/attenuationColor':
194
+ targetProperty = 'attenuationColor';
195
+ break;
196
+ case 'extensions/KHR_materials_volume/attenuationDistance':
197
+ targetProperty = 'attenuationDistance';
198
+ break;
199
+
200
+ // KHR_materials_iridescence
201
+ case 'extensions/KHR_materials_iridescence/iridescenceFactor':
202
+ targetProperty = 'iridescence';
203
+ break;
204
+ case 'extensions/KHR_materials_iridescence/iridescenceIor':
205
+ targetProperty = 'iridescenceIOR';
206
+ break;
207
+ case 'extensions/KHR_materials_iridescence/iridescenceThicknessMinimum':
208
+ targetProperty = 'iridescenceThicknessRange[0]';
209
+ break;
210
+ case 'extensions/KHR_materials_iridescence/iridescenceThicknessMaximum':
211
+ targetProperty = 'iridescenceThicknessRange[1]';
212
+ break;
213
+
214
+ // KHR_materials_clearcoat
215
+ case 'extensions/KHR_materials_clearcoat/clearcoatFactor':
216
+ targetProperty = 'clearcoat';
217
+ break;
218
+ case 'extensions/KHR_materials_clearcoat/clearcoatRoughnessFactor':
219
+ targetProperty = 'clearcoatRoughness';
220
+ break;
221
+
222
+ // KHR_materials_sheen
223
+ case 'extensions/KHR_materials_sheen/sheenColorFactor':
224
+ targetProperty = 'sheenColor';
225
+ break;
226
+ case 'extensions/KHR_materials_sheen/sheenRoughnessFactor':
227
+ targetProperty = 'sheenRoughness';
228
+ break;
229
+
230
+ // KHR_materials_specular
231
+ case 'extensions/KHR_materials_specular/specularFactor':
232
+ targetProperty = 'specularIntensity';
233
+ break;
234
+ case 'extensions/KHR_materials_specular/specularColorFactor':
235
+ targetProperty = 'specularColor';
236
+ break;
237
+
238
+ }
239
+
240
+ path = pathStart + targetProperty;
241
+ if ( _animationPointerDebug ) console.log( 'PROPERTY PATH', pathStart, targetProperty, path );
242
+ break;
243
+
244
+ case ANIMATION_TARGET_TYPE.node:
245
+ const pathIndexNode = ( '/nodes/' + targetId.toString() + '/' ).length;
246
+ const pathStartNode = path.substring( 0, pathIndexNode );
247
+ targetProperty = path.substring( pathIndexNode );
248
+
249
+ switch ( targetProperty ) {
250
+
251
+ case 'translation':
252
+ targetProperty = 'position';
253
+ break;
254
+ case 'rotation':
255
+ targetProperty = 'quaternion';
256
+ break;
257
+ case 'scale':
258
+ targetProperty = 'scale';
259
+ break;
260
+ case 'weights':
261
+ targetProperty = 'morphTargetInfluences';
262
+ break;
263
+
264
+ }
265
+
266
+ path = pathStartNode + targetProperty;
267
+ break;
268
+
269
+ case ANIMATION_TARGET_TYPE.light:
270
+ const pathIndexLight = ( '/extensions/KHR_lights_punctual/lights/' + targetId.toString() + '/' ).length;
271
+ targetProperty = path.substring( pathIndexLight );
272
+
273
+ switch ( targetProperty ) {
274
+
275
+ case 'color':
276
+ break;
277
+ case 'intensity':
278
+ break;
279
+ case 'spot/innerConeAngle':
280
+ // TODO would need to set .penumbra, but requires calculations on every animation change (?)
281
+ targetProperty = 'penumbra';
282
+ break;
283
+ case 'spot/outerConeAngle':
284
+ targetProperty = 'angle';
285
+ break;
286
+ case 'range':
287
+ targetProperty = 'distance';
288
+ break;
289
+
290
+ }
291
+
292
+ path = '/lights/' + targetId.toString() + '/' + targetProperty;
293
+ break;
294
+
295
+ case ANIMATION_TARGET_TYPE.camera:
296
+ const pathIndexCamera = ( '/cameras/' + targetId.toString() + '/' ).length;
297
+ const pathStartCamera = path.substring( 0, pathIndexCamera );
298
+ targetProperty = path.substring( pathIndexCamera );
299
+
300
+ switch ( targetProperty ) {
301
+
302
+ case 'perspective/yfov':
303
+ targetProperty = 'fov';
304
+ break;
305
+ case 'perspective/znear':
306
+ case 'orthographic/znear':
307
+ targetProperty = 'near';
308
+ break;
309
+ case 'perspective/zfar':
310
+ case 'orthographic/zfar':
311
+ targetProperty = 'far';
312
+ break;
313
+ case 'perspective/aspect':
314
+ targetProperty = 'aspect';
315
+ break;
316
+ // these two write to the same target property since three.js orthographic camera only supports 'zoom'.
317
+ // TODO should there be a warning for either of them? E.g. a warning for "xmag" so that "yfov" + "ymag" work by default?
318
+ case 'orthographic/xmag':
319
+ targetProperty = 'zoom';
320
+ break;
321
+ case 'orthographic/ymag':
322
+ targetProperty = 'zoom';
323
+ break;
324
+
325
+ }
326
+
327
+ path = pathStartCamera + targetProperty;
328
+ break;
329
+
330
+ }
331
+
332
+ if ( this.animationPointerResolver?.resolvePath ) {
333
+
334
+ path = this.animationPointerResolver.resolvePath( path );
335
+
336
+ }
337
+
338
+ target.extensions[ KHR_ANIMATION_POINTER ].pointer = path;
339
+
340
+ }
341
+
342
+ if ( targetId === null || isNaN( targetId ) ) {
343
+
344
+ console.warn( 'Failed resolving animation node id: ' + targetId, target );
345
+ return;
346
+
347
+ }
348
+
349
+ let depPromise;
350
+
351
+ if ( type === ANIMATION_TARGET_TYPE.node )
352
+ depPromise = this.parser.getDependency( 'node', targetId );
353
+ else if ( type === ANIMATION_TARGET_TYPE.material )
354
+ depPromise = this.parser.getDependency( 'material', targetId );
355
+ else if ( type === ANIMATION_TARGET_TYPE.light )
356
+ depPromise = this.parser.getDependency( 'light', targetId );
357
+ else if ( type === ANIMATION_TARGET_TYPE.camera )
358
+ depPromise = this.parser.getDependency( 'camera', targetId );
359
+ else
360
+ console.error( 'Unhandled type', type );
361
+
362
+ return depPromise;
363
+
364
+ }
365
+
366
+ createAnimationTracksWithAnimationPointer( node, inputAccessor, outputAccessor, sampler, target ) {
367
+
368
+ const useExtension = target.extensions && target.extensions[ KHR_ANIMATION_POINTER ] && target.path && target.path === 'pointer';
369
+ if ( ! useExtension ) return null;
370
+
371
+ let animationPointerPropertyPath = target.extensions[ KHR_ANIMATION_POINTER ].pointer;
372
+ if ( ! animationPointerPropertyPath ) return null;
373
+
374
+ const tracks = [];
375
+
376
+ animationPointerPropertyPath = animationPointerPropertyPath.replaceAll( '/', '.' );
377
+ // replace node/material/camera/light ID by UUID
378
+ const parts = animationPointerPropertyPath.split( '.' );
379
+ const hasName = node.name !== undefined && node.name !== null;
380
+ var nodeTargetName = hasName ? node.name : node.uuid;
381
+ parts[ 2 ] = nodeTargetName;
382
+
383
+ // specially handle the morphTargetInfluences property for multi-material meshes
384
+ // in which case the target object is a Group and the children are the actual targets
385
+ // see NE-3311
386
+ if ( parts[ 3 ] === 'morphTargetInfluences' ) {
387
+
388
+ if ( node.type === 'Group' ) {
389
+
390
+ if ( _animationPointerDebug )
391
+ console.log( 'Detected multi-material skinnedMesh export', animationPointerPropertyPath, node );
392
+
393
+ // We assume the children are skinned meshes
394
+ for ( const ch of node.children ) {
395
+
396
+ if ( ch instanceof SkinnedMesh && ch.morphTargetInfluences ) {
397
+
398
+ parts[ 3 ] = ch.name;
399
+ parts[ 4 ] = 'morphTargetInfluences';
400
+ __createTrack( this.parser );
401
+
402
+ }
403
+
404
+ }
405
+
406
+ return tracks;
407
+
408
+ }
409
+
410
+ }
411
+
412
+ // default
413
+ __createTrack( this.parser );
414
+
415
+ /** Create a new track using the current parts array */
416
+ function __createTrack( parser ) {
417
+
418
+ animationPointerPropertyPath = parts.join( '.' );
419
+
420
+ if ( _animationPointerDebug )
421
+ console.log( node, inputAccessor, outputAccessor, target, animationPointerPropertyPath );
422
+
423
+ let TypedKeyframeTrack;
424
+
425
+ switch ( outputAccessor.itemSize ) {
426
+
427
+ case 1:
428
+ TypedKeyframeTrack = NumberKeyframeTrack;
429
+ break;
430
+ case 2:
431
+ case 3:
432
+ TypedKeyframeTrack = VectorKeyframeTrack;
433
+ break;
434
+ case 4:
435
+
436
+ if ( animationPointerPropertyPath.endsWith( '.quaternion' ) )
437
+ TypedKeyframeTrack = QuaternionKeyframeTrack;
438
+ else
439
+ TypedKeyframeTrack = ColorKeyframeTrack;
440
+
441
+ break;
442
+
443
+ }
444
+
445
+ const interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear;
446
+
447
+ let outputArray = parser._getArrayFromAccessor( outputAccessor );
448
+
449
+ // convert fov values from radians to degrees
450
+ if ( animationPointerPropertyPath.endsWith( '.fov' ) ) {
451
+
452
+ outputArray = outputArray.map( value => value / Math.PI * 180 );
453
+
454
+ }
455
+
456
+ const track = new TypedKeyframeTrack(
457
+ animationPointerPropertyPath,
458
+ inputAccessor.array,
459
+ outputArray,
460
+ interpolation
461
+ );
462
+
463
+ // Override interpolation with custom factory method.
464
+ if ( interpolation === 'CUBICSPLINE' ) {
465
+
466
+ parser._createCubicSplineTrackInterpolant( track );
467
+
468
+ }
469
+
470
+ tracks.push( track );
471
+
472
+ // glTF has opacity animation as last component of baseColorFactor,
473
+ // so we need to split that up here and create a separate opacity track if that is animated.
474
+ if ( animationPointerPropertyPath && outputAccessor.itemSize === 4 &&
475
+ animationPointerPropertyPath.startsWith( '.materials.' ) && animationPointerPropertyPath.endsWith( '.color' ) ) {
476
+
477
+ const opacityArray = new Float32Array( outputArray.length / 4 );
478
+
479
+ for ( let j = 0, jl = outputArray.length / 4; j < jl; j += 1 ) {
480
+
481
+ opacityArray[ j ] = outputArray[ j * 4 + 3 ];
482
+
483
+ }
484
+
485
+ const opacityTrack = new TypedKeyframeTrack(
486
+ animationPointerPropertyPath.replace( '.color', '.opacity' ),
487
+ inputAccessor.array,
488
+ opacityArray,
489
+ interpolation
490
+ );
491
+
492
+ // Override interpolation with custom factory method.
493
+ if ( interpolation === 'CUBICSPLINE' ) {
494
+
495
+ parser._createCubicSplineTrackInterpolant( track );
496
+
497
+ }
498
+
499
+ tracks.push( opacityTrack );
500
+
501
+ }
502
+
503
+ }
504
+
505
+ return tracks;
506
+
507
+ }
508
+
509
+ _tryResolveTargetId( path, type ) {
510
+
511
+ let name = '';
512
+ if ( type === 'node' ) {
513
+
514
+ name = path.substring( '/nodes/'.length );
515
+
516
+ } else if ( type === 'material' ) {
517
+
518
+ name = path.substring( '/materials/'.length );
519
+
520
+ } else if ( type === 'light' ) {
521
+
522
+ name = path.substring( '/extensions/KHR_lights_punctual/lights/'.length );
523
+
524
+ } else if ( type === 'camera' ) {
525
+
526
+ name = path.substring( '/cameras/'.length );
527
+
528
+ }
529
+
530
+ name = name.substring( 0, name.indexOf( '/' ) );
531
+ const index = Number.parseInt( name );
532
+ return index;
533
+
534
+ }
535
+
536
+ /* MOSTLY DUPLICATE of GLTFLoader.loadAnimation, but also tries to resolve KHR_animation_pointer. */
537
+ /**
538
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
539
+ * @param {number} animationIndex
540
+ * @return {Promise<AnimationClip>}
541
+ */
542
+ loadAnimation( animationIndex ) {
543
+
544
+ const me = this;
545
+ const json = this.parser.json;
546
+ const parser = this.parser;
547
+
548
+ const animationDef = json.animations[ animationIndex ];
549
+ const animationName = animationDef.name ? animationDef.name : 'animation_' + animationIndex;
550
+
551
+ const pendingNodes = [];
552
+ const pendingInputAccessors = [];
553
+ const pendingOutputAccessors = [];
554
+ const pendingSamplers = [];
555
+ const pendingTargets = [];
556
+
557
+ for ( let i = 0, il = animationDef.channels.length; i < il; i ++ ) {
558
+
559
+ const channel = animationDef.channels[ i ];
560
+ const sampler = animationDef.samplers[ channel.sampler ];
561
+ const target = channel.target;
562
+ const input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
563
+ const output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;
564
+
565
+ let nodeDependency = me.loadAnimationTargetFromChannelWithAnimationPointer( channel );
566
+ if ( ! nodeDependency )
567
+ nodeDependency = me.loadAnimationTargetFromChannel( channel );
568
+
569
+ pendingNodes.push( nodeDependency );
570
+ pendingInputAccessors.push( parser.getDependency( 'accessor', input ) );
571
+ pendingOutputAccessors.push( parser.getDependency( 'accessor', output ) );
572
+ pendingSamplers.push( sampler );
573
+ pendingTargets.push( target );
574
+
575
+ }
576
+
577
+ return Promise.all( [
578
+
579
+ Promise.all( pendingNodes ),
580
+ Promise.all( pendingInputAccessors ),
581
+ Promise.all( pendingOutputAccessors ),
582
+ Promise.all( pendingSamplers ),
583
+ Promise.all( pendingTargets )
584
+
585
+ ] ).then( function ( dependencies ) {
586
+
587
+ const nodes = dependencies[ 0 ];
588
+ const inputAccessors = dependencies[ 1 ];
589
+ const outputAccessors = dependencies[ 2 ];
590
+ const samplers = dependencies[ 3 ];
591
+ const targets = dependencies[ 4 ];
592
+
593
+ const tracks = [];
594
+
595
+ for ( let i = 0, il = nodes.length; i < il; i ++ ) {
596
+
597
+ const node = nodes[ i ];
598
+ const inputAccessor = inputAccessors[ i ];
599
+ const outputAccessor = outputAccessors[ i ];
600
+ const sampler = samplers[ i ];
601
+ const target = targets[ i ];
602
+
603
+ if ( node === undefined ) continue;
604
+
605
+ if ( node.updateMatrix ) {
606
+
607
+ node.updateMatrix();
608
+ node.matrixAutoUpdate = true;
609
+
610
+ }
611
+
612
+ let createdTracks = me.createAnimationTracksWithAnimationPointer( node, inputAccessor, outputAccessor, sampler, target );
613
+ if ( ! createdTracks )
614
+ createdTracks = parser._createAnimationTracks( node, inputAccessor, outputAccessor, sampler, target );
615
+
616
+ if ( createdTracks ) {
617
+
618
+ for ( let k = 0; k < createdTracks.length; k ++ ) {
619
+
620
+ tracks.push( createdTracks[ k ] );
621
+
622
+ }
623
+
624
+ }
625
+
626
+ }
627
+
628
+ return new AnimationClip( animationName, undefined, tracks );
629
+
630
+ } );
631
+
632
+ }
633
+
634
+ }
635
+
636
+
637
+
638
+ let _havePatchedPropertyBindings = false;
639
+
640
+ // HACK monkey patching findNode to ensure we can map to other types required by KHR_animation_pointer.
641
+ /** @type {PropertyBinding.findNode | null} */
642
+ let findNodeFn = null;
643
+
644
+ function _ensurePropertyBindingPatch() {
645
+
646
+ if (_havePatchedPropertyBindings) return;
647
+ _havePatchedPropertyBindings = true;
648
+
649
+ const findNode = (findNodeFn ||= PropertyBinding.findNode);
650
+
651
+ // "node" is the Animator component in our case
652
+ // "path" is the animated property path, just with translated material names.
653
+ PropertyBinding.findNode = function (node, path) {
654
+
655
+ if (path.startsWith('.materials.')) {
656
+
657
+ if (_animationPointerDebug) console.log('FIND', path);
658
+
659
+ const remainingPath = path.substring('.materials.'.length).substring(path.indexOf('.'));
660
+ const nextIndex = remainingPath.indexOf('.');
661
+ const uuid = nextIndex < 0 ? remainingPath : remainingPath.substring(0, nextIndex);
662
+ let res = null;
663
+ node.traverse(x => {
664
+
665
+ if (res !== null || (x.type !== 'Mesh' && x.type !== 'SkinnedMesh')) return;
666
+ if (x['material'] && (x['material'].uuid === uuid || x['material'].name === uuid)) {
667
+
668
+ res = x['material'];
669
+ if (_animationPointerDebug) console.log(res, remainingPath);
670
+ if (res !== null) {
671
+
672
+ if (remainingPath.endsWith('.map'))
673
+ res = res['map'];
674
+ else if (remainingPath.endsWith('.emissiveMap'))
675
+ res = res['emissiveMap'];
676
+
677
+ // TODO other texture slots only make sense if three.js actually supports them
678
+ // (currently only .map can have repeat/offset)
679
+
680
+ }
681
+
682
+ }
683
+
684
+ });
685
+
686
+ return res;
687
+
688
+ } else if (path.startsWith('.nodes.') || path.startsWith('.lights.') || path.startsWith('.cameras.')) {
689
+
690
+ const sections = path.split('.');
691
+ let currentTarget = undefined;
692
+ for (let i = 1; i < sections.length; i++) {
693
+
694
+ const val = sections[i];
695
+ const isUUID = val.length == 36;
696
+ if (isUUID) {
697
+
698
+ // access by UUID
699
+ currentTarget = node.getObjectByProperty('uuid', val);
700
+
701
+ } else if (currentTarget && currentTarget[val]) {
702
+
703
+ // access by index
704
+ const index = Number.parseInt(val);
705
+ /** @type {string|number} */
706
+ let key = val;
707
+ if (index >= 0) key = index;
708
+ currentTarget = currentTarget[key];
709
+ if (_animationPointerDebug)
710
+ console.log(currentTarget);
711
+
712
+ } else {
713
+
714
+ // access by node name
715
+ const foundNode = node.getObjectByName(val);
716
+
717
+ if (foundNode)
718
+ currentTarget = foundNode;
719
+
720
+ }
721
+
722
+ }
723
+
724
+ if (!currentTarget) {
725
+
726
+ const originalFindResult = findNode(node, sections[2]);
727
+
728
+ if (!originalFindResult)
729
+ console.warn(KHR_ANIMATION_POINTER + ': Property binding not found', path, node, node.name, sections);
730
+
731
+ return originalFindResult;
732
+
733
+ }
734
+
735
+ if (_animationPointerDebug)
736
+ console.log('NODE', path, currentTarget);
737
+
738
+ return currentTarget;
739
+
740
+ }
741
+
742
+ return findNode(node, path);
743
+
744
+ };
745
+
746
746
  }