UNPKG

@gltf-transform/functions

Version:

Functions for common glTF modifications, written using the core API

1,700 lines (1,622 loc) 277 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); } } /** @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; if (!Math.hypot) Math.hypot = function () { var y = 0, i = arguments.length; while (i--) { y += arguments[i] * arguments[i]; } return Math.sqrt(y); }; /** * Inverts a mat4 * * @param {mat4} out the receiving matrix * @param {ReadonlyMat4} a the source matrix * @returns {mat4} out */ 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 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 return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; } /** * 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, vec); * let quatMat = mat4.create(); * quat4.toMat4(quat, quatMat); * mat4.multiply(dest, quatMat); * mat4.scale(dest, scale) * * @param {mat4} out mat4 receiving operation result * @param {quat4} 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; } /** * 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} out */ 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; }; })(); const NAME$p = 'dedup'; const DEDUP_DEFAULTS = { keepUniqueNames: false, propertyTypes: [core.PropertyType.ACCESSOR, core.PropertyType.MESH, core.PropertyType.TEXTURE, core.PropertyType.MATERIAL, core.PropertyType.SKIN] }; /** * Removes duplicate {@link Accessor}, {@link Mesh}, {@link Texture}, and {@link Material} * properties. Partially based on a * [gist by mattdesl](https://gist.github.com/mattdesl/aea40285e2d73916b6b9101b36d84da8). Only * accessors in mesh primitives, morph targets, and animation samplers are processed. * * Example: * * ```ts * document.getRoot().listMeshes(); // → [Mesh, Mesh, Mesh] * * await document.transform(dedup({propertyTypes: [PropertyType.MESH]})); * * document.getRoot().listMeshes(); // → [Mesh] * ``` * * @category Transforms */ function dedup(_options = DEDUP_DEFAULTS) { const options = assignDefaults(DEDUP_DEFAULTS, _options); const propertyTypes = new Set(options.propertyTypes); for (const propertyType of options.propertyTypes) { if (!DEDUP_DEFAULTS.propertyTypes.includes(propertyType)) { throw new Error(`${NAME$p}: Unsupported deduplication on type "${propertyType}".`); } } return createTransform(NAME$p, document => { const logger = document.getLogger(); if (propertyTypes.has(core.PropertyType.ACCESSOR)) dedupAccessors(document); if (propertyTypes.has(core.PropertyType.TEXTURE)) dedupImages(document, options); if (propertyTypes.has(core.PropertyType.MATERIAL)) dedupMaterials(document, options); if (propertyTypes.has(core.PropertyType.MESH)) dedupMeshes(document, options); if (propertyTypes.has(core.PropertyType.SKIN)) dedupSkins(document, options); logger.debug(`${NAME$p}: Complete.`); }); } function dedupAccessors(document) { const logger = document.getLogger(); // Find all accessors used for mesh and animation data. const indicesMap = new Map(); const attributeMap = new Map(); const inputMap = new Map(); const outputMap = new Map(); const meshes = document.getRoot().listMeshes(); meshes.forEach(mesh => { mesh.listPrimitives().forEach(primitive => { primitive.listAttributes().forEach(accessor => hashAccessor(accessor, attributeMap)); hashAccessor(primitive.getIndices(), indicesMap); }); }); for (const animation of document.getRoot().listAnimations()) { for (const sampler of animation.listSamplers()) { hashAccessor(sampler.getInput(), inputMap); hashAccessor(sampler.getOutput(), outputMap); } } // Add accessor to the appropriate hash group. Hashes are _non-unique_, // intended to quickly compare everything accept the underlying array. function hashAccessor(accessor, group) { if (!accessor) return; const hash = [accessor.getCount(), accessor.getType(), accessor.getComponentType(), accessor.getNormalized(), accessor.getSparse()].join(':'); let hashSet = group.get(hash); if (!hashSet) group.set(hash, hashSet = new Set()); hashSet.add(accessor); } // Find duplicate accessors of a given type. function detectDuplicates(accessors, duplicates) { for (let i = 0; i < accessors.length; i++) { const a = accessors[i]; const aData = core.BufferUtils.toView(a.getArray()); if (duplicates.has(a)) continue; for (let j = i + 1; j < accessors.length; j++) { const b = accessors[j]; if (duplicates.has(b)) continue; // Just compare the arrays — everything else was covered by the // hash. Comparing uint8 views is faster than comparing the // original typed arrays. if (core.BufferUtils.equals(aData, core.BufferUtils.toView(b.getArray()))) { duplicates.set(b, a); } } } } let total = 0; const duplicates = new Map(); for (const group of [attributeMap, indicesMap, inputMap, outputMap]) { for (const hashGroup of group.values()) { total += hashGroup.size; detectDuplicates(Array.from(hashGroup), duplicates); } } logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${total} accessors.`); // Dissolve duplicate vertex attributes and indices. meshes.forEach(mesh => { mesh.listPrimitives().forEach(primitive => { primitive.listAttributes().forEach(accessor => { if (duplicates.has(accessor)) { primitive.swap(accessor, duplicates.get(accessor)); } }); const indices = primitive.getIndices(); if (indices && duplicates.has(indices)) { primitive.swap(indices, duplicates.get(indices)); } }); }); // Dissolve duplicate animation sampler inputs and outputs. for (const animation of document.getRoot().listAnimations()) { for (const sampler of animation.listSamplers()) { const input = sampler.getInput(); const output = sampler.getOutput(); if (input && duplicates.has(input)) { sampler.swap(input, duplicates.get(input)); } if (output && duplicates.has(output)) { sampler.swap(output, duplicates.get(output)); } } } Array.from(duplicates.keys()).forEach(accessor => accessor.dispose()); } function dedupMeshes(document, options) { const logger = document.getLogger(); const root = document.getRoot(); // Create Reference -> ID lookup table. const refs = new Map(); root.listAccessors().forEach((accessor, index) => refs.set(accessor, index)); root.listMaterials().forEach((material, index) => refs.set(material, index)); // For each mesh, create a hashkey. const numMeshes = root.listMeshes().length; const uniqueMeshes = new Map(); for (const src of root.listMeshes()) { // For each mesh, create a hashkey. const srcKeyItems = []; for (const prim of src.listPrimitives()) { srcKeyItems.push(createPrimitiveKey(prim, refs)); } // If another mesh exists with the same key, replace all instances with that, and dispose // of the duplicate. If not, just cache it. let meshKey = ''; if (options.keepUniqueNames) meshKey += src.getName() + ';'; meshKey += srcKeyItems.join(';'); if (uniqueMeshes.has(meshKey)) { const targetMesh = uniqueMeshes.get(meshKey); src.listParents().forEach(parent => { if (parent.propertyType !== core.PropertyType.ROOT) { parent.swap(src, targetMesh); } }); src.dispose(); } else { uniqueMeshes.set(meshKey, src); } } logger.debug(`${NAME$p}: Merged ${numMeshes - uniqueMeshes.size} of ${numMeshes} meshes.`); } function dedupImages(document, options) { const logger = document.getLogger(); const root = document.getRoot(); const textures = root.listTextures(); const duplicates = new Map(); // Compare each texture to every other texture — O(n²) — and mark duplicates for replacement. for (let i = 0; i < textures.length; i++) { const a = textures[i]; const aData = a.getImage(); if (duplicates.has(a)) continue; for (let j = i + 1; j < textures.length; j++) { const b = textures[j]; const bData = b.getImage(); if (duplicates.has(b)) continue; // URIs are intentionally not compared. if (a.getMimeType() !== b.getMimeType()) continue; if (options.keepUniqueNames && a.getName() !== b.getName()) continue; const aSize = a.getSize(); const bSize = b.getSize(); if (!aSize || !bSize) continue; if (aSize[0] !== bSize[0]) continue; if (aSize[1] !== bSize[1]) continue; if (!aData || !bData) continue; if (core.BufferUtils.equals(aData, bData)) { duplicates.set(b, a); } } } logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${root.listTextures().length} textures.`); Array.from(duplicates.entries()).forEach(([src, dst]) => { src.listParents().forEach(property => { if (!(property instanceof core.Root)) property.swap(src, dst); }); src.dispose(); }); } function dedupMaterials(document, options) { const logger = document.getLogger(); const root = document.getRoot(); const materials = root.listMaterials(); const duplicates = new Map(); const modifierCache = new Map(); const skip = new Set(); if (!options.keepUniqueNames) { skip.add('name'); } // Compare each material to every other material — O(n²) — and mark duplicates for replacement. for (let i = 0; i < materials.length; i++) { const a = materials[i]; if (duplicates.has(a)) continue; if (hasModifier(a, modifierCache)) continue; for (let j = i + 1; j < materials.length; j++) { const b = materials[j]; if (duplicates.has(b)) continue; if (hasModifier(b, modifierCache)) continue; if (a.equals(b, skip)) { duplicates.set(b, a); } } } logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${materials.length} materials.`); Array.from(duplicates.entries()).forEach(([src, dst]) => { src.listParents().forEach(property => { if (!(property instanceof core.Root)) property.swap(src, dst); }); src.dispose(); }); } function dedupSkins(document, options) { const logger = document.getLogger(); const root = document.getRoot(); const skins = root.listSkins(); const duplicates = new Map(); const skip = new Set(['joints']); if (!options.keepUniqueNames) { skip.add('name'); } for (let i = 0; i < skins.length; i++) { const a = skins[i]; if (duplicates.has(a)) continue; for (let j = i + 1; j < skins.length; j++) { const b = skins[j]; if (duplicates.has(b)) continue; // Check joints with shallow equality, not deep equality. // See: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/RecursiveSkeletons if (a.equals(b, skip) && shallowEqualsArray(a.listJoints(), b.listJoints())) { duplicates.set(b, a); } } } logger.debug(`${NAME$p}: Merged ${duplicates.size} of ${skins.length} skins.`); Array.from(duplicates.entries()).forEach(([src, dst]) => { src.listParents().forEach(property => { if (!(property instanceof core.Root)) property.swap(src, dst); }); src.dispose(); }); } /** Generates a key unique to the content of a primitive or target. */ function createPrimitiveKey(prim, refs) { const primKeyItems = []; for (const semantic of prim.listSemantics()) { const attribute = prim.getAttribute(semantic); primKeyItems.push(semantic + ':' + refs.get(attribute)); } if (prim instanceof core.Primitive) { const indices = prim.getIndices(); if (indices) { primKeyItems.push('indices:' + refs.get(indices)); } const material = prim.getMaterial(); if (material) { primKeyItems.push('material:' + refs.get(material)); } primKeyItems.push('mode:' + prim.getMode()); for (const target of prim.listTargets()) { primKeyItems.push('target:' + createPrimitiveKey(target, refs)); } } return primKeyItems.join(','); } /** * Detects dependencies modified by a parent reference, to conservatively prevent merging. When * implementing extensions like KHR_animation_pointer, the 'modifyChild' attribute should be added * to graph edges connecting the animation channel to the animated target property. * * NOTICE: Implementation is conservative, and could prevent merging two materials sharing the * same animated "Clearcoat" ExtensionProperty. While that scenario is possible for an in-memory * glTF Transform graph, valid glTF input files do not have that risk. */ function hasModifier(prop, cache) { if (cache.has(prop)) return cache.get(prop); const graph = prop.getGraph(); const visitedNodes = new Set(); const edgeQueue = graph.listParentEdges(prop); // Search dependency subtree for 'modifyChild' attribute. while (edgeQueue.length > 0) { const edge = edgeQueue.pop(); if (edge.getAttributes().modifyChild === true) { cache.set(prop, true); return true; } const child = edge.getChild(); if (visitedNodes.has(child)) continue; for (const childEdge of graph.listChildEdges(child)) { edgeQueue.push(childEdge); } } cache.set(prop, false); return false; } /** * 4 Dimensional Vector * @module vec4 */ /** * Creates a new, empty vec4 * * @returns {vec4} a new 4D vector */ function create() { var out = new ARRAY_TYPE(4); if (ARRAY_TYPE != Float32Array) { out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0; } return out; } /** * Adds two vec4's * * @param {vec4} out the receiving vector * @param {ReadonlyVec4} a the first operand * @param {ReadonlyVec4} b the second operand * @returns {vec4} out */ function add(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; out[2] = a[2] + b[2]; out[3] = a[3] + b[3]; return out; } /** * Subtracts vector b from vector a * * @param {vec4} out the receiving vector * @param {ReadonlyVec4} a the first operand * @param {ReadonlyVec4} b the second operand * @returns {vec4} out */ function subtract(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; out[3] = a[3] - b[3]; return out; } /** * Multiplies two vec4's * * @param {vec4} out the receiving vector * @param {ReadonlyVec4} a the first operand * @param {ReadonlyVec4} b the second operand * @returns {vec4} out */ function multiply(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; out[3] = a[3] * b[3]; return out; } /** * Scales a vec4 by a scalar number * * @param {vec4} out the receiving vector * @param {ReadonlyVec4} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec4} out */ function scale(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; out[3] = a[3] * b; return out; } /** * Calculates the length of a vec4 * * @param {ReadonlyVec4} a vector to calculate length of * @returns {Number} length of a */ function length(a) { var x = a[0]; var y = a[1]; var z = a[2]; var w = a[3]; return Math.hypot(x, y, z, w); } /** * Alias for {@link vec4.subtract} * @function */ var sub = subtract; /** * Alias for {@link vec4.multiply} * @function */ var mul = multiply; /** * Alias for {@link vec4.length} * @function */ var len = length; /** * Perform some operation over an array of vec4s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec4s 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(); return function (a, stride, offset, count, fn, arg) { var i, l; if (!stride) { stride = 4; } 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]; vec[3] = a[i + 3]; fn(vec, vec, arg); a[i] = vec[0]; a[i + 1] = vec[1]; a[i + 2] = vec[2]; a[i + 3] = vec[3]; } return a; }; })(); const SRGB_PATTERN = /color|emissive|diffuse/i; /** * Returns the color space (if any) implied by the {@link Material} slots to * which a texture is assigned, or null for non-color textures. If the texture * is not connected to any {@link Material}, this function will also return * null — any metadata in the image file will be ignored. * * Under current glTF specifications, only 'srgb' and non-color (null) textures * are used. * * Example: * * ```typescript * import { getTextureColorSpace } from '@gltf-transform/functions'; * * const baseColorTexture = material.getBaseColorTexture(); * const normalTexture = material.getNormalTexture(); * * getTextureColorSpace(baseColorTexture); // → 'srgb' * getTextureColorSpace(normalTexture); // → null * ``` */ function getTextureColorSpace(texture) { const graph = texture.getGraph(); const edges = graph.listParentEdges(texture); const isSRGB = edges.some(edge => { return edge.getAttributes().isColor || SRGB_PATTERN.test(edge.getName()); }); return isSRGB ? 'srgb' : null; } /** * Lists all {@link TextureInfo} definitions associated with a given * {@link Texture}. May be used to determine which UV transforms * and texCoord indices are applied to the material, without explicitly * checking the material properties and extensions. * * Example: * * ```typescript * // Find TextureInfo instances associated with the texture. * const results = listTextureInfo(texture); * * // Find which UV sets (TEXCOORD_0, TEXCOORD_1, ...) are required. * const texCoords = results.map((info) => info.getTexCoord()); * // → [0, 1] * ``` */ function listTextureInfo(texture) { const graph = texture.getGraph(); const results = new Set(); for (const textureEdge of graph.listParentEdges(texture)) { const parent = textureEdge.getParent(); const name = textureEdge.getName() + 'Info'; for (const edge of graph.listChildEdges(parent)) { const child = edge.getChild(); if (child instanceof core.TextureInfo && edge.getName() === name) { results.add(child); } } } return Array.from(results); } /** * Lists all {@link TextureInfo} definitions associated with any {@link Texture} * on the given {@link Material}. May be used to determine which UV transforms * and texCoord indices are applied to the material, without explicitly * checking the material properties and extensions. * * Example: * * ```typescript * const results = listTextureInfoByMaterial(material); * * const texCoords = results.map((info) => info.getTexCoord()); * // → [0, 1] * ``` */ function listTextureInfoByMaterial(material) { const graph = material.getGraph(); const visited = new Set(); const results = new Set(); function traverse(prop) { const textureInfoNames = new Set(); for (const edge of graph.listChildEdges(prop)) { if (edge.getChild() instanceof core.Texture) { textureInfoNames.add(edge.getName() + 'Info'); } } for (const edge of graph.listChildEdges(prop)) { const child = edge.getChild(); if (visited.has(child)) continue; visited.add(child); if (child instanceof core.TextureInfo && textureInfoNames.has(edge.getName())) { results.add(child); } else if (child instanceof core.ExtensionProperty) { traverse(child); } } } traverse(material); return Array.from(results); } /** * Returns names of all texture slots using the given texture. * * Example: * * ```js * const slots = listTextureSlots(texture); * // → ['occlusionTexture', 'metallicRoughnessTexture'] * ``` */ function listTextureSlots(texture) { const document = core.Document.fromGraph(texture.getGraph()); const root = document.getRoot(); const slots = texture.getGraph().listParentEdges(texture).filter(edge => edge.getParent() !== root).map(edge => edge.getName()); return Array.from(new Set(slots)); } function _catch(body, recover) { try { var result = body(); } catch (e) { return recover(e); } if (result && result.then) { return result.then(void 0, recover); } return result; } const maybeGetPixels = function (texture) { return Promise.resolve(_catch(function () { return Promise.resolve(ndarrayPixels.getPixels(texture.getImage(), texture.getMimeType())); }, function () { return null; })); }; const getTextureFactor = function (texture) { return Promise.resolve(maybeGetPixels(texture)).then(function (pixels) { if (!pixels) return null; const min = [Infinity, Infinity, Infinity, Infinity]; const max = [-Infinity, -Infinity, -Infinity, -Infinity]; const target = [0, 0, 0, 0]; const [width, height] = pixels.shape; for (let i = 0; i < width; i++) { for (let j = 0; j < height; j++) { for (let k = 0; k < 4; k++) { min[k] = Math.min(min[k], pixels.get(i, j, k)); max[k] = Math.max(max[k], pixels.get(i, j, k)); } } if (len(sub(target, max, min)) / 255 > EPS) { return null; } } return scale(target, add(target, max, min), 0.5 / 255); }); }; /********************************************************************************************** * Prune solid (single-color) textures. */ const pruneSolidTextures = function (document) { try { const root = document.getRoot(); const graph = document.getGraph(); const logger = document.getLogger(); const textures = root.listTextures(); const pending = textures.map(function (texture) { return Promise.resolve(getTextureFactor(texture)).then(function (factor) { var _texture$getSize; if (!factor) return; if (getTextureColorSpace(texture) === 'srgb') { core.ColorUtils.convertSRGBToLinear(factor, factor); } const name = texture.getName() || texture.getURI(); const size = (_texture$getSize = texture.getSize()) == null ? void 0 : _texture$getSize.join('x'); const slots = listTextureSlots(texture); for (const edge of graph.listParentEdges(texture)) { const parent = edge.getParent(); if (parent !== root && applyMaterialFactor(parent, factor, edge.getName(), logger)) { edge.dispose(); } } if (texture.listParents().length === 1) { texture.dispose(); logger.debug(`${NAME$o}: Removed solid-color texture "${name}" (${size}px ${slots.join(', ')})`); } }); }); return Promise.resolve(Promise.all(pending)).then(function () {}); } catch (e) { return Promise.reject(e); } }; const NAME$o = 'prune'; const EPS = 3 / 255; const PRUNE_DEFAULTS = { propertyTypes: [core.PropertyType.NODE, core.PropertyType.SKIN, core.PropertyType.MESH, core.PropertyType.CAMERA, core.PropertyType.PRIMITIVE, core.PropertyType.PRIMITIVE_TARGET, core.PropertyType.ANIMATION, core.PropertyType.MATERIAL, core.PropertyType.TEXTURE, core.PropertyType.ACCESSOR, core.PropertyType.BUFFER], keepLeaves: false, keepAttributes: false,