UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

193 lines (170 loc) 7.21 kB
import { Document, Primitive, PropertyType, Transform } from '@gltf-transform/core'; import { dedup } from './dedup.js'; import { prune } from './prune.js'; import { EMPTY_U32, VertexStream, hashLookup } from './hash-table.js'; import { assignDefaults, ceilPowerOfTwo, createTransform, formatDeltaOp } from './utils.js'; import { compactPrimitive } from './compact-primitive.js'; import { VertexCountMethod, getPrimitiveVertexCount } from './get-vertex-count.js'; /** * CONTRIBUTOR NOTES * * Ideally a weld() implementation should be fast, robust, and tunable. The * writeup below tracks my attempts to solve for these constraints. * * (Approach #1) Follow the mergeVertices() implementation of three.js, * hashing vertices with a string concatenation of all vertex attributes. * The approach does not allow per-attribute tolerance in local units. * * (Approach #2) Sort points along the X axis, then make cheaper * searches up/down the sorted list for merge candidates. While this allows * simpler comparison based on specified tolerance, it's much slower, even * for cases where choice of the X vs. Y or Z axes is reasonable. * * (Approach #3) Attempted a Delaunay triangulation in three dimensions, * expecting it would be an n * log(n) algorithm, but the only implementation * I found (with delaunay-triangulate) appeared to be much slower than that, * and was notably slower than the sort-based approach, just building the * Delaunay triangulation alone. * * (Approach #4) Hybrid of (1) and (2), assigning vertices to a spatial * grid, then searching the local neighborhood (27 cells) for weld candidates. * * (Approach #5) Based on Meshoptimizer's implementation, when tolerance=0 * use a hashtable to find bitwise-equal vertices quickly. Vastly faster than * previous approaches, but without tolerance options. * * RESULTS: For the "Lovecraftian" sample model linked below, after joining, * a primitive with 873,000 vertices can be welded down to 230,000 vertices. * https://sketchfab.com/3d-models/sculpt-january-day-19-lovecraftian-34ad2501108e4fceb9394f5b816b9f42 * * - (1) Not tested, but prior results suggest not robust enough. * - (2) 30s * - (3) 660s * - (4) 5s exhaustive, 1.5s non-exhaustive * - (5) 0.2s * * As of April 2024, the lossy weld was removed, leaving only approach #5. An * upcoming Meshoptimizer release will include a simplifyWithAttributes * function allowing simplification with weighted consideration of vertex * attributes, which I hope to support. With that, weld() may remain faster, * simpler, and more maintainable. */ const NAME = 'weld'; /** Options for the {@link weld} function. */ export interface WeldOptions { /** Whether to overwrite existing indices. */ overwrite?: boolean; /** * 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; } export const WELD_DEFAULTS: Required<WeldOptions> = { overwrite: true, cleanup: true, }; /** * Welds {@link Primitive Primitives}, merging bitwise identical vertices. When * merged and indexed, data is shared more efficiently between vertices. File size * can be reduced, and the GPU uses the vertex cache more efficiently. * * Example: * * ```javascript * import { weld, getSceneVertexCount, VertexCountMethod } from '@gltf-transform/functions'; * * const scene = document.getDefaultScene(); * const srcVertexCount = getSceneVertexCount(scene, VertexCountMethod.UPLOAD); * await document.transform(weld()); * const dstVertexCount = getSceneVertexCount(scene, VertexCountMethod.UPLOAD); * ``` * * @category Transforms */ export function weld(_options: WeldOptions = WELD_DEFAULTS): Transform { const options = assignDefaults(WELD_DEFAULTS, _options); return createTransform(NAME, async (doc: Document): Promise<void> => { const logger = doc.getLogger(); for (const mesh of doc.getRoot().listMeshes()) { for (const prim of mesh.listPrimitives()) { weldPrimitive(prim, options); if (getPrimitiveVertexCount(prim, VertexCountMethod.RENDER) === 0) { prim.dispose(); } } if (mesh.listPrimitives().length === 0) mesh.dispose(); } // Welding removes degenerate meshes; prune leaf nodes afterward. if (options.cleanup) { await doc.transform( prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE], keepAttributes: true, keepIndices: true, keepLeaves: false, }), dedup({ propertyTypes: [PropertyType.ACCESSOR] }), ); } logger.debug(`${NAME}: Complete.`); }); } /** * Welds a {@link Primitive}, merging bitwise identical vertices. When merged * and indexed, data is shared more efficiently between vertices. File size can * be reduced, and the GPU uses the vertex cache more efficiently. * * Example: * * ```javascript * import { weldPrimitive, getMeshVertexCount, VertexCountMethod } from '@gltf-transform/functions'; * * const mesh = document.getRoot().listMeshes() * .find((mesh) => mesh.getName() === 'Gizmo'); * * const srcVertexCount = getMeshVertexCount(mesh, VertexCountMethod.UPLOAD); * * for (const prim of mesh.listPrimitives()) { * weldPrimitive(prim); * } * * const dstVertexCount = getMeshVertexCount(mesh, VertexCountMethod.UPLOAD); * ``` */ export function weldPrimitive(prim: Primitive, _options: WeldOptions = WELD_DEFAULTS): void { const graph = prim.getGraph(); const document = Document.fromGraph(graph)!; const logger = document.getLogger(); const options = { ...WELD_DEFAULTS, ..._options }; if (prim.getIndices() && !options.overwrite) return; if (prim.getMode() === Primitive.Mode.POINTS) return; const srcVertexCount = prim.getAttribute('POSITION')!.getCount(); const srcIndices = prim.getIndices(); const srcIndicesArray = srcIndices?.getArray(); const srcIndicesCount = srcIndices ? srcIndices.getCount() : srcVertexCount; const stream = new VertexStream(prim); const tableSize = ceilPowerOfTwo(srcVertexCount + srcVertexCount / 4); const table = new Uint32Array(tableSize).fill(EMPTY_U32); const writeMap = new Uint32Array(srcVertexCount).fill(EMPTY_U32); // oldIndex → newIndex // (1) Compare and identify indices to weld. let dstVertexCount = 0; for (let i = 0; i < srcIndicesCount; i++) { const srcIndex = srcIndicesArray ? srcIndicesArray[i] : i; if (writeMap[srcIndex] !== EMPTY_U32) continue; const hashIndex = hashLookup(table, tableSize, stream, srcIndex, EMPTY_U32); const dstIndex = table[hashIndex]; if (dstIndex === EMPTY_U32) { table[hashIndex] = srcIndex; writeMap[srcIndex] = dstVertexCount++; } else { writeMap[srcIndex] = writeMap[dstIndex]; } } logger.debug(`${NAME}: ${formatDeltaOp(srcVertexCount, dstVertexCount)} vertices.`); compactPrimitive(prim, writeMap, dstVertexCount); }