@gltf-transform/functions
Version:
Functions for common glTF modifications, written using the core API
203 lines (173 loc) • 6.8 kB
text/typescript
import {
Accessor,
AnimationSampler,
ComponentTypeToTypedArray,
Document,
GLTF,
MathUtils,
PropertyType,
Root,
Transform,
TypedArray,
} from '@gltf-transform/core';
import { dedup } from './dedup.js';
import { assignDefaults, createTransform } from './utils.js';
import { resampleDebug } from 'keyframe-resample';
const NAME = 'resample';
const EMPTY_ARRAY = new Float32Array(0);
export interface ResampleOptions {
ready?: Promise<void>;
resample?: unknown; // glTF-Transform/issues/996
tolerance?: number;
/**
* 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 RESAMPLE_DEFAULTS: Required<ResampleOptions> = {
ready: Promise.resolve(),
resample: resampleDebug,
tolerance: 1e-4,
cleanup: true,
};
/**
* Resample {@link AnimationChannel AnimationChannels}, losslessly deduplicating keyframes to
* reduce file size. Duplicate keyframes are commonly present in animation 'baked' by the
* authoring software to apply IK constraints or other software-specific features.
*
* Optionally, a WebAssembly implementation from the
* [`keyframe-resample`](https://github.com/donmccurdy/keyframe-resample-wasm) library may be
* provided. The WebAssembly version is usually much faster at processing large animation
* sequences, but may not be compatible with all runtimes and JavaScript build tools.
*
* Result: (0,0,0,0,1,1,1,0,0,0,0,0,0,0) → (0,0,1,1,0,0)
*
* Example:
*
* ```
* import { resample } from '@gltf-transform/functions';
* import { ready, resample as resampleWASM } from 'keyframe-resample';
*
* // JavaScript (slower)
* await document.transform(resample());
*
* // WebAssembly (faster)
* await document.transform(resample({ ready, resample: resampleWASM }));
* ```
*
* @privateRemarks Implementation based on THREE.KeyframeTrack#optimize().
* @category Transforms
*/
export function resample(_options: ResampleOptions = RESAMPLE_DEFAULTS): Transform {
const options = assignDefaults(RESAMPLE_DEFAULTS, _options);
return createTransform(NAME, async (document: Document): Promise<void> => {
const accessorsVisited = new Set<Accessor>();
const srcAccessorCount = document.getRoot().listAccessors().length;
const logger = document.getLogger();
const ready = options.ready;
const resample = options.resample as typeof resampleDebug;
await ready;
for (const animation of document.getRoot().listAnimations()) {
const samplerTargetPaths = new Map<AnimationSampler, GLTF.AnimationChannelTargetPath>();
for (const channel of animation.listChannels()) {
samplerTargetPaths.set(channel.getSampler()!, channel.getTargetPath()!);
}
for (const sampler of animation.listSamplers()) {
const samplerInterpolation = sampler.getInterpolation();
if (samplerInterpolation === 'STEP' || samplerInterpolation === 'LINEAR') {
const input = sampler.getInput()!;
const output = sampler.getOutput()!;
accessorsVisited.add(input);
accessorsVisited.add(output);
// biome-ignore format: Readability.
const tmpTimes = toFloat32Array(
input.getArray()!,
input.getComponentType(),
input.getNormalized()
);
const tmpValues = toFloat32Array(
output.getArray()!,
output.getComponentType(),
output.getNormalized(),
);
const elementSize = tmpValues.length / tmpTimes.length;
const srcCount = tmpTimes.length;
let dstCount: number;
if (samplerInterpolation === 'STEP') {
dstCount = resample(tmpTimes, tmpValues, 'step', options.tolerance);
} else if (samplerTargetPaths.get(sampler) === 'rotation') {
dstCount = resample(tmpTimes, tmpValues, 'slerp', options.tolerance);
} else {
dstCount = resample(tmpTimes, tmpValues, 'lerp', options.tolerance);
}
if (dstCount < srcCount) {
// Clone the input/output accessors, without cloning their underlying
// arrays. Then assign the resampled data.
const srcTimes = input.getArray()!;
const srcValues = output.getArray()!;
const dstTimes = fromFloat32Array(
new Float32Array(tmpTimes.buffer, tmpTimes.byteOffset, dstCount),
input.getComponentType(),
input.getNormalized(),
);
const dstValues = fromFloat32Array(
new Float32Array(tmpValues.buffer, tmpValues.byteOffset, dstCount * elementSize),
output.getComponentType(),
output.getNormalized(),
);
input.setArray(EMPTY_ARRAY);
output.setArray(EMPTY_ARRAY);
sampler.setInput(input.clone().setArray(dstTimes));
sampler.setOutput(output.clone().setArray(dstValues));
input.setArray(srcTimes);
output.setArray(srcValues);
}
}
}
}
for (const accessor of Array.from(accessorsVisited.values())) {
const used = accessor.listParents().some((p) => !(p instanceof Root));
if (!used) accessor.dispose();
}
// Resampling may result in duplicate input or output sampler
// accessors. Find and remove the duplicates after processing.
const dstAccessorCount = document.getRoot().listAccessors().length;
if (dstAccessorCount > srcAccessorCount && options.cleanup) {
await document.transform(dedup({ propertyTypes: [PropertyType.ACCESSOR] }));
}
logger.debug(`${NAME}: Complete.`);
});
}
/** Returns a copy of the source array, as a denormalized Float32Array. */
function toFloat32Array(
srcArray: TypedArray,
componentType: GLTF.AccessorComponentType,
normalized: boolean,
): Float32Array {
if (srcArray instanceof Float32Array) return srcArray.slice();
const dstArray = new Float32Array(srcArray);
if (!normalized) return dstArray;
for (let i = 0; i < dstArray.length; i++) {
dstArray[i] = MathUtils.decodeNormalizedInt(dstArray[i], componentType);
}
return dstArray;
}
/** Returns a copy of the source array, with specified component type and normalization. */
function fromFloat32Array(
srcArray: Float32Array,
componentType: GLTF.AccessorComponentType,
normalized: boolean,
): TypedArray {
if (componentType === Accessor.ComponentType.FLOAT) return srcArray.slice();
const TypedArray = ComponentTypeToTypedArray[componentType];
const dstArray = new TypedArray(srcArray.length);
for (let i = 0; i < dstArray.length; i++) {
dstArray[i] = normalized ? MathUtils.encodeNormalizedInt(srcArray[i], componentType) : srcArray[i];
}
return dstArray;
}