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

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 (91) hide show
  1. package/dist/dist.dev.js +1362 -313
  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 +15 -1
  10. package/dist/gltf/create-gltf-model.d.ts.map +1 -1
  11. package/dist/gltf/create-gltf-model.js +154 -48
  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 +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/gltf/gltf-extension-support.d.ts +10 -0
  22. package/dist/gltf/gltf-extension-support.d.ts.map +1 -0
  23. package/dist/gltf/gltf-extension-support.js +173 -0
  24. package/dist/gltf/gltf-extension-support.js.map +1 -0
  25. package/dist/index.cjs +1302 -276
  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 +73 -28
  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 +101 -61
  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 +570 -54
  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/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
  54. package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
  55. package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
  56. package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
  57. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +11 -5
  58. package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
  59. package/dist/webgl-to-webgpu/convert-webgl-sampler.js +16 -12
  60. package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
  61. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -9
  62. package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
  63. package/dist/webgl-to-webgpu/convert-webgl-topology.js +2 -14
  64. package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
  65. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts +27 -0
  66. package/dist/webgl-to-webgpu/gltf-webgl-constants.d.ts.map +1 -0
  67. package/dist/webgl-to-webgpu/gltf-webgl-constants.js +34 -0
  68. package/dist/webgl-to-webgpu/gltf-webgl-constants.js.map +1 -0
  69. package/package.json +8 -9
  70. package/src/gltf/animations/animations.ts +17 -5
  71. package/src/gltf/animations/interpolate.ts +49 -68
  72. package/src/gltf/create-gltf-model.ts +214 -48
  73. package/src/gltf/create-scenegraph-from-gltf.ts +134 -11
  74. package/src/gltf/gltf-animator.ts +34 -25
  75. package/src/gltf/gltf-extension-support.ts +214 -0
  76. package/src/index.ts +11 -2
  77. package/src/parsers/parse-gltf-animations.ts +94 -33
  78. package/src/parsers/parse-gltf-lights.ts +218 -0
  79. package/src/parsers/parse-gltf.ts +170 -90
  80. package/src/parsers/parse-pbr-material.ts +870 -80
  81. package/src/pbr/pbr-environment.ts +44 -21
  82. package/src/pbr/pbr-material.ts +18 -3
  83. package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
  84. package/src/webgl-to-webgpu/convert-webgl-sampler.ts +38 -29
  85. package/src/webgl-to-webgpu/convert-webgl-topology.ts +2 -14
  86. package/src/webgl-to-webgpu/gltf-webgl-constants.ts +35 -0
  87. package/dist/utils/deep-copy.d.ts +0 -3
  88. package/dist/utils/deep-copy.d.ts.map +0 -1
  89. package/dist/utils/deep-copy.js +0 -21
  90. package/dist/utils/deep-copy.js.map +0 -1
  91. package/src/utils/deep-copy.ts +0 -22
@@ -1,15 +1,36 @@
1
- import {GLTFNodePostprocessed} from '@loaders.gl/gltf';
2
1
  import {log} from '@luma.gl/core';
3
2
  import {Quaternion} from '@math.gl/core';
4
- import {GLTFAnimationChannel, GLTFAnimationSampler} from './animations';
3
+ import {GLTFAnimationPath, GLTFAnimationSampler} from './animations';
4
+ import {GroupNode} from '@luma.gl/engine';
5
5
 
6
- const scratchQuaternion = new Quaternion();
6
+ /** Applies an evaluated animation value to a scenegraph node. */
7
+ function updateTargetPath(
8
+ target: GroupNode,
9
+ path: GLTFAnimationPath,
10
+ newValue: number[]
11
+ ): GroupNode | null {
12
+ switch (path) {
13
+ case 'translation':
14
+ return target.setPosition(newValue).updateMatrix();
7
15
 
16
+ case 'rotation':
17
+ return target.setRotation(newValue).updateMatrix();
18
+
19
+ case 'scale':
20
+ return target.setScale(newValue).updateMatrix();
21
+
22
+ default:
23
+ log.warn(`Bad animation path ${path}`)();
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** Evaluates a glTF animation sampler at the supplied time and applies the result to a node. */
8
29
  export function interpolate(
9
30
  time: number,
10
31
  {input, interpolation, output}: GLTFAnimationSampler,
11
- target: GLTFNodePostprocessed,
12
- path: GLTFAnimationChannel['path']
32
+ target: GroupNode,
33
+ path: GLTFAnimationPath
13
34
  ) {
14
35
  const maxTime = input[input.length - 1];
15
36
  const animationTime = time % maxTime;
@@ -17,44 +38,18 @@ export function interpolate(
17
38
  const nextIndex = input.findIndex(t => t >= animationTime);
18
39
  const previousIndex = Math.max(0, nextIndex - 1);
19
40
 
20
- if (!Array.isArray(target[path])) {
21
- switch (path) {
22
- case 'translation':
23
- target[path] = [0, 0, 0];
24
- break;
25
-
26
- case 'rotation':
27
- target[path] = [0, 0, 0, 1];
28
- break;
29
-
30
- case 'scale':
31
- target[path] = [1, 1, 1];
32
- break;
33
-
34
- default:
35
- log.warn(`Bad animation path ${path}`)();
36
- }
37
- }
38
-
39
- // assert(target[path].length === output[previousIndex].length);
40
41
  const previousTime = input[previousIndex];
41
42
  const nextTime = input[nextIndex];
42
43
 
43
44
  switch (interpolation) {
44
45
  case 'STEP':
45
- stepInterpolate(target, path, output[previousIndex] as number[]);
46
+ stepInterpolate(target, path, output[previousIndex]);
46
47
  break;
47
48
 
48
49
  case 'LINEAR':
49
50
  if (nextTime > previousTime) {
50
51
  const ratio = (animationTime - previousTime) / (nextTime - previousTime);
51
- linearInterpolate(
52
- target,
53
- path,
54
- output[previousIndex] as number[],
55
- output[nextIndex] as number[],
56
- ratio
57
- );
52
+ linearInterpolate(target, path, output[previousIndex], output[nextIndex], ratio);
58
53
  }
59
54
  break;
60
55
 
@@ -63,10 +58,10 @@ export function interpolate(
63
58
  const ratio = (animationTime - previousTime) / (nextTime - previousTime);
64
59
  const tDiff = nextTime - previousTime;
65
60
 
66
- const p0 = output[3 * previousIndex + 1] as number[];
67
- const outTangent0 = output[3 * previousIndex + 2] as number[];
68
- const inTangent1 = output[3 * nextIndex + 0] as number[];
69
- const p1 = output[3 * nextIndex + 1] as number[];
61
+ const p0 = output[3 * previousIndex + 1];
62
+ const outTangent0 = output[3 * previousIndex + 2];
63
+ const inTangent1 = output[3 * nextIndex + 0];
64
+ const p1 = output[3 * nextIndex + 1];
70
65
 
71
66
  cubicsplineInterpolate(target, path, {p0, outTangent0, inTangent1, p1, tDiff, ratio});
72
67
  }
@@ -78,34 +73,31 @@ export function interpolate(
78
73
  }
79
74
  }
80
75
 
76
+ /** Applies linear interpolation between two keyframes. */
81
77
  function linearInterpolate(
82
- target: GLTFNodePostprocessed,
83
- path: GLTFAnimationChannel['path'],
78
+ target: GroupNode,
79
+ path: GLTFAnimationPath,
84
80
  start: number[],
85
81
  stop: number[],
86
82
  ratio: number
87
83
  ) {
88
- if (!target[path]) {
89
- throw new Error();
90
- }
91
-
92
84
  if (path === 'rotation') {
93
85
  // SLERP when path is rotation
94
- scratchQuaternion.slerp({start, target: stop, ratio});
95
- for (let i = 0; i < scratchQuaternion.length; i++) {
96
- target[path][i] = scratchQuaternion[i];
97
- }
86
+ updateTargetPath(target, path, new Quaternion().slerp({start, target: stop, ratio}));
98
87
  } else {
99
88
  // regular interpolation
89
+ const newVal = [];
100
90
  for (let i = 0; i < start.length; i++) {
101
- target[path][i] = ratio * stop[i] + (1 - ratio) * start[i];
91
+ newVal[i] = ratio * stop[i] + (1 - ratio) * start[i];
102
92
  }
93
+ updateTargetPath(target, path, newVal);
103
94
  }
104
95
  }
105
96
 
97
+ /** Applies glTF cubic spline interpolation between two keyframes. */
106
98
  function cubicsplineInterpolate(
107
- target: GLTFNodePostprocessed,
108
- path: GLTFAnimationChannel['path'],
99
+ target: GroupNode,
100
+ path: GLTFAnimationPath,
109
101
  {
110
102
  p0,
111
103
  outTangent0,
@@ -122,32 +114,21 @@ function cubicsplineInterpolate(
122
114
  ratio: number;
123
115
  }
124
116
  ) {
125
- if (!target[path]) {
126
- throw new Error();
127
- }
128
-
129
117
  // TODO: Quaternion might need normalization
130
- for (let i = 0; i < target[path].length; i++) {
118
+ const newVal = [];
119
+ for (let i = 0; i < p0.length; i++) {
131
120
  const m0 = outTangent0[i] * tDiff;
132
121
  const m1 = inTangent1[i] * tDiff;
133
- target[path][i] =
122
+ newVal[i] =
134
123
  (2 * Math.pow(t, 3) - 3 * Math.pow(t, 2) + 1) * p0[i] +
135
124
  (Math.pow(t, 3) - 2 * Math.pow(t, 2) + t) * m0 +
136
125
  (-2 * Math.pow(t, 3) + 3 * Math.pow(t, 2)) * p1[i] +
137
126
  (Math.pow(t, 3) - Math.pow(t, 2)) * m1;
138
127
  }
128
+ updateTargetPath(target, path, newVal);
139
129
  }
140
130
 
141
- function stepInterpolate(
142
- target: GLTFNodePostprocessed,
143
- path: GLTFAnimationChannel['path'],
144
- value: number[]
145
- ) {
146
- if (!target[path]) {
147
- throw new Error();
148
- }
149
-
150
- for (let i = 0; i < value.length; i++) {
151
- target[path][i] = value[i];
152
- }
131
+ /** Applies step interpolation by copying the current keyframe value. */
132
+ function stepInterpolate(target: GroupNode, path: GLTFAnimationPath, value: number[]) {
133
+ updateTargetPath(target, path, value);
153
134
  }
@@ -2,56 +2,111 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {Device, type RenderPipelineParameters, log} from '@luma.gl/core';
6
- import {pbrMaterial, ShaderModule} from '@luma.gl/shadertools';
7
- import {Geometry, Model, ModelNode, type ModelProps} from '@luma.gl/engine';
5
+ import {
6
+ Buffer,
7
+ Device,
8
+ Sampler,
9
+ Texture,
10
+ TextureView,
11
+ type Binding,
12
+ type RenderPipelineParameters,
13
+ log
14
+ } from '@luma.gl/core';
15
+ import {DynamicTexture} from '@luma.gl/engine';
16
+ import {pbrMaterial, skin} from '@luma.gl/shadertools';
17
+ import {
18
+ Geometry,
19
+ Material,
20
+ MaterialFactory,
21
+ Model,
22
+ ModelNode,
23
+ type ModelProps
24
+ } from '@luma.gl/engine';
8
25
  import {type ParsedPBRMaterial} from '../pbr/pbr-material';
9
26
 
10
27
  const SHADER = /* WGSL */ `
11
- layout(0) positions: vec4; // in vec4 POSITION;
12
-
13
- #ifdef HAS_NORMALS
14
- in vec4 normals; // in vec4 NORMAL;
15
- #endif
16
-
17
- #ifdef HAS_TANGENTS
18
- in vec4 TANGENT;
19
- #endif
28
+ struct VertexInputs {
29
+ @location(0) positions: vec3f,
30
+ #ifdef HAS_NORMALS
31
+ @location(1) normals: vec3f,
32
+ #endif
33
+ #ifdef HAS_TANGENTS
34
+ @location(2) TANGENT: vec4f,
35
+ #endif
36
+ #ifdef HAS_UV
37
+ @location(3) texCoords: vec2f,
38
+ #endif
39
+ #ifdef HAS_SKIN
40
+ @location(4) JOINTS_0: vec4u,
41
+ @location(5) WEIGHTS_0: vec4f,
42
+ #endif
43
+ };
20
44
 
21
- #ifdef HAS_UV
22
- // in vec2 TEXCOORD_0;
23
- in vec2 texCoords;
24
- #endif
45
+ struct FragmentInputs {
46
+ @builtin(position) position: vec4f,
47
+ @location(0) pbrPosition: vec3f,
48
+ @location(1) pbrUV: vec2f,
49
+ @location(2) pbrNormal: vec3f,
50
+ #ifdef HAS_TANGENTS
51
+ @location(3) pbrTangent: vec4f,
52
+ #endif
53
+ };
25
54
 
26
55
  @vertex
27
- void main(void) {
28
- vec4 _NORMAL = vec4(0.);
29
- vec4 _TANGENT = vec4(0.);
30
- vec2 _TEXCOORD_0 = vec2(0.);
31
-
32
- #ifdef HAS_NORMALS
33
- _NORMAL = normals;
34
- #endif
35
-
36
- #ifdef HAS_TANGENTS
37
- _TANGENT = TANGENT;
38
- #endif
39
-
40
- #ifdef HAS_UV
41
- _TEXCOORD_0 = texCoords;
42
- #endif
43
-
44
- pbr_setPositionNormalTangentUV(positions, _NORMAL, _TANGENT, _TEXCOORD_0);
45
- gl_Position = u_MVPMatrix * positions;
46
- }
56
+ fn vertexMain(inputs: VertexInputs) -> FragmentInputs {
57
+ var outputs: FragmentInputs;
58
+ var position = vec4f(inputs.positions, 1.0);
59
+ var normal = vec3f(0.0, 0.0, 1.0);
60
+ var tangent = vec4f(1.0, 0.0, 0.0, 1.0);
61
+ var uv = vec2f(0.0, 0.0);
62
+
63
+ #ifdef HAS_NORMALS
64
+ normal = inputs.normals;
65
+ #endif
66
+ #ifdef HAS_UV
67
+ uv = inputs.texCoords;
68
+ #endif
69
+ #ifdef HAS_TANGENTS
70
+ tangent = inputs.TANGENT;
71
+ #endif
72
+ #ifdef HAS_SKIN
73
+ let skinMatrix = getSkinMatrix(inputs.WEIGHTS_0, inputs.JOINTS_0);
74
+ position = skinMatrix * position;
75
+ normal = normalize((skinMatrix * vec4f(normal, 0.0)).xyz);
76
+ #ifdef HAS_TANGENTS
77
+ tangent = vec4f(normalize((skinMatrix * vec4f(tangent.xyz, 0.0)).xyz), tangent.w);
78
+ #endif
79
+ #endif
80
+
81
+ let worldPosition = pbrProjection.modelMatrix * position;
82
+
83
+ #ifdef HAS_NORMALS
84
+ normal = normalize((pbrProjection.normalMatrix * vec4f(normal, 0.0)).xyz);
85
+ #endif
86
+ #ifdef HAS_TANGENTS
87
+ let worldTangent = normalize((pbrProjection.modelMatrix * vec4f(tangent.xyz, 0.0)).xyz);
88
+ outputs.pbrTangent = vec4f(worldTangent, tangent.w);
89
+ #endif
90
+
91
+ outputs.position = pbrProjection.modelViewProjectionMatrix * position;
92
+ outputs.pbrPosition = worldPosition.xyz / worldPosition.w;
93
+ outputs.pbrUV = uv;
94
+ outputs.pbrNormal = normal;
95
+ return outputs;
96
+ }
47
97
 
48
98
  @fragment
49
- out vec4 fragmentColor;
50
-
51
- void main(void) {
52
- vec3 pos = pbr_vPosition;
53
- fragmentColor = pbr_filterColor(vec4(1.0));
54
- }
99
+ fn fragmentMain(inputs: FragmentInputs) -> @location(0) vec4f {
100
+ fragmentInputs.pbr_vPosition = inputs.pbrPosition;
101
+ fragmentInputs.pbr_vUV = inputs.pbrUV;
102
+ fragmentInputs.pbr_vNormal = inputs.pbrNormal;
103
+ #ifdef HAS_TANGENTS
104
+ let tangent = normalize(inputs.pbrTangent.xyz);
105
+ let bitangent = normalize(cross(inputs.pbrNormal, tangent)) * inputs.pbrTangent.w;
106
+ fragmentInputs.pbr_vTBN = mat3x3f(tangent, bitangent, inputs.pbrNormal);
107
+ #endif
108
+ return pbr_filterColor(vec4f(1.0));
109
+ }
55
110
  `;
56
111
 
57
112
  // TODO rename attributes to POSITION/NORMAL etc
@@ -76,6 +131,11 @@ const vs = /* glsl */ `\
76
131
  in vec2 texCoords;
77
132
  #endif
78
133
 
134
+ #ifdef HAS_SKIN
135
+ in uvec4 JOINTS_0;
136
+ in vec4 WEIGHTS_0;
137
+ #endif
138
+
79
139
  void main(void) {
80
140
  vec4 _NORMAL = vec4(0.);
81
141
  vec4 _TANGENT = vec4(0.);
@@ -93,8 +153,17 @@ const vs = /* glsl */ `\
93
153
  _TEXCOORD_0 = texCoords;
94
154
  #endif
95
155
 
96
- pbr_setPositionNormalTangentUV(positions, _NORMAL, _TANGENT, _TEXCOORD_0);
97
- gl_Position = pbrProjection.modelViewProjectionMatrix * positions;
156
+ vec4 pos = positions;
157
+
158
+ #ifdef HAS_SKIN
159
+ mat4 skinMat = getSkinMatrix(WEIGHTS_0, JOINTS_0);
160
+ pos = skinMat * pos;
161
+ _NORMAL = skinMat * _NORMAL;
162
+ _TANGENT = vec4((skinMat * vec4(_TANGENT.xyz, 0.)).xyz, _TANGENT.w);
163
+ #endif
164
+
165
+ pbr_setPositionNormalTangentUV(pos, _NORMAL, _TANGENT, _TEXCOORD_0);
166
+ gl_Position = pbrProjection.modelViewProjectionMatrix * pos;
98
167
  }
99
168
  `;
100
169
 
@@ -108,14 +177,52 @@ const fs = /* glsl */ `\
108
177
  }
109
178
  `;
110
179
 
180
+ /** Options used to instantiate a `ModelNode` for one glTF primitive. */
111
181
  export type CreateGLTFModelOptions = {
182
+ /** Optional id assigned to the generated model. */
112
183
  id?: string;
184
+ /** Vertex count override for non-indexed primitives. */
113
185
  vertexCount?: number;
186
+ /** Geometry converted from the glTF primitive. */
114
187
  geometry: Geometry;
188
+ /** Parsed PBR material state for the primitive. */
115
189
  parsedPPBRMaterial: ParsedPBRMaterial;
190
+ /** Pre-created material aligned with the source glTF material entry, when available. */
191
+ material?: Material | null;
192
+ /** Additional model props merged into the generated model. */
116
193
  modelOptions?: Partial<ModelProps>;
117
194
  };
118
195
 
196
+ export type CreateGLTFMaterialOptions = {
197
+ id?: string;
198
+ parsedPPBRMaterial: ParsedPBRMaterial;
199
+ materialFactory?: MaterialFactory;
200
+ };
201
+
202
+ export function createGLTFMaterial(device: Device, options: CreateGLTFMaterialOptions): Material {
203
+ const materialFactory =
204
+ options.materialFactory || new MaterialFactory(device, {modules: [pbrMaterial]});
205
+
206
+ const pbrMaterialProps = {...options.parsedPPBRMaterial.uniforms};
207
+ delete pbrMaterialProps.camera;
208
+ const materialBindings = Object.fromEntries(
209
+ Object.entries({
210
+ ...pbrMaterialProps,
211
+ ...options.parsedPPBRMaterial.bindings
212
+ }).filter(
213
+ ([name, value]) => materialFactory.ownsBinding(name) && isMaterialBindingResource(value)
214
+ )
215
+ ) as Record<string, Binding | DynamicTexture>;
216
+
217
+ const material = materialFactory.createMaterial({
218
+ id: options.id,
219
+ bindings: materialBindings
220
+ });
221
+ material.setProps({pbrMaterial: pbrMaterialProps});
222
+
223
+ return material;
224
+ }
225
+
119
226
  /** Creates a luma.gl Model from GLTF data*/
120
227
  export function createGLTFModel(device: Device, options: CreateGLTFModelOptions): ModelNode {
121
228
  const {id, geometry, parsedPPBRMaterial, vertexCount, modelOptions = {}} = options;
@@ -144,22 +251,81 @@ export function createGLTFModel(device: Device, options: CreateGLTFModelOptions)
144
251
  geometry,
145
252
  topology: geometry.topology,
146
253
  vertexCount,
147
- modules: [pbrMaterial as unknown as ShaderModule],
254
+ modules: [pbrMaterial, skin],
148
255
  ...modelOptions,
149
256
 
150
257
  defines: {...parsedPPBRMaterial.defines, ...modelOptions.defines},
151
258
  parameters: {...parameters, ...parsedPPBRMaterial.parameters, ...modelOptions.parameters}
152
259
  };
153
260
 
261
+ const material =
262
+ options.material ||
263
+ createGLTFMaterial(device, {
264
+ id: id ? `${id}-material` : undefined,
265
+ parsedPPBRMaterial
266
+ });
267
+ modelProps.material = material;
268
+
154
269
  const model = new Model(device, modelProps);
155
270
 
156
- const {camera, ...pbrMaterialProps} = {
271
+ const sceneShaderInputValues = {
157
272
  ...parsedPPBRMaterial.uniforms,
158
273
  ...modelOptions.uniforms,
159
274
  ...parsedPPBRMaterial.bindings,
160
275
  ...modelOptions.bindings
161
276
  };
162
-
163
- model.shaderInputs.setProps({pbrMaterial: pbrMaterialProps, pbrProjection: {camera}});
277
+ const sceneShaderInputProps = getSceneShaderInputProps(
278
+ model.shaderInputs.getModules(),
279
+ material,
280
+ sceneShaderInputValues
281
+ );
282
+ model.shaderInputs.setProps(sceneShaderInputProps);
164
283
  return new ModelNode({managedResources, model});
165
284
  }
285
+
286
+ function isMaterialBindingResource(value: unknown): boolean {
287
+ return (
288
+ value instanceof Buffer ||
289
+ value instanceof DynamicTexture ||
290
+ value instanceof Sampler ||
291
+ value instanceof Texture ||
292
+ value instanceof TextureView
293
+ );
294
+ }
295
+
296
+ function getSceneShaderInputProps(
297
+ modules: Array<{
298
+ name: string;
299
+ uniformTypes?: Readonly<Record<string, unknown>>;
300
+ bindingLayout?: ReadonlyArray<{name: string}>;
301
+ }>,
302
+ material: Material,
303
+ shaderInputValues: Record<string, unknown>
304
+ ): Record<string, Record<string, unknown>> {
305
+ const propertyToModuleNameMap = new Map<string, string>();
306
+ for (const module of modules) {
307
+ for (const uniformName of Object.keys(module.uniformTypes || {})) {
308
+ propertyToModuleNameMap.set(uniformName, module.name);
309
+ }
310
+ for (const binding of module.bindingLayout || []) {
311
+ propertyToModuleNameMap.set(binding.name, module.name);
312
+ }
313
+ }
314
+
315
+ const sceneShaderInputProps: Record<string, Record<string, unknown>> = {};
316
+ for (const [propertyName, value] of Object.entries(shaderInputValues)) {
317
+ if (value === undefined) {
318
+ continue;
319
+ }
320
+
321
+ const moduleName = propertyToModuleNameMap.get(propertyName);
322
+ if (!moduleName || material.ownsModule(moduleName)) {
323
+ continue;
324
+ }
325
+
326
+ sceneShaderInputProps[moduleName] ||= {};
327
+ sceneShaderInputProps[moduleName][propertyName] = value;
328
+ }
329
+
330
+ return sceneShaderInputProps;
331
+ }
@@ -3,25 +3,148 @@
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
+ import {Light} from '@luma.gl/shadertools';
8
9
  import {parseGLTF, type ParseGLTFOptions} from '../parsers/parse-gltf';
10
+ import {parseGLTFLights} from '../parsers/parse-gltf-lights';
9
11
  import {GLTFAnimator} from './gltf-animator';
10
12
  import {parseGLTFAnimations} from '../parsers/parse-gltf-animations';
11
- import {deepCopy} from '../utils/deep-copy';
13
+ import {getGLTFExtensionSupport, type GLTFExtensionSupport} from './gltf-extension-support';
12
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
+ };
27
+
28
+ /** Scenegraph bundle returned from a parsed glTF asset. */
29
+ export type GLTFScenegraphs = {
30
+ /** Scene roots produced from the glTF scenes array. */
31
+ scenes: GroupNode[];
32
+ /** Materials aligned with the source glTF `materials` array. */
33
+ materials: Material[];
34
+ /** Animation controller for glTF animations. */
35
+ animator: GLTFAnimator;
36
+ /** Parsed punctual lights from the asset. */
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;
44
+
45
+ /** Map from glTF mesh ids to generated mesh group nodes. */
46
+ gltfMeshIdToNodeMap: Map<string, GroupNode>;
47
+ /** Map from glTF node indices to generated scenegraph nodes. */
48
+ gltfNodeIndexToNodeMap: Map<number, GroupNode>;
49
+ /** Map from glTF node ids to generated scenegraph nodes. */
50
+ gltfNodeIdToNodeMap: Map<string, GroupNode>;
51
+
52
+ /** Original post-processed glTF document. */
53
+ gltf: GLTFPostprocessed;
54
+ };
55
+
56
+ /** Converts a post-processed glTF asset into luma.gl scenegraph nodes and animation helpers. */
13
57
  export function createScenegraphsFromGLTF(
14
58
  device: Device,
15
59
  gltf: GLTFPostprocessed,
16
60
  options?: ParseGLTFOptions
17
- ): {
18
- scenes: GroupNode[];
19
- animator: GLTFAnimator;
20
- } {
21
- gltf = deepCopy(gltf);
22
- const scenes = parseGLTF(device, gltf, options);
23
- // Note: There is a nasty dependency on injected nodes in the glTF
61
+ ): GLTFScenegraphs {
62
+ const {scenes, materials, gltfMeshIdToNodeMap, gltfNodeIdToNodeMap, gltfNodeIndexToNodeMap} =
63
+ parseGLTF(device, gltf, options);
64
+
24
65
  const animations = parseGLTFAnimations(gltf);
25
- const animator = new GLTFAnimator({animations});
26
- return {scenes, animator};
66
+ const animator = new GLTFAnimator({animations, gltfNodeIdToNodeMap});
67
+ const lights = parseGLTFLights(gltf);
68
+ const extensionSupport = getGLTFExtensionSupport(gltf);
69
+ const sceneBounds = scenes.map(scene => getScenegraphBounds(scene.getBounds()));
70
+ const modelBounds = getCombinedScenegraphBounds(sceneBounds);
71
+
72
+ return {
73
+ scenes,
74
+ materials,
75
+ animator,
76
+ lights,
77
+ extensionSupport,
78
+ sceneBounds,
79
+ modelBounds,
80
+ gltfMeshIdToNodeMap,
81
+ gltfNodeIdToNodeMap,
82
+ gltfNodeIndexToNodeMap,
83
+ gltf
84
+ };
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);
27
150
  }