UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

1,575 lines (1,539 loc) 275 kB
var core = require('@gltf-transform/core'); var ndarrayPixels = require('ndarray-pixels'); var extensions = require('@gltf-transform/extensions'); var ktxParse = require('ktx-parse'); var ndarray = require('ndarray'); var ndarrayLanczos = require('ndarray-lanczos'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var ndarray__default = /*#__PURE__*/_interopDefaultLegacy(ndarray); /** * Maps pixels from source to target textures, with a per-pixel callback. * @hidden */ const rewriteTexture = function (source, target, fn) { try { if (!source) return Promise.resolve(null); const srcImage = source.getImage(); if (!srcImage) return Promise.resolve(null); return Promise.resolve(ndarrayPixels.getPixels(srcImage, source.getMimeType())).then(function (pixels) { for (let i = 0; i < pixels.shape[0]; ++i) { for (let j = 0; j < pixels.shape[1]; ++j) { fn(pixels, i, j); } } return Promise.resolve(ndarrayPixels.savePixels(pixels, 'image/png')).then(function (dstImage) { return target.setImage(dstImage).setMimeType('image/png'); }); }); } catch (e) { return Promise.reject(e); } }; /** @hidden */ const { POINTS: POINTS$1, LINES: LINES$2, LINE_STRIP: LINE_STRIP$3, LINE_LOOP: LINE_LOOP$3, TRIANGLES: TRIANGLES$2, TRIANGLE_STRIP: TRIANGLE_STRIP$3, TRIANGLE_FAN: TRIANGLE_FAN$3 } = core.Primitive.Mode; /** * Prepares a function used in an {@link Document#transform} pipeline. Use of this wrapper is * optional, and plain functions may be used in transform pipelines just as well. The wrapper is * used internally so earlier pipeline stages can detect and optimize based on later stages. * @hidden */ function createTransform(name, fn) { Object.defineProperty(fn, 'name', { value: name }); return fn; } /** @hidden */ function isTransformPending(context, initial, pending) { if (!context) return false; const initialIndex = context.stack.lastIndexOf(initial); const pendingIndex = context.stack.lastIndexOf(pending); return initialIndex < pendingIndex; } /** * Performs a shallow merge on an 'options' object and a 'defaults' object. * Equivalent to `{...defaults, ...options}` _except_ that `undefined` values * in the 'options' object are ignored. * * @hidden */ function assignDefaults(defaults, options) { const result = { ...defaults }; for (const key in options) { if (options[key] !== undefined) { // biome-ignore lint/suspicious/noExplicitAny: TODO result[key] = options[key]; } } return result; } function getGLPrimitiveCount(prim) { const indices = prim.getIndices(); const position = prim.getAttribute('POSITION'); // Reference: https://www.khronos.org/opengl/wiki/Primitive switch (prim.getMode()) { case core.Primitive.Mode.POINTS: return indices ? indices.getCount() : position.getCount(); case core.Primitive.Mode.LINES: return indices ? indices.getCount() / 2 : position.getCount() / 2; case core.Primitive.Mode.LINE_LOOP: return indices ? indices.getCount() : position.getCount(); case core.Primitive.Mode.LINE_STRIP: return indices ? indices.getCount() - 1 : position.getCount() - 1; case core.Primitive.Mode.TRIANGLES: return indices ? indices.getCount() / 3 : position.getCount() / 3; case core.Primitive.Mode.TRIANGLE_STRIP: case core.Primitive.Mode.TRIANGLE_FAN: return indices ? indices.getCount() - 2 : position.getCount() - 2; default: throw new Error('Unexpected mode: ' + prim.getMode()); } } /** @hidden */ class SetMap { constructor() { this._map = new Map(); } get size() { return this._map.size; } has(k) { return this._map.has(k); } add(k, v) { let entry = this._map.get(k); if (!entry) { entry = new Set(); this._map.set(k, entry); } entry.add(v); return this; } get(k) { return this._map.get(k) || new Set(); } keys() { return this._map.keys(); } } /** @hidden */ function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1000; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } const _longFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }); /** @hidden */ function formatLong(x) { return _longFormatter.format(x); } /** @hidden */ function formatDelta(a, b, decimals = 2) { const prefix = a > b ? '–' : '+'; const suffix = '%'; return prefix + (Math.abs(a - b) / a * 100).toFixed(decimals) + suffix; } /** @hidden */ function formatDeltaOp(a, b) { return `${formatLong(a)} → ${formatLong(b)} (${formatDelta(a, b)})`; } /** * Returns a list of all unique vertex attributes on the given primitive and * its morph targets. * @hidden */ function deepListAttributes(prim) { const accessors = []; for (const attribute of prim.listAttributes()) { accessors.push(attribute); } for (const target of prim.listTargets()) { for (const attribute of target.listAttributes()) { accessors.push(attribute); } } return Array.from(new Set(accessors)); } /** @hidden */ function deepSwapAttribute(prim, src, dst) { prim.swap(src, dst); for (const target of prim.listTargets()) { target.swap(src, dst); } } /** * Disposes of a {@link Primitive} and any {@link Accessor Accesors} for which * it is the last remaining parent. * @hidden */ function deepDisposePrimitive(prim) { const indices = prim.getIndices(); const attributes = deepListAttributes(prim); prim.dispose(); if (indices && !isUsed(indices)) { indices.dispose(); } for (const attribute of attributes) { if (!isUsed(attribute)) { attribute.dispose(); } } } /** @hidden */ function shallowEqualsArray(a, b) { if (a == null && b == null) return true; if (a == null || b == null) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false; } return true; } /** Clones an {@link Accessor} without creating a copy of its underlying TypedArray data. */ function shallowCloneAccessor(document, accessor) { return document.createAccessor(accessor.getName()).setArray(accessor.getArray()).setType(accessor.getType()).setBuffer(accessor.getBuffer()).setNormalized(accessor.getNormalized()).setSparse(accessor.getSparse()); } /** @hidden */ function createIndices(count, maxIndex = count) { const array = createIndicesEmpty(count, maxIndex); for (let i = 0; i < array.length; i++) array[i] = i; return array; } /** @hidden */ function createIndicesEmpty(count, maxIndex = count) { return maxIndex <= 65534 ? new Uint16Array(count) : new Uint32Array(count); } /** @hidden */ function isUsed(prop) { return prop.listParents().some(parent => parent.propertyType !== core.PropertyType.ROOT); } /** @hidden */ function isEmptyObject(object) { for (const _key in object) return false; return true; } /** * Creates a unique key associated with the structure and draw call characteristics of * a {@link Primitive}, independent of its vertex content. Helper method, used to * identify candidate Primitives for joining. * @hidden */ function createPrimGroupKey(prim) { const document = core.Document.fromGraph(prim.getGraph()); const material = prim.getMaterial(); const materialIndex = document.getRoot().listMaterials().indexOf(material); const mode = BASIC_MODE_MAPPING[prim.getMode()]; const indices = !!prim.getIndices(); const attributes = prim.listSemantics().sort().map(semantic => { const attribute = prim.getAttribute(semantic); const elementSize = attribute.getElementSize(); const componentType = attribute.getComponentType(); return `${semantic}:${elementSize}:${componentType}`; }).join('+'); const targets = prim.listTargets().map(target => { return target.listSemantics().sort().map(semantic => { const attribute = prim.getAttribute(semantic); const elementSize = attribute.getElementSize(); const componentType = attribute.getComponentType(); return `${semantic}:${elementSize}:${componentType}`; }).join('+'); }).join('~'); return `${materialIndex}|${mode}|${indices}|${attributes}|${targets}`; } /** * Scales `size` NxN dimensions to fit within `limit` NxN dimensions, without * changing aspect ratio. If `size` <= `limit` in all dimensions, returns `size`. * @hidden */ function fitWithin(size, limit) { const [maxWidth, maxHeight] = limit; const [srcWidth, srcHeight] = size; if (srcWidth <= maxWidth && srcHeight <= maxHeight) return size; let dstWidth = srcWidth; let dstHeight = srcHeight; if (dstWidth > maxWidth) { dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth)); dstWidth = maxWidth; } if (dstHeight > maxHeight) { dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight)); dstHeight = maxHeight; } return [dstWidth, dstHeight]; } /** * Scales `size` NxN dimensions to the specified power of two. * @hidden */ function fitPowerOfTwo(size, method) { if (isPowerOfTwo(size[0]) && isPowerOfTwo(size[1])) { return size; } switch (method) { case 'nearest-pot': return size.map(nearestPowerOfTwo); case 'ceil-pot': return size.map(ceilPowerOfTwo$1); case 'floor-pot': return size.map(floorPowerOfTwo); } } function isPowerOfTwo(value) { if (value <= 2) return true; return (value & value - 1) === 0 && value !== 0; } function nearestPowerOfTwo(value) { if (value <= 4) return 4; const lo = floorPowerOfTwo(value); const hi = ceilPowerOfTwo$1(value); if (hi - value > value - lo) return lo; return hi; } function floorPowerOfTwo(value) { return Math.pow(2, Math.floor(Math.log(value) / Math.LN2)); } function ceilPowerOfTwo$1(value) { return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2)); } /** * Mapping from any glTF primitive mode to its equivalent basic mode, as returned by * {@link convertPrimitiveMode}. * @hidden */ const BASIC_MODE_MAPPING = { [POINTS$1]: POINTS$1, [LINES$2]: LINES$2, [LINE_STRIP$3]: LINES$2, [LINE_LOOP$3]: LINES$2, [TRIANGLES$2]: TRIANGLES$2, [TRIANGLE_STRIP$3]: TRIANGLES$2, [TRIANGLE_FAN$3]: TRIANGLES$2 }; const NAME$q = 'center'; const CENTER_DEFAULTS = { pivot: 'center' }; /** * Centers the {@link Scene} at the origin, or above/below it. Transformations from animation, * skinning, and morph targets are not taken into account. * * Example: * * ```ts * await document.transform(center({pivot: 'below'})); * ``` * * @category Transforms */ function center(_options = CENTER_DEFAULTS) { const options = assignDefaults(CENTER_DEFAULTS, _options); return createTransform(NAME$q, doc => { const logger = doc.getLogger(); const root = doc.getRoot(); const isAnimated = root.listAnimations().length > 0 || root.listSkins().length > 0; doc.getRoot().listScenes().forEach((scene, index) => { logger.debug(`${NAME$q}: Scene ${index + 1} / ${root.listScenes().length}.`); let pivot; if (typeof options.pivot === 'string') { const bbox = core.getBounds(scene); pivot = [(bbox.max[0] - bbox.min[0]) / 2 + bbox.min[0], (bbox.max[1] - bbox.min[1]) / 2 + bbox.min[1], (bbox.max[2] - bbox.min[2]) / 2 + bbox.min[2]]; if (options.pivot === 'above') pivot[1] = bbox.max[1]; if (options.pivot === 'below') pivot[1] = bbox.min[1]; } else { pivot = options.pivot; } logger.debug(`${NAME$q}: Pivot "${pivot.join(', ')}".`); const offset = [-1 * pivot[0], -1 * pivot[1], -1 * pivot[2]]; if (isAnimated) { logger.debug(`${NAME$q}: Model contains animation or skin. Adding a wrapper node.`); const offsetNode = doc.createNode('Pivot').setTranslation(offset); scene.listChildren().forEach(child => offsetNode.addChild(child)); scene.addChild(offsetNode); } else { logger.debug(`${NAME$q}: Skipping wrapper, offsetting all root nodes.`); scene.listChildren().forEach(child => { const t = child.getTranslation(); child.setTranslation([t[0] + offset[0], t[1] + offset[1], t[2] + offset[2]]); }); } }); logger.debug(`${NAME$q}: Complete.`); }); } /** * Finds the parent {@link Scene Scenes} associated with the given {@link Node}. * In most cases a Node is associated with only one Scene, but it is possible * for a Node to be located in two or more Scenes, or none at all. * * Example: * * ```typescript * import { listNodeScenes } from '@gltf-transform/functions'; * * const node = document.getRoot().listNodes() * .find((node) => node.getName() === 'MyNode'); * * const scenes = listNodeScenes(node); * ``` */ function listNodeScenes(node) { const visited = new Set(); let child = node; let parent; while (parent = child.getParentNode()) { if (visited.has(parent)) { throw new Error('Circular dependency in scene graph.'); } visited.add(parent); child = parent; } return child.listParents().filter(parent => parent instanceof core.Scene); } /** * Clears the parent of the given {@link Node}, leaving it attached * directly to its {@link Scene}. Inherited transforms will be applied * to the Node. This operation changes the Node's local transform, * but leaves its world transform unchanged. * * Example: * * ```typescript * import { clearNodeParent } from '@gltf-transform/functions'; * * scene.traverse((node) => { ... }); // Scene → … → Node * * clearNodeParent(node); * * scene.traverse((node) => { ... }); // Scene → Node * ``` * * To clear _all_ transforms of a Node, first clear its inherited transforms with * {@link clearNodeParent}, then clear the local transform with {@link clearNodeTransform}. */ function clearNodeParent(node) { const scenes = listNodeScenes(node); const parent = node.getParentNode(); if (!parent) return node; // Apply inherited transforms to local matrix. Skinned meshes are not affected // by the node parent's transform, and can be ignored. Updates to IBMs and TRS // animations are out of scope in this context. node.setMatrix(node.getWorldMatrix()); // Add to Scene roots. parent.removeChild(node); for (const scene of scenes) scene.addChild(node); return node; } /** * Common utilities * @module glMatrix */ var ARRAY_TYPE = typeof Float32Array !== "undefined" ? Float32Array : Array; /** * Inverts a mat4 * * @param {mat4} out the receiving matrix * @param {ReadonlyMat4} a the source matrix * @returns {mat4 | null} out, or null if source matrix is not invertible */ function invert$1(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; var a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; var a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; var a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; var b00 = a00 * a11 - a01 * a10; var b01 = a00 * a12 - a02 * a10; var b02 = a00 * a13 - a03 * a10; var b03 = a01 * a12 - a02 * a11; var b04 = a01 * a13 - a03 * a11; var b05 = a02 * a13 - a03 * a12; var b06 = a20 * a31 - a21 * a30; var b07 = a20 * a32 - a22 * a30; var b08 = a20 * a33 - a23 * a30; var b09 = a21 * a32 - a22 * a31; var b10 = a21 * a33 - a23 * a31; var b11 = a22 * a33 - a23 * a32; // Calculate the determinant var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return null; } det = 1.0 / det; out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return out; } /** * Calculates the determinant of a mat4 * * @param {ReadonlyMat4} a the source matrix * @returns {Number} determinant of a */ function determinant(a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; var a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; var a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; var a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; var b0 = a00 * a11 - a01 * a10; var b1 = a00 * a12 - a02 * a10; var b2 = a01 * a12 - a02 * a11; var b3 = a20 * a31 - a21 * a30; var b4 = a20 * a32 - a22 * a30; var b5 = a21 * a32 - a22 * a31; var b6 = a00 * b5 - a01 * b4 + a02 * b3; var b7 = a10 * b5 - a11 * b4 + a12 * b3; var b8 = a20 * b2 - a21 * b1 + a22 * b0; var b9 = a30 * b2 - a31 * b1 + a32 * b0; // Calculate the determinant return a13 * b6 - a03 * b7 + a33 * b8 - a23 * b9; } /** * Multiplies two mat4s * * @param {mat4} out the receiving matrix * @param {ReadonlyMat4} a the first operand * @param {ReadonlyMat4} b the second operand * @returns {mat4} out */ function multiply$2(out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3]; var a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; var a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; var a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; // Cache only the current line of the second matrix var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30; out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31; out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32; out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33; return out; } /** * Creates a matrix from a vector scaling * This is equivalent to (but much faster than): * * mat4.identity(dest); * mat4.scale(dest, dest, vec); * * @param {mat4} out mat4 receiving operation result * @param {ReadonlyVec3} v Scaling vector * @returns {mat4} out */ function fromScaling(out, v) { out[0] = v[0]; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = v[1]; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = v[2]; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; } /** * Creates a matrix from a quaternion rotation, vector translation and vector scale * This is equivalent to (but much faster than): * * mat4.identity(dest); * mat4.translate(dest, dest, vec); * let quatMat = mat4.create(); * mat4.fromQuat(quatMat, quat); * mat4.multiply(dest, dest, quatMat); * mat4.scale(dest, dest, scale) * * @param {mat4} out mat4 receiving operation result * @param {quat} q Rotation quaternion * @param {ReadonlyVec3} v Translation vector * @param {ReadonlyVec3} s Scaling vector * @returns {mat4} out */ function fromRotationTranslationScale(out, q, v, s) { // Quaternion math var x = q[0], y = q[1], z = q[2], w = q[3]; var x2 = x + x; var y2 = y + y; var z2 = z + z; var xx = x * x2; var xy = x * y2; var xz = x * z2; var yy = y * y2; var yz = y * z2; var zz = z * z2; var wx = w * x2; var wy = w * y2; var wz = w * z2; var sx = s[0]; var sy = s[1]; var sz = s[2]; out[0] = (1 - (yy + zz)) * sx; out[1] = (xy + wz) * sx; out[2] = (xz - wy) * sx; out[3] = 0; out[4] = (xy - wz) * sy; out[5] = (1 - (xx + zz)) * sy; out[6] = (yz + wx) * sy; out[7] = 0; out[8] = (xz + wy) * sz; out[9] = (yz - wx) * sz; out[10] = (1 - (xx + yy)) * sz; out[11] = 0; out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; out[15] = 1; return out; } /** * Various methods of estimating a vertex count. For some background on why * multiple definitions of a vertex count should exist, see [_Vertex Count * Higher in Engine than in 3D Software_](https://shahriyarshahrabi.medium.com/vertex-count-higher-in-engine-than-in-3d-software-badc348ada66). * Totals for a {@link Scene}, {@link Node}, or {@link Mesh} will not * necessarily match the sum of the totals for each {@link Primitive}. Choose * the appropriate method for a relevant total or estimate: * * - {@link getSceneVertexCount} * - {@link getNodeVertexCount} * - {@link getMeshVertexCount} * - {@link getPrimitiveVertexCount} * * Many rendering features, such as volumetric transmission, may lead * to additional passes over some or all vertices. These tradeoffs are * implementation-dependent, and not considered here. */ exports.VertexCountMethod = void 0; (function (VertexCountMethod) { /** * Expected number of vertices processed by the vertex shader for one render * pass, without considering the vertex cache. */ VertexCountMethod["RENDER"] = "render"; /** * Expected number of vertices processed by the vertex shader for one render * pass, assuming an Average Transform to Vertex Ratio (ATVR) of 1. Approaching * this result requires optimizing for locality of vertex references (see * {@link reorder}). * * References: * - [ACMR and ATVR](https://www.realtimerendering.com/blog/acmr-and-atvr/), Real-Time Rendering */ VertexCountMethod["RENDER_CACHED"] = "render-cached"; /** * Expected number of vertices uploaded to the GPU, assuming that a client * uploads each unique {@link Accessor} only once. Unless glTF vertex * attributes are pre-processed to a known buffer layout, and the client is * optimized for that buffer layout, this total will be optimistic. */ VertexCountMethod["UPLOAD"] = "upload"; /** * Expected number of vertices uploaded to the GPU, assuming that a client * uploads each unique {@link Primitive} individually, duplicating vertex * attribute {@link Accessor Accessors} shared by multiple primitives, but * never uploading the same Mesh or Primitive to GPU memory more than once. */ VertexCountMethod["UPLOAD_NAIVE"] = "upload-naive"; /** * Total number of unique vertices represented, considering all attributes of * each vertex, and removing any duplicates. Has no direct relationship to * runtime characteristics, but may be helpful in identifying asset * optimization opportunities. * * @hidden TODO(feat): Not yet implemented. * @internal */ VertexCountMethod["DISTINCT"] = "distinct"; /** * Total number of unique vertices represented, considering only vertex * positions, and removing any duplicates. Has no direct relationship to * runtime characteristics, but may be helpful in identifying asset * optimization opportunities. * * @hidden TODO(feat): Not yet implemented. * @internal */ VertexCountMethod["DISTINCT_POSITION"] = "distinct-position"; /** * Number of vertex positions never used by any {@link Primitive}. If all * vertices are unused, this total will match `UPLOAD`. */ VertexCountMethod["UNUSED"] = "unused"; })(exports.VertexCountMethod || (exports.VertexCountMethod = {})); /** * Computes total number of vertices in a {@link Scene}, by the * specified method. Totals for the Scene will not necessarily match the sum * of the totals for each {@link Mesh} or {@link Primitive} within it. See * {@link VertexCountMethod} for available methods. */ function getSceneVertexCount(scene, method) { return _getSubtreeVertexCount(scene, method); } /** * Computes total number of vertices in a {@link Node}, by the * specified method. Totals for the node will not necessarily match the sum * of the totals for each {@link Mesh} or {@link Primitive} within it. See * {@link VertexCountMethod} for available methods. */ function getNodeVertexCount(node, method) { return _getSubtreeVertexCount(node, method); } function _getSubtreeVertexCount(node, method) { const instancedMeshes = []; const nonInstancedMeshes = []; const meshes = []; node.traverse(node => { const mesh = node.getMesh(); const batch = node.getExtension('EXT_mesh_gpu_instancing'); if (batch && mesh) { meshes.push(mesh); instancedMeshes.push([batch.listAttributes()[0].getCount(), mesh]); } else if (mesh) { meshes.push(mesh); nonInstancedMeshes.push(mesh); } }); const prims = meshes.flatMap(mesh => mesh.listPrimitives()); const positions = prims.map(prim => prim.getAttribute('POSITION')); const uniquePositions = Array.from(new Set(positions)); const uniqueMeshes = Array.from(new Set(meshes)); const uniquePrims = Array.from(new Set(uniqueMeshes.flatMap(mesh => mesh.listPrimitives()))); switch (method) { case exports.VertexCountMethod.RENDER: case exports.VertexCountMethod.RENDER_CACHED: return _sum(nonInstancedMeshes.map(mesh => getMeshVertexCount(mesh, method))) + _sum(instancedMeshes.map(([batch, mesh]) => batch * getMeshVertexCount(mesh, method))); case exports.VertexCountMethod.UPLOAD_NAIVE: return _sum(uniqueMeshes.map(mesh => getMeshVertexCount(mesh, method))); case exports.VertexCountMethod.UPLOAD: return _sum(uniquePositions.map(attribute => attribute.getCount())); case exports.VertexCountMethod.DISTINCT: case exports.VertexCountMethod.DISTINCT_POSITION: return _assertNotImplemented(method); case exports.VertexCountMethod.UNUSED: return _sumUnused(uniquePrims); default: return _assertUnreachable(method); } } /** * Computes total number of vertices in a {@link Mesh}, by the * specified method. Totals for the Mesh will not necessarily match the sum * of the totals for each {@link Primitive} within it. See * {@link VertexCountMethod} for available methods. */ function getMeshVertexCount(mesh, method) { const prims = mesh.listPrimitives(); const uniquePrims = Array.from(new Set(prims)); const uniquePositions = Array.from(new Set(uniquePrims.map(prim => prim.getAttribute('POSITION')))); switch (method) { case exports.VertexCountMethod.RENDER: case exports.VertexCountMethod.RENDER_CACHED: case exports.VertexCountMethod.UPLOAD_NAIVE: return _sum(prims.map(prim => getPrimitiveVertexCount(prim, method))); case exports.VertexCountMethod.UPLOAD: return _sum(uniquePositions.map(attribute => attribute.getCount())); case exports.VertexCountMethod.DISTINCT: case exports.VertexCountMethod.DISTINCT_POSITION: return _assertNotImplemented(method); case exports.VertexCountMethod.UNUSED: return _sumUnused(uniquePrims); default: return _assertUnreachable(method); } } /** * Computes total number of vertices in a {@link Primitive}, by the * specified method. See {@link VertexCountMethod} for available methods. */ function getPrimitiveVertexCount(prim, method) { const position = prim.getAttribute('POSITION'); const indices = prim.getIndices(); switch (method) { case exports.VertexCountMethod.RENDER: return indices ? indices.getCount() : position.getCount(); case exports.VertexCountMethod.RENDER_CACHED: return indices ? new Set(indices.getArray()).size : position.getCount(); case exports.VertexCountMethod.UPLOAD_NAIVE: case exports.VertexCountMethod.UPLOAD: return position.getCount(); case exports.VertexCountMethod.DISTINCT: case exports.VertexCountMethod.DISTINCT_POSITION: return _assertNotImplemented(method); case exports.VertexCountMethod.UNUSED: return indices ? position.getCount() - new Set(indices.getArray()).size : 0; default: return _assertUnreachable(method); } } function _sum(values) { let total = 0; for (let i = 0; i < values.length; i++) { total += values[i]; } return total; } function _sumUnused(prims) { const attributeIndexMap = new Map(); for (const prim of prims) { const position = prim.getAttribute('POSITION'); const indices = prim.getIndices(); const indicesSet = attributeIndexMap.get(position) || new Set(); indicesSet.add(indices); attributeIndexMap.set(position, indicesSet); } let unused = 0; for (const [position, indicesSet] of attributeIndexMap) { if (indicesSet.has(null)) continue; const usedIndices = new Uint8Array(position.getCount()); for (const indices of indicesSet) { const indicesArray = indices.getArray(); for (let i = 0, il = indicesArray.length; i < il; i++) { usedIndices[indicesArray[i]] = 1; } } for (let i = 0, il = position.getCount(); i < il; i++) { if (usedIndices[i] === 0) unused++; } } return unused; } function _assertNotImplemented(x) { throw new Error(`Not implemented: ${x}`); } function _assertUnreachable(x) { throw new Error(`Unexpected value: ${x}`); } /** Flags 'empty' values in a Uint32Array index. */ const EMPTY_U32$1 = 2 ** 32 - 1; class VertexStream { constructor(prim) { this.attributes = []; /** Temporary vertex views in 4-byte-aligned memory. */ this.u8 = void 0; this.u32 = void 0; let byteStride = 0; for (const attribute of deepListAttributes(prim)) { byteStride += this._initAttribute(attribute); } this.u8 = new Uint8Array(byteStride); this.u32 = new Uint32Array(this.u8.buffer); } _initAttribute(attribute) { const array = attribute.getArray(); const u8 = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); const byteStride = attribute.getElementSize() * attribute.getComponentSize(); const paddedByteStride = core.BufferUtils.padNumber(byteStride); this.attributes.push({ u8, byteStride, paddedByteStride }); return paddedByteStride; } hash(index) { // Load vertex into 4-byte-aligned view. let byteOffset = 0; for (const { u8, byteStride, paddedByteStride } of this.attributes) { for (let i = 0; i < paddedByteStride; i++) { if (i < byteStride) { this.u8[byteOffset + i] = u8[index * byteStride + i]; } else { this.u8[byteOffset + i] = 0; } } byteOffset += paddedByteStride; } // Compute hash. return murmurHash2(0, this.u32); } equal(a, b) { for (const { u8, byteStride } of this.attributes) { for (let j = 0; j < byteStride; j++) { if (u8[a * byteStride + j] !== u8[b * byteStride + j]) { return false; } } } return true; } } /** * References: * - https://github.com/mikolalysenko/murmurhash-js/blob/f19136e9f9c17f8cddc216ca3d44ec7c5c502f60/murmurhash2_gc.js#L14 * - https://github.com/zeux/meshoptimizer/blob/e47e1be6d3d9513153188216455bdbed40a206ef/src/indexgenerator.cpp#L12 */ function murmurHash2(h, key) { // MurmurHash2 const m = 0x5bd1e995; const r = 24; for (let i = 0, il = key.length; i < il; i++) { let k = key[i]; k = Math.imul(k, m) >>> 0; k = (k ^ k >> r) >>> 0; k = Math.imul(k, m) >>> 0; h = Math.imul(h, m) >>> 0; h = (h ^ k) >>> 0; } return h; } function hashLookup(table, buckets, stream, key, empty = EMPTY_U32$1) { const hashmod = buckets - 1; const hashval = stream.hash(key); let bucket = hashval & hashmod; for (let probe = 0; probe <= hashmod; probe++) { const item = table[bucket]; if (item === empty || stream.equal(item, key)) { return bucket; } bucket = bucket + probe + 1 & hashmod; // Hash collision. } throw new Error('Hash table full.'); } /** * Rewrites a {@link Primitive} such that all unused vertices in its vertex * attributes are removed. When multiple Primitives share vertex attributes, * each indexing only a few, compaction can be used to produce Primitives * each having smaller, independent vertex streams instead. * * Regardless of whether the Primitive is indexed or contains unused vertices, * compaction will clone every {@link Accessor}. The resulting Primitive will * share no Accessors with other Primitives, allowing later changes to * the vertex stream to be applied in isolation. * * Example: * * ```javascript * import { compactPrimitive, transformMesh } from '@gltf-transform/functions'; * import { fromTranslation } from 'gl-matrix/mat4'; * * const mesh = document.getRoot().listMeshes().find((mesh) => mesh.getName() === 'MyMesh'); * const prim = mesh.listPrimitives().find((prim) => { ... }); * * // Compact primitive, removing unused vertices and detaching shared vertex * // attributes. Without compaction, `transformPrimitive` might affect other * // primitives sharing the same vertex attributes. * compactPrimitive(prim); * * // Transform primitive vertices, y += 10. * transformPrimitive(prim, fromTranslation([], [0, 10, 0])); * ``` * * Parameters 'remap' and 'dstVertexCount' are optional. When either is * provided, the other must be provided as well. If one or both are missing, * both will be computed from the mesh indices. * * @param remap - Mapping. Array index represents vertex index in the source * attributes, array value represents index in the resulting compacted * primitive. When omitted, calculated from indices. * @param dstVertexcount - Number of unique vertices in compacted primitive. * When omitted, calculated from indices. */ // TODO(cleanup): Additional signatures currently break greendoc/parse. // export function compactPrimitive(prim: Primitive): Primitive; // export function compactPrimitive(prim: Primitive, remap: TypedArray, dstVertexCount: number): Primitive; function compactPrimitive(prim, remap, dstVertexCount) { const document = core.Document.fromGraph(prim.getGraph()); if (!remap || !dstVertexCount) { [remap, dstVertexCount] = createCompactPlan(prim); } // Remap indices. const srcIndices = prim.getIndices(); const srcIndicesArray = srcIndices ? srcIndices.getArray() : null; const srcIndicesCount = getPrimitiveVertexCount(prim, exports.VertexCountMethod.RENDER); const dstIndices = document.createAccessor(); const dstIndicesCount = srcIndicesCount; // primitive count does not change. const dstIndicesArray = createIndicesEmpty(dstIndicesCount, dstVertexCount); for (let i = 0; i < dstIndicesCount; i++) { dstIndicesArray[i] = remap[srcIndicesArray ? srcIndicesArray[i] : i]; } prim.setIndices(dstIndices.setArray(dstIndicesArray)); // Remap vertices. const srcAttributesPrev = deepListAttributes(prim); for (const srcAttribute of prim.listAttributes()) { const dstAttribute = shallowCloneAccessor(document, srcAttribute); compactAttribute(srcAttribute, srcIndices, remap, dstAttribute, dstVertexCount); prim.swap(srcAttribute, dstAttribute); } for (const target of prim.listTargets()) { for (const srcAttribute of target.listAttributes()) { const dstAttribute = shallowCloneAccessor(document, srcAttribute); compactAttribute(srcAttribute, srcIndices, remap, dstAttribute, dstVertexCount); target.swap(srcAttribute, dstAttribute); } } // Clean up accessors. if (srcIndices && srcIndices.listParents().length === 1) { srcIndices.dispose(); } for (const srcAttribute of srcAttributesPrev) { if (srcAttribute.listParents().length === 1) { srcAttribute.dispose(); } } return prim; } /** * Copies srcAttribute to dstAttribute, using the given indices and remap (srcIndex -> dstIndex). * Any existing array in dstAttribute is replaced. Vertices not used by the index are eliminated, * leaving a compact attribute. * @hidden * @internal */ function compactAttribute(srcAttribute, srcIndices, remap, dstAttribute, dstVertexCount) { const elementSize = srcAttribute.getElementSize(); const srcArray = srcAttribute.getArray(); const srcIndicesArray = srcIndices ? srcIndices.getArray() : null; const srcIndicesCount = srcIndices ? srcIndices.getCount() : srcAttribute.getCount(); const dstArray = new srcArray.constructor(dstVertexCount * elementSize); const dstDone = new Uint8Array(dstVertexCount); for (let i = 0; i < srcIndicesCount; i++) { const srcIndex = srcIndicesArray ? srcIndicesArray[i] : i; const dstIndex = remap[srcIndex]; if (dstDone[dstIndex]) continue; for (let j = 0; j < elementSize; j++) { dstArray[dstIndex * elementSize + j] = srcArray[srcIndex * elementSize + j]; } dstDone[dstIndex] = 1; } return dstAttribute.setArray(dstArray); } /** * Creates a 'remap' and 'dstVertexCount' plan for indexed primitives, * such that they can be rewritten with {@link compactPrimitive} removing * any non-rendered vertices. * @hidden * @internal */ function createCompactPlan(prim) { const srcVertexCount = getPrimitiveVertexCount(prim, exports.VertexCountMethod.UPLOAD); const indices = prim.getIndices(); const indicesArray = indices ? indices.getArray() : null; if (!indices || !indicesArray) { return [createIndices(srcVertexCount, 1_000_000), srcVertexCount]; } const remap = new Uint32Array(srcVertexCount).fill(EMPTY_U32$1); let dstVertexCount = 0; for (let i = 0; i < indicesArray.length; i++) { const srcIndex = indicesArray[i]; if (remap[srcIndex] === EMPTY_U32$1) { remap[srcIndex] = dstVertexCount++; } } return [remap, dstVertexCount]; } /** * 3x3 Matrix * @module mat3 */ /** * Creates a new identity mat3 * * @returns {mat3} a new 3x3 matrix */ function create$2() { var out = new ARRAY_TYPE(9); if (ARRAY_TYPE != Float32Array) { out[1] = 0; out[2] = 0; out[3] = 0; out[5] = 0; out[6] = 0; out[7] = 0; } out[0] = 1; out[4] = 1; out[8] = 1; return out; } /** * Copies the upper-left 3x3 values into the given mat3. * * @param {mat3} out the receiving 3x3 matrix * @param {ReadonlyMat4} a the source 4x4 matrix * @returns {mat3} out */ function fromMat4(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[4]; out[4] = a[5]; out[5] = a[6]; out[6] = a[8]; out[7] = a[9]; out[8] = a[10]; return out; } /** * Transpose the values of a mat3 * * @param {mat3} out the receiving matrix * @param {ReadonlyMat3} a the source matrix * @returns {mat3} out */ function transpose(out, a) { // If we are transposing ourselves we can skip a few steps but have to cache some values if (out === a) { var a01 = a[1], a02 = a[2], a12 = a[5]; out[1] = a[3]; out[2] = a[6]; out[3] = a01; out[5] = a[7]; out[6] = a02; out[7] = a12; } else { out[0] = a[0]; out[1] = a[3]; out[2] = a[6]; out[3] = a[1]; out[4] = a[4]; out[5] = a[7]; out[6] = a[2]; out[7] = a[5]; out[8] = a[8]; } return out; } /** * Inverts a mat3 * * @param {mat3} out the receiving matrix * @param {ReadonlyMat3} a the source matrix * @returns {mat3 | null} out, or null if source matrix is not invertible */ function invert(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2]; var a10 = a[3], a11 = a[4], a12 = a[5]; var a20 = a[6], a21 = a[7], a22 = a[8]; var b01 = a22 * a11 - a12 * a21; var b11 = -a22 * a10 + a12 * a20; var b21 = a21 * a10 - a11 * a20; // Calculate the determinant var det = a00 * b01 + a01 * b11 + a02 * b21; if (!det) { return null; } det = 1.0 / det; out[0] = b01 * det; out[1] = (-a22 * a01 + a02 * a21) * det; out[2] = (a12 * a01 - a02 * a11) * det; out[3] = b11 * det; out[4] = (a22 * a00 - a02 * a20) * det; out[5] = (-a12 * a00 + a02 * a10) * det; out[6] = b21 * det; out[7] = (-a21 * a00 + a01 * a20) * det; out[8] = (a11 * a00 - a01 * a10) * det; return out; } /** * 3 Dimensional Vector * @module vec3 */ /** * Creates a new, empty vec3 * * @returns {vec3} a new 3D vector */ function create$1() { var out = new ARRAY_TYPE(3); if (ARRAY_TYPE != Float32Array) { out[0] = 0; out[1] = 0; out[2] = 0; } return out; } /** * Multiplies two vec3's * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the first operand * @param {ReadonlyVec3} b the second operand * @returns {vec3} out */ function multiply$1(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; return out; } /** * Returns the minimum of two vec3's * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the first operand * @param {ReadonlyVec3} b the second operand * @returns {vec3} out */ function min(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); out[2] = Math.min(a[2], b[2]); return out; } /** * Returns the maximum of two vec3's * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the first operand * @param {ReadonlyVec3} b the second operand * @returns {vec3} out */ function max(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); out[2] = Math.max(a[2], b[2]); return out; } /** * Scales a vec3 by a scalar number * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec3} out */ function scale$1(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; return out; } /** * Normalize a vec3 * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a vector to normalize * @returns {vec3} out */ function normalize(out, a) { var x = a[0]; var y = a[1]; var z = a[2]; var len = x * x + y * y + z * z; if (len > 0) { //TODO: evaluate use of glm_invsqrt here? len = 1 / Math.sqrt(len); } out[0] = a[0] * len; out[1] = a[1] * len; out[2] = a[2] * len; return out; } /** * Transforms the vec3 with a mat4. * 4th vector component is implicitly '1' * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the vector to transform * @param {ReadonlyMat4} m matrix to transform with * @returns {vec3} out */ function transformMat4(out, a, m) { var x = a[0], y = a[1], z = a[2]; var w = m[3] * x + m[7] * y + m[11] * z + m[15]; w = w || 1.0; out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w; out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w; out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w; return out; } /** * Transforms the vec3 with a mat3. * * @param {vec3} out the receiving vector * @param {ReadonlyVec3} a the vector to transform * @param {ReadonlyMat3} m the 3x3 matrix to transform with * @returns {vec3} out */ function transformMat3(out, a, m) { var x = a[0], y = a[1], z = a[2]; out[0] = x * m[0] + y * m[3] + z * m[6]; out[1] = x * m[1] + y * m[4] + z * m[7]; out[2] = x * m[2] + y * m[5] + z * m[8]; return out; } /** * Alias for {@link vec3.multiply} * @function */ var mul$1 = multiply$1; /** * Perform some operation over an array of vec3s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ (function () { var vec = create$1(); return function (a, stride, offset, count, fn, arg) { var i, l; if (!stride) { stride = 3; } if (!offset) { offset = 0; } if (count) { l = Math.min(count * stride + offset, a.length); } else { l = a.length; } for (i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i + 1]; vec[2] = a[i + 2]; fn(vec, vec, arg); a[i] = vec[0]; a[i + 1] = vec[1]; a[i + 2] = vec[2]; } return a; }; })(); /** * 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$p = 'weld'; const WELD_DEFAULTS = { overwrite: 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 */ function weld(_options = WELD_DEFAULTS) { const options = assignDefaults(WELD_DEFAULTS, _options); return createTransform(NAME$p, function (doc) { try { const logger = doc.getLogger(); for (const mesh of doc.getRoot().listMeshes()) { for (const prim of mesh.listPrimitives()) { weldPrimitive(prim, options); if (getPrimitiveVertexCount(prim, exports.VertexCountMethod.RENDER) === 0) { deepDisposePrimitive(prim); } } if (mesh.listPrimitives().length === 0) mesh.dispose(); } logger.debug(`${NAME$p}: Complete.`); return Promise.resolve(); } catch (e) { return Promise.reject(e); } }); } /** * 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); * ``` */ function weldPrimitive(prim, _options = WELD_DEFAULTS) { const graph = prim.getGraph(); const document = core.Document.fromGraph(graph); const logger = document.getLogger(); const options = { ...WELD_DEFAULTS, ..._options }; if (prim.getIndices() && !options.overwrite) return; if (prim.getMode() === core.Primitive.Mode.POINTS) return; const srcVertexCount = prim.getAttribute('POSITION').getCount(); const srcIndices = prim.getIndices(); const srcIndicesArray = srcIndices == null ? void 0 : srcIndi