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

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 (65) hide show
  1. package/dist/dist.dev.js +942 -141
  2. package/dist/dist.min.js +4 -4
  3. package/dist/gltf/create-gltf-model.d.ts +9 -1
  4. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  5. package/dist/gltf/create-gltf-model.js +58 -4
  6. package/dist/gltf/create-gltf-model.js.map +1 -1
  7. package/dist/gltf/create-scenegraph-from-gltf.d.ts +22 -1
  8. package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
  9. package/dist/gltf/create-scenegraph-from-gltf.js +63 -1
  10. package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
  11. package/dist/gltf/gltf-extension-support.d.ts +10 -0
  12. package/dist/gltf/gltf-extension-support.d.ts.map +1 -0
  13. package/dist/gltf/gltf-extension-support.js +173 -0
  14. package/dist/gltf/gltf-extension-support.js.map +1 -0
  15. package/dist/index.cjs +899 -114
  16. package/dist/index.cjs.map +4 -4
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
  22. package/dist/parsers/parse-gltf-animations.js +34 -12
  23. package/dist/parsers/parse-gltf-animations.js.map +1 -1
  24. package/dist/parsers/parse-gltf-lights.d.ts.map +1 -1
  25. package/dist/parsers/parse-gltf-lights.js +86 -20
  26. package/dist/parsers/parse-gltf-lights.js.map +1 -1
  27. package/dist/parsers/parse-gltf.d.ts +3 -1
  28. package/dist/parsers/parse-gltf.d.ts.map +1 -1
  29. package/dist/parsers/parse-gltf.js +41 -9
  30. package/dist/parsers/parse-gltf.js.map +1 -1
  31. package/dist/parsers/parse-pbr-material.d.ts +69 -1
  32. package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
  33. package/dist/parsers/parse-pbr-material.js +429 -42
  34. package/dist/parsers/parse-pbr-material.js.map +1 -1
  35. package/dist/pbr/pbr-environment.d.ts.map +1 -1
  36. package/dist/pbr/pbr-environment.js +14 -12
  37. package/dist/pbr/pbr-environment.js.map +1 -1
  38. package/dist/pbr/pbr-material.d.ts +8 -3
  39. package/dist/pbr/pbr-material.d.ts.map +1 -1
  40. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +5 -5
  41. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  42. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +12 -12
  43. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  44. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +1 -10
  45. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  46. package/dist/webgl-to-webgpu/convert-webgl-topology.js +1 -15
  47. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  48. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  49. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  50. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  51. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  52. package/package.json +5 -6
  53. package/src/gltf/create-gltf-model.ts +113 -5
  54. package/src/gltf/create-scenegraph-from-gltf.ts +97 -6
  55. package/src/gltf/gltf-extension-support.ts +214 -0
  56. package/src/index.ts +10 -1
  57. package/src/parsers/parse-gltf-animations.ts +39 -15
  58. package/src/parsers/parse-gltf-lights.ts +114 -25
  59. package/src/parsers/parse-gltf.ts +86 -19
  60. package/src/parsers/parse-pbr-material.ts +664 -69
  61. package/src/pbr/pbr-environment.ts +29 -16
  62. package/src/pbr/pbr-material.ts +13 -3
  63. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +29 -29
  64. package/src/webgl-to-webgpu/convert-webgl-topology.ts +1 -15
  65. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
@@ -3,22 +3,44 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {Device} from '@luma.gl/core';
6
- import {GroupNode} from '@luma.gl/engine';
6
+ import {GroupNode, Material} from '@luma.gl/engine';
7
7
  import {GLTFPostprocessed} from '@loaders.gl/gltf';
8
8
  import {Light} from '@luma.gl/shadertools';
9
9
  import {parseGLTF, type ParseGLTFOptions} from '../parsers/parse-gltf';
10
10
  import {parseGLTFLights} from '../parsers/parse-gltf-lights';
11
11
  import {GLTFAnimator} from './gltf-animator';
12
12
  import {parseGLTFAnimations} from '../parsers/parse-gltf-animations';
13
+ import {getGLTFExtensionSupport, type GLTFExtensionSupport} from './gltf-extension-support';
14
+
15
+ export type GLTFScenegraphBounds = {
16
+ /** World-space axis-aligned bounds for the scene or model. */
17
+ bounds: [[number, number, number], [number, number, number]] | null;
18
+ /** World-space center of the bounds. */
19
+ center: [number, number, number];
20
+ /** World-space bounds size on each axis. */
21
+ size: [number, number, number];
22
+ /** Half of the world-space bounds diagonal, clamped to a small practical minimum. */
23
+ radius: number;
24
+ /** Suggested orbit distance for a 60-degree field of view camera. */
25
+ recommendedOrbitDistance: number;
26
+ };
13
27
 
14
28
  /** Scenegraph bundle returned from a parsed glTF asset. */
15
29
  export type GLTFScenegraphs = {
16
30
  /** Scene roots produced from the glTF scenes array. */
17
31
  scenes: GroupNode[];
32
+ /** Materials aligned with the source glTF `materials` array. */
33
+ materials: Material[];
18
34
  /** Animation controller for glTF animations. */
19
35
  animator: GLTFAnimator;
20
36
  /** Parsed punctual lights from the asset. */
21
37
  lights: Light[];
38
+ /** Extensions reported by the asset and whether luma.gl supports them. */
39
+ extensionSupport: Map<string, GLTFExtensionSupport>;
40
+ /** Camera-friendly bounds for each scene in `scenes`, in matching order. */
41
+ sceneBounds: GLTFScenegraphBounds[];
42
+ /** Combined camera-friendly bounds for the entire loaded asset. */
43
+ modelBounds: GLTFScenegraphBounds;
22
44
 
23
45
  /** Map from glTF mesh ids to generated mesh group nodes. */
24
46
  gltfMeshIdToNodeMap: Map<string, GroupNode>;
@@ -37,23 +59,92 @@ export function createScenegraphsFromGLTF(
37
59
  gltf: GLTFPostprocessed,
38
60
  options?: ParseGLTFOptions
39
61
  ): GLTFScenegraphs {
40
- const {scenes, gltfMeshIdToNodeMap, gltfNodeIdToNodeMap, gltfNodeIndexToNodeMap} = parseGLTF(
41
- device,
42
- gltf,
43
- options
44
- );
62
+ const {scenes, materials, gltfMeshIdToNodeMap, gltfNodeIdToNodeMap, gltfNodeIndexToNodeMap} =
63
+ parseGLTF(device, gltf, options);
45
64
 
46
65
  const animations = parseGLTFAnimations(gltf);
47
66
  const animator = new GLTFAnimator({animations, gltfNodeIdToNodeMap});
48
67
  const lights = parseGLTFLights(gltf);
68
+ const extensionSupport = getGLTFExtensionSupport(gltf);
69
+ const sceneBounds = scenes.map(scene => getScenegraphBounds(scene.getBounds()));
70
+ const modelBounds = getCombinedScenegraphBounds(sceneBounds);
49
71
 
50
72
  return {
51
73
  scenes,
74
+ materials,
52
75
  animator,
53
76
  lights,
77
+ extensionSupport,
78
+ sceneBounds,
79
+ modelBounds,
54
80
  gltfMeshIdToNodeMap,
55
81
  gltfNodeIdToNodeMap,
56
82
  gltfNodeIndexToNodeMap,
57
83
  gltf
58
84
  };
59
85
  }
86
+
87
+ function getScenegraphBounds(bounds: [number[], number[]] | null): GLTFScenegraphBounds {
88
+ if (!bounds) {
89
+ return {
90
+ bounds: null,
91
+ center: [0, 0, 0],
92
+ size: [0, 0, 0],
93
+ radius: 0.5,
94
+ recommendedOrbitDistance: 1
95
+ };
96
+ }
97
+
98
+ const normalizedBounds: [[number, number, number], [number, number, number]] = [
99
+ [bounds[0][0], bounds[0][1], bounds[0][2]],
100
+ [bounds[1][0], bounds[1][1], bounds[1][2]]
101
+ ];
102
+ const size: [number, number, number] = [
103
+ normalizedBounds[1][0] - normalizedBounds[0][0],
104
+ normalizedBounds[1][1] - normalizedBounds[0][1],
105
+ normalizedBounds[1][2] - normalizedBounds[0][2]
106
+ ];
107
+ const center: [number, number, number] = [
108
+ normalizedBounds[0][0] + size[0] * 0.5,
109
+ normalizedBounds[0][1] + size[1] * 0.5,
110
+ normalizedBounds[0][2] + size[2] * 0.5
111
+ ];
112
+ const maxHalfExtent = Math.max(size[0], size[1], size[2]) * 0.5;
113
+ const radius = Math.max(0.5 * Math.hypot(size[0], size[1], size[2]), 0.001);
114
+
115
+ return {
116
+ bounds: normalizedBounds,
117
+ center,
118
+ size,
119
+ radius,
120
+ recommendedOrbitDistance: Math.max(
121
+ (Math.max(maxHalfExtent, 0.001) / Math.tan(Math.PI / 6)) * 1.15,
122
+ radius * 1.1
123
+ )
124
+ };
125
+ }
126
+
127
+ function getCombinedScenegraphBounds(sceneBounds: GLTFScenegraphBounds[]): GLTFScenegraphBounds {
128
+ let combinedBounds: [[number, number, number], [number, number, number]] | null = null;
129
+
130
+ for (const sceneBoundInfo of sceneBounds) {
131
+ if (!sceneBoundInfo.bounds) {
132
+ continue;
133
+ }
134
+
135
+ if (!combinedBounds) {
136
+ combinedBounds = [
137
+ [...sceneBoundInfo.bounds[0]] as [number, number, number],
138
+ [...sceneBoundInfo.bounds[1]] as [number, number, number]
139
+ ];
140
+ continue;
141
+ }
142
+
143
+ for (let axis = 0; axis < 3; axis++) {
144
+ combinedBounds[0][axis] = Math.min(combinedBounds[0][axis], sceneBoundInfo.bounds[0][axis]);
145
+ combinedBounds[1][axis] = Math.max(combinedBounds[1][axis], sceneBoundInfo.bounds[1][axis]);
146
+ }
147
+ }
148
+
149
+ return getScenegraphBounds(combinedBounds);
150
+ }
@@ -0,0 +1,214 @@
1
+ // luma.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import type {GLTFPostprocessed} from '@loaders.gl/gltf';
6
+
7
+ export type GLTFExtensionSupportLevel = 'built-in' | 'parsed-and-wired' | 'loader-only' | 'none';
8
+
9
+ export type GLTFExtensionSupport = {
10
+ extensionName: string;
11
+ supported: boolean;
12
+ supportLevel: GLTFExtensionSupportLevel;
13
+ comment: string;
14
+ };
15
+
16
+ type GLTFExtensionSupportDefinition = Omit<GLTFExtensionSupport, 'extensionName' | 'supported'>;
17
+
18
+ type GLTFPostprocessedWithRemovedExtensions = GLTFPostprocessed & {
19
+ extensionsRemoved?: string[];
20
+ lights?: unknown[];
21
+ };
22
+
23
+ const UNKNOWN_EXTENSION_SUPPORT: GLTFExtensionSupportDefinition = {
24
+ supportLevel: 'none',
25
+ comment: 'Not currently listed in the luma.gl glTF extension support registry.'
26
+ };
27
+
28
+ const GLTF_EXTENSION_SUPPORT_REGISTRY: Record<string, GLTFExtensionSupportDefinition> = {
29
+ KHR_draco_mesh_compression: {
30
+ supportLevel: 'built-in',
31
+ comment: 'Decoded by loaders.gl before luma.gl builds the scenegraph.'
32
+ },
33
+ EXT_meshopt_compression: {
34
+ supportLevel: 'built-in',
35
+ comment: 'Meshopt-compressed primitives are decoded during load.'
36
+ },
37
+ KHR_mesh_quantization: {
38
+ supportLevel: 'built-in',
39
+ comment: 'Quantized accessors are unpacked before geometry creation.'
40
+ },
41
+ KHR_lights_punctual: {
42
+ supportLevel: 'built-in',
43
+ comment: 'Parsed into luma.gl Light objects.'
44
+ },
45
+ KHR_materials_unlit: {
46
+ supportLevel: 'built-in',
47
+ comment: 'Unlit materials bypass the default lighting path.'
48
+ },
49
+ KHR_materials_emissive_strength: {
50
+ supportLevel: 'built-in',
51
+ comment: 'Applied by the stock PBR shader.'
52
+ },
53
+ KHR_texture_basisu: {
54
+ supportLevel: 'built-in',
55
+ comment: 'BasisU / KTX2 textures pass through when the device supports them.'
56
+ },
57
+ KHR_texture_transform: {
58
+ supportLevel: 'built-in',
59
+ comment: 'UV transforms are applied during load.'
60
+ },
61
+ EXT_texture_webp: {
62
+ supportLevel: 'loader-only',
63
+ comment:
64
+ 'Texture source is resolved during load; final support depends on browser and device decode support.'
65
+ },
66
+ EXT_texture_avif: {
67
+ supportLevel: 'loader-only',
68
+ comment:
69
+ 'Texture source is resolved during load; final support depends on browser and device decode support.'
70
+ },
71
+ KHR_materials_specular: {
72
+ supportLevel: 'built-in',
73
+ comment: 'The stock shader now applies specular factors and textures to the dielectric F0 term.'
74
+ },
75
+ KHR_materials_ior: {
76
+ supportLevel: 'built-in',
77
+ comment: 'The stock shader now drives dielectric reflectance from the glTF IOR value.'
78
+ },
79
+ KHR_materials_transmission: {
80
+ supportLevel: 'built-in',
81
+ comment:
82
+ 'The stock shader now applies transmission to the base layer and exposes transparency through alpha, without a scene-color refraction buffer.'
83
+ },
84
+ KHR_materials_volume: {
85
+ supportLevel: 'built-in',
86
+ comment: 'Thickness and attenuation now tint transmitted light in the stock shader.'
87
+ },
88
+ KHR_materials_clearcoat: {
89
+ supportLevel: 'built-in',
90
+ comment: 'The stock shader now adds a secondary clearcoat specular lobe.'
91
+ },
92
+ KHR_materials_sheen: {
93
+ supportLevel: 'built-in',
94
+ comment: 'The stock shader now adds a sheen lobe for cloth-like materials.'
95
+ },
96
+ KHR_materials_iridescence: {
97
+ supportLevel: 'built-in',
98
+ comment:
99
+ 'The stock shader now tints specular response with a view-dependent thin-film iridescence approximation.'
100
+ },
101
+ KHR_materials_anisotropy: {
102
+ supportLevel: 'built-in',
103
+ comment:
104
+ 'The stock shader now shapes highlights and IBL response with an anisotropy-direction approximation.'
105
+ },
106
+ KHR_materials_pbrSpecularGlossiness: {
107
+ supportLevel: 'loader-only',
108
+ comment:
109
+ 'Extension data can be loaded, but it is not translated into the default metallic-roughness material path.'
110
+ },
111
+ KHR_materials_variants: {
112
+ supportLevel: 'loader-only',
113
+ comment: 'Variant metadata can be loaded, but applications must choose and apply variants.'
114
+ },
115
+ EXT_mesh_gpu_instancing: {
116
+ supportLevel: 'none',
117
+ comment: 'GPU instancing data is not yet converted into luma.gl instanced draw setup.'
118
+ },
119
+ KHR_node_visibility: {
120
+ supportLevel: 'none',
121
+ comment: 'Node-visibility animations and toggles are not mapped onto runtime scenegraph state.'
122
+ },
123
+ KHR_animation_pointer: {
124
+ supportLevel: 'none',
125
+ comment: 'Animation pointers are not mapped onto runtime scenegraph updates.'
126
+ },
127
+ KHR_materials_diffuse_transmission: {
128
+ supportLevel: 'none',
129
+ comment: 'Diffuse-transmission shading is not implemented in the stock PBR shader.'
130
+ },
131
+ KHR_materials_dispersion: {
132
+ supportLevel: 'none',
133
+ comment: 'Chromatic dispersion is not implemented in the stock PBR shader.'
134
+ },
135
+ KHR_materials_volume_scatter: {
136
+ supportLevel: 'none',
137
+ comment: 'Volume scattering is not implemented in the stock PBR shader.'
138
+ },
139
+ KHR_xmp: {
140
+ supportLevel: 'none',
141
+ comment: 'Metadata payloads remain in the loaded glTF, but luma.gl does not interpret them.'
142
+ },
143
+ KHR_xmp_json_ld: {
144
+ supportLevel: 'none',
145
+ comment: 'Metadata is preserved in the glTF, but luma.gl does not interpret it.'
146
+ },
147
+ EXT_lights_image_based: {
148
+ supportLevel: 'none',
149
+ comment: 'Use loadPBREnvironment() or custom environment setup instead.'
150
+ },
151
+ EXT_texture_video: {
152
+ supportLevel: 'none',
153
+ comment: 'Video textures are not created automatically by the stock pipeline.'
154
+ },
155
+ MSFT_lod: {
156
+ supportLevel: 'none',
157
+ comment: 'Level-of-detail switching is not implemented in the stock scenegraph loader.'
158
+ }
159
+ };
160
+
161
+ export function getGLTFExtensionSupport(
162
+ gltf: GLTFPostprocessed
163
+ ): Map<string, GLTFExtensionSupport> {
164
+ const extensionNames = Array.from(collectGLTFExtensionNames(gltf)).sort();
165
+ const extensionSupportEntries: [string, GLTFExtensionSupport][] = extensionNames.map(
166
+ extensionName => {
167
+ const extensionSupportDefinition =
168
+ GLTF_EXTENSION_SUPPORT_REGISTRY[extensionName] || UNKNOWN_EXTENSION_SUPPORT;
169
+
170
+ return [
171
+ extensionName,
172
+ {
173
+ extensionName,
174
+ supported: extensionSupportDefinition.supportLevel === 'built-in',
175
+ supportLevel: extensionSupportDefinition.supportLevel,
176
+ comment: extensionSupportDefinition.comment
177
+ }
178
+ ];
179
+ }
180
+ );
181
+
182
+ return new Map(extensionSupportEntries);
183
+ }
184
+
185
+ function collectGLTFExtensionNames(gltf: GLTFPostprocessed): Set<string> {
186
+ const gltfWithRemovedExtensions = gltf as GLTFPostprocessedWithRemovedExtensions;
187
+ const extensionNames = new Set<string>();
188
+
189
+ addExtensionNames(extensionNames, gltf.extensionsUsed);
190
+ addExtensionNames(extensionNames, gltf.extensionsRequired);
191
+ addExtensionNames(extensionNames, gltfWithRemovedExtensions.extensionsRemoved);
192
+ addExtensionNames(extensionNames, Object.keys(gltf.extensions || {}));
193
+
194
+ if (gltfWithRemovedExtensions.lights?.length || gltf.nodes.some(node => 'light' in node)) {
195
+ extensionNames.add('KHR_lights_punctual');
196
+ }
197
+
198
+ if (
199
+ gltf.materials.some(material => {
200
+ const gltfMaterial = material as typeof material & {unlit?: boolean};
201
+ return gltfMaterial.unlit || gltfMaterial.extensions?.KHR_materials_unlit;
202
+ })
203
+ ) {
204
+ extensionNames.add('KHR_materials_unlit');
205
+ }
206
+
207
+ return extensionNames;
208
+ }
209
+
210
+ function addExtensionNames(extensionNames: Set<string>, newExtensionNames: string[] = []): void {
211
+ for (const extensionName of newExtensionNames) {
212
+ extensionNames.add(extensionName);
213
+ }
214
+ }
package/src/index.ts CHANGED
@@ -6,5 +6,14 @@ export {parsePBRMaterial, type ParsePBRMaterialOptions} from './parsers/parse-pb
6
6
  export {parseGLTFLights} from './parsers/parse-gltf-lights';
7
7
 
8
8
  // glTF Scenegraph Instantiator
9
- export {createScenegraphsFromGLTF, type GLTFScenegraphs} from './gltf/create-scenegraph-from-gltf';
9
+ export {
10
+ createScenegraphsFromGLTF,
11
+ type GLTFScenegraphBounds,
12
+ type GLTFScenegraphs
13
+ } from './gltf/create-scenegraph-from-gltf';
14
+ export {
15
+ getGLTFExtensionSupport,
16
+ type GLTFExtensionSupport,
17
+ type GLTFExtensionSupportLevel
18
+ } from './gltf/gltf-extension-support';
10
19
  export {GLTFAnimator} from './gltf/gltf-animator';
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ import {log} from '@luma.gl/core';
5
6
  import {type GLTFAccessorPostprocessed, type GLTFPostprocessed} from '@loaders.gl/gltf';
6
7
  import {
7
8
  GLTFAnimationPath,
@@ -18,32 +19,55 @@ export function parseGLTFAnimations(gltf: GLTFPostprocessed): GLTFAnimation[] {
18
19
  const accessorCache1D = new Map<GLTFAccessorPostprocessed, number[]>();
19
20
  const accessorCache2D = new Map<GLTFAccessorPostprocessed, number[][]>();
20
21
 
21
- return gltfAnimations.map((animation, index) => {
22
+ return gltfAnimations.flatMap((animation, index) => {
22
23
  const name = animation.name || `Animation-${index}`;
23
- const samplers: GLTFAnimationSampler[] = animation.samplers.map(
24
- ({input, interpolation = 'LINEAR', output}) => ({
25
- input: accessorToJsArray1D(gltf.accessors[input], accessorCache1D),
26
- interpolation,
27
- output: accessorToJsArray2D(gltf.accessors[output], accessorCache2D)
28
- })
29
- );
30
-
31
- const channels: GLTFAnimationChannel[] = animation.channels.map(({sampler, target}) => {
24
+ const samplerCache = new Map<number, GLTFAnimationSampler>();
25
+ const channels: GLTFAnimationChannel[] = animation.channels.flatMap(({sampler, target}) => {
26
+ const path = getSupportedAnimationPath(target.path);
27
+ if (!path) {
28
+ return [];
29
+ }
30
+
32
31
  const targetNode = gltf.nodes[target.node ?? 0];
33
32
  if (!targetNode) {
34
33
  throw new Error(`Cannot find animation target ${target.node}`);
35
34
  }
35
+
36
+ let parsedSampler = samplerCache.get(sampler);
37
+ if (!parsedSampler) {
38
+ const gltfSampler = animation.samplers[sampler];
39
+ if (!gltfSampler) {
40
+ throw new Error(`Cannot find animation sampler ${sampler}`);
41
+ }
42
+ const {input, interpolation = 'LINEAR', output} = gltfSampler;
43
+ parsedSampler = {
44
+ input: accessorToJsArray1D(gltf.accessors[input], accessorCache1D),
45
+ interpolation,
46
+ output: accessorToJsArray2D(gltf.accessors[output], accessorCache2D)
47
+ };
48
+ samplerCache.set(sampler, parsedSampler);
49
+ }
50
+
36
51
  return {
37
- sampler: samplers[sampler],
52
+ sampler: parsedSampler,
38
53
  targetNodeId: targetNode.id,
39
- path: target.path as GLTFAnimationPath
54
+ path
40
55
  };
41
56
  });
42
57
 
43
- return {name, channels};
58
+ return channels.length ? [{name, channels}] : [];
44
59
  });
45
60
  }
46
61
 
62
+ function getSupportedAnimationPath(path: string): GLTFAnimationPath | null {
63
+ if (path === 'pointer') {
64
+ log.warn('KHR_animation_pointer channels are not supported and will be skipped')();
65
+ return null;
66
+ }
67
+
68
+ return path as GLTFAnimationPath;
69
+ }
70
+
47
71
  /** Converts a scalar accessor into a cached JavaScript number array. */
48
72
  function accessorToJsArray1D(
49
73
  accessor: GLTFAccessorPostprocessed,
@@ -61,7 +85,7 @@ function accessorToJsArray1D(
61
85
  return result;
62
86
  }
63
87
 
64
- /** Converts a vector or matrix accessor into a cached JavaScript array-of-arrays. */
88
+ /** Converts a scalar, vector, or matrix accessor into a cached JavaScript array-of-arrays. */
65
89
  function accessorToJsArray2D(
66
90
  accessor: GLTFAccessorPostprocessed,
67
91
  accessorCache: Map<GLTFAccessorPostprocessed, number[][]>
@@ -71,7 +95,7 @@ function accessorToJsArray2D(
71
95
  }
72
96
 
73
97
  const {typedArray: array, components} = accessorToTypedArray(accessor);
74
- assert(components > 1, 'accessorToJsArray2D must have more than 1 component');
98
+ assert(components >= 1, 'accessorToJsArray2D must have at least 1 component');
75
99
 
76
100
  const result = [];
77
101
 
@@ -1,6 +1,8 @@
1
1
  import {Matrix4} from '@math.gl/core';
2
2
  import type {GLTFNodePostprocessed, GLTFPostprocessed} from '@loaders.gl/gltf';
3
- import type {DirectionalLight, Light, PointLight} from '@luma.gl/shadertools';
3
+ import type {DirectionalLight, Light, PointLight, SpotLight} from '@luma.gl/shadertools';
4
+
5
+ const GLTF_COLOR_FACTOR = 255;
4
6
 
5
7
  /** Parse KHR_lights_punctual extension into luma.gl light definitions */
6
8
  export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
@@ -13,6 +15,8 @@ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
13
15
  }
14
16
 
15
17
  const lights: Light[] = [];
18
+ const parentNodeById = createParentNodeMap(gltf.nodes || []);
19
+ const worldMatrixByNodeId = new Map<string, Matrix4>();
16
20
 
17
21
  for (const node of gltf.nodes || []) {
18
22
  const lightIndex =
@@ -28,19 +32,22 @@ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
28
32
  continue;
29
33
  }
30
34
 
31
- const color = (gltfLight.color || [1, 1, 1]) as [number, number, number];
35
+ const color = normalizeGLTFLightColor(
36
+ (gltfLight.color || [1, 1, 1]) as [number, number, number]
37
+ );
32
38
  const intensity = gltfLight.intensity ?? 1;
33
39
  const range = gltfLight.range;
40
+ const worldMatrix = getNodeWorldMatrix(node, parentNodeById, worldMatrixByNodeId);
34
41
 
35
42
  switch (gltfLight.type) {
36
43
  case 'directional':
37
- lights.push(parseDirectionalLight(node, color, intensity));
44
+ lights.push(parseDirectionalLight(worldMatrix, color, intensity));
38
45
  break;
39
46
  case 'point':
40
- lights.push(parsePointLight(node, color, intensity, range));
47
+ lights.push(parsePointLight(worldMatrix, color, intensity, range));
41
48
  break;
42
49
  case 'spot':
43
- lights.push(parsePointLight(node, color, intensity, range));
50
+ lights.push(parseSpotLight(worldMatrix, color, intensity, range, gltfLight.spot));
44
51
  break;
45
52
  default:
46
53
  // Unsupported light type
@@ -51,16 +58,23 @@ export function parseGLTFLights(gltf: GLTFPostprocessed): Light[] {
51
58
  return lights;
52
59
  }
53
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
+
54
68
  /**
55
69
  * Converts a glTF punctual light attached to a node into a point light.
56
70
  */
57
71
  function parsePointLight(
58
- node: GLTFNodePostprocessed,
72
+ worldMatrix: Matrix4,
59
73
  color: [number, number, number],
60
74
  intensity: number,
61
75
  range?: number
62
76
  ): PointLight {
63
- const position = getNodePosition(node);
77
+ const position = getLightPosition(worldMatrix);
64
78
 
65
79
  let attenuation: Readonly<[number, number, number]> = [1, 0, 0];
66
80
  if (range !== undefined && range > 0) {
@@ -80,11 +94,11 @@ function parsePointLight(
80
94
  * Converts a glTF punctual light attached to a node into a directional light.
81
95
  */
82
96
  function parseDirectionalLight(
83
- node: GLTFNodePostprocessed,
97
+ worldMatrix: Matrix4,
84
98
  color: [number, number, number],
85
99
  intensity: number
86
100
  ): DirectionalLight {
87
- const direction = getNodeDirection(node);
101
+ const direction = getLightDirection(worldMatrix);
88
102
 
89
103
  return {
90
104
  type: 'directional',
@@ -95,35 +109,110 @@ function parseDirectionalLight(
95
109
  }
96
110
 
97
111
  /**
98
- * Resolves the world-space position of a glTF node from its matrix or translation.
112
+ * Converts a glTF punctual light attached to a node into a spot light.
99
113
  */
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];
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];
103
127
  }
104
128
 
105
- if (node.translation) {
106
- return [...node.translation] as [number, number, number];
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;
107
167
  }
108
168
 
109
- return [0, 0, 0];
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;
110
179
  }
111
180
 
112
181
  /**
113
- * Resolves the forward direction of a glTF node from its matrix or rotation.
182
+ * Resolves a glTF node's local matrix from its matrix or TRS components.
114
183
  */
115
- function getNodeDirection(node: GLTFNodePostprocessed): [number, number, number] {
184
+ function getNodeLocalMatrix(node: GLTFNodePostprocessed): Matrix4 {
116
185
  if (node.matrix) {
117
- return new Matrix4(node.matrix).transformDirection([0, 0, -1]) as [number, number, number];
186
+ return new Matrix4(node.matrix);
187
+ }
188
+
189
+ const matrix = new Matrix4();
190
+
191
+ if (node.translation) {
192
+ matrix.translate(node.translation);
118
193
  }
119
194
 
120
195
  if (node.rotation) {
121
- return new Matrix4().fromQuaternion(node.rotation).transformDirection([0, 0, -1]) as [
122
- number,
123
- number,
124
- number
125
- ];
196
+ matrix.multiplyRight(new Matrix4().fromQuaternion(node.rotation));
197
+ }
198
+
199
+ if (node.scale) {
200
+ matrix.scale(node.scale);
126
201
  }
127
202
 
128
- return [0, 0, -1];
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];
129
218
  }