@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.
- package/dist/dist.dev.js +397 -220
- package/dist/dist.min.js +98 -46
- package/dist/gltf/animations/animations.d.ts +16 -4
- package/dist/gltf/animations/animations.d.ts.map +1 -1
- package/dist/gltf/animations/interpolate.d.ts +4 -3
- package/dist/gltf/animations/interpolate.d.ts.map +1 -1
- package/dist/gltf/animations/interpolate.js +27 -36
- package/dist/gltf/animations/interpolate.js.map +1 -1
- package/dist/gltf/create-gltf-model.d.ts +6 -0
- package/dist/gltf/create-gltf-model.d.ts.map +1 -1
- package/dist/gltf/create-gltf-model.js +96 -44
- package/dist/gltf/create-gltf-model.js.map +1 -1
- package/dist/gltf/create-scenegraph-from-gltf.d.ts +15 -1
- package/dist/gltf/create-scenegraph-from-gltf.d.ts.map +1 -1
- package/dist/gltf/create-scenegraph-from-gltf.js +12 -6
- package/dist/gltf/create-scenegraph-from-gltf.js.map +1 -1
- package/dist/gltf/gltf-animator.d.ts +26 -0
- package/dist/gltf/gltf-animator.d.ts.map +1 -1
- package/dist/gltf/gltf-animator.js +22 -19
- package/dist/gltf/gltf-animator.js.map +1 -1
- package/dist/index.cjs +378 -210
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parsers/parse-gltf-animations.d.ts +1 -0
- package/dist/parsers/parse-gltf-animations.d.ts.map +1 -1
- package/dist/parsers/parse-gltf-animations.js +46 -23
- package/dist/parsers/parse-gltf-animations.js.map +1 -1
- package/dist/parsers/parse-gltf-lights.d.ts.map +1 -1
- package/dist/parsers/parse-gltf-lights.js +40 -12
- package/dist/parsers/parse-gltf-lights.js.map +1 -1
- package/dist/parsers/parse-gltf.d.ts +16 -1
- package/dist/parsers/parse-gltf.d.ts.map +1 -1
- package/dist/parsers/parse-gltf.js +65 -57
- package/dist/parsers/parse-gltf.js.map +1 -1
- package/dist/parsers/parse-pbr-material.d.ts +46 -1
- package/dist/parsers/parse-pbr-material.d.ts.map +1 -1
- package/dist/parsers/parse-pbr-material.js +137 -13
- package/dist/parsers/parse-pbr-material.js.map +1 -1
- package/dist/pbr/pbr-environment.d.ts +6 -0
- package/dist/pbr/pbr-environment.d.ts.map +1 -1
- package/dist/pbr/pbr-environment.js +1 -0
- package/dist/pbr/pbr-environment.js.map +1 -1
- package/dist/pbr/pbr-material.d.ts +5 -0
- package/dist/pbr/pbr-material.d.ts.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts +12 -1
- package/dist/webgl-to-webgpu/convert-webgl-attribute.d.ts.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-attribute.js +3 -0
- package/dist/webgl-to-webgpu/convert-webgl-attribute.js.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts +6 -0
- package/dist/webgl-to-webgpu/convert-webgl-sampler.d.ts.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-sampler.js +4 -0
- package/dist/webgl-to-webgpu/convert-webgl-sampler.js.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts +2 -0
- package/dist/webgl-to-webgpu/convert-webgl-topology.d.ts.map +1 -1
- package/dist/webgl-to-webgpu/convert-webgl-topology.js +2 -0
- package/dist/webgl-to-webgpu/convert-webgl-topology.js.map +1 -1
- package/package.json +5 -5
- package/src/gltf/animations/animations.ts +17 -5
- package/src/gltf/animations/interpolate.ts +49 -68
- package/src/gltf/create-gltf-model.ts +101 -43
- package/src/gltf/create-scenegraph-from-gltf.ts +39 -11
- package/src/gltf/gltf-animator.ts +34 -25
- package/src/index.ts +1 -2
- package/src/parsers/parse-gltf-animations.ts +63 -26
- package/src/parsers/parse-gltf-lights.ts +51 -13
- package/src/parsers/parse-gltf.ts +90 -77
- package/src/parsers/parse-pbr-material.ts +204 -14
- package/src/pbr/pbr-environment.ts +10 -0
- package/src/pbr/pbr-material.ts +5 -0
- package/src/webgl-to-webgpu/convert-webgl-attribute.ts +12 -1
- package/src/webgl-to-webgpu/convert-webgl-sampler.ts +9 -0
- package/src/webgl-to-webgpu/convert-webgl-topology.ts +2 -0
- package/dist/utils/deep-copy.d.ts +0 -3
- package/dist/utils/deep-copy.d.ts.map +0 -1
- package/dist/utils/deep-copy.js +0 -21
- package/dist/utils/deep-copy.js.map +0 -1
- 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,
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
25
|
+
input: accessorToJsArray1D(gltf.accessors[input], accessorCache1D),
|
|
21
26
|
interpolation,
|
|
22
|
-
output:
|
|
27
|
+
output: accessorToJsArray2D(gltf.accessors[output], accessorCache2D)
|
|
23
28
|
})
|
|
24
29
|
);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
): number[]
|
|
39
|
-
if (
|
|
40
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
16
|
-
|
|
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[
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
gltfNode
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
87
|
+
// Nodes can have children nodes and one optional child mesh at the same time.
|
|
79
88
|
if (gltfNode.mesh) {
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
/** Creates a mesh group node containing one model node per glTF primitive. */
|
|
130
|
+
function createNodeForGLTFMesh(
|
|
117
131
|
device: Device,
|
|
118
|
-
gltfMesh: GLTFMeshPostprocessed
|
|
132
|
+
gltfMesh: GLTFMeshPostprocessed,
|
|
119
133
|
options: Required<ParseGLTFOptions>
|
|
120
134
|
): GroupNode {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
144
|
+
return mesh;
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
|
|
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)) {
|