Skip to content

Commit

Permalink
feat(functions): Add uninstance() and createInstanceNodes()
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy committed Oct 10, 2024
1 parent 22ac8c4 commit 4a3e76a
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 2 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/properties/accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ export class Accessor extends ExtensibleProperty<IAccessor> {
* }
* ```
*/
public getElement(index: number, target: number[]): number[] {
public getElement<T extends number[]>(index: number, target: T): T {
const normalized = this.getNormalized();
const elementSize = this.getElementSize();
const componentType = this.getComponentType();
Expand Down
1 change: 1 addition & 0 deletions packages/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/functions/src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const INSTANCE_DEFAULTS: Required<InstanceOptions> = {
};

/**
* 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.
Expand Down
133 changes: 133 additions & 0 deletions packages/functions/src/uninstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Accessor, Document, Node, Transform, vec3, vec4 } from '@gltf-transform/core';

Check warning on line 1 in packages/functions/src/uninstance.ts

View workflow job for this annotation

GitHub Actions / node (v20)

'vec3' is defined but never used

Check warning on line 1 in packages/functions/src/uninstance.ts

View workflow job for this annotation

GitHub Actions / node (v20)

'vec4' is defined but never used
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();

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);
}

// 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<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 });
}
63 changes: 63 additions & 0 deletions packages/functions/test/uninstance.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});

0 comments on commit 4a3e76a

Please sign in to comment.