Skip to content

Commit acec46c

Browse files
Refactor conversion of glTF nodes to luma.gl nodes, add skin capability
1 parent 059cb3c commit acec46c

File tree

15 files changed

+393
-266
lines changed

15 files changed

+393
-266
lines changed

examples/tutorials/hello-gltf/app.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import {AnimationLoopTemplate, AnimationProps, GroupNode, ModelNode} from '@luma.gl/engine';
5+
import {AnimationLoopTemplate, AnimationProps, ModelNode} from '@luma.gl/engine';
66
import {Device} from '@luma.gl/core';
77
import {load} from '@loaders.gl/core';
88
import {LightingProps} from '@luma.gl/shadertools';
9-
import {createScenegraphsFromGLTF, GLTFAnimator} from '@luma.gl/gltf';
9+
import {createScenegraphsFromGLTF} from '@luma.gl/gltf';
1010
import {GLTFLoader, postProcessGLTF} from '@loaders.gl/gltf';
1111
import {Matrix4} from '@math.gl/core';
1212

1313
/* eslint-disable camelcase */
1414

1515
const MODEL_DIRECTORY_URL =
16-
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/';
17-
const MODEL_LIST_URL =
18-
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/model-index.json';
16+
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models';
17+
const MODEL_LIST_URL = `${MODEL_DIRECTORY_URL}/model-index.json`;
1918

2019
const lightSources = {
2120
ambientLight: {
@@ -43,11 +42,10 @@ const lightSources = {
4342

4443
export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
4544
device: Device;
46-
scenes: GroupNode[] = [];
47-
animator?: GLTFAnimator;
45+
scenegraphsFromGLTF?: ReturnType<typeof createScenegraphsFromGLTF>;
4846
center = [0, 0, 0];
4947
cameraPos = [0, 0, 0];
50-
time: number = 0;
48+
mouseCameraTime: number = 0;
5149
options: Record<string, boolean> = {
5250
cameraAnimation: true,
5351
gltfAnimation: false,
@@ -79,33 +77,40 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
7977
}
8078
);
8179
});
80+
81+
this.device.getDefaultCanvasContext().canvas.addEventListener('mousemove', event => {
82+
const e = event as MouseEvent;
83+
if (e.buttons) {
84+
this.mouseCameraTime -= e.movementX * 3.5;
85+
}
86+
});
8287
}
8388

8489
onFinalize() {
85-
this.scenes[0].traverse(node => (node as ModelNode).model.destroy());
90+
this.scenegraphsFromGLTF?.scenes[0].traverse(node => (node as ModelNode).model.destroy());
8691
}
8792

8893
onRender({aspect, device, time}: AnimationProps): void {
89-
if (!this.scenes?.length) return;
94+
if (!this.scenegraphsFromGLTF?.scenes?.length) return;
9095
const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1], clearDepth: 1});
9196

9297
const far = 2 * this.cameraPos[0];
9398
const near = far / 1000;
9499
const projectionMatrix = new Matrix4().perspective({fovy: Math.PI / 3, aspect, near, far});
95-
const cameraTime = this.options['cameraAnimation'] ? time : 0;
100+
const cameraTime = this.options['cameraAnimation'] ? time : this.mouseCameraTime;
96101
const cameraPos = [
97102
this.cameraPos[0] * Math.sin(0.001 * cameraTime),
98103
this.cameraPos[1],
99104
this.cameraPos[2] * Math.cos(0.001 * cameraTime)
100105
];
101106

102107
if (this.options['gltfAnimation']) {
103-
this.animator?.setTime(time);
108+
this.scenegraphsFromGLTF.animator?.setTime(time);
104109
}
105110

106111
const viewMatrix = new Matrix4().lookAt({eye: cameraPos, center: this.center});
107112

108-
this.scenes[0].traverse((node, {worldMatrix: modelMatrix}) => {
113+
this.scenegraphsFromGLTF.scenes[0].traverse((node, {worldMatrix: modelMatrix}) => {
109114
const {model} = node as ModelNode;
110115

111116
const modelViewProjectionMatrix = new Matrix4(projectionMatrix)
@@ -119,6 +124,11 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
119124
modelViewProjectionMatrix,
120125
modelMatrix,
121126
normalMatrix: new Matrix4(modelMatrix).invert().transpose()
127+
},
128+
skin: {
129+
// TODO: This is required to trigger getUniforms() of skin.
130+
// Fix it, then remove this.
131+
scenegraphsFromGLTF: this.scenegraphsFromGLTF
122132
}
123133
});
124134
model.draw(renderPass);
@@ -143,20 +153,17 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
143153
);
144154
const processedGLTF = postProcessGLTF(gltf);
145155

146-
const {scenes, animator} = createScenegraphsFromGLTF(this.device, processedGLTF, {
156+
this.scenegraphsFromGLTF = createScenegraphsFromGLTF(this.device, processedGLTF, {
147157
lights: true,
148158
imageBasedLightingEnvironment: undefined,
149159
pbrDebug: false
150160
});
151161

152-
this.scenes = scenes;
153-
this.animator = animator;
154-
155162
// Calculate nice camera view
156163
// TODO move to utility in gltf module
157164
let min = [Infinity, Infinity, Infinity];
158165
let max = [0, 0, 0];
159-
this.scenes[0].traverse(node => {
166+
this.scenegraphsFromGLTF?.scenes[0].traverse(node => {
160167
const {bounds} = node as ModelNode;
161168
min = min.map((n, i) => Math.min(n, bounds[0][i], bounds[1][i]));
162169
max = max.map((n, i) => Math.max(n, bounds[0][i], bounds[1][i]));
@@ -167,6 +174,8 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate {
167174
canvas.style.opacity = '1';
168175
showError();
169176
} catch (error) {
177+
// eslint-disable-next-line no-console
178+
console.error(error);
170179
showError(error as Error);
171180
}
172181
}
@@ -194,6 +203,7 @@ function setModelMenu(
194203
});
195204

196205
modelSelector.append(...options);
206+
modelSelector.value = currentItem;
197207
}
198208

199209
function setOptionsUI(options: Record<string, boolean>) {

modules/engine/src/scenegraph/group-node.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,20 @@ export class GroupNode extends ScenegraphNode {
108108
}
109109
}
110110
}
111+
112+
preorderTraversal(
113+
visitor: (node: ScenegraphNode, context: {worldMatrix: Matrix4}) => void,
114+
{worldMatrix = new Matrix4()} = {}
115+
) {
116+
const modelMatrix = new Matrix4(worldMatrix).multiplyRight(this.matrix);
117+
visitor(this, {worldMatrix: modelMatrix});
118+
119+
for (const child of this.children) {
120+
if (child instanceof GroupNode) {
121+
child.preorderTraversal(visitor, {worldMatrix: modelMatrix});
122+
} else {
123+
visitor(child, {worldMatrix: modelMatrix});
124+
}
125+
}
126+
}
111127
}

modules/engine/src/scenegraph/scenegraph-node.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
import {Vector3, Matrix4, NumericArray} from '@math.gl/core';
66
import {uid} from '../utils/uid';
77

8+
function assert(condition: boolean, message?: string): asserts condition {
9+
if (!condition) {
10+
throw new Error(message);
11+
}
12+
}
13+
814
/** Properties for creating a new Scenegraph */
915
export type ScenegraphNodeProps = {
1016
id?: string;
@@ -57,19 +63,19 @@ export class ScenegraphNode {
5763
}
5864

5965
setPosition(position: any): this {
60-
// assert(position.length === 3, 'setPosition requires vector argument');
66+
assert(position.length === 3, 'setPosition requires vector argument');
6167
this.position = position;
6268
return this;
6369
}
6470

6571
setRotation(rotation: any): this {
66-
// assert(rotation.length === 3, 'setRotation requires vector argument');
72+
assert(rotation.length === 3 || rotation.length === 4, 'setRotation requires vector argument');
6773
this.rotation = rotation;
6874
return this;
6975
}
7076

7177
setScale(scale: any): this {
72-
// assert(scale.length === 3, 'setScale requires vector argument');
78+
assert(scale.length === 3, 'setScale requires vector argument');
7379
this.scale = scale;
7480
return this;
7581
}
@@ -105,19 +111,20 @@ export class ScenegraphNode {
105111
}
106112

107113
updateMatrix(): this {
108-
const pos = this.position;
109-
const rot = this.rotation;
110-
const scale = this.scale;
111-
112114
this.matrix.identity();
113-
this.matrix.translate(pos);
114-
this.matrix.rotateXYZ(rot);
115-
this.matrix.scale(scale);
115+
this.matrix.translate(this.position);
116+
if (this.rotation.length === 4) {
117+
const rotationMatrix = new Matrix4().fromQuaternion(this.rotation);
118+
this.matrix.multiplyRight(rotationMatrix);
119+
} else {
120+
this.matrix.rotateXYZ(this.rotation);
121+
}
122+
this.matrix.scale(this.scale);
123+
116124
return this;
117125
}
118126

119-
update(options: {position?: any; rotation?: any; scale?: any} = {}): this {
120-
const {position, rotation, scale} = options;
127+
update({position, rotation, scale}: {position?: any; rotation?: any; scale?: any} = {}): this {
121128
if (position) {
122129
this.setPosition(position);
123130
}
@@ -127,7 +134,9 @@ export class ScenegraphNode {
127134
if (scale) {
128135
this.setScale(scale);
129136
}
137+
130138
this.updateMatrix();
139+
131140
return this;
132141
}
133142

@@ -188,18 +197,20 @@ export class ScenegraphNode {
188197
// this.display = props.display;
189198
// }
190199

191-
if ('position' in props) {
200+
if (props?.position) {
192201
this.setPosition(props.position);
193202
}
194-
if ('rotation' in props) {
203+
if (props?.rotation) {
195204
this.setRotation(props.rotation);
196205
}
197-
if ('scale' in props) {
206+
if (props?.scale) {
198207
this.setScale(props.scale);
199208
}
200209

210+
this.updateMatrix();
211+
201212
// Matrix overwrites other props
202-
if ('matrix' in props) {
213+
if (props?.matrix) {
203214
this.setMatrix(props.matrix);
204215
}
205216

modules/gltf/src/gltf/animations/animations.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import {GLTFNodePostprocessed} from '@loaders.gl/gltf';
5+
import {GroupNode} from '@luma.gl/engine';
66

77
export type GLTFAnimation = {
88
name: string;
99
channels: GLTFAnimationChannel[];
1010
};
1111

12+
export type GLTFAnimationPath = 'translation' | 'rotation' | 'scale' | 'weights';
13+
1214
export type GLTFAnimationChannel = {
13-
path: 'translation' | 'rotation' | 'scale' | 'weights';
15+
path: GLTFAnimationPath;
1416
sampler: GLTFAnimationSampler;
15-
target: GLTFNodePostprocessed;
17+
target: GroupNode;
1618
};
1719

1820
export type GLTFAnimationSampler = {
1921
input: number[];
2022
interpolation: string;
21-
output: number[] | number[][];
23+
output: number[][];
2224
};

0 commit comments

Comments
 (0)