@gltf-transform/functions
Version:
Functions for common glTF modifications, written using the core API
110 lines (96 loc) • 3.48 kB
text/typescript
import { Document, Node, PropertyType, Transform } from '@gltf-transform/core';
import { clearNodeParent } from './clear-node-parent.js';
import { prune } from './prune.js';
import { assignDefaults, createTransform } from './utils.js';
const NAME = 'flatten';
/** Options for the {@link flatten} function. */
export interface FlattenOptions {
/**
* Whether to perform cleanup steps after completing the operation. Recommended, and enabled by
* default. Cleanup removes temporary resources created during the operation, but may also remove
* pre-existing unused or duplicate resources in the {@link Document}. Applications that require
* keeping these resources may need to disable cleanup, instead calling {@link dedup} and
* {@link prune} manually (with customized options) later in the processing pipeline.
* @experimental
*/
cleanup?: boolean;
}
export const FLATTEN_DEFAULTS: Required<FlattenOptions> = {
cleanup: true,
};
/**
* Flattens the scene graph, leaving {@link Node Nodes} with
* {@link Mesh Meshes}, {@link Camera Cameras}, and other attachments
* as direct children of the {@link Scene}. Skeletons and their
* descendants are left in their original Node structure.
*
* {@link Animation} targeting a Node or its parents will
* prevent that Node from being moved.
*
* Example:
*
* ```ts
* import { flatten } from '@gltf-transform/functions';
*
* await document.transform(flatten());
* ```
*
* @category Transforms
*/
export function flatten(_options: FlattenOptions = FLATTEN_DEFAULTS): Transform {
const options = assignDefaults(FLATTEN_DEFAULTS, _options);
return createTransform(NAME, async (document: Document): Promise<void> => {
const root = document.getRoot();
const logger = document.getLogger();
// (1) Mark joints.
const joints = new Set<Node>();
for (const skin of root.listSkins()) {
for (const joint of skin.listJoints()) {
joints.add(joint);
}
}
// (2) Mark nodes with TRS animation.
const animated = new Set<Node>();
for (const animation of root.listAnimations()) {
for (const channel of animation.listChannels()) {
const node = channel.getTargetNode();
if (node && channel.getTargetPath() !== 'weights') {
animated.add(node);
}
}
}
// (3) Mark descendants of joints and animated nodes.
const hasJointParent = new Set<Node>();
const hasAnimatedParent = new Set<Node>();
for (const scene of root.listScenes()) {
scene.traverse((node) => {
const parent = node.getParentNode();
if (!parent) return;
if (joints.has(parent) || hasJointParent.has(parent)) {
hasJointParent.add(node);
}
if (animated.has(parent) || hasAnimatedParent.has(parent)) {
hasAnimatedParent.add(node);
}
});
}
// (4) For each affected node, in top-down order, clear parents.
for (const scene of root.listScenes()) {
scene.traverse((node) => {
if (animated.has(node)) return;
if (hasJointParent.has(node)) return;
if (hasAnimatedParent.has(node)) return;
clearNodeParent(node);
});
}
// TODO(feat): Transform animation channels, accounting for previously inherited transforms.
if (animated.size) {
logger.debug(`${NAME}: Flattening node hierarchies with TRS animation not yet supported.`);
}
// (5) Clean up leaf nodes.
if (options.cleanup) {
await document.transform(prune({ propertyTypes: [PropertyType.NODE], keepLeaves: false }));
}
logger.debug(`${NAME}: Complete.`);
});
}