@gltf-transform/functions
Version:
Functions for common glTF modifications, written using the core API
148 lines (126 loc) • 5.13 kB
text/typescript
import { Accessor, Document, Node, Transform } from '@gltf-transform/core';
import { EXTMeshGPUInstancing, InstancedMesh } from '@gltf-transform/extensions';
import { createTransform } from './utils.js';
const NAME = 'uninstance';
export interface UninstanceOptions {}
const UNINSTANCE_DEFAULTS: Required<UninstanceOptions> = {};
/**
* Removes extension {@link EXTMeshGPUInstancing}, reversing the effects of the
* {@link instance} transform or similar instancing operations. For each {@link Node}
* associated with an {@link InstancedMesh}, the Node's {@link Mesh} and InstancedMesh will
* be detached. In their place, one Node per instance will be attached to the original
* Node as children, associated with the same Mesh. The extension, `EXT_mesh_gpu_instancing`,
* will be removed from the {@link Document}.
*
* In applications that support `EXT_mesh_gpu_instancing`, removing the extension
* is likely to substantially increase draw calls and reduce performance. Removing
* the extension may be helpful for compatibility in applications without such support.
*
* Example:
*
* ```ts
* import { uninstance } from '@gltf-transform/functions';
*
* document.getRoot().listNodes(); // → [ Node x 10 ]
*
* await document.transform(uninstance());
*
* document.getRoot().listNodes(); // → [ Node x 1000 ]
* ```
*
* @category Transforms
*/
export function uninstance(_options: UninstanceOptions = UNINSTANCE_DEFAULTS): Transform {
return createTransform(NAME, async (document: Document): Promise<void> => {
const logger = document.getLogger();
const root = document.getRoot();
const instanceAttributes = new Set<Accessor>();
for (const srcNode of document.getRoot().listNodes()) {
const batch = srcNode.getExtension<InstancedMesh>('EXT_mesh_gpu_instancing');
if (!batch) continue;
// For each instance, attach a new Node under the source Node.
for (const instanceNode of createInstanceNodes(srcNode)) {
srcNode.addChild(instanceNode);
}
for (const instanceAttribute of batch.listAttributes()) {
instanceAttributes.add(instanceAttribute);
}
srcNode.setMesh(null);
batch.dispose();
}
// Clean up unused instance attributes.
for (const attribute of instanceAttributes) {
if (attribute.listParents().every((parent) => parent === root)) {
attribute.dispose();
}
}
// Remove Extension from Document.
document.createExtension(EXTMeshGPUInstancing).dispose();
logger.debug(`${NAME}: Complete.`);
});
}
/**
* Given a {@link Node} with an {@link InstancedMesh} extension, returns a list
* containing one Node per instance in the InstancedMesh. Each Node will have
* the transform (translation/rotation/scale) of the corresponding instance,
* and will be assigned to the same {@link Mesh}.
*
* May be used to unpack instancing previously applied with {@link instance}
* and {@link EXTMeshGPUInstancing}. For a transform that applies this operation
* to the entire {@link Document}, see {@link uninstance}.
*
* Example:
* ```javascript
* import { createInstanceNodes } from '@gltf-transform/functions';
*
* for (const instanceNode of createInstanceNodes(batchNode)) {
* batchNode.addChild(instanceNode);
* }
*
* batchNode.setMesh(null).setExtension('EXTMeshGPUInstancing', null);
* ```
*/
export function createInstanceNodes(batchNode: Node): Node[] {
const batch = batchNode.getExtension<InstancedMesh>('EXT_mesh_gpu_instancing');
if (!batch) return [];
const semantics = batch.listSemantics();
if (semantics.length === 0) return [];
const document = Document.fromGraph(batchNode.getGraph())!;
const instanceCount = batch.listAttributes()[0].getCount();
const instanceCountDigits = String(instanceCount).length;
const mesh = batchNode.getMesh();
const batchName = batchNode.getName();
const instanceNodes = [];
// For each instance construct a Node, assign attributes, and push to list.
for (let i = 0; i < instanceCount; i++) {
const instanceNode = document.createNode().setMesh(mesh);
// MyNode_001, MyNode_002, ...
if (batchName) {
const paddedIndex = String(i).padStart(instanceCountDigits, '0');
instanceNode.setName(`${batchName}_${paddedIndex}`);
}
// TRS attributes are applied to node transform; all other attributes are extras.
for (const semantic of semantics) {
const attribute = batch.getAttribute(semantic)!;
switch (semantic) {
case 'TRANSLATION':
instanceNode.setTranslation(attribute.getElement(i, [0, 0, 0]));
break;
case 'ROTATION':
instanceNode.setRotation(attribute.getElement(i, [0, 0, 0, 1]));
break;
case 'SCALE':
instanceNode.setScale(attribute.getElement(i, [1, 1, 1]));
break;
default:
_setInstanceExtras(instanceNode, semantic, attribute, i);
}
}
instanceNodes.push(instanceNode);
}
return instanceNodes;
}
function _setInstanceExtras(node: Node, semantic: string, attribute: Accessor, index: number): void {
const value = attribute.getType() === 'SCALAR' ? attribute.getScalar(index) : attribute.getElement(index, []);
node.setExtras({ ...node.getExtras(), [semantic]: value });
}