-
-
Notifications
You must be signed in to change notification settings - Fork 149
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(functions): Add uninstance() and createInstanceNodes()
- Loading branch information
1 parent
22ac8c4
commit 4a3e76a
Showing
5 changed files
with
199 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / node (v20)
|
||
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 }); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
); | ||
}); |