UNPKG

@gltf-transform/core

Version:

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.

293 lines (254 loc) 9.16 kB
import { multiply } from 'gl-matrix/mat4'; import { RefSet } from 'property-graph'; import { type mat4, type Nullable, PropertyType, type vec3, type vec4 } from '../constants.js'; import { MathUtils } from '../utils/index.js'; import type { Camera } from './camera.js'; import { ExtensibleProperty, type IExtensibleProperty } from './extensible-property.js'; import type { Mesh } from './mesh.js'; import { COPY_IDENTITY } from './property.js'; import type { Scene } from './scene.js'; import type { Skin } from './skin.js'; interface INode extends IExtensibleProperty { translation: vec3; rotation: vec4; scale: vec3; weights: number[]; camera: Camera; mesh: Mesh; skin: Skin; children: RefSet<Node>; } /** * *Nodes are the objects that comprise a {@link Scene}.* * * Each Node may have one or more children, and a transform (position, rotation, and scale) that * applies to all of its descendants. A Node may also reference (or "instantiate") other resources * at its location, including {@link Mesh}, Camera, Light, and Skin properties. A Node cannot be * part of more than one {@link Scene}. * * A Node's local transform is represented with array-like objects, intended to be compatible with * [gl-matrix](https://github.com/toji/gl-matrix), or with the `toArray`/`fromArray` methods of * libraries like three.js and babylon.js. * * Usage: * * ```ts * const node = doc.createNode('myNode') * .setMesh(mesh) * .setTranslation([0, 0, 0]) * .addChild(otherNode); * ``` * * References: * - [glTF → Nodes and Hierarchy](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#nodes-and-hierarchy) * * @category Properties */ export class Node extends ExtensibleProperty<INode> { public declare propertyType: PropertyType.NODE; protected init(): void { this.propertyType = PropertyType.NODE; } protected getDefaults(): Nullable<INode> { return Object.assign(super.getDefaults() as IExtensibleProperty, { translation: [0, 0, 0] as vec3, rotation: [0, 0, 0, 1] as vec4, scale: [1, 1, 1] as vec3, weights: [], camera: null, mesh: null, skin: null, children: new RefSet<Node>(), }); } public copy(other: this, resolve: typeof COPY_IDENTITY = COPY_IDENTITY): this { // Node cannot be copied, only cloned. Copying is shallow, but Nodes cannot have more than // one parent. Rather than leaving one of the two Nodes without children, throw an error here. if (resolve === COPY_IDENTITY) throw new Error('Node cannot be copied.'); return super.copy(other, resolve); } /********************************************************************************************** * Local transform. */ /** Returns the translation (position) of this Node in local space. */ public getTranslation(): vec3 { return this.get('translation'); } /** Returns the rotation (quaternion) of this Node in local space. */ public getRotation(): vec4 { return this.get('rotation'); } /** Returns the scale of this Node in local space. */ public getScale(): vec3 { return this.get('scale'); } /** Sets the translation (position) of this Node in local space. */ public setTranslation(translation: vec3): this { return this.set('translation', translation); } /** Sets the rotation (quaternion) of this Node in local space. */ public setRotation(rotation: vec4): this { return this.set('rotation', rotation); } /** Sets the scale of this Node in local space. */ public setScale(scale: vec3): this { return this.set('scale', scale); } /** Returns the local matrix of this Node. */ public getMatrix(): mat4 { return MathUtils.compose( this.get('translation'), this.get('rotation'), this.get('scale'), [] as unknown as mat4, ); } /** Sets the local matrix of this Node. Matrix will be decomposed to TRS properties. */ public setMatrix(matrix: mat4): this { const translation = this.get('translation').slice() as vec3; const rotation = this.get('rotation').slice() as vec4; const scale = this.get('scale').slice() as vec3; MathUtils.decompose(matrix, translation, rotation, scale); return this.set('translation', translation).set('rotation', rotation).set('scale', scale); } /********************************************************************************************** * World transform. */ /** Returns the translation (position) of this Node in world space. */ public getWorldTranslation(): vec3 { const t = [0, 0, 0] as vec3; MathUtils.decompose(this.getWorldMatrix(), t, [0, 0, 0, 1], [1, 1, 1]); return t; } /** Returns the rotation (quaternion) of this Node in world space. */ public getWorldRotation(): vec4 { const r = [0, 0, 0, 1] as vec4; MathUtils.decompose(this.getWorldMatrix(), [0, 0, 0], r, [1, 1, 1]); return r; } /** Returns the scale of this Node in world space. */ public getWorldScale(): vec3 { const s = [1, 1, 1] as vec3; MathUtils.decompose(this.getWorldMatrix(), [0, 0, 0], [0, 0, 0, 1], s); return s; } /** Returns the world matrix of this Node. */ public getWorldMatrix(): mat4 { // Build ancestor chain. const ancestors: Node[] = []; for (let node: Node | null = this; node != null; node = node.getParentNode()) { ancestors.push(node); } // Compute world matrix. let ancestor: Node | undefined; const worldMatrix = ancestors.pop()!.getMatrix(); while ((ancestor = ancestors.pop())) { multiply(worldMatrix, worldMatrix, ancestor.getMatrix()); } return worldMatrix; } /********************************************************************************************** * Scene hierarchy. */ /** * Adds the given Node as a child of this Node. * * Requirements: * * 1. Nodes MAY be root children of multiple {@link Scene Scenes} * 2. Nodes MUST NOT be children of >1 Node * 3. Nodes MUST NOT be children of both Nodes and {@link Scene Scenes} * * The `addChild` method enforces these restrictions automatically, and will * remove the new child from previous parents where needed. This behavior * may change in future major releases of the library. */ public addChild(child: Node): this { // Remove existing parents. const parentNode = child.getParentNode(); if (parentNode) parentNode.removeChild(child); for (const parent of child.listParents()) { if (parent.propertyType === PropertyType.SCENE) { (parent as Scene).removeChild(child); } } return this.addRef('children', child); } /** Removes a Node from this Node's child Node list. */ public removeChild(child: Node): this { return this.removeRef('children', child); } /** Lists all child Nodes of this Node. */ public listChildren(): Node[] { return this.listRefs('children'); } /** * Returns the Node's unique parent Node within the scene graph. If the * Node has no parents, or is a direct child of the {@link Scene} * ("root node"), this method returns null. * * Unrelated to {@link Property.listParents}, which lists all resource * references from properties of any type ({@link Skin}, {@link Root}, ...). */ public getParentNode(): Node | null { for (const parent of this.listParents()) { if (parent.propertyType === PropertyType.NODE) { return parent as Node; } } return null; } /********************************************************************************************** * Attachments. */ /** Returns the {@link Mesh}, if any, instantiated at this Node. */ public getMesh(): Mesh | null { return this.getRef('mesh'); } /** * Sets a {@link Mesh} to be instantiated at this Node. A single mesh may be instantiated by * multiple Nodes; reuse of this sort is strongly encouraged. */ public setMesh(mesh: Mesh | null): this { return this.setRef('mesh', mesh); } /** Returns the {@link Camera}, if any, instantiated at this Node. */ public getCamera(): Camera | null { return this.getRef('camera'); } /** Sets a {@link Camera} to be instantiated at this Node. */ public setCamera(camera: Camera | null): this { return this.setRef('camera', camera); } /** Returns the {@link Skin}, if any, instantiated at this Node. */ public getSkin(): Skin | null { return this.getRef('skin'); } /** Sets a {@link Skin} to be instantiated at this Node. */ public setSkin(skin: Skin | null): this { return this.setRef('skin', skin); } /** * Initial weights of each {@link PrimitiveTarget} for the mesh instance at this Node. * Most engines only support 4-8 active morph targets at a time. */ public getWeights(): number[] { return this.get('weights'); } /** * Initial weights of each {@link PrimitiveTarget} for the mesh instance at this Node. * Most engines only support 4-8 active morph targets at a time. */ public setWeights(weights: number[]): this { return this.set('weights', weights); } /********************************************************************************************** * Helpers. */ /** Visits this {@link Node} and its descendants, top-down. */ public traverse(fn: (node: Node) => void): this { fn(this); for (const child of this.listChildren()) child.traverse(fn); return this; } }