@gltf-transform/functions
Version:
Functions for common glTF modifications, written using the core API
163 lines (140 loc) • 5.42 kB
text/typescript
import { Accessor, Document, GLTF, Primitive, PropertyType, Transform } from '@gltf-transform/core';
import { prune } from './prune.js';
import { assignDefaults, createTransform, deepListAttributes, SetMap, shallowCloneAccessor } from './utils.js';
import type { MeshoptEncoder } from 'meshoptimizer';
import { compactAttribute } from './compact-primitive.js';
const NAME = 'reorder';
/** Options for the {@link reorder} function. */
export interface ReorderOptions {
/** MeshoptEncoder instance. */
encoder: unknown;
/**
* Whether the order should be optimal for transmission size (recommended for Web)
* or for GPU rendering performance. Default is 'size'.
*/
target?: 'size' | 'performance';
/**
* Whether to perform cleanup steps after completing the operation. Recommended, and enabled by
* default. Cleanup removes temporary resources created during the operation, but may also remove
* pre-existing unused or duplicate resources in the {@link Document}. Applications that require
* keeping these resources may need to disable cleanup, instead calling {@link dedup} and
* {@link prune} manually (with customized options) later in the processing pipeline.
* @experimental
*/
cleanup?: boolean;
}
const REORDER_DEFAULTS: Required<Omit<ReorderOptions, 'encoder'>> = {
target: 'size',
cleanup: true,
};
/**
* Optimizes {@link Mesh} {@link Primitive Primitives} for locality of reference. Choose whether
* the order should be optimal for transmission size (recommended for Web) or for GPU rendering
* performance. Requires a MeshoptEncoder instance from the Meshoptimizer library.
*
* Example:
*
* ```ts
* import { MeshoptEncoder } from 'meshoptimizer';
* import { reorder } from '@gltf-transform/functions';
*
* await MeshoptEncoder.ready;
*
* await document.transform(
* reorder({encoder: MeshoptEncoder})
* );
* ```
*
* @category Transforms
*/
export function reorder(_options: ReorderOptions): Transform {
const options = assignDefaults(REORDER_DEFAULTS, _options);
const encoder = options.encoder as typeof MeshoptEncoder | undefined;
if (!encoder) {
throw new Error(`${NAME}: encoder dependency required — install "meshoptimizer".`);
}
return createTransform(NAME, async (document: Document): Promise<void> => {
const logger = document.getLogger();
await encoder.ready;
const plan = createLayoutPlan(document);
for (const srcIndices of plan.indicesToAttributes.keys()) {
let indicesArray = srcIndices.getArray()!;
if (!(indicesArray instanceof Uint32Array)) {
indicesArray = new Uint32Array(indicesArray);
} else {
indicesArray = indicesArray.slice();
}
// Compute optimal order.
const [remap, unique] = encoder.reorderMesh(
indicesArray,
plan.indicesToMode.get(srcIndices) === Primitive.Mode.TRIANGLES,
options.target === 'size',
);
const dstIndices = shallowCloneAccessor(document, srcIndices);
dstIndices.setArray(unique <= 65534 ? new Uint16Array(indicesArray) : indicesArray);
// Update affected primitives.
for (const srcAttribute of plan.indicesToAttributes.get(srcIndices)) {
const dstAttribute = shallowCloneAccessor(document, srcAttribute);
compactAttribute(srcAttribute, srcIndices, remap, dstAttribute, unique);
for (const prim of plan.indicesToPrimitives.get(srcIndices)) {
if (prim.getIndices() === srcIndices) {
prim.swap(srcIndices, dstIndices);
}
prim.swap(srcAttribute, dstAttribute);
for (const target of prim.listTargets()) {
target.swap(srcAttribute, dstAttribute);
}
}
}
}
// Clean up any attributes left unused by earlier cloning.
if (options.cleanup) {
await document.transform(
prune({
propertyTypes: [PropertyType.ACCESSOR],
keepAttributes: true,
keepIndices: true,
}),
);
}
if (!plan.indicesToAttributes.size) {
logger.warn(`${NAME}: No qualifying primitives found; may need to weld first.`);
} else {
logger.debug(`${NAME}: Complete.`);
}
});
}
/** @hidden */
interface LayoutPlan {
indicesToMode: Map<Accessor, GLTF.MeshPrimitiveMode>;
indicesToPrimitives: SetMap<Accessor, Primitive>;
indicesToAttributes: SetMap<Accessor, Accessor>;
attributesToPrimitives: SetMap<Accessor, Primitive>;
}
/**
* Constructs a plan for processing vertex streams, based on unique
* index:attribute[] groups. Where different indices are used with the same
* attributes, we'll end up splitting the primitives to not share attributes,
* which appears to be consistent with the Meshopt implementation.
*
* @hidden
*/
function createLayoutPlan(document: Document): LayoutPlan {
const indicesToMode = new Map<Accessor, GLTF.MeshPrimitiveMode>();
const indicesToPrimitives = new SetMap<Accessor, Primitive>();
const indicesToAttributes = new SetMap<Accessor, Accessor>();
const attributesToPrimitives = new SetMap<Accessor, Primitive>();
for (const mesh of document.getRoot().listMeshes()) {
for (const prim of mesh.listPrimitives()) {
const indices = prim.getIndices();
if (!indices) continue;
indicesToMode.set(indices, prim.getMode());
indicesToPrimitives.add(indices, prim);
for (const attribute of deepListAttributes(prim)) {
indicesToAttributes.add(indices, attribute);
attributesToPrimitives.add(attribute, prim);
}
}
}
return { indicesToPrimitives, indicesToAttributes, indicesToMode, attributesToPrimitives };
}