UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

127 lines (105 loc) 3.89 kB
import { Document, ILogger, PropertyType, Transform } from '@gltf-transform/core'; import { prune } from './prune.js'; import { assignDefaults, createTransform } from './utils.js'; const NAME = 'partition'; export interface PartitionOptions { animations?: boolean | Array<string>; meshes?: boolean | Array<string>; } const PARTITION_DEFAULTS: Required<PartitionOptions> = { animations: true, meshes: true, }; /** * Partitions the binary payload of a glTF file so separate mesh or animation data is in separate * `.bin` {@link Buffer}s. This technique may be useful for engines that support lazy-loading * specific binary resources as needed over the application lifecycle. * * Example: * * ```ts * document.getRoot().listBuffers(); // → [Buffer] * * await document.transform(partition({meshes: true})); * * document.getRoot().listBuffers(); // → [Buffer, Buffer, ...] * ``` * * @category Transforms */ export function partition(_options: PartitionOptions = PARTITION_DEFAULTS): Transform { const options = assignDefaults(PARTITION_DEFAULTS, _options); return createTransform(NAME, async (doc: Document): Promise<void> => { const logger = doc.getLogger(); if (options.meshes !== false) partitionMeshes(doc, logger, options); if (options.animations !== false) partitionAnimations(doc, logger, options); if (!options.meshes && !options.animations) { logger.warn(`${NAME}: Select animations or meshes to create a partition.`); } await doc.transform(prune({ propertyTypes: [PropertyType.BUFFER] })); logger.debug(`${NAME}: Complete.`); }); } function partitionMeshes(doc: Document, logger: ILogger, options: Required<PartitionOptions>): void { const existingURIs = new Set<string>( doc .getRoot() .listBuffers() .map((b) => b.getURI()), ); doc.getRoot() .listMeshes() .forEach((mesh, meshIndex) => { if (Array.isArray(options.meshes) && !options.meshes.includes(mesh.getName())) { logger.debug(`${NAME}: Skipping mesh #${meshIndex} with name "${mesh.getName()}".`); return; } logger.debug(`${NAME}: Creating buffer for mesh "${mesh.getName()}".`); const buffer = doc .createBuffer(mesh.getName()) .setURI(createBufferURI(mesh.getName() || 'mesh', existingURIs)); mesh.listPrimitives().forEach((primitive) => { const indices = primitive.getIndices(); if (indices) indices.setBuffer(buffer); primitive.listAttributes().forEach((attribute) => attribute.setBuffer(buffer)); primitive.listTargets().forEach((primTarget) => { primTarget.listAttributes().forEach((attribute) => attribute.setBuffer(buffer)); }); }); }); } function partitionAnimations(doc: Document, logger: ILogger, options: Required<PartitionOptions>): void { const existingURIs = new Set<string>( doc .getRoot() .listBuffers() .map((b) => b.getURI()), ); doc.getRoot() .listAnimations() .forEach((anim, animIndex) => { if (Array.isArray(options.animations) && !options.animations.includes(anim.getName())) { logger.debug(`${NAME}: Skipping animation #${animIndex} with name "${anim.getName()}".`); return; } logger.debug(`${NAME}: Creating buffer for animation "${anim.getName()}".`); const buffer = doc .createBuffer(anim.getName()) .setURI(createBufferURI(anim.getName() || 'animation', existingURIs)); anim.listSamplers().forEach((sampler) => { const input = sampler.getInput(); const output = sampler.getOutput(); if (input) input.setBuffer(buffer); if (output) output.setBuffer(buffer); }); }); } const SANITIZE_BASENAME_RE = /[^\w0–9-]+/g; function createBufferURI(basename: string, existing: Set<string>): string { basename = basename.replace(SANITIZE_BASENAME_RE, ''); let uri = `${basename}.bin`; let i = 1; while (existing.has(uri)) uri = `${basename}_${i++}.bin`; existing.add(uri); return uri; }