diff --git a/packages/core/src/properties/accessor.ts b/packages/core/src/properties/accessor.ts index 2d4e5a87..6bfa1f14 100644 --- a/packages/core/src/properties/accessor.ts +++ b/packages/core/src/properties/accessor.ts @@ -419,7 +419,7 @@ export class Accessor extends ExtensibleProperty { * } * ``` */ - public getElement(index: number, target: number[]): number[] { + public getElement(index: number, target: T): T { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); diff --git a/packages/functions/src/index.ts b/packages/functions/src/index.ts index e81f04c1..0a16eac3 100644 --- a/packages/functions/src/index.ts +++ b/packages/functions/src/index.ts @@ -42,6 +42,7 @@ export * from './texture-compress.js'; export * from './tangents.js'; export * from './transform-mesh.js'; export * from './transform-primitive.js'; +export * from './uninstance.js'; export * from './unlit.js'; export * from './unpartition.js'; export { diff --git a/packages/functions/src/instance.ts b/packages/functions/src/instance.ts index b22a6e61..a71a0595 100644 --- a/packages/functions/src/instance.ts +++ b/packages/functions/src/instance.ts @@ -14,7 +14,7 @@ export const INSTANCE_DEFAULTS: Required = { }; /** - * Creates GPU instances (with `EXT_mesh_gpu_instancing`) for shared {@link Mesh} references. In + * Creates GPU instances (with {@link EXTMeshGPUInstancing}) for shared {@link Mesh} references. In * engines supporting the extension, reused Meshes will be drawn with GPU instancing, greatly * reducing draw calls and improving performance in many cases. If you're not sure that identical * Meshes share vertex data and materials ("linked duplicates"), run {@link dedup} first to link them. diff --git a/packages/functions/src/uninstance.ts b/packages/functions/src/uninstance.ts new file mode 100644 index 00000000..88266842 --- /dev/null +++ b/packages/functions/src/uninstance.ts @@ -0,0 +1,133 @@ +import { Accessor, Document, Node, Transform, vec3, vec4 } 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 = {}; + +/** + * 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 => { + const logger = document.getLogger(); + + for (const srcNode of document.getRoot().listNodes()) { + const batch = srcNode.getExtension('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); + } + + // Remove Mesh and Extension from source Node. + srcNode.setMesh(null).setExtension('EXT_mesh_gpu_instancing', null); + } + + // 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('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 }); +} diff --git a/packages/functions/test/uninstance.test.ts b/packages/functions/test/uninstance.test.ts new file mode 100644 index 00000000..f701badc --- /dev/null +++ b/packages/functions/test/uninstance.test.ts @@ -0,0 +1,63 @@ +import test from 'ava'; +import { Document } from '@gltf-transform/core'; +import { EXTMeshGPUInstancing } from '@gltf-transform/extensions'; +import { uninstance } from '@gltf-transform/functions'; +import { logger } from '@gltf-transform/test-utils'; + +test('basic', async (t) => { + const document = new Document().setLogger(logger); + const buffer = document.createBuffer(); + + // prettier-ignore + const translation = document + .createAccessor() + .setType('VEC3') + .setArray(new Uint8Array([ + 0, 0, 0, + 0, 0, 128, + 0, 0, 255 + ])) + .setNormalized(true) + .setBuffer(buffer); + const id = document + .createAccessor() + .setType('SCALAR') + .setArray(new Uint16Array([100, 101, 102])) + .setBuffer(buffer); + + const batchExtension = document.createExtension(EXTMeshGPUInstancing); + const batch = batchExtension + .createInstancedMesh() + .setAttribute('TRANSLATION', translation) + .setAttribute('_INSTANCE_ID', id); + + const mesh = document.createMesh(); + const batchNode = document.createNode('Batch').setMesh(mesh).setExtension('EXT_mesh_gpu_instancing', batch); + document.createScene().addChild(batchNode); + + await document.transform(uninstance()); + + t.is(batchNode.getMesh(), null, 'batchNode.mesh == null'); + t.is(batchNode.getExtension('EXT_mesh_gpu_instancing'), null, 'node extension removed'); + t.deepEqual(document.getRoot().listExtensionsUsed(), [], 'document extension removed'); + + t.deepEqual( + batchNode.listChildren().map((child) => child.getName()), + ['Batch_0', 'Batch_1', 'Batch_2'], + 'sets instance names', + ); + t.deepEqual( + batchNode.listChildren().map((child) => child.getTranslation()), + [ + [0, 0, 0], + [0, 0, 0.5019607843137255], + [0, 0, 1], + ], + 'sets instance translations', + ); + t.deepEqual( + batchNode.listChildren().map((child) => child.getExtras()), + [{ _INSTANCE_ID: 100 }, { _INSTANCE_ID: 101 }, { _INSTANCE_ID: 102 }], + 'sets instance extras', + ); +});