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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/dist.dev.js +4064 -1967
  2. package/dist/dist.min.js +117 -46
  3. package/dist/gltf/animations/animations.d.ts +57 -5
  4. package/dist/gltf/animations/animations.d.ts.map +1 -1
  5. package/dist/gltf/animations/interpolate.d.ts +6 -3
  6. package/dist/gltf/animations/interpolate.d.ts.map +1 -1
  7. package/dist/gltf/animations/interpolate.js +47 -51
  8. package/dist/gltf/animations/interpolate.js.map +1 -1
  9. package/dist/gltf/create-gltf-model.d.ts +15 -1
  10. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  11. package/dist/gltf/create-gltf-model.js +168 -43
  12. package/dist/gltf/create-gltf-model.js.map +1 -1
  13. package/dist/gltf/create-scenegraph-from-gltf.d.ts +39 -2
  14. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  15. package/dist/gltf/create-scenegraph-from-gltf.js +76 -6
  16. package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
  17. package/dist/gltf/gltf-animator.d.ts +37 -0
  18. package/dist/gltf/gltf-animator.d.ts.map +1 -1
  19. package/dist/gltf/gltf-animator.js +112 -17
  20. package/dist/gltf/gltf-animator.js.map +1 -1
  21. package/dist/gltf/gltf-extension-support.d.ts +13 -0
  22. package/dist/gltf/gltf-extension-support.d.ts.map +1 -0
  23. package/dist/gltf/gltf-extension-support.js +178 -0
  24. package/dist/gltf/gltf-extension-support.js.map +1 -0
  25. package/dist/index.cjs +1806 -298
  26. package/dist/index.cjs.map +4 -4
  27. package/dist/index.d.ts +3 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/parsers/parse-gltf-animations.d.ts +1 -0
  32. package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
  33. package/dist/parsers/parse-gltf-animations.js +373 -27
  34. package/dist/parsers/parse-gltf-animations.js.map +1 -1
  35. package/dist/parsers/parse-gltf-lights.d.ts +5 -0
  36. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -0
  37. package/dist/parsers/parse-gltf-lights.js +163 -0
  38. package/dist/parsers/parse-gltf-lights.js.map +1 -0
  39. package/dist/parsers/parse-gltf.d.ts +19 -2
  40. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  41. package/dist/parsers/parse-gltf.js +120 -67
  42. package/dist/parsers/parse-gltf.js.map +1 -1
  43. package/dist/parsers/parse-pbr-material.d.ts +115 -2
  44. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  45. package/dist/parsers/parse-pbr-material.js +602 -53
  46. package/dist/parsers/parse-pbr-material.js.map +1 -1
  47. package/dist/pbr/pbr-environment.d.ts +10 -4
  48. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  49. package/dist/pbr/pbr-environment.js +18 -15
  50. package/dist/pbr/pbr-environment.js.map +1 -1
  51. package/dist/pbr/pbr-material.d.ts +13 -3
  52. package/dist/pbr/pbr-material.d.ts.map +1 -1
  53. package/dist/pbr/texture-transform.d.ts +24 -0
  54. package/dist/pbr/texture-transform.d.ts.map +1 -0
  55. package/dist/pbr/texture-transform.js +98 -0
  56. package/dist/pbr/texture-transform.js.map +1 -0
  57. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  58. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  59. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  60. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  61. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +11 -5
  62. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  63. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +16 -12
  64. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  65. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -9
  66. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  67. package/dist/webgl-to-webgpu/convert-webgl-topology.js +3 -15
  68. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  69. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  70. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  71. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  72. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  73. package/package.json +8 -9
  74. package/src/gltf/animations/animations.ts +88 -6
  75. package/src/gltf/animations/interpolate.ts +84 -96
  76. package/src/gltf/create-gltf-model.ts +233 -48
  77. package/src/gltf/create-scenegraph-from-gltf.ts +134 -11
  78. package/src/gltf/gltf-animator.ts +198 -20
  79. package/src/gltf/gltf-extension-support.ts +226 -0
  80. package/src/index.ts +11 -2
  81. package/src/parsers/parse-gltf-animations.ts +533 -32
  82. package/src/parsers/parse-gltf-lights.ts +218 -0
  83. package/src/parsers/parse-gltf.ts +189 -96
  84. package/src/parsers/parse-pbr-material.ts +974 -79
  85. package/src/pbr/pbr-environment.ts +44 -21
  86. package/src/pbr/pbr-material.ts +18 -3
  87. package/src/pbr/texture-transform.ts +263 -0
  88. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  89. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +38 -29
  90. package/src/webgl-to-webgpu/convert-webgl-topology.ts +3 -15
  91. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
  92. package/dist/utils/deep-copy.d.ts +0 -3
  93. package/dist/utils/deep-copy.d.ts.map +0 -1
  94. package/dist/utils/deep-copy.js +0 -21
  95. package/dist/utils/deep-copy.js.map +0 -1
  96. package/src/utils/deep-copy.ts +0 -22
@@ -0,0 +1,218 @@
1
+ import {Matrix4} from '@math.gl/core';
2
+ import type {GLTFNodePostprocessed, GLTFPostprocessed} from '@loaders.gl/gltf';
3
+ import type {DirectionalLight, Light, PointLight, SpotLight} from '@luma.gl/shadertools';
4
+
5
+ const GLTF_COLOR_FACTOR = 255;
6
+
7
+ /** Parse KHR_lights_punctual extension into luma.gl light definitions */
8
+ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
9
+ const lightDefs =
10
+ // `postProcessGLTF()` moves KHR_lights_punctual into `gltf.lights`.
11
+ (gltf as GLTFPostprocessed & {lights?: any[]}).lights ||
12
+ gltf.extensions?.['KHR_lights_punctual']?.['lights'];
13
+ if (!lightDefs || !Array.isArray(lightDefs) || lightDefs.length === 0) {
14
+ return [];
15
+ }
16
+
17
+ const lights: Light[] = [];
18
+ const parentNodeById = createParentNodeMap(gltf.nodes || []);
19
+ const worldMatrixByNodeId = new Map<string, Matrix4>();
20
+
21
+ for (const node of gltf.nodes || []) {
22
+ const lightIndex =
23
+ (node as GLTFNodePostprocessed & {light?: number}).light ??
24
+ node.extensions?.KHR_lights_punctual?.light;
25
+ if (typeof lightIndex !== 'number') {
26
+ // eslint-disable-next-line no-continue
27
+ continue;
28
+ }
29
+ const gltfLight = lightDefs[lightIndex];
30
+ if (!gltfLight) {
31
+ // eslint-disable-next-line no-continue
32
+ continue;
33
+ }
34
+
35
+ const color = normalizeGLTFLightColor(
36
+ (gltfLight.color || [1, 1, 1]) as [number, number, number]
37
+ );
38
+ const intensity = gltfLight.intensity ?? 1;
39
+ const range = gltfLight.range;
40
+ const worldMatrix = getNodeWorldMatrix(node, parentNodeById, worldMatrixByNodeId);
41
+
42
+ switch (gltfLight.type) {
43
+ case 'directional':
44
+ lights.push(parseDirectionalLight(worldMatrix, color, intensity));
45
+ break;
46
+ case 'point':
47
+ lights.push(parsePointLight(worldMatrix, color, intensity, range));
48
+ break;
49
+ case 'spot':
50
+ lights.push(parseSpotLight(worldMatrix, color, intensity, range, gltfLight.spot));
51
+ break;
52
+ default:
53
+ // Unsupported light type
54
+ break;
55
+ }
56
+ }
57
+
58
+ return lights;
59
+ }
60
+
61
+ /**
62
+ * Converts glTF colors from the 0-1 spec range to luma.gl's 0-255 light convention.
63
+ */
64
+ function normalizeGLTFLightColor(color: [number, number, number]): [number, number, number] {
65
+ return color.map(component => component * GLTF_COLOR_FACTOR) as [number, number, number];
66
+ }
67
+
68
+ /**
69
+ * Converts a glTF punctual light attached to a node into a point light.
70
+ */
71
+ function parsePointLight(
72
+ worldMatrix: Matrix4,
73
+ color: [number, number, number],
74
+ intensity: number,
75
+ range?: number
76
+ ): PointLight {
77
+ const position = getLightPosition(worldMatrix);
78
+
79
+ let attenuation: Readonly<[number, number, number]> = [1, 0, 0];
80
+ if (range !== undefined && range > 0) {
81
+ attenuation = [1, 0, 1 / (range * range)] as [number, number, number];
82
+ }
83
+
84
+ return {
85
+ type: 'point',
86
+ position,
87
+ color,
88
+ intensity,
89
+ attenuation
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Converts a glTF punctual light attached to a node into a directional light.
95
+ */
96
+ function parseDirectionalLight(
97
+ worldMatrix: Matrix4,
98
+ color: [number, number, number],
99
+ intensity: number
100
+ ): DirectionalLight {
101
+ const direction = getLightDirection(worldMatrix);
102
+
103
+ return {
104
+ type: 'directional',
105
+ direction,
106
+ color,
107
+ intensity
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Converts a glTF punctual light attached to a node into a spot light.
113
+ */
114
+ function parseSpotLight(
115
+ worldMatrix: Matrix4,
116
+ color: [number, number, number],
117
+ intensity: number,
118
+ range?: number,
119
+ spot: {innerConeAngle?: number; outerConeAngle?: number} = {}
120
+ ): SpotLight {
121
+ const position = getLightPosition(worldMatrix);
122
+ const direction = getLightDirection(worldMatrix);
123
+
124
+ let attenuation: Readonly<[number, number, number]> = [1, 0, 0];
125
+ if (range !== undefined && range > 0) {
126
+ attenuation = [1, 0, 1 / (range * range)] as [number, number, number];
127
+ }
128
+
129
+ return {
130
+ type: 'spot',
131
+ position,
132
+ direction,
133
+ color,
134
+ intensity,
135
+ attenuation,
136
+ innerConeAngle: spot.innerConeAngle ?? 0,
137
+ outerConeAngle: spot.outerConeAngle ?? Math.PI / 4
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Builds a parent lookup so punctual lights can be resolved in world space.
143
+ */
144
+ function createParentNodeMap(nodes: GLTFNodePostprocessed[]): Map<string, GLTFNodePostprocessed> {
145
+ const parentNodeById = new Map<string, GLTFNodePostprocessed>();
146
+
147
+ for (const node of nodes) {
148
+ for (const childNode of node.children || []) {
149
+ parentNodeById.set(childNode.id, node);
150
+ }
151
+ }
152
+
153
+ return parentNodeById;
154
+ }
155
+
156
+ /**
157
+ * Resolves a glTF node's world matrix from its local transform and parent chain.
158
+ */
159
+ function getNodeWorldMatrix(
160
+ node: GLTFNodePostprocessed,
161
+ parentNodeById: Map<string, GLTFNodePostprocessed>,
162
+ worldMatrixByNodeId: Map<string, Matrix4>
163
+ ): Matrix4 {
164
+ const cachedWorldMatrix = worldMatrixByNodeId.get(node.id);
165
+ if (cachedWorldMatrix) {
166
+ return cachedWorldMatrix;
167
+ }
168
+
169
+ const localMatrix = getNodeLocalMatrix(node);
170
+ const parentNode = parentNodeById.get(node.id);
171
+ const worldMatrix = parentNode
172
+ ? new Matrix4(
173
+ getNodeWorldMatrix(parentNode, parentNodeById, worldMatrixByNodeId)
174
+ ).multiplyRight(localMatrix)
175
+ : localMatrix;
176
+
177
+ worldMatrixByNodeId.set(node.id, worldMatrix);
178
+ return worldMatrix;
179
+ }
180
+
181
+ /**
182
+ * Resolves a glTF node's local matrix from its matrix or TRS components.
183
+ */
184
+ function getNodeLocalMatrix(node: GLTFNodePostprocessed): Matrix4 {
185
+ if (node.matrix) {
186
+ return new Matrix4(node.matrix);
187
+ }
188
+
189
+ const matrix = new Matrix4();
190
+
191
+ if (node.translation) {
192
+ matrix.translate(node.translation);
193
+ }
194
+
195
+ if (node.rotation) {
196
+ matrix.multiplyRight(new Matrix4().fromQuaternion(node.rotation));
197
+ }
198
+
199
+ if (node.scale) {
200
+ matrix.scale(node.scale);
201
+ }
202
+
203
+ return matrix;
204
+ }
205
+
206
+ /**
207
+ * Resolves the world-space position of a glTF light node.
208
+ */
209
+ function getLightPosition(worldMatrix: Matrix4): [number, number, number] {
210
+ return worldMatrix.transformAsPoint([0, 0, 0]) as [number, number, number];
211
+ }
212
+
213
+ /**
214
+ * Resolves the world-space forward direction of a glTF light node.
215
+ */
216
+ function getLightDirection(worldMatrix: Matrix4): [number, number, number] {
217
+ return worldMatrix.transformDirection([0, 0, -1]) as [number, number, number];
218
+ }
@@ -3,26 +3,40 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {Device, type PrimitiveTopology} from '@luma.gl/core';
6
- import {Geometry, GeometryAttribute, GroupNode, ModelNode, type ModelProps} from '@luma.gl/engine';
7
- import {Matrix4} from '@math.gl/core';
8
6
  import {
7
+ Geometry,
8
+ GeometryAttribute,
9
+ GroupNode,
10
+ Material,
11
+ MaterialFactory,
12
+ ModelNode,
13
+ type ModelProps
14
+ } from '@luma.gl/engine';
15
+ import {
16
+ type GLTFMaterialPostprocessed,
9
17
  type GLTFMeshPostprocessed,
10
18
  type GLTFNodePostprocessed,
11
19
  type GLTFPostprocessed
12
20
  } from '@loaders.gl/gltf';
13
- import {type GLTFScenePostprocessed} from '@loaders.gl/gltf/dist/lib/types/gltf-postprocessed-schema';
21
+ import {pbrMaterial} from '@luma.gl/shadertools';
14
22
 
15
23
  import {type PBREnvironment} from '../pbr/pbr-environment';
16
24
  import {convertGLDrawModeToTopology} from '../webgl-to-webgpu/convert-webgl-topology';
17
- import {createGLTFModel} from '../gltf/create-gltf-model';
25
+ import {createGLTFMaterial, createGLTFModel} from '../gltf/create-gltf-model';
18
26
 
19
27
  import {parsePBRMaterial} from './parse-pbr-material';
20
28
 
29
+ /** Options that influence how a post-processed glTF is turned into a luma.gl scenegraph. */
21
30
  export type ParseGLTFOptions = {
31
+ /** Additional model props applied to each generated primitive model. */
22
32
  modelOptions?: Partial<ModelProps>;
33
+ /** Enables shader-level PBR debug output. */
23
34
  pbrDebug?: boolean;
35
+ /** Optional image-based lighting environment. */
24
36
  imageBasedLightingEnvironment?: PBREnvironment;
37
+ /** Enables punctual light extraction. */
25
38
  lights?: boolean;
39
+ /** Enables tangent usage when available. */
26
40
  useTangents?: boolean;
27
41
  };
28
42
 
@@ -41,124 +55,184 @@ const defaultOptions: Required<ParseGLTFOptions> = {
41
55
  export function parseGLTF(
42
56
  device: Device,
43
57
  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)
58
+ options: ParseGLTFOptions = {}
59
+ ): {
60
+ /** Scene roots generated from `gltf.scenes`. */
61
+ scenes: GroupNode[];
62
+ /** Materials aligned with the source `gltf.materials` array. */
63
+ materials: Material[];
64
+ /** Map from glTF mesh ids to generated mesh group nodes. */
65
+ gltfMeshIdToNodeMap: Map<string, GroupNode>;
66
+ /** Map from glTF node indices to generated scenegraph nodes. */
67
+ gltfNodeIndexToNodeMap: Map<number, GroupNode>;
68
+ /** Map from glTF node ids to generated scenegraph nodes. */
69
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
70
+ } {
71
+ const combinedOptions = {...defaultOptions, ...options};
72
+ const materialFactory = new MaterialFactory(device, {modules: [pbrMaterial]});
73
+ const materials = (gltf.materials || []).map((gltfMaterial, materialIndex) =>
74
+ createGLTFMaterial(device, {
75
+ id: getGLTFMaterialId(gltfMaterial, materialIndex),
76
+ parsedPPBRMaterial: parsePBRMaterial(
77
+ device,
78
+ gltfMaterial as any,
79
+ {},
80
+ {
81
+ ...combinedOptions,
82
+ gltf,
83
+ validateAttributes: false
84
+ }
85
+ ),
86
+ materialFactory
87
+ })
49
88
  );
50
- return sceneNodes;
51
- }
89
+ const gltfMaterialIdToMaterialMap = new Map<string, Material>();
90
+ (gltf.materials || []).forEach((gltfMaterial, materialIndex) => {
91
+ gltfMaterialIdToMaterialMap.set(gltfMaterial.id, materials[materialIndex]);
92
+ });
52
93
 
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
94
+ const gltfMeshIdToNodeMap = new Map<string, GroupNode>();
95
+ gltf.meshes.forEach((gltfMesh, idx) => {
96
+ const newMesh = createNodeForGLTFMesh(
97
+ device,
98
+ gltfMesh,
99
+ gltf,
100
+ gltfMaterialIdToMaterialMap,
101
+ combinedOptions
102
+ );
103
+ gltfMeshIdToNodeMap.set(gltfMesh.id, newMesh);
64
104
  });
65
- return sceneNode;
66
- }
67
105
 
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));
106
+ const gltfNodeIndexToNodeMap = new Map<number, GroupNode>();
107
+ const gltfNodeIdToNodeMap = new Map<string, GroupNode>();
108
+ // Step 1/2: Generate a GroupNode for each gltf node. (1:1 mapping).
109
+ gltf.nodes.forEach((gltfNode, idx) => {
110
+ const newNode = createNodeForGLTFNode(device, gltfNode, combinedOptions);
111
+ gltfNodeIndexToNodeMap.set(idx, newNode);
112
+ gltfNodeIdToNodeMap.set(gltfNode.id, newNode);
113
+ });
77
114
 
78
- // Node can have children nodes and meshes at the same time
115
+ // Step 2/2: Go though each gltf node and attach the children.
116
+ // This guarantees that each gltf node will have exactly one luma GroupNode.
117
+ gltf.nodes.forEach((gltfNode, idx) => {
118
+ gltfNodeIndexToNodeMap.get(idx)!.add(
119
+ (gltfNode.children ?? []).map(({id}) => {
120
+ const child = gltfNodeIdToNodeMap.get(id);
121
+ if (!child) throw new Error(`Cannot find child ${id} of node ${idx}`);
122
+ return child;
123
+ })
124
+ );
125
+
126
+ // Nodes can have children nodes and one optional child mesh at the same time.
79
127
  if (gltfNode.mesh) {
80
- children.push(createMesh(device, gltfNode.mesh, options));
128
+ const mesh = gltfMeshIdToNodeMap.get(gltfNode.mesh.id);
129
+ if (!mesh) {
130
+ throw new Error(`Cannot find mesh child ${gltfNode.mesh.id} of node ${idx}`);
131
+ }
132
+ gltfNodeIndexToNodeMap.get(idx)!.add(mesh);
81
133
  }
134
+ });
82
135
 
83
- const node = new GroupNode({
84
- id: gltfNode.name || gltfNode.id,
136
+ const scenes = gltf.scenes.map(gltfScene => {
137
+ const children = (gltfScene.nodes || []).map(({id}) => {
138
+ const child = gltfNodeIdToNodeMap.get(id);
139
+ if (!child)
140
+ throw new Error(`Cannot find child ${id} of scene ${gltfScene.name || gltfScene.id}`);
141
+ return child;
142
+ });
143
+ return new GroupNode({
144
+ id: gltfScene.name || gltfScene.id,
85
145
  children
86
146
  });
147
+ });
87
148
 
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;
112
-
113
- return gltfNode._node;
149
+ return {scenes, materials, gltfMeshIdToNodeMap, gltfNodeIdToNodeMap, gltfNodeIndexToNodeMap};
114
150
  }
115
151
 
116
- function createMesh(
152
+ /** Creates a `GroupNode` for one glTF node transform. */
153
+ function createNodeForGLTFNode(
117
154
  device: Device,
118
- gltfMesh: GLTFMeshPostprocessed & {_mesh?: GroupNode},
155
+ gltfNode: GLTFNodePostprocessed,
119
156
  options: Required<ParseGLTFOptions>
120
157
  ): 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
- }
133
-
134
- return gltfMesh._mesh;
158
+ return new GroupNode({
159
+ id: gltfNode.name || gltfNode.id,
160
+ children: [],
161
+ matrix: gltfNode.matrix,
162
+ position: gltfNode.translation,
163
+ rotation: gltfNode.rotation,
164
+ scale: gltfNode.scale
165
+ });
135
166
  }
136
167
 
137
- function createPrimitive(
168
+ /** Creates a mesh group node containing one model node per glTF primitive. */
169
+ function createNodeForGLTFMesh(
138
170
  device: Device,
139
- gltfPrimitive: any,
140
- i: number,
141
171
  gltfMesh: GLTFMeshPostprocessed,
172
+ gltf: GLTFPostprocessed,
173
+ gltfMaterialIdToMaterialMap: Map<string, Material>,
142
174
  options: Required<ParseGLTFOptions>
143
- ): ModelNode {
144
- const id = gltfPrimitive.name || `${gltfMesh.name || gltfMesh.id}-primitive-${i}`;
145
- const topology = convertGLDrawModeToTopology(gltfPrimitive.mode || 4);
175
+ ): GroupNode {
176
+ const gltfPrimitives = gltfMesh.primitives || [];
177
+ const primitives = gltfPrimitives.map((gltfPrimitive, i) =>
178
+ createNodeForGLTFPrimitive({
179
+ device,
180
+ gltfPrimitive,
181
+ primitiveIndex: i,
182
+ gltfMesh,
183
+ gltf,
184
+ gltfMaterialIdToMaterialMap,
185
+ options
186
+ })
187
+ );
188
+ const mesh = new GroupNode({
189
+ id: gltfMesh.name || gltfMesh.id,
190
+ children: primitives
191
+ });
192
+
193
+ return mesh;
194
+ }
195
+
196
+ /** Input options for creating one renderable glTF primitive model node. */
197
+ type CreateNodeForGLTFPrimitiveOptions = {
198
+ device: Device;
199
+ gltfPrimitive: any;
200
+ primitiveIndex: number;
201
+ gltfMesh: GLTFMeshPostprocessed;
202
+ gltf: GLTFPostprocessed;
203
+ gltfMaterialIdToMaterialMap: Map<string, Material>;
204
+ options: Required<ParseGLTFOptions>;
205
+ };
206
+
207
+ /** Creates a renderable model node for one glTF primitive. */
208
+ function createNodeForGLTFPrimitive({
209
+ device,
210
+ gltfPrimitive,
211
+ primitiveIndex,
212
+ gltfMesh,
213
+ gltf,
214
+ gltfMaterialIdToMaterialMap,
215
+ options
216
+ }: CreateNodeForGLTFPrimitiveOptions): ModelNode {
217
+ const id = gltfPrimitive.name || `${gltfMesh.name || gltfMesh.id}-primitive-${primitiveIndex}`;
218
+ const topology = convertGLDrawModeToTopology(gltfPrimitive.mode ?? 4);
146
219
  const vertexCount = gltfPrimitive.indices
147
220
  ? gltfPrimitive.indices.count
148
221
  : getVertexCount(gltfPrimitive.attributes);
149
222
 
150
223
  const geometry = createGeometry(id, gltfPrimitive, topology);
151
224
 
152
- const parsedPPBRMaterial = parsePBRMaterial(
153
- device,
154
- gltfPrimitive.material,
155
- geometry.attributes,
156
- options
157
- );
225
+ const parsedPPBRMaterial = parsePBRMaterial(device, gltfPrimitive.material, geometry.attributes, {
226
+ ...options,
227
+ gltf
228
+ });
158
229
 
159
230
  const modelNode = createGLTFModel(device, {
160
231
  id,
161
- geometry: createGeometry(id, gltfPrimitive, topology),
232
+ geometry,
233
+ material: gltfPrimitive.material
234
+ ? gltfMaterialIdToMaterialMap.get(gltfPrimitive.material.id) || null
235
+ : null,
162
236
  parsedPPBRMaterial,
163
237
  modelOptions: options.modelOptions,
164
238
  vertexCount
@@ -171,22 +245,41 @@ function createPrimitive(
171
245
  return modelNode;
172
246
  }
173
247
 
248
+ /** Computes the vertex count for a primitive without indices. */
174
249
  function getVertexCount(attributes: any) {
175
- throw new Error('getVertexCount not implemented');
250
+ let vertexCount = Infinity;
251
+ for (const attribute of Object.values(attributes)) {
252
+ if (attribute) {
253
+ const {value, size, components} = attribute as any;
254
+ const attributeSize = size ?? components;
255
+ if (value?.length !== undefined && attributeSize >= 1) {
256
+ vertexCount = Math.min(vertexCount, value.length / attributeSize);
257
+ }
258
+ }
259
+ }
260
+ if (!Number.isFinite(vertexCount)) {
261
+ throw new Error('Could not determine vertex count from attributes');
262
+ }
263
+ return vertexCount;
176
264
  }
177
265
 
266
+ /** Converts glTF primitive attributes and indices into a luma.gl `Geometry`. */
178
267
  function createGeometry(id: string, gltfPrimitive: any, topology: PrimitiveTopology): Geometry {
179
268
  const attributes: Record<string, GeometryAttribute> = {};
180
269
  for (const [attributeName, attribute] of Object.entries(gltfPrimitive.attributes)) {
181
- const {components, size, value} = attribute as GeometryAttribute;
270
+ const {components, size, value, normalized} = attribute as GeometryAttribute;
182
271
 
183
- attributes[attributeName] = {size: size ?? components, value};
272
+ attributes[attributeName] = {size: size ?? components, value, normalized};
184
273
  }
185
274
 
186
275
  return new Geometry({
187
276
  id,
188
277
  topology,
189
- indices: gltfPrimitive.indices.value,
278
+ indices: gltfPrimitive.indices?.value,
190
279
  attributes
191
280
  });
192
281
  }
282
+
283
+ function getGLTFMaterialId(gltfMaterial: GLTFMaterialPostprocessed, materialIndex: number): string {
284
+ return gltfMaterial.name || gltfMaterial.id || `material-${materialIndex}`;
285
+ }