Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(functions): Add uninstance() and createInstanceNodes() #1525

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## v4.x

### v4.1 (🚧 Unreleased)

**Features:**

- functions: Adds `uninstance()` and `createInstanceNodes()` [#1525](https://github.com/donmccurdy/glTF-Transform/pull/1525)

### v4.0

**Breaking changes:**
Expand Down
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
147 changes: 147 additions & 0 deletions packages/functions/src/uninstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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();
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 });
}
65 changes: 65 additions & 0 deletions packages/functions/test/uninstance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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',
);
t.is(translation.isDisposed(), true, 'disposes translation attribute');
t.is(id.isDisposed(), true, 'disposes id attribute');
});
Loading