@luma.gl/gltf 9.3.0-alpha.4 → 9.3.0-alpha.6

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 (79) hide show
  1. package/dist/dist.dev.js +397 -220
  2. package/dist/dist.min.js +98 -46
  3. package/dist/gltf/animations/animations.d.ts +16 -4
  4. package/dist/gltf/animations/animations.d.ts.map +1 -1
  5. package/dist/gltf/animations/interpolate.d.ts +4 -3
  6. package/dist/gltf/animations/interpolate.d.ts.map +1 -1
  7. package/dist/gltf/animations/interpolate.js +27 -36
  8. package/dist/gltf/animations/interpolate.js.map +1 -1
  9. package/dist/gltf/create-gltf-model.d.ts +6 -0
  10. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  11. package/dist/gltf/create-gltf-model.js +96 -44
  12. package/dist/gltf/create-gltf-model.js.map +1 -1
  13. package/dist/gltf/create-scenegraph-from-gltf.d.ts +15 -1
  14. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  15. package/dist/gltf/create-scenegraph-from-gltf.js +12 -6
  16. package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
  17. package/dist/gltf/gltf-animator.d.ts +26 -0
  18. package/dist/gltf/gltf-animator.d.ts.map +1 -1
  19. package/dist/gltf/gltf-animator.js +22 -19
  20. package/dist/gltf/gltf-animator.js.map +1 -1
  21. package/dist/index.cjs +378 -210
  22. package/dist/index.cjs.map +4 -4
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/parsers/parse-gltf-animations.d.ts +1 -0
  27. package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
  28. package/dist/parsers/parse-gltf-animations.js +46 -23
  29. package/dist/parsers/parse-gltf-animations.js.map +1 -1
  30. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -1
  31. package/dist/parsers/parse-gltf-lights.js +40 -12
  32. package/dist/parsers/parse-gltf-lights.js.map +1 -1
  33. package/dist/parsers/parse-gltf.d.ts +16 -1
  34. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  35. package/dist/parsers/parse-gltf.js +65 -57
  36. package/dist/parsers/parse-gltf.js.map +1 -1
  37. package/dist/parsers/parse-pbr-material.d.ts +46 -1
  38. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  39. package/dist/parsers/parse-pbr-material.js +137 -13
  40. package/dist/parsers/parse-pbr-material.js.map +1 -1
  41. package/dist/pbr/pbr-environment.d.ts +6 -0
  42. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  43. package/dist/pbr/pbr-environment.js +1 -0
  44. package/dist/pbr/pbr-environment.js.map +1 -1
  45. package/dist/pbr/pbr-material.d.ts +5 -0
  46. package/dist/pbr/pbr-material.d.ts.map +1 -1
  47. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  48. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  49. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  50. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  51. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +6 -0
  52. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  53. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +4 -0
  54. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  55. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -0
  56. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  57. package/dist/webgl-to-webgpu/convert-webgl-topology.js +2 -0
  58. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  59. package/package.json +5 -5
  60. package/src/gltf/animations/animations.ts +17 -5
  61. package/src/gltf/animations/interpolate.ts +49 -68
  62. package/src/gltf/create-gltf-model.ts +101 -43
  63. package/src/gltf/create-scenegraph-from-gltf.ts +39 -11
  64. package/src/gltf/gltf-animator.ts +34 -25
  65. package/src/index.ts +1 -2
  66. package/src/parsers/parse-gltf-animations.ts +63 -26
  67. package/src/parsers/parse-gltf-lights.ts +51 -13
  68. package/src/parsers/parse-gltf.ts +90 -77
  69. package/src/parsers/parse-pbr-material.ts +204 -14
  70. package/src/pbr/pbr-environment.ts +10 -0
  71. package/src/pbr/pbr-material.ts +5 -0
  72. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  73. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +9 -0
  74. package/src/webgl-to-webgpu/convert-webgl-topology.ts +2 -0
  75. package/dist/utils/deep-copy.d.ts +0 -3
  76. package/dist/utils/deep-copy.d.ts.map +0 -1
  77. package/dist/utils/deep-copy.js +0 -21
  78. package/dist/utils/deep-copy.js.map +0 -1
  79. package/src/utils/deep-copy.ts +0 -22
@@ -2,32 +2,47 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {GLTFNodePostprocessed} from '@loaders.gl/gltf';
6
5
  import {log} from '@luma.gl/core';
7
6
  import {GroupNode} from '@luma.gl/engine';
8
- import {Matrix4} from '@math.gl/core';
9
7
  import {GLTFAnimation} from './animations/animations';
10
8
  import {interpolate} from './animations/interpolate';
11
9
 
10
+ /** Construction props for a single glTF animation controller. */
12
11
  type GLTFSingleAnimatorProps = {
12
+ /** Animation data to evaluate. */
13
13
  animation: GLTFAnimation;
14
+ /** Mapping from glTF node ids to scenegraph nodes. */
15
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
16
+ /** Start time in seconds. */
14
17
  startTime?: number;
18
+ /** Whether playback is active. */
15
19
  playing?: boolean;
20
+ /** Playback speed multiplier. */
16
21
  speed?: number;
17
22
  };
18
23
 
24
+ /** Evaluates one glTF animation against the generated scenegraph. */
19
25
  class GLTFSingleAnimator {
26
+ /** Animation definition being played. */
20
27
  animation: GLTFAnimation;
28
+ /** Target scenegraph lookup table. */
29
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
30
+ /** Playback start time in seconds. */
21
31
  startTime: number = 0;
32
+ /** Whether playback is currently enabled. */
22
33
  playing: boolean = true;
34
+ /** Playback speed multiplier. */
23
35
  speed: number = 1;
24
36
 
37
+ /** Creates a single-animation controller. */
25
38
  constructor(props: GLTFSingleAnimatorProps) {
26
39
  this.animation = props.animation;
40
+ this.gltfNodeIdToNodeMap = props.gltfNodeIdToNodeMap;
27
41
  this.animation.name ||= 'unnamed';
28
42
  Object.assign(this, props);
29
43
  }
30
44
 
45
+ /** Advances the animation to the supplied wall-clock time in milliseconds. */
31
46
  setTime(timeMs: number) {
32
47
  if (!this.playing) {
33
48
  return;
@@ -36,24 +51,36 @@ class GLTFSingleAnimator {
36
51
  const absTime = timeMs / 1000;
37
52
  const time = (absTime - this.startTime) * this.speed;
38
53
 
39
- this.animation.channels.forEach(({sampler, target, path}) => {
40
- interpolate(time, sampler, target, path);
41
- applyTranslationRotationScale(target, (target as any)._node as GroupNode);
54
+ this.animation.channels.forEach(({sampler, targetNodeId, path}) => {
55
+ const targetNode = this.gltfNodeIdToNodeMap.get(targetNodeId);
56
+ if (!targetNode) {
57
+ throw new Error(`Cannot find animation target node ${targetNodeId}`);
58
+ }
59
+
60
+ interpolate(time, sampler, targetNode, path);
42
61
  });
43
62
  }
44
63
  }
45
64
 
65
+ /** Construction props for {@link GLTFAnimator}. */
46
66
  export type GLTFAnimatorProps = {
67
+ /** Parsed animations from the source glTF. */
47
68
  animations: GLTFAnimation[];
69
+ /** Mapping from glTF node ids to scenegraph nodes. */
70
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
48
71
  };
49
72
 
73
+ /** Coordinates playback of every animation found in a glTF scene. */
50
74
  export class GLTFAnimator {
75
+ /** Individual animation controllers. */
51
76
  animations: GLTFSingleAnimator[];
52
77
 
78
+ /** Creates an animator for the supplied glTF scenegraph. */
53
79
  constructor(props: GLTFAnimatorProps) {
54
80
  this.animations = props.animations.map((animation, index) => {
55
81
  const name = animation.name || `Animation-${index}`;
56
82
  return new GLTFSingleAnimator({
83
+ gltfNodeIdToNodeMap: props.gltfNodeIdToNodeMap,
57
84
  animation: {name, channels: animation.channels}
58
85
  });
59
86
  });
@@ -65,31 +92,13 @@ export class GLTFAnimator {
65
92
  this.setTime(time);
66
93
  }
67
94
 
95
+ /** Advances every animation to the supplied wall-clock time in milliseconds. */
68
96
  setTime(time: number): void {
69
97
  this.animations.forEach(animation => animation.setTime(time));
70
98
  }
71
99
 
100
+ /** Returns the per-animation controllers managed by this animator. */
72
101
  getAnimations() {
73
102
  return this.animations;
74
103
  }
75
104
  }
76
-
77
- // TODO: share with GLTFInstantiator
78
- const scratchMatrix = new Matrix4();
79
-
80
- function applyTranslationRotationScale(gltfNode: GLTFNodePostprocessed, node: GroupNode) {
81
- node.matrix.identity();
82
-
83
- if (gltfNode.translation) {
84
- node.matrix.translate(gltfNode.translation);
85
- }
86
-
87
- if (gltfNode.rotation) {
88
- const rotationMatrix = scratchMatrix.fromQuaternion(gltfNode.rotation);
89
- node.matrix.multiplyRight(rotationMatrix);
90
- }
91
-
92
- if (gltfNode.scale) {
93
- node.matrix.scale(gltfNode.scale);
94
- }
95
- }
package/src/index.ts CHANGED
@@ -3,9 +3,8 @@
3
3
  export {loadPBREnvironment, type PBREnvironment} from './pbr/pbr-environment';
4
4
  export {type ParsedPBRMaterial} from './pbr/pbr-material';
5
5
  export {parsePBRMaterial, type ParsePBRMaterialOptions} from './parsers/parse-pbr-material';
6
- export {} from './pbr/pbr-environment';
7
6
  export {parseGLTFLights} from './parsers/parse-gltf-lights';
8
7
 
9
8
  // glTF Scenegraph Instantiator
10
- export {createScenegraphsFromGLTF} from './gltf/create-scenegraph-from-gltf';
9
+ export {createScenegraphsFromGLTF, type GLTFScenegraphs} from './gltf/create-scenegraph-from-gltf';
11
10
  export {GLTFAnimator} from './gltf/gltf-animator';
@@ -4,6 +4,7 @@
4
4
 
5
5
  import {type GLTFAccessorPostprocessed, type GLTFPostprocessed} from '@loaders.gl/gltf';
6
6
  import {
7
+ GLTFAnimationPath,
7
8
  type GLTFAnimation,
8
9
  type GLTFAnimationChannel,
9
10
  type GLTFAnimationSampler
@@ -11,45 +12,81 @@ import {
11
12
 
12
13
  import {accessorToTypedArray} from '..//webgl-to-webgpu/convert-webgl-attribute';
13
14
 
15
+ /** Parses glTF animation records into the runtime animation model used by `GLTFAnimator`. */
14
16
  export function parseGLTFAnimations(gltf: GLTFPostprocessed): GLTFAnimation[] {
15
17
  const gltfAnimations = gltf.animations || [];
18
+ const accessorCache1D = new Map<GLTFAccessorPostprocessed, number[]>();
19
+ const accessorCache2D = new Map<GLTFAccessorPostprocessed, number[][]>();
20
+
16
21
  return gltfAnimations.map((animation, index) => {
17
22
  const name = animation.name || `Animation-${index}`;
18
23
  const samplers: GLTFAnimationSampler[] = animation.samplers.map(
19
24
  ({input, interpolation = 'LINEAR', output}) => ({
20
- input: accessorToJsArray(gltf.accessors[input]) as number[],
25
+ input: accessorToJsArray1D(gltf.accessors[input], accessorCache1D),
21
26
  interpolation,
22
- output: accessorToJsArray(gltf.accessors[output])
27
+ output: accessorToJsArray2D(gltf.accessors[output], accessorCache2D)
23
28
  })
24
29
  );
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
+
31
+ const channels: GLTFAnimationChannel[] = animation.channels.map(({sampler, target}) => {
32
+ const targetNode = gltf.nodes[target.node ?? 0];
33
+ if (!targetNode) {
34
+ throw new Error(`Cannot find animation target ${target.node}`);
35
+ }
36
+ return {
37
+ sampler: samplers[sampler],
38
+ targetNodeId: targetNode.id,
39
+ path: target.path as GLTFAnimationPath
40
+ };
41
+ });
42
+
30
43
  return {name, channels};
31
44
  });
32
45
  }
33
46
 
34
- //
35
-
36
- function accessorToJsArray(
37
- accessor: GLTFAccessorPostprocessed & {_animation?: number[] | number[][]}
38
- ): number[] | number[][] {
39
- if (!accessor._animation) {
40
- const {typedArray: array, components} = accessorToTypedArray(accessor);
41
-
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;
51
- }
47
+ /** Converts a scalar accessor into a cached JavaScript number array. */
48
+ function accessorToJsArray1D(
49
+ accessor: GLTFAccessorPostprocessed,
50
+ accessorCache: Map<GLTFAccessorPostprocessed, number[]>
51
+ ): number[] {
52
+ if (accessorCache.has(accessor)) {
53
+ return accessorCache.get(accessor)!;
52
54
  }
53
55
 
54
- return accessor._animation;
56
+ const {typedArray: array, components} = accessorToTypedArray(accessor);
57
+ assert(components === 1, 'accessorToJsArray1D must have exactly 1 component');
58
+ const result = Array.from(array);
59
+
60
+ accessorCache.set(accessor, result);
61
+ return result;
62
+ }
63
+
64
+ /** Converts a vector or matrix accessor into a cached JavaScript array-of-arrays. */
65
+ function accessorToJsArray2D(
66
+ accessor: GLTFAccessorPostprocessed,
67
+ accessorCache: Map<GLTFAccessorPostprocessed, number[][]>
68
+ ): number[][] {
69
+ if (accessorCache.has(accessor)) {
70
+ return accessorCache.get(accessor)!;
71
+ }
72
+
73
+ const {typedArray: array, components} = accessorToTypedArray(accessor);
74
+ assert(components > 1, 'accessorToJsArray2D must have more than 1 component');
75
+
76
+ const result = [];
77
+
78
+ // Slice array
79
+ for (let i = 0; i < array.length; i += components) {
80
+ result.push(Array.from(array.slice(i, i + components)));
81
+ }
82
+
83
+ accessorCache.set(accessor, result);
84
+ return result;
85
+ }
86
+
87
+ /** Throws when the supplied condition is false. */
88
+ function assert(condition: boolean, message?: string): asserts condition {
89
+ if (!condition) {
90
+ throw new Error(message);
91
+ }
55
92
  }
@@ -4,7 +4,10 @@ import type {DirectionalLight, Light, PointLight} from '@luma.gl/shadertools';
4
4
 
5
5
  /** Parse KHR_lights_punctual extension into luma.gl light definitions */
6
6
  export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
7
- const lightDefs = gltf.extensions?.['KHR_lights_punctual']?.['lights'];
7
+ const lightDefs =
8
+ // `postProcessGLTF()` moves KHR_lights_punctual into `gltf.lights`.
9
+ (gltf as GLTFPostprocessed & {lights?: any[]}).lights ||
10
+ gltf.extensions?.['KHR_lights_punctual']?.['lights'];
8
11
  if (!lightDefs || !Array.isArray(lightDefs) || lightDefs.length === 0) {
9
12
  return [];
10
13
  }
@@ -12,12 +15,14 @@ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
12
15
  const lights: Light[] = [];
13
16
 
14
17
  for (const node of gltf.nodes || []) {
15
- const nodeLight = node.extensions?.KHR_lights_punctual;
16
- if (!nodeLight || typeof nodeLight.light !== 'number') {
18
+ const lightIndex =
19
+ (node as GLTFNodePostprocessed & {light?: number}).light ??
20
+ node.extensions?.KHR_lights_punctual?.light;
21
+ if (typeof lightIndex !== 'number') {
17
22
  // eslint-disable-next-line no-continue
18
23
  continue;
19
24
  }
20
- const gltfLight = lightDefs[nodeLight.light];
25
+ const gltfLight = lightDefs[lightIndex];
21
26
  if (!gltfLight) {
22
27
  // eslint-disable-next-line no-continue
23
28
  continue;
@@ -46,15 +51,16 @@ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
46
51
  return lights;
47
52
  }
48
53
 
54
+ /**
55
+ * Converts a glTF punctual light attached to a node into a point light.
56
+ */
49
57
  function parsePointLight(
50
58
  node: GLTFNodePostprocessed,
51
59
  color: [number, number, number],
52
60
  intensity: number,
53
61
  range?: number
54
62
  ): PointLight {
55
- const position: Readonly<[number, number, number]> = node.translation
56
- ? ([...node.translation] as [number, number, number])
57
- : ([0, 0, 0] as [number, number, number]);
63
+ const position = getNodePosition(node);
58
64
 
59
65
  let attenuation: Readonly<[number, number, number]> = [1, 0, 0];
60
66
  if (range !== undefined && range > 0) {
@@ -70,17 +76,15 @@ function parsePointLight(
70
76
  };
71
77
  }
72
78
 
79
+ /**
80
+ * Converts a glTF punctual light attached to a node into a directional light.
81
+ */
73
82
  function parseDirectionalLight(
74
83
  node: GLTFNodePostprocessed,
75
84
  color: [number, number, number],
76
85
  intensity: number
77
86
  ): DirectionalLight {
78
- let direction: [number, number, number] = [0, 0, -1];
79
-
80
- if (node.rotation) {
81
- const orientation = new Matrix4().fromQuaternion(node.rotation);
82
- direction = orientation.transformDirection([0, 0, -1]) as [number, number, number];
83
- }
87
+ const direction = getNodeDirection(node);
84
88
 
85
89
  return {
86
90
  type: 'directional',
@@ -89,3 +93,37 @@ function parseDirectionalLight(
89
93
  intensity
90
94
  };
91
95
  }
96
+
97
+ /**
98
+ * Resolves the world-space position of a glTF node from its matrix or translation.
99
+ */
100
+ function getNodePosition(node: GLTFNodePostprocessed): [number, number, number] {
101
+ if (node.matrix) {
102
+ return new Matrix4(node.matrix).transformAsPoint([0, 0, 0]) as [number, number, number];
103
+ }
104
+
105
+ if (node.translation) {
106
+ return [...node.translation] as [number, number, number];
107
+ }
108
+
109
+ return [0, 0, 0];
110
+ }
111
+
112
+ /**
113
+ * Resolves the forward direction of a glTF node from its matrix or rotation.
114
+ */
115
+ function getNodeDirection(node: GLTFNodePostprocessed): [number, number, number] {
116
+ if (node.matrix) {
117
+ return new Matrix4(node.matrix).transformDirection([0, 0, -1]) as [number, number, number];
118
+ }
119
+
120
+ if (node.rotation) {
121
+ return new Matrix4().fromQuaternion(node.rotation).transformDirection([0, 0, -1]) as [
122
+ number,
123
+ number,
124
+ number
125
+ ];
126
+ }
127
+
128
+ return [0, 0, -1];
129
+ }
@@ -4,13 +4,11 @@
4
4
 
5
5
  import {Device, type PrimitiveTopology} from '@luma.gl/core';
6
6
  import {Geometry, GeometryAttribute, GroupNode, ModelNode, type ModelProps} from '@luma.gl/engine';
7
- import {Matrix4} from '@math.gl/core';
8
7
  import {
9
8
  type GLTFMeshPostprocessed,
10
9
  type GLTFNodePostprocessed,
11
10
  type GLTFPostprocessed
12
11
  } from '@loaders.gl/gltf';
13
- import {type GLTFScenePostprocessed} from '@loaders.gl/gltf/dist/lib/types/gltf-postprocessed-schema';
14
12
 
15
13
  import {type PBREnvironment} from '../pbr/pbr-environment';
16
14
  import {convertGLDrawModeToTopology} from '../webgl-to-webgpu/convert-webgl-topology';
@@ -18,11 +16,17 @@ import {createGLTFModel} from '../gltf/create-gltf-model';
18
16
 
19
17
  import {parsePBRMaterial} from './parse-pbr-material';
20
18
 
19
+ /** Options that influence how a post-processed glTF is turned into a luma.gl scenegraph. */
21
20
  export type ParseGLTFOptions = {
21
+ /** Additional model props applied to each generated primitive model. */
22
22
  modelOptions?: Partial<ModelProps>;
23
+ /** Enables shader-level PBR debug output. */
23
24
  pbrDebug?: boolean;
25
+ /** Optional image-based lighting environment. */
24
26
  imageBasedLightingEnvironment?: PBREnvironment;
27
+ /** Enables punctual light extraction. */
25
28
  lights?: boolean;
29
+ /** Enables tangent usage when available. */
26
30
  useTangents?: boolean;
27
31
  };
28
32
 
@@ -41,100 +45,107 @@ const defaultOptions: Required<ParseGLTFOptions> = {
41
45
  export function parseGLTF(
42
46
  device: Device,
43
47
  gltf: GLTFPostprocessed,
44
- options_: ParseGLTFOptions = {}
45
- ): GroupNode[] {
46
- const options = {...defaultOptions, ...options_};
47
- const sceneNodes = gltf.scenes.map(gltfScene =>
48
- createScene(device, gltfScene, gltf.nodes, options)
49
- );
50
- return sceneNodes;
51
- }
48
+ options: ParseGLTFOptions = {}
49
+ ): {
50
+ /** Scene roots generated from `gltf.scenes`. */
51
+ scenes: GroupNode[];
52
+ /** Map from glTF mesh ids to generated mesh group nodes. */
53
+ gltfMeshIdToNodeMap: Map<string, GroupNode>;
54
+ /** Map from glTF node indices to generated scenegraph nodes. */
55
+ gltfNodeIndexToNodeMap: Map<number, GroupNode>;
56
+ /** Map from glTF node ids to generated scenegraph nodes. */
57
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
58
+ } {
59
+ const combinedOptions = {...defaultOptions, ...options};
60
+
61
+ const gltfMeshIdToNodeMap = new Map<string, GroupNode>();
62
+ gltf.meshes.forEach((gltfMesh, idx) => {
63
+ const newMesh = createNodeForGLTFMesh(device, gltfMesh, combinedOptions);
64
+ gltfMeshIdToNodeMap.set(gltfMesh.id, newMesh);
65
+ });
52
66
 
53
- function createScene(
54
- device: Device,
55
- gltfScene: GLTFScenePostprocessed,
56
- gltfNodes: GLTFNodePostprocessed[],
57
- options: Required<ParseGLTFOptions>
58
- ): GroupNode {
59
- const gltfSceneNodes = gltfScene.nodes || [];
60
- const nodes = gltfSceneNodes.map(node => createNode(device, node, gltfNodes, options));
61
- const sceneNode = new GroupNode({
62
- id: gltfScene.name || gltfScene.id,
63
- children: nodes
67
+ const gltfNodeIndexToNodeMap = new Map<number, GroupNode>();
68
+ const gltfNodeIdToNodeMap = new Map<string, GroupNode>();
69
+ // Step 1/2: Generate a GroupNode for each gltf node. (1:1 mapping).
70
+ gltf.nodes.forEach((gltfNode, idx) => {
71
+ const newNode = createNodeForGLTFNode(device, gltfNode, combinedOptions);
72
+ gltfNodeIndexToNodeMap.set(idx, newNode);
73
+ gltfNodeIdToNodeMap.set(gltfNode.id, newNode);
64
74
  });
65
- return sceneNode;
66
- }
67
75
 
68
- function createNode(
69
- device: Device,
70
- gltfNode: GLTFNodePostprocessed & {_node?: GroupNode},
71
- gltfNodes: GLTFNodePostprocessed[],
72
- options: Required<ParseGLTFOptions>
73
- ): GroupNode {
74
- if (!gltfNode._node) {
75
- const gltfChildren = gltfNode.children || [];
76
- const children = gltfChildren.map(child => createNode(device, child, gltfNodes, options));
76
+ // Step 2/2: Go though each gltf node and attach the children.
77
+ // This guarantees that each gltf node will have exactly one luma GroupNode.
78
+ gltf.nodes.forEach((gltfNode, idx) => {
79
+ gltfNodeIndexToNodeMap.get(idx)!.add(
80
+ (gltfNode.children ?? []).map(({id}) => {
81
+ const child = gltfNodeIdToNodeMap.get(id);
82
+ if (!child) throw new Error(`Cannot find child ${id} of node ${idx}`);
83
+ return child;
84
+ })
85
+ );
77
86
 
78
- // Node can have children nodes and meshes at the same time
87
+ // Nodes can have children nodes and one optional child mesh at the same time.
79
88
  if (gltfNode.mesh) {
80
- children.push(createMesh(device, gltfNode.mesh, options));
89
+ const mesh = gltfMeshIdToNodeMap.get(gltfNode.mesh.id);
90
+ if (!mesh) {
91
+ throw new Error(`Cannot find mesh child ${gltfNode.mesh.id} of node ${idx}`);
92
+ }
93
+ gltfNodeIndexToNodeMap.get(idx)!.add(mesh);
81
94
  }
95
+ });
82
96
 
83
- const node = new GroupNode({
84
- id: gltfNode.name || gltfNode.id,
97
+ const scenes = gltf.scenes.map(gltfScene => {
98
+ const children = (gltfScene.nodes || []).map(({id}) => {
99
+ const child = gltfNodeIdToNodeMap.get(id);
100
+ if (!child)
101
+ throw new Error(`Cannot find child ${id} of scene ${gltfScene.name || gltfScene.id}`);
102
+ return child;
103
+ });
104
+ return new GroupNode({
105
+ id: gltfScene.name || gltfScene.id,
85
106
  children
86
107
  });
108
+ });
87
109
 
88
- if (gltfNode.matrix) {
89
- node.setMatrix(gltfNode.matrix);
90
- } else {
91
- node.matrix.identity();
92
-
93
- if (gltfNode.translation) {
94
- node.matrix.translate(gltfNode.translation);
95
- }
96
-
97
- if (gltfNode.rotation) {
98
- const rotationMatrix = new Matrix4().fromQuaternion(gltfNode.rotation);
99
- node.matrix.multiplyRight(rotationMatrix);
100
- }
101
-
102
- if (gltfNode.scale) {
103
- node.matrix.scale(gltfNode.scale);
104
- }
105
- }
106
- gltfNode._node = node;
107
- }
108
-
109
- // Copy _node so that gltf-animator can access
110
- const topLevelNode = gltfNodes.find(node => node.id === gltfNode.id) as any;
111
- topLevelNode._node = gltfNode._node;
110
+ return {scenes, gltfMeshIdToNodeMap, gltfNodeIdToNodeMap, gltfNodeIndexToNodeMap};
111
+ }
112
112
 
113
- return gltfNode._node;
113
+ /** Creates a `GroupNode` for one glTF node transform. */
114
+ function createNodeForGLTFNode(
115
+ device: Device,
116
+ gltfNode: GLTFNodePostprocessed,
117
+ options: Required<ParseGLTFOptions>
118
+ ): GroupNode {
119
+ return new GroupNode({
120
+ id: gltfNode.name || gltfNode.id,
121
+ children: [],
122
+ matrix: gltfNode.matrix,
123
+ position: gltfNode.translation,
124
+ rotation: gltfNode.rotation,
125
+ scale: gltfNode.scale
126
+ });
114
127
  }
115
128
 
116
- function createMesh(
129
+ /** Creates a mesh group node containing one model node per glTF primitive. */
130
+ function createNodeForGLTFMesh(
117
131
  device: Device,
118
- gltfMesh: GLTFMeshPostprocessed & {_mesh?: GroupNode},
132
+ gltfMesh: GLTFMeshPostprocessed,
119
133
  options: Required<ParseGLTFOptions>
120
134
  ): GroupNode {
121
- // TODO: avoid changing the gltf
122
- if (!gltfMesh._mesh) {
123
- const gltfPrimitives = gltfMesh.primitives || [];
124
- const primitives = gltfPrimitives.map((gltfPrimitive, i) =>
125
- createPrimitive(device, gltfPrimitive, i, gltfMesh, options)
126
- );
127
- const mesh = new GroupNode({
128
- id: gltfMesh.name || gltfMesh.id,
129
- children: primitives
130
- });
131
- gltfMesh._mesh = mesh;
132
- }
135
+ const gltfPrimitives = gltfMesh.primitives || [];
136
+ const primitives = gltfPrimitives.map((gltfPrimitive, i) =>
137
+ createNodeForGLTFPrimitive(device, gltfPrimitive, i, gltfMesh, options)
138
+ );
139
+ const mesh = new GroupNode({
140
+ id: gltfMesh.name || gltfMesh.id,
141
+ children: primitives
142
+ });
133
143
 
134
- return gltfMesh._mesh;
144
+ return mesh;
135
145
  }
136
146
 
137
- function createPrimitive(
147
+ /** Creates a renderable model node for one glTF primitive. */
148
+ function createNodeForGLTFPrimitive(
138
149
  device: Device,
139
150
  gltfPrimitive: any,
140
151
  i: number,
@@ -171,10 +182,12 @@ function createPrimitive(
171
182
  return modelNode;
172
183
  }
173
184
 
185
+ /** Computes the vertex count for a primitive without indices. */
174
186
  function getVertexCount(attributes: any) {
175
187
  throw new Error('getVertexCount not implemented');
176
188
  }
177
189
 
190
+ /** Converts glTF primitive attributes and indices into a luma.gl `Geometry`. */
178
191
  function createGeometry(id: string, gltfPrimitive: any, topology: PrimitiveTopology): Geometry {
179
192
  const attributes: Record<string, GeometryAttribute> = {};
180
193
  for (const [attributeName, attribute] of Object.entries(gltfPrimitive.attributes)) {