UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

148 lines (126 loc) 5.13 kB
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 }); }