@needle-tools/three-animation-pointer 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.3]
8
+ - Fix: morph target (blend shape) weight animation bound to the wrong object when a multi-material mesh is used by multiple nodes. three.js clones a shared mesh per node, so every node's `Group` has identically named submeshes; the submesh segment of `.nodes.<node>.<submesh>.morphTargetInfluences` was looked up from the mixer root instead of inside `<node>`, so all tracks bound to the first node's submeshes. Child names are now resolved within the already resolved node.
9
+
10
+ ## [1.1.2] - 2026-06-17
11
+ - Fix: don't mutate the `KHR_animation_pointer.pointer` value in `parser.json`. The resolved three.js property path is now kept in plugin-local state, so other plugins / app code can still read the original spec-defined pointer after load — by @0b5vr in [#2](https://github.com/needle-tools/three-animation-pointer/pull/2)
12
+
7
13
  ## [1.1.1] - 2026-06-12
8
14
  - Fix: morph target (blend shape) weight animation was silently dropped for multi-material meshes without skin/bones. The multi-material morph branch only bound children that were `SkinnedMesh`; such meshes load as a `Group` of plain `Mesh` children, so no track was created (and the empty-but-truthy result skipped the fallback). Now any child with `morphTargetInfluences` is bound.
9
15
 
@@ -1,53 +1,53 @@
1
- import { InterpolateDiscrete as w, InterpolateLinear as F, AnimationClip as S, PropertyBinding as C, QuaternionKeyframeTrack as M, ColorKeyframeTrack as W, VectorKeyframeTrack as B, BooleanKeyframeTrack as D, NumberKeyframeTrack as E } from "three";
2
- const d = {
1
+ import { InterpolateDiscrete as M, InterpolateLinear as F, AnimationClip as w, PropertyBinding as C, QuaternionKeyframeTrack as S, ColorKeyframeTrack as W, VectorKeyframeTrack as B, BooleanKeyframeTrack as D, NumberKeyframeTrack as O } from "three";
2
+ const k = {
3
3
  node: "node",
4
4
  material: "material",
5
5
  camera: "camera",
6
6
  light: "light"
7
- }, y = "KHR_animation_pointer", O = {
7
+ }, R = "KHR_animation_pointer", E = {
8
8
  // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
9
9
  // keyframe track will be initialized with a default interpolation type, then modified.
10
10
  CUBICSPLINE: void 0,
11
11
  LINEAR: F,
12
- STEP: w
12
+ STEP: M
13
13
  };
14
- class z {
14
+ class G {
15
15
  /** @type {import("three/examples/jsm/loaders/GLTFLoader").GLTFParser} */
16
- constructor(a) {
17
- this.name = y, this.parser = a, this.animationPointerResolver = null;
16
+ constructor(r) {
17
+ this.name = R, this.parser = r, this.animationPointerResolver = null, this.pointerPathMap = /* @__PURE__ */ new Map();
18
18
  }
19
19
  /**
20
20
  * @param {import("..").AnimationPointerResolver | null} animationPointerResolver
21
21
  */
22
- setAnimationPointerResolver(a) {
23
- return this.animationPointerResolver = a, this;
22
+ setAnimationPointerResolver(r) {
23
+ return this.animationPointerResolver = r, this;
24
24
  }
25
25
  /* DUPLICATE of functionality in GLTFLoader */
26
- loadAnimationTargetFromChannel(a) {
27
- const t = a.target, r = t.node !== void 0 ? t.node : t.id;
28
- return this.parser.getDependency("node", r);
26
+ loadAnimationTargetFromChannel(r) {
27
+ const t = r.target, a = t.node !== void 0 ? t.node : t.id;
28
+ return this.parser.getDependency("node", a);
29
29
  }
30
- loadAnimationTargetFromChannelWithAnimationPointer(a) {
31
- var m;
30
+ loadAnimationTargetFromChannelWithAnimationPointer(r) {
31
+ var o;
32
32
  L();
33
- const t = a.target, r = t.extensions && t.extensions[y] && t.path && t.path === "pointer";
34
- if (!r) return null;
35
- let e, s = d.node, n;
36
- if (r) {
37
- const h = t.extensions[y];
38
- let o = h.pointer;
39
- if (!o) {
40
- console.warn("Invalid path", h, t);
33
+ const t = r.target, a = t.extensions && t.extensions[R] && t.path && t.path === "pointer";
34
+ if (!a) return null;
35
+ let e, s = k.node, n;
36
+ if (a) {
37
+ const m = t.extensions[R];
38
+ let i = m.pointer;
39
+ if (!i) {
40
+ console.warn("Invalid path", m, t);
41
41
  return;
42
42
  }
43
- if (o.startsWith("/materials/") ? s = d.material : o.startsWith("/extensions/KHR_lights_punctual/lights/") ? s = d.light : o.startsWith("/cameras/") && (s = d.camera), n = this._tryResolveTargetId(o, s), n === null || isNaN(n)) {
44
- console.warn("Failed resolving animation node id: " + n, o);
43
+ if (i.startsWith("/materials/") ? s = k.material : i.startsWith("/extensions/KHR_lights_punctual/lights/") ? s = k.light : i.startsWith("/cameras/") && (s = k.camera), n = this._tryResolveTargetId(i, s), n === null || isNaN(n)) {
44
+ console.warn("Failed resolving animation node id: " + n, i);
45
45
  return;
46
46
  }
47
47
  switch (s) {
48
- case d.material:
49
- const T = ("/materials/" + n.toString() + "/").length, g = o.substring(0, T);
50
- switch (e = o.substring(T), e) {
48
+ case k.material:
49
+ const h = ("/materials/" + n.toString() + "/").length, y = i.substring(0, h);
50
+ switch (e = i.substring(h), e) {
51
51
  case "pbrMetallicRoughness/baseColorFactor":
52
52
  e = "color";
53
53
  break;
@@ -130,13 +130,13 @@ class z {
130
130
  e = "specularColor";
131
131
  break;
132
132
  }
133
- o = g + e;
133
+ i = y + e;
134
134
  break;
135
- case d.node:
136
- const _ = ("/nodes/" + n.toString() + "/").length, b = o.substring(0, _);
137
- if (e = o.substring(_), e.startsWith("weights/")) {
138
- const f = e.substring(8);
139
- /^\d+$/.test(f) ? e = "morphTargetInfluences[" + f + "]" : console.warn(y + ": invalid weight index", e, o);
135
+ case k.node:
136
+ const g = ("/nodes/" + n.toString() + "/").length, d = i.substring(0, g);
137
+ if (e = i.substring(g), e.startsWith("weights/")) {
138
+ const l = e.substring(8);
139
+ /^\d+$/.test(l) ? e = "morphTargetInfluences[" + l + "]" : console.warn(R + ": invalid weight index", e, i);
140
140
  } else
141
141
  switch (e) {
142
142
  case "translation":
@@ -155,11 +155,11 @@ class z {
155
155
  e = "visible";
156
156
  break;
157
157
  }
158
- o = b + e;
158
+ i = d + e;
159
159
  break;
160
- case d.light:
161
- const u = ("/extensions/KHR_lights_punctual/lights/" + n.toString() + "/").length;
162
- switch (e = o.substring(u), e) {
160
+ case k.light:
161
+ const p = ("/extensions/KHR_lights_punctual/lights/" + n.toString() + "/").length;
162
+ switch (e = i.substring(p), e) {
163
163
  case "color":
164
164
  break;
165
165
  case "intensity":
@@ -174,11 +174,11 @@ class z {
174
174
  e = "distance";
175
175
  break;
176
176
  }
177
- o = "/lights/" + n.toString() + "/" + e;
177
+ i = "/lights/" + n.toString() + "/" + e;
178
178
  break;
179
- case d.camera:
180
- const l = ("/cameras/" + n.toString() + "/").length, k = o.substring(0, l);
181
- switch (e = o.substring(l), e) {
179
+ case k.camera:
180
+ const c = ("/cameras/" + n.toString() + "/").length, b = i.substring(0, c);
181
+ switch (e = i.substring(c), e) {
182
182
  case "perspective/yfov":
183
183
  e = "fov";
184
184
  break;
@@ -200,91 +200,94 @@ class z {
200
200
  e = "zoom";
201
201
  break;
202
202
  }
203
- o = k + e;
203
+ i = b + e;
204
204
  break;
205
205
  }
206
- (m = this.animationPointerResolver) != null && m.resolvePath && (o = this.animationPointerResolver.resolvePath(o)), t.extensions[y].pointer = o;
206
+ (o = this.animationPointerResolver) != null && o.resolvePath && (i = this.animationPointerResolver.resolvePath(i));
207
+ const I = m.pointer;
208
+ this.pointerPathMap.set(I, i);
207
209
  }
208
210
  if (n == null || isNaN(n)) {
209
211
  console.warn("Failed resolving animation node id: " + n, t);
210
212
  return;
211
213
  }
212
- let i;
213
- return s === d.node ? i = this.parser.getDependency("node", n) : s === d.material ? i = this.parser.getDependency("material", n) : s === d.light ? i = this.parser.getDependency("light", n) : s === d.camera ? i = this.parser.getDependency("camera", n) : console.error("Unhandled type", s), i;
214
+ let u;
215
+ return s === k.node ? u = this.parser.getDependency("node", n) : s === k.material ? u = this.parser.getDependency("material", n) : s === k.light ? u = this.parser.getDependency("light", n) : s === k.camera ? u = this.parser.getDependency("camera", n) : console.error("Unhandled type", s), u;
214
216
  }
215
- createAnimationTracksWithAnimationPointer(a, t, r, e, s) {
216
- var b;
217
- if (!(s.extensions && s.extensions[y] && s.path && s.path === "pointer")) return null;
218
- let i = s.extensions[y].pointer;
219
- if (!i) return null;
217
+ createAnimationTracksWithAnimationPointer(r, t, a, e, s) {
218
+ var d;
219
+ if (!(s.extensions && s.extensions[R] && s.path && s.path === "pointer")) return null;
220
+ const u = s.extensions[R].pointer;
221
+ let o = this.pointerPathMap.get(u);
222
+ if (!o) return null;
220
223
  const m = [];
221
- i = i.replaceAll("/", ".");
222
- const h = i.split(".");
223
- var T = a.name !== void 0 && a.name !== null ? a.name : a.uuid;
224
- if (h[2] = T, (b = h[3]) != null && b.startsWith("morphTargetInfluences")) {
225
- const u = h[3];
226
- if (a.type === "Group") {
227
- for (const l of a.children)
228
- l.morphTargetInfluences && (h[3] = l.name, h[4] = u, _(this.parser));
224
+ o = o.replaceAll("/", ".");
225
+ const i = o.split(".");
226
+ var h = r.name !== void 0 && r.name !== null ? r.name : r.uuid;
227
+ if (i[2] = h, (d = i[3]) != null && d.startsWith("morphTargetInfluences")) {
228
+ const p = i[3];
229
+ if (r.type === "Group") {
230
+ for (const c of r.children)
231
+ c.morphTargetInfluences && (i[3] = c.name, i[4] = p, g(this.parser));
229
232
  return m;
230
233
  }
231
234
  }
232
- _(this.parser);
233
- function g(u, l) {
235
+ g(this.parser);
236
+ function y(p, c) {
234
237
  try {
235
- const k = l.split(".").filter(Boolean), c = k[k.length - 1].replace(/\[.*\]$/, "");
236
- return c in u ? typeof u[c] == "boolean" : !1;
238
+ const b = c.split(".").filter(Boolean), f = b[b.length - 1].replace(/\[.*\]$/, "");
239
+ return f in p ? typeof p[f] == "boolean" : !1;
237
240
  } catch {
238
241
  return !1;
239
242
  }
240
243
  }
241
- function _(u) {
242
- i = h.join(".");
243
- let l, k = !1;
244
- switch (r.itemSize) {
244
+ function g(p) {
245
+ o = i.join(".");
246
+ let c, b = !1;
247
+ switch (a.itemSize) {
245
248
  case 1:
246
- Object.prototype.toString.call(r.array) === "[object Uint8Array]" && // UNSIGNED_BYTE as a strong hint
247
- g(a, i) ? (l = D, k = !0) : l = E;
249
+ Object.prototype.toString.call(a.array) === "[object Uint8Array]" && // UNSIGNED_BYTE as a strong hint
250
+ y(r, o) ? (c = D, b = !0) : c = O;
248
251
  break;
249
252
  case 2:
250
253
  case 3:
251
- l = B;
254
+ c = B;
252
255
  break;
253
256
  case 4:
254
- i.endsWith(".quaternion") ? l = M : l = W;
257
+ o.endsWith(".quaternion") ? c = S : c = W;
255
258
  break;
256
259
  }
257
- if (!l) {
258
- console.warn("Unsupported output accessor format", r);
260
+ if (!c) {
261
+ console.warn("Unsupported output accessor format", a);
259
262
  return;
260
263
  }
261
- const f = e.interpolation !== void 0 ? O[e.interpolation] : F;
262
- let c = u._getArrayFromAccessor(r);
263
- i.endsWith(".fov") && (c = c.map((p) => p / Math.PI * 180)), k && (c = c.map((p) => p > 0));
264
- const R = new l(
265
- i,
264
+ const l = e.interpolation !== void 0 ? E[e.interpolation] : F;
265
+ let f = p._getArrayFromAccessor(a);
266
+ o.endsWith(".fov") && (f = f.map((x) => x / Math.PI * 180)), b && (f = f.map((x) => x > 0));
267
+ const _ = new c(
268
+ o,
266
269
  t.array,
267
- c,
268
- f
270
+ f,
271
+ l
269
272
  );
270
- if (f === "CUBICSPLINE" && u._createCubicSplineTrackInterpolant(R), m.push(R), i && r.itemSize === 4 && i.startsWith(".materials.") && i.endsWith(".color")) {
271
- const p = new Float32Array(c.length / 4);
272
- for (let x = 0, A = c.length / 4; x < A; x += 1)
273
- p[x] = c[x * 4 + 3];
274
- const v = new l(
275
- i.replace(".color", ".opacity"),
273
+ if (l === "CUBICSPLINE" && p._createCubicSplineTrackInterpolant(_), m.push(_), o && a.itemSize === 4 && o.startsWith(".materials.") && o.endsWith(".color")) {
274
+ const x = new Float32Array(f.length / 4);
275
+ for (let T = 0, A = f.length / 4; T < A; T += 1)
276
+ x[T] = f[T * 4 + 3];
277
+ const P = new c(
278
+ o.replace(".color", ".opacity"),
276
279
  t.array,
277
- p,
278
- f
280
+ x,
281
+ l
279
282
  );
280
- f === "CUBICSPLINE" && u._createCubicSplineTrackInterpolant(R), m.push(v);
283
+ l === "CUBICSPLINE" && p._createCubicSplineTrackInterpolant(_), m.push(P);
281
284
  }
282
285
  }
283
286
  return m;
284
287
  }
285
- _tryResolveTargetId(a, t) {
286
- let r = "";
287
- return t === "node" ? r = a.substring(7) : t === "material" ? r = a.substring(11) : t === "light" ? r = a.substring(39) : t === "camera" && (r = a.substring(9)), r = r.substring(0, r.indexOf("/")), Number.parseInt(r);
288
+ _tryResolveTargetId(r, t) {
289
+ let a = "";
290
+ return t === "node" ? a = r.substring(7) : t === "material" ? a = r.substring(11) : t === "light" ? a = r.substring(39) : t === "camera" && (a = r.substring(9)), a = a.substring(0, a.indexOf("/")), Number.parseInt(a);
288
291
  }
289
292
  /* MOSTLY DUPLICATE of GLTFLoader.loadAnimation, but also tries to resolve KHR_animation_pointer. */
290
293
  /**
@@ -292,73 +295,82 @@ class z {
292
295
  * @param {number} animationIndex
293
296
  * @return {Promise<AnimationClip>}
294
297
  */
295
- loadAnimation(a) {
296
- const t = this, r = this.parser.json, e = this.parser, s = r.animations[a], n = s.name ? s.name : "animation_" + a, i = [], m = [], h = [], o = [], T = [];
297
- for (let g = 0, _ = s.channels.length; g < _; g++) {
298
- const b = s.channels[g], u = s.samplers[b.sampler], l = b.target, k = s.parameters !== void 0 ? s.parameters[u.input] : u.input, f = s.parameters !== void 0 ? s.parameters[u.output] : u.output;
299
- let c = t.loadAnimationTargetFromChannelWithAnimationPointer(b);
300
- c || (c = t.loadAnimationTargetFromChannel(b)), i.push(c), m.push(e.getDependency("accessor", k)), h.push(e.getDependency("accessor", f)), o.push(u), T.push(l);
298
+ loadAnimation(r) {
299
+ const t = this, a = this.parser.json, e = this.parser, s = a.animations[r], n = s.name ? s.name : "animation_" + r, u = [], o = [], m = [], i = [], I = [];
300
+ for (let h = 0, y = s.channels.length; h < y; h++) {
301
+ const g = s.channels[h], d = s.samplers[g.sampler], p = g.target, c = s.parameters !== void 0 ? s.parameters[d.input] : d.input, b = s.parameters !== void 0 ? s.parameters[d.output] : d.output;
302
+ let l = t.loadAnimationTargetFromChannelWithAnimationPointer(g);
303
+ l || (l = t.loadAnimationTargetFromChannel(g)), u.push(l), o.push(e.getDependency("accessor", c)), m.push(e.getDependency("accessor", b)), i.push(d), I.push(p);
301
304
  }
302
305
  return Promise.all([
303
- Promise.all(i),
304
- Promise.all(m),
305
- Promise.all(h),
306
+ Promise.all(u),
306
307
  Promise.all(o),
307
- Promise.all(T)
308
- ]).then(function(g) {
309
- const _ = g[0], b = g[1], u = g[2], l = g[3], k = g[4], f = [];
310
- for (let c = 0, R = _.length; c < R; c++) {
311
- const p = _[c], v = b[c], x = u[c], A = l[c], N = k[c];
312
- if (p === void 0) continue;
313
- p.updateMatrix && (p.updateMatrix(), p.matrixAutoUpdate = !0);
314
- let I = t.createAnimationTracksWithAnimationPointer(p, v, x, A, N);
315
- if (I || (I = e._createAnimationTracks(p, v, x, A, N)), I)
316
- for (let K = 0; K < I.length; K++)
317
- f.push(I[K]);
308
+ Promise.all(m),
309
+ Promise.all(i),
310
+ Promise.all(I)
311
+ ]).then(function(h) {
312
+ const y = h[0], g = h[1], d = h[2], p = h[3], c = h[4], b = [];
313
+ for (let l = 0, f = y.length; l < f; l++) {
314
+ const _ = y[l], x = g[l], P = d[l], T = p[l], A = c[l];
315
+ if (_ === void 0) continue;
316
+ _.updateMatrix && (_.updateMatrix(), _.matrixAutoUpdate = !0);
317
+ let N = t.createAnimationTracksWithAnimationPointer(_, x, P, T, A);
318
+ if (N || (N = e._createAnimationTracks(_, x, P, T, A)), N)
319
+ for (let K = 0; K < N.length; K++)
320
+ b.push(N[K]);
318
321
  }
319
- return new S(n, void 0, f);
322
+ return new w(n, void 0, b);
320
323
  });
321
324
  }
322
325
  }
323
- let H = !1, U = null;
326
+ function U(v, r) {
327
+ for (const t of v.children) {
328
+ const a = t.getObjectByName(r);
329
+ if (a) return a;
330
+ }
331
+ }
332
+ let H = !1, j = null;
324
333
  function L() {
325
334
  if (H) return;
326
335
  H = !0;
327
- const P = U || (U = C.findNode);
328
- C.findNode = function(a, t) {
329
- if (!t) return P(a, t);
336
+ const v = j || (j = C.findNode);
337
+ C.findNode = function(r, t) {
338
+ if (!t) return v(r, t);
330
339
  if (t.startsWith(".materials.")) {
331
- const r = t.substring(11).substring(t.indexOf(".")), e = r.indexOf("."), s = e < 0 ? r : r.substring(0, e);
340
+ const a = t.substring(11).substring(t.indexOf(".")), e = a.indexOf("."), s = e < 0 ? a : a.substring(0, e);
332
341
  let n = null;
333
- return a.traverse((i) => {
334
- n !== null || i.type !== "Mesh" && i.type !== "SkinnedMesh" || i.material && (i.material.uuid === s || i.material.name === s) && (n = i.material, n !== null && (r.endsWith(".map") ? n = n.map : r.endsWith(".emissiveMap") && (n = n.emissiveMap)));
342
+ return r.traverse((u) => {
343
+ n !== null || u.type !== "Mesh" && u.type !== "SkinnedMesh" || u.material && (u.material.uuid === s || u.material.name === s) && (n = u.material, n !== null && (a.endsWith(".map") ? n = n.map : a.endsWith(".emissiveMap") && (n = n.emissiveMap)));
335
344
  }), n;
336
345
  } else if (t.startsWith(".nodes.") || t.startsWith(".lights.") || t.startsWith(".cameras.")) {
337
- const r = t.split(".");
346
+ const a = t.split(".");
338
347
  let e;
339
- for (let s = 1; s < r.length; s++) {
340
- const n = r[s];
348
+ for (let s = 1; s < a.length; s++) {
349
+ const n = a[s];
341
350
  if (n.length == 36)
342
- e = a.getObjectByProperty("uuid", n);
351
+ e = r.getObjectByProperty("uuid", n);
343
352
  else if (e && e[n]) {
344
- const m = Number.parseInt(n);
345
- let h = n;
346
- m >= 0 && (h = m), e = e[h];
353
+ const o = Number.parseInt(n);
354
+ let m = n;
355
+ o >= 0 && (m = o), e = e[m];
356
+ } else if (e && e.isObject3D) {
357
+ const o = U(e, n);
358
+ o && (e = o);
347
359
  } else {
348
- const m = a.getObjectByName(n);
349
- m && (e = m);
360
+ const o = r.getObjectByName(n);
361
+ o && (e = o);
350
362
  }
351
363
  }
352
364
  if (!e) {
353
- const s = P(a, r[2]);
354
- return s || console.warn(y + ": Property binding not found", t, a, a.name, r), s;
365
+ const s = v(r, a[2]);
366
+ return s || console.warn(R + ": Property binding not found", t, r, r.name, a), s;
355
367
  }
356
368
  return e;
357
369
  }
358
- return P(a, t);
370
+ return v(r, t);
359
371
  };
360
372
  }
361
373
  export {
362
- z as GLTFAnimationPointerExtension,
363
- z as default
374
+ G as GLTFAnimationPointerExtension,
375
+ G as default
364
376
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/three-animation-pointer",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "KHR_animation_pointer support for three.js",
5
5
  "type": "module",
6
6
  "main": "dist/three-animation-pointer.js",
@@ -54,6 +54,12 @@ export class GLTFAnimationPointerExtension {
54
54
  /** @type {import("..").AnimationPointerResolver | null} */
55
55
  this.animationPointerResolver = null;
56
56
 
57
+ /**
58
+ * @type {Map<string, string>}
59
+ * Maps from original pointer paths to resolved three.js property paths.
60
+ */
61
+ this.pointerPathMap = new Map();
62
+
57
63
  }
58
64
 
59
65
  /**
@@ -351,7 +357,8 @@ export class GLTFAnimationPointerExtension {
351
357
 
352
358
  }
353
359
 
354
- target.extensions[ KHR_ANIMATION_POINTER ].pointer = path;
360
+ const pointer = ext.pointer;
361
+ this.pointerPathMap.set( pointer, path );
355
362
 
356
363
  }
357
364
 
@@ -384,7 +391,8 @@ export class GLTFAnimationPointerExtension {
384
391
  const useExtension = target.extensions && target.extensions[ KHR_ANIMATION_POINTER ] && target.path && target.path === 'pointer';
385
392
  if ( ! useExtension ) return null;
386
393
 
387
- let animationPointerPropertyPath = target.extensions[ KHR_ANIMATION_POINTER ].pointer;
394
+ const pointer = target.extensions[ KHR_ANIMATION_POINTER ].pointer;
395
+ let animationPointerPropertyPath = this.pointerPathMap.get( pointer );
388
396
  if ( ! animationPointerPropertyPath ) return null;
389
397
 
390
398
  const tracks = [];
@@ -698,6 +706,25 @@ export class GLTFAnimationPointerExtension {
698
706
 
699
707
 
700
708
 
709
+ /**
710
+ * Like Object3D.getObjectByName, but does not match the object itself.
711
+ * @param {import("three").Object3D} parent
712
+ * @param {string} name
713
+ * @returns {import("three").Object3D | undefined}
714
+ */
715
+ function _findDescendantByName( parent, name ) {
716
+
717
+ for ( const child of parent.children ) {
718
+
719
+ const found = child.getObjectByName( name );
720
+ if ( found ) return found;
721
+
722
+ }
723
+
724
+ return undefined;
725
+
726
+ }
727
+
701
728
  let _havePatchedPropertyBindings = false;
702
729
 
703
730
  // HACK monkey patching findNode to ensure we can map to other types required by KHR_animation_pointer.
@@ -776,6 +803,17 @@ function _ensurePropertyBindingPatch() {
776
803
  if (_animationPointerDebug)
777
804
  console.log(currentTarget);
778
805
 
806
+ } else if (currentTarget && currentTarget.isObject3D) {
807
+
808
+ // access by child node name, e.g. the submeshes of a multi-material mesh:
809
+ // `.nodes.<node>.<submesh>.morphTargetInfluences`.
810
+ // Must resolve inside the already resolved node: a glTF mesh used by multiple nodes
811
+ // is cloned per node, so every node has identically named submeshes.
812
+ const foundNode = _findDescendantByName(currentTarget, val);
813
+
814
+ if (foundNode)
815
+ currentTarget = foundNode;
816
+
779
817
  } else {
780
818
 
781
819
  // access by node name
@@ -810,4 +848,4 @@ function _ensurePropertyBindingPatch() {
810
848
 
811
849
  };
812
850
 
813
- }
851
+ }