-
Notifications
You must be signed in to change notification settings - Fork 218
Refactor conversion of glTF nodes to luma.gl nodes, add skin capability #2391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,20 +2,19 @@ | |
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
|
||
import {AnimationLoopTemplate, AnimationProps, GroupNode, ModelNode} from '@luma.gl/engine'; | ||
import {AnimationLoopTemplate, AnimationProps, ModelNode} from '@luma.gl/engine'; | ||
import {Device} from '@luma.gl/core'; | ||
import {load} from '@loaders.gl/core'; | ||
import {LightingProps} from '@luma.gl/shadertools'; | ||
import {createScenegraphsFromGLTF, GLTFAnimator} from '@luma.gl/gltf'; | ||
import {createScenegraphsFromGLTF} from '@luma.gl/gltf'; | ||
import {GLTFLoader, postProcessGLTF} from '@loaders.gl/gltf'; | ||
import {Matrix4} from '@math.gl/core'; | ||
|
||
/* eslint-disable camelcase */ | ||
|
||
const MODEL_DIRECTORY_URL = | ||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/'; | ||
const MODEL_LIST_URL = | ||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/model-index.json'; | ||
'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models'; | ||
const MODEL_LIST_URL = `${MODEL_DIRECTORY_URL}/model-index.json`; | ||
|
||
const lightSources = { | ||
ambientLight: { | ||
|
@@ -43,11 +42,10 @@ const lightSources = { | |
|
||
export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { | ||
device: Device; | ||
scenes: GroupNode[] = []; | ||
animator?: GLTFAnimator; | ||
scenegraphsFromGLTF?: ReturnType<typeof createScenegraphsFromGLTF>; | ||
center = [0, 0, 0]; | ||
cameraPos = [0, 0, 0]; | ||
time: number = 0; | ||
mouseCameraTime: number = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe a TSDoc on this line (what is mouse camera time)? |
||
options: Record<string, boolean> = { | ||
cameraAnimation: true, | ||
gltfAnimation: false, | ||
|
@@ -79,33 +77,40 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { | |
} | ||
); | ||
}); | ||
|
||
this.device.getDefaultCanvasContext().canvas.addEventListener('mousemove', event => { | ||
const e = event as MouseEvent; | ||
if (e.buttons) { | ||
this.mouseCameraTime -= e.movementX * 3.5; | ||
} | ||
}); | ||
} | ||
|
||
onFinalize() { | ||
this.scenes[0].traverse(node => (node as ModelNode).model.destroy()); | ||
this.scenegraphsFromGLTF?.scenes[0].traverse(node => (node as ModelNode).model.destroy()); | ||
} | ||
|
||
onRender({aspect, device, time}: AnimationProps): void { | ||
if (!this.scenes?.length) return; | ||
if (!this.scenegraphsFromGLTF?.scenes?.length) return; | ||
const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1], clearDepth: 1}); | ||
|
||
const far = 2 * this.cameraPos[0]; | ||
const near = far / 1000; | ||
const projectionMatrix = new Matrix4().perspective({fovy: Math.PI / 3, aspect, near, far}); | ||
const cameraTime = this.options['cameraAnimation'] ? time : 0; | ||
const cameraTime = this.options['cameraAnimation'] ? time : this.mouseCameraTime; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Can we type the options so that we don't need index signatures? |
||
const cameraPos = [ | ||
this.cameraPos[0] * Math.sin(0.001 * cameraTime), | ||
this.cameraPos[1], | ||
this.cameraPos[2] * Math.cos(0.001 * cameraTime) | ||
]; | ||
|
||
if (this.options['gltfAnimation']) { | ||
this.animator?.setTime(time); | ||
this.scenegraphsFromGLTF.animator?.setTime(time); | ||
} | ||
|
||
const viewMatrix = new Matrix4().lookAt({eye: cameraPos, center: this.center}); | ||
|
||
this.scenes[0].traverse((node, {worldMatrix: modelMatrix}) => { | ||
this.scenegraphsFromGLTF.scenes[0].traverse((node, {worldMatrix: modelMatrix}) => { | ||
const {model} = node as ModelNode; | ||
|
||
const modelViewProjectionMatrix = new Matrix4(projectionMatrix) | ||
|
@@ -119,6 +124,11 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { | |
modelViewProjectionMatrix, | ||
modelMatrix, | ||
normalMatrix: new Matrix4(modelMatrix).invert().transpose() | ||
}, | ||
skin: { | ||
// TODO: This is required to trigger getUniforms() of skin. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add to glTF tracker task? |
||
// Fix it, then remove this. | ||
scenegraphsFromGLTF: this.scenegraphsFromGLTF | ||
} | ||
}); | ||
model.draw(renderPass); | ||
|
@@ -143,20 +153,17 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { | |
); | ||
const processedGLTF = postProcessGLTF(gltf); | ||
|
||
const {scenes, animator} = createScenegraphsFromGLTF(this.device, processedGLTF, { | ||
this.scenegraphsFromGLTF = createScenegraphsFromGLTF(this.device, processedGLTF, { | ||
lights: true, | ||
imageBasedLightingEnvironment: undefined, | ||
pbrDebug: false | ||
}); | ||
|
||
this.scenes = scenes; | ||
this.animator = animator; | ||
|
||
// Calculate nice camera view | ||
// TODO move to utility in gltf module | ||
let min = [Infinity, Infinity, Infinity]; | ||
let max = [0, 0, 0]; | ||
this.scenes[0].traverse(node => { | ||
this.scenegraphsFromGLTF?.scenes[0].traverse(node => { | ||
const {bounds} = node as ModelNode; | ||
min = min.map((n, i) => Math.min(n, bounds[0][i], bounds[1][i])); | ||
max = max.map((n, i) => Math.max(n, bounds[0][i], bounds[1][i])); | ||
|
@@ -167,6 +174,8 @@ export default class AppAnimationLoopTemplate extends AnimationLoopTemplate { | |
canvas.style.opacity = '1'; | ||
showError(); | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error(error); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Do we need both console and showError? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! because the showError() doesn't have stack trace. |
||
showError(error as Error); | ||
} | ||
} | ||
|
@@ -194,6 +203,7 @@ function setModelMenu( | |
}); | ||
|
||
modelSelector.append(...options); | ||
modelSelector.value = currentItem; | ||
} | ||
|
||
function setOptionsUI(options: Record<string, boolean>) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,4 +108,20 @@ export class GroupNode extends ScenegraphNode { | |
} | ||
} | ||
} | ||
|
||
preorderTraversal( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is a traverse(... {..., traversalOrder: 'breadth-first' | 'depth-first'}) or something like that? |
||
visitor: (node: ScenegraphNode, context: {worldMatrix: Matrix4}) => void, | ||
{worldMatrix = new Matrix4()} = {} | ||
) { | ||
const modelMatrix = new Matrix4(worldMatrix).multiplyRight(this.matrix); | ||
visitor(this, {worldMatrix: modelMatrix}); | ||
|
||
for (const child of this.children) { | ||
if (child instanceof GroupNode) { | ||
child.preorderTraversal(visitor, {worldMatrix: modelMatrix}); | ||
} else { | ||
visitor(child, {worldMatrix: modelMatrix}); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,12 @@ | |
import {Vector3, Matrix4, NumericArray} from '@math.gl/core'; | ||
import {uid} from '../utils/uid'; | ||
|
||
function assert(condition: boolean, message?: string): asserts condition { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't luma.gl/core export an |
||
if (!condition) { | ||
throw new Error(message); | ||
} | ||
} | ||
|
||
/** Properties for creating a new Scenegraph */ | ||
export type ScenegraphNodeProps = { | ||
id?: string; | ||
|
@@ -57,19 +63,19 @@ export class ScenegraphNode { | |
} | ||
|
||
setPosition(position: any): this { | ||
// assert(position.length === 3, 'setPosition requires vector argument'); | ||
assert(position.length === 3, 'setPosition requires vector argument'); | ||
this.position = position; | ||
return this; | ||
} | ||
|
||
setRotation(rotation: any): this { | ||
// assert(rotation.length === 3, 'setRotation requires vector argument'); | ||
assert(rotation.length === 3 || rotation.length === 4, 'setRotation requires vector argument'); | ||
this.rotation = rotation; | ||
return this; | ||
} | ||
|
||
setScale(scale: any): this { | ||
// assert(scale.length === 3, 'setScale requires vector argument'); | ||
assert(scale.length === 3, 'setScale requires vector argument'); | ||
this.scale = scale; | ||
return this; | ||
} | ||
|
@@ -105,19 +111,20 @@ export class ScenegraphNode { | |
} | ||
|
||
updateMatrix(): this { | ||
const pos = this.position; | ||
const rot = this.rotation; | ||
const scale = this.scale; | ||
|
||
this.matrix.identity(); | ||
this.matrix.translate(pos); | ||
this.matrix.rotateXYZ(rot); | ||
this.matrix.scale(scale); | ||
this.matrix.translate(this.position); | ||
if (this.rotation.length === 4) { | ||
const rotationMatrix = new Matrix4().fromQuaternion(this.rotation); | ||
this.matrix.multiplyRight(rotationMatrix); | ||
} else { | ||
this.matrix.rotateXYZ(this.rotation); | ||
} | ||
this.matrix.scale(this.scale); | ||
|
||
return this; | ||
} | ||
|
||
update(options: {position?: any; rotation?: any; scale?: any} = {}): this { | ||
const {position, rotation, scale} = options; | ||
update({position, rotation, scale}: {position?: any; rotation?: any; scale?: any} = {}): this { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe add TSDoc to each method? |
||
if (position) { | ||
this.setPosition(position); | ||
} | ||
|
@@ -127,7 +134,9 @@ export class ScenegraphNode { | |
if (scale) { | ||
this.setScale(scale); | ||
} | ||
|
||
this.updateMatrix(); | ||
|
||
return this; | ||
} | ||
|
||
|
@@ -188,18 +197,20 @@ export class ScenegraphNode { | |
// this.display = props.display; | ||
// } | ||
|
||
if ('position' in props) { | ||
if (props?.position) { | ||
this.setPosition(props.position); | ||
} | ||
if ('rotation' in props) { | ||
if (props?.rotation) { | ||
this.setRotation(props.rotation); | ||
} | ||
if ('scale' in props) { | ||
if (props?.scale) { | ||
this.setScale(props.scale); | ||
} | ||
|
||
this.updateMatrix(); | ||
|
||
// Matrix overwrites other props | ||
if ('matrix' in props) { | ||
if (props?.matrix) { | ||
this.setMatrix(props.matrix); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,21 +2,23 @@ | |
// SPDX-License-Identifier: MIT | ||
// Copyright (c) vis.gl contributors | ||
|
||
import {GLTFNodePostprocessed} from '@loaders.gl/gltf'; | ||
import {GroupNode} from '@luma.gl/engine'; | ||
|
||
export type GLTFAnimation = { | ||
name: string; | ||
channels: GLTFAnimationChannel[]; | ||
}; | ||
|
||
export type GLTFAnimationPath = 'translation' | 'rotation' | 'scale' | 'weights'; | ||
|
||
export type GLTFAnimationChannel = { | ||
path: 'translation' | 'rotation' | 'scale' | 'weights'; | ||
path: GLTFAnimationPath; | ||
sampler: GLTFAnimationSampler; | ||
target: GLTFNodePostprocessed; | ||
target: GroupNode; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: could the target be by node There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using GroupNode makes this generic and detaches it from glTF. What is node key? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It detaches it from glTF but links it to the "dynamic" Node structure, forcing apps to keep track of generated nodes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, I'll keep GroupNode for this PR. |
||
}; | ||
|
||
export type GLTFAnimationSampler = { | ||
input: number[]; | ||
interpolation: string; | ||
output: number[] | number[][]; | ||
output: number[][]; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: That is an ugly type, maybe we can import a type instead of brute force extracting from the function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I did before but you said that type is not useful.