var propertyGraph = require('property-graph'); function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return n; } /** * Current version of the package. * @hidden */ const VERSION = `v${"4.1.0"}`; /** @hidden */ const GLB_BUFFER = '@glb.bin'; /** String IDs for core {@link Property} types. */ exports.PropertyType = void 0; (function (PropertyType) { PropertyType["ACCESSOR"] = "Accessor"; PropertyType["ANIMATION"] = "Animation"; PropertyType["ANIMATION_CHANNEL"] = "AnimationChannel"; PropertyType["ANIMATION_SAMPLER"] = "AnimationSampler"; PropertyType["BUFFER"] = "Buffer"; PropertyType["CAMERA"] = "Camera"; PropertyType["MATERIAL"] = "Material"; PropertyType["MESH"] = "Mesh"; PropertyType["PRIMITIVE"] = "Primitive"; PropertyType["PRIMITIVE_TARGET"] = "PrimitiveTarget"; PropertyType["NODE"] = "Node"; PropertyType["ROOT"] = "Root"; PropertyType["SCENE"] = "Scene"; PropertyType["SKIN"] = "Skin"; PropertyType["TEXTURE"] = "Texture"; PropertyType["TEXTURE_INFO"] = "TextureInfo"; })(exports.PropertyType || (exports.PropertyType = {})); /** Vertex layout method. */ exports.VertexLayout = void 0; (function (VertexLayout) { /** * Stores vertex attributes in a single buffer view per mesh primitive. Interleaving vertex * data may improve performance by reducing page-thrashing in GPU memory. */ VertexLayout["INTERLEAVED"] = "interleaved"; /** * Stores each vertex attribute in a separate buffer view. May decrease performance by causing * page-thrashing in GPU memory. Some 3D engines may prefer this layout, e.g. for simplicity. */ VertexLayout["SEPARATE"] = "separate"; })(exports.VertexLayout || (exports.VertexLayout = {})); /** Accessor usage. */ var BufferViewUsage$1; (function (BufferViewUsage) { BufferViewUsage["ARRAY_BUFFER"] = "ARRAY_BUFFER"; BufferViewUsage["ELEMENT_ARRAY_BUFFER"] = "ELEMENT_ARRAY_BUFFER"; BufferViewUsage["INVERSE_BIND_MATRICES"] = "INVERSE_BIND_MATRICES"; BufferViewUsage["OTHER"] = "OTHER"; BufferViewUsage["SPARSE"] = "SPARSE"; })(BufferViewUsage$1 || (BufferViewUsage$1 = {})); /** Texture channels. */ exports.TextureChannel = void 0; (function (TextureChannel) { TextureChannel[TextureChannel["R"] = 4096] = "R"; TextureChannel[TextureChannel["G"] = 256] = "G"; TextureChannel[TextureChannel["B"] = 16] = "B"; TextureChannel[TextureChannel["A"] = 1] = "A"; })(exports.TextureChannel || (exports.TextureChannel = {})); exports.Format = void 0; (function (Format) { Format["GLTF"] = "GLTF"; Format["GLB"] = "GLB"; })(exports.Format || (exports.Format = {})); const ComponentTypeToTypedArray = { '5120': Int8Array, '5121': Uint8Array, '5122': Int16Array, '5123': Uint16Array, '5125': Uint32Array, '5126': Float32Array }; /** * 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); }; /** * 3 Dimensional Vector * @module vec3 */ /** * Creates a new, empty vec3 * * @returns {vec3} a new 3D vector */ function create() { var out = new ARRAY_TYPE(3); if (ARRAY_TYPE != Float32Array) { out[0] = 0; out[1] = 0; out[2] = 0; } return out; } /** * Calculates the length of a vec3 * * @param {ReadonlyVec3} 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]; return Math.hypot(x, y, z); } /** * 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; } /** * 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(); 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; }; })(); /** @hidden Implemented in /core for use by /extensions, publicly exported from /functions. */ function getBounds(node) { const resultBounds = createBounds(); const parents = node.propertyType === exports.PropertyType.NODE ? [node] : node.listChildren(); for (const parent of parents) { parent.traverse(node => { const mesh = node.getMesh(); if (!mesh) return; // Compute mesh bounds and update result. const meshBounds = getMeshBounds(mesh, node.getWorldMatrix()); if (meshBounds.min.every(isFinite) && meshBounds.max.every(isFinite)) { expandBounds(meshBounds.min, resultBounds); expandBounds(meshBounds.max, resultBounds); } }); } return resultBounds; } /** Computes mesh bounds in world space. */ function getMeshBounds(mesh, worldMatrix) { const meshBounds = createBounds(); // We can't transform a local AABB into world space and still have a tight AABB in world space, // so we need to compute the world AABB vertex by vertex here. for (const prim of mesh.listPrimitives()) { const position = prim.getAttribute('POSITION'); const indices = prim.getIndices(); if (!position) continue; let localPos = [0, 0, 0]; let worldPos = [0, 0, 0]; for (let i = 0, il = indices ? indices.getCount() : position.getCount(); i < il; i++) { const index = indices ? indices.getScalar(i) : i; localPos = position.getElement(index, localPos); worldPos = transformMat4(worldPos, localPos, worldMatrix); expandBounds(worldPos, meshBounds); } } return meshBounds; } /** Expands bounds of target by given source. */ function expandBounds(point, target) { for (let i = 0; i < 3; i++) { target.min[i] = Math.min(point[i], target.min[i]); target.max[i] = Math.max(point[i], target.max[i]); } } /** Creates new bounds with min=Infinity, max=-Infinity. */ function createBounds() { return { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] }; } /** * *Common utilities for working with Uint8Array and Buffer objects.* * * @category Utilities */ class BufferUtils { /** Creates a byte array from a Data URI. */ static createBufferFromDataURI(dataURI) { if (typeof Buffer === 'undefined') { // Browser. const byteString = atob(dataURI.split(',')[1]); const ia = new Uint8Array(byteString.length); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return ia; } else { // Node.js. const data = dataURI.split(',')[1]; const isBase64 = dataURI.indexOf('base64') >= 0; return Buffer.from(data, isBase64 ? 'base64' : 'utf8'); } } /** Encodes text to a byte array. */ static encodeText(text) { return new TextEncoder().encode(text); } /** Decodes a byte array to text. */ static decodeText(array) { return new TextDecoder().decode(array); } /** * Concatenates N byte arrays. */ static concat(arrays) { let totalByteLength = 0; for (const array of arrays) { totalByteLength += array.byteLength; } const result = new Uint8Array(totalByteLength); let byteOffset = 0; for (const array of arrays) { result.set(array, byteOffset); byteOffset += array.byteLength; } return result; } /** * Pads a Uint8Array to the next 4-byte boundary. * * Reference: [glTF → Data Alignment](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment) */ static pad(srcArray, paddingByte) { if (paddingByte === void 0) { paddingByte = 0; } const paddedLength = this.padNumber(srcArray.byteLength); if (paddedLength === srcArray.byteLength) return srcArray; const dstArray = new Uint8Array(paddedLength); dstArray.set(srcArray); if (paddingByte !== 0) { for (let i = srcArray.byteLength; i < paddedLength; i++) { dstArray[i] = paddingByte; } } return dstArray; } /** Pads a number to 4-byte boundaries. */ static padNumber(v) { return Math.ceil(v / 4) * 4; } /** Returns true if given byte array instances are equal. */ static equals(a, b) { if (a === b) return true; if (a.byteLength !== b.byteLength) return false; let i = a.byteLength; while (i--) { if (a[i] !== b[i]) return false; } return true; } /** * Returns a Uint8Array view of a typed array, with the same underlying ArrayBuffer. * * A shorthand for: * * ```js * const buffer = new Uint8Array( * array.buffer, * array.byteOffset + byteOffset, * Math.min(array.byteLength, byteLength) * ); * ``` * */ static toView(a, byteOffset, byteLength) { if (byteOffset === void 0) { byteOffset = 0; } if (byteLength === void 0) { byteLength = Infinity; } return new Uint8Array(a.buffer, a.byteOffset + byteOffset, Math.min(a.byteLength, byteLength)); } static assertView(view) { if (view && !ArrayBuffer.isView(view)) { throw new Error(`Method requires Uint8Array parameter; received "${typeof view}".`); } return view; } } /** * *Common utilities for working with colors in vec3, vec4, or hexadecimal form.* * * Provides methods to convert linear components (vec3, vec4) to sRGB hex values. All colors in * the glTF specification, excluding color textures, are linear. Hexadecimal values, in sRGB * colorspace, are accessible through helper functions in the API as a convenience. * * ```typescript * // Hex (sRGB) to factor (linear). * const factor = ColorUtils.hexToFactor(0xFFCCCC, []); * * // Factor (linear) to hex (sRGB). * const hex = ColorUtils.factorToHex([1, .25, .25]) * ``` * * @category Utilities */ class ColorUtils { /** * Converts sRGB hexadecimal to linear components. * @typeParam T vec3 or vec4 linear components. */ static hexToFactor(hex, target) { hex = Math.floor(hex); const _target = target; _target[0] = (hex >> 16 & 255) / 255; _target[1] = (hex >> 8 & 255) / 255; _target[2] = (hex & 255) / 255; return this.convertSRGBToLinear(target, target); } /** * Converts linear components to sRGB hexadecimal. * @typeParam T vec3 or vec4 linear components. */ static factorToHex(factor) { const target = [...factor]; const [r, g, b] = this.convertLinearToSRGB(factor, target); return r * 255 << 16 ^ g * 255 << 8 ^ b * 255 << 0; } /** * Converts sRGB components to linear components. * @typeParam T vec3 or vec4 linear components. */ static convertSRGBToLinear(source, target) { const _source = source; const _target = target; for (let i = 0; i < 3; i++) { _target[i] = _source[i] < 0.04045 ? _source[i] * 0.0773993808 : Math.pow(_source[i] * 0.9478672986 + 0.0521327014, 2.4); } return target; } /** * Converts linear components to sRGB components. * @typeParam T vec3 or vec4 linear components. */ static convertLinearToSRGB(source, target) { const _source = source; const _target = target; for (let i = 0; i < 3; i++) { _target[i] = _source[i] < 0.0031308 ? _source[i] * 12.92 : 1.055 * Math.pow(_source[i], 0.41666) - 0.055; } return target; } } /** JPEG image support. */ class JPEGImageUtils { match(array) { return array.length >= 3 && array[0] === 255 && array[1] === 216 && array[2] === 255; } getSize(array) { // Skip 4 chars, they are for signature let view = new DataView(array.buffer, array.byteOffset + 4); let i, next; while (view.byteLength) { // read length of the next block i = view.getUint16(0, false); // i = buffer.readUInt16BE(0); // ensure correct format validateJPEGBuffer(view, i); // 0xFFC0 is baseline standard(SOF) // 0xFFC1 is baseline optimized(SOF) // 0xFFC2 is progressive(SOF2) next = view.getUint8(i + 1); if (next === 0xc0 || next === 0xc1 || next === 0xc2) { return [view.getUint16(i + 7, false), view.getUint16(i + 5, false)]; } // move to the next block view = new DataView(array.buffer, view.byteOffset + i + 2); } throw new TypeError('Invalid JPG, no size found'); } getChannels(_buffer) { return 3; } } /** * PNG image support. * * PNG signature: 'PNG\r\n\x1a\n' * PNG image header chunk name: 'IHDR' */ class PNGImageUtils { match(array) { return array.length >= 8 && array[0] === 0x89 && array[1] === 0x50 && array[2] === 0x4e && array[3] === 0x47 && array[4] === 0x0d && array[5] === 0x0a && array[6] === 0x1a && array[7] === 0x0a; } getSize(array) { const view = new DataView(array.buffer, array.byteOffset); const magic = BufferUtils.decodeText(array.slice(12, 16)); if (magic === PNGImageUtils.PNG_FRIED_CHUNK_NAME) { return [view.getUint32(32, false), view.getUint32(36, false)]; } return [view.getUint32(16, false), view.getUint32(20, false)]; } getChannels(_buffer) { return 4; } } /** * *Common utilities for working with image data.* * * @category Utilities */ // Used to detect "fried" png's: http://www.jongware.com/pngdefry.html PNGImageUtils.PNG_FRIED_CHUNK_NAME = 'CgBI'; class ImageUtils { /** Registers support for a new image format; useful for certain extensions. */ static registerFormat(mimeType, impl) { this.impls[mimeType] = impl; } /** * Returns detected MIME type of the given image buffer. Note that for image * formats with support provided by extensions, the extension must be * registered with an I/O class before it can be detected by ImageUtils. */ static getMimeType(buffer) { for (const mimeType in this.impls) { if (this.impls[mimeType].match(buffer)) { return mimeType; } } return null; } /** Returns the dimensions of the image. */ static getSize(buffer, mimeType) { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getSize(buffer); } /** * Returns a conservative estimate of the number of channels in the image. For some image * formats, the method may return 4 indicating the possibility of an alpha channel, without * the ability to guarantee that an alpha channel is present. */ static getChannels(buffer, mimeType) { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getChannels(buffer); } /** Returns a conservative estimate of the GPU memory required by this image. */ static getVRAMByteLength(buffer, mimeType) { if (!this.impls[mimeType]) return null; if (this.impls[mimeType].getVRAMByteLength) { return this.impls[mimeType].getVRAMByteLength(buffer); } let uncompressedBytes = 0; const channels = 4; // See https://github.com/donmccurdy/glTF-Transform/issues/151. const resolution = this.getSize(buffer, mimeType); if (!resolution) return null; while (resolution[0] > 1 || resolution[1] > 1) { uncompressedBytes += resolution[0] * resolution[1] * channels; resolution[0] = Math.max(Math.floor(resolution[0] / 2), 1); resolution[1] = Math.max(Math.floor(resolution[1] / 2), 1); } uncompressedBytes += 1 * 1 * channels; return uncompressedBytes; } /** Returns the preferred file extension for the given MIME type. */ static mimeTypeToExtension(mimeType) { if (mimeType === 'image/jpeg') return 'jpg'; return mimeType.split('/').pop(); } /** Returns the MIME type for the given file extension. */ static extensionToMimeType(extension) { if (extension === 'jpg') return 'image/jpeg'; if (!extension) return ''; return `image/${extension}`; } } ImageUtils.impls = { 'image/jpeg': new JPEGImageUtils(), 'image/png': new PNGImageUtils() }; function validateJPEGBuffer(view, i) { // index should be within buffer limits if (i > view.byteLength) { throw new TypeError('Corrupt JPG, exceeded buffer limits'); } // Every JPEG block must begin with a 0xFF if (view.getUint8(i) !== 0xff) { throw new TypeError('Invalid JPG, marker table corrupted'); } return view; } /** * *Utility class for working with file systems and URI paths.* * * @category Utilities */ class FileUtils { /** * Extracts the basename from a file path, e.g. "folder/model.glb" -> "model". * See: {@link HTTPUtils.basename} */ static basename(uri) { const fileName = uri.split(/[\\/]/).pop(); return fileName.substring(0, fileName.lastIndexOf('.')); } /** * Extracts the extension from a file path, e.g. "folder/model.glb" -> "glb". * See: {@link HTTPUtils.extension} */ static extension(uri) { if (uri.startsWith('data:image/')) { const mimeType = uri.match(/data:(image\/\w+)/)[1]; return ImageUtils.mimeTypeToExtension(mimeType); } else if (uri.startsWith('data:model/gltf+json')) { return 'gltf'; } else if (uri.startsWith('data:model/gltf-binary')) { return 'glb'; } else if (uri.startsWith('data:application/')) { return 'bin'; } return uri.split(/[\\/]/).pop().split(/[.]/).pop(); } } /* eslint-disable @typescript-eslint/no-explicit-any */ // Reference: https://github.com/jonschlinkert/is-plain-object function isObject(o) { return Object.prototype.toString.call(o) === '[object Object]'; } function isPlainObject(o) { if (isObject(o) === false) return false; // If has modified constructor const ctor = o.constructor; if (ctor === undefined) return true; // If has modified prototype const prot = ctor.prototype; if (isObject(prot) === false) return false; // If constructor does not have an Object-specific method if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) { return false; } // Most likely a plain Object return true; } var _Logger; /** Logger verbosity thresholds. */ exports.Verbosity = void 0; (function (Verbosity) { /** No events are logged. */ Verbosity[Verbosity["SILENT"] = 4] = "SILENT"; /** Only error events are logged. */ Verbosity[Verbosity["ERROR"] = 3] = "ERROR"; /** Only error and warn events are logged. */ Verbosity[Verbosity["WARN"] = 2] = "WARN"; /** Only error, warn, and info events are logged. (DEFAULT) */ Verbosity[Verbosity["INFO"] = 1] = "INFO"; /** All events are logged. */ Verbosity[Verbosity["DEBUG"] = 0] = "DEBUG"; })(exports.Verbosity || (exports.Verbosity = {})); /** * *Logger utility class.* * * @category Utilities */ class Logger { /** Constructs a new Logger instance. */ constructor(verbosity) { this.verbosity = void 0; this.verbosity = verbosity; } /** Logs an event at level {@link Logger.Verbosity.DEBUG}. */ debug(text) { if (this.verbosity <= Logger.Verbosity.DEBUG) { console.debug(text); } } /** Logs an event at level {@link Logger.Verbosity.INFO}. */ info(text) { if (this.verbosity <= Logger.Verbosity.INFO) { console.info(text); } } /** Logs an event at level {@link Logger.Verbosity.WARN}. */ warn(text) { if (this.verbosity <= Logger.Verbosity.WARN) { console.warn(text); } } /** Logs an event at level {@link Logger.Verbosity.ERROR}. */ error(text) { if (this.verbosity <= Logger.Verbosity.ERROR) { console.error(text); } } } _Logger = Logger; /** Logger verbosity thresholds. */ Logger.Verbosity = exports.Verbosity; /** Default logger instance. */ Logger.DEFAULT_INSTANCE = new _Logger(_Logger.Verbosity.INFO); /** * 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(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; } /** * Returns the scaling factor component of a transformation * matrix. If a matrix is built with fromRotationTranslationScale * with a normalized Quaternion paramter, the returned vector will be * the same as the scaling vector * originally supplied. * @param {vec3} out Vector to receive scaling factor component * @param {ReadonlyMat4} mat Matrix to be decomposed (input) * @return {vec3} out */ function getScaling(out, mat) { var m11 = mat[0]; var m12 = mat[1]; var m13 = mat[2]; var m21 = mat[4]; var m22 = mat[5]; var m23 = mat[6]; var m31 = mat[8]; var m32 = mat[9]; var m33 = mat[10]; out[0] = Math.hypot(m11, m12, m13); out[1] = Math.hypot(m21, m22, m23); out[2] = Math.hypot(m31, m32, m33); return out; } /** * Returns a quaternion representing the rotational component * of a transformation matrix. If a matrix is built with * fromRotationTranslation, the returned quaternion will be the * same as the quaternion originally supplied. * @param {quat} out Quaternion to receive the rotation component * @param {ReadonlyMat4} mat Matrix to be decomposed (input) * @return {quat} out */ function getRotation(out, mat) { var scaling = new ARRAY_TYPE(3); getScaling(scaling, mat); var is1 = 1 / scaling[0]; var is2 = 1 / scaling[1]; var is3 = 1 / scaling[2]; var sm11 = mat[0] * is1; var sm12 = mat[1] * is2; var sm13 = mat[2] * is3; var sm21 = mat[4] * is1; var sm22 = mat[5] * is2; var sm23 = mat[6] * is3; var sm31 = mat[8] * is1; var sm32 = mat[9] * is2; var sm33 = mat[10] * is3; var trace = sm11 + sm22 + sm33; var S = 0; if (trace > 0) { S = Math.sqrt(trace + 1.0) * 2; out[3] = 0.25 * S; out[0] = (sm23 - sm32) / S; out[1] = (sm31 - sm13) / S; out[2] = (sm12 - sm21) / S; } else if (sm11 > sm22 && sm11 > sm33) { S = Math.sqrt(1.0 + sm11 - sm22 - sm33) * 2; out[3] = (sm23 - sm32) / S; out[0] = 0.25 * S; out[1] = (sm12 + sm21) / S; out[2] = (sm31 + sm13) / S; } else if (sm22 > sm33) { S = Math.sqrt(1.0 + sm22 - sm11 - sm33) * 2; out[3] = (sm31 - sm13) / S; out[0] = (sm12 + sm21) / S; out[1] = 0.25 * S; out[2] = (sm23 + sm32) / S; } else { S = Math.sqrt(1.0 + sm33 - sm11 - sm22) * 2; out[3] = (sm12 - sm21) / S; out[0] = (sm31 + sm13) / S; out[1] = (sm23 + sm32) / S; out[2] = 0.25 * S; } return out; } /** @hidden */ class MathUtils { static identity(v) { return v; } static eq(a, b, tolerance) { if (tolerance === void 0) { tolerance = 10e-6; } if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (Math.abs(a[i] - b[i]) > tolerance) return false; } return true; } static clamp(value, min, max) { if (value < min) return min; if (value > max) return max; return value; } // TODO(perf): Compare performance if we replace the switch with individual functions. static decodeNormalizedInt(i, componentType) { // Hardcode enums from accessor.ts to avoid a circular dependency. switch (componentType) { case 5126: // FLOAT return i; case 5123: // UNSIGNED_SHORT return i / 65535.0; case 5121: // UNSIGNED_BYTE return i / 255.0; case 5122: // SHORT return Math.max(i / 32767.0, -1.0); case 5120: // BYTE return Math.max(i / 127.0, -1.0); default: throw new Error('Invalid component type.'); } } // TODO(perf): Compare performance if we replace the switch with individual functions. static encodeNormalizedInt(f, componentType) { // Hardcode enums from accessor.ts to avoid a circular dependency. switch (componentType) { case 5126: // FLOAT return f; case 5123: // UNSIGNED_SHORT return Math.round(MathUtils.clamp(f, 0, 1) * 65535.0); case 5121: // UNSIGNED_BYTE return Math.round(MathUtils.clamp(f, 0, 1) * 255.0); case 5122: // SHORT return Math.round(MathUtils.clamp(f, -1, 1) * 32767.0); case 5120: // BYTE return Math.round(MathUtils.clamp(f, -1, 1) * 127.0); default: throw new Error('Invalid component type.'); } } /** * Decompose a mat4 to TRS properties. * * Equivalent to the Matrix4 decompose() method in three.js, and intentionally not using the * gl-matrix version. See: https://github.com/toji/gl-matrix/issues/408 * * @param srcMat Matrix element, to be decomposed to TRS properties. * @param dstTranslation Translation element, to be overwritten. * @param dstRotation Rotation element, to be overwritten. * @param dstScale Scale element, to be overwritten. */ static decompose(srcMat, dstTranslation, dstRotation, dstScale) { let sx = length([srcMat[0], srcMat[1], srcMat[2]]); const sy = length([srcMat[4], srcMat[5], srcMat[6]]); const sz = length([srcMat[8], srcMat[9], srcMat[10]]); // if determine is negative, we need to invert one scale const det = determinant(srcMat); if (det < 0) sx = -sx; dstTranslation[0] = srcMat[12]; dstTranslation[1] = srcMat[13]; dstTranslation[2] = srcMat[14]; // scale the rotation part const _m1 = srcMat.slice(); const invSX = 1 / sx; const invSY = 1 / sy; const invSZ = 1 / sz; _m1[0] *= invSX; _m1[1] *= invSX; _m1[2] *= invSX; _m1[4] *= invSY; _m1[5] *= invSY; _m1[6] *= invSY; _m1[8] *= invSZ; _m1[9] *= invSZ; _m1[10] *= invSZ; getRotation(dstRotation, _m1); dstScale[0] = sx; dstScale[1] = sy; dstScale[2] = sz; } /** * Compose TRS properties to a mat4. * * Equivalent to the Matrix4 compose() method in three.js, and intentionally not using the * gl-matrix version. See: https://github.com/toji/gl-matrix/issues/408 * * @param srcTranslation Translation element of matrix. * @param srcRotation Rotation element of matrix. * @param srcScale Scale element of matrix. * @param dstMat Matrix element, to be modified and returned. * @returns dstMat, overwritten to mat4 equivalent of given TRS properties. */ static compose(srcTranslation, srcRotation, srcScale, dstMat) { const te = dstMat; const x = srcRotation[0], y = srcRotation[1], z = srcRotation[2], w = srcRotation[3]; const x2 = x + x, y2 = y + y, z2 = z + z; const xx = x * x2, xy = x * y2, xz = x * z2; const yy = y * y2, yz = y * z2, zz = z * z2; const wx = w * x2, wy = w * y2, wz = w * z2; const sx = srcScale[0], sy = srcScale[1], sz = srcScale[2]; te[0] = (1 - (yy + zz)) * sx; te[1] = (xy + wz) * sx; te[2] = (xz - wy) * sx; te[3] = 0; te[4] = (xy - wz) * sy; te[5] = (1 - (xx + zz)) * sy; te[6] = (yz + wx) * sy; te[7] = 0; te[8] = (xz + wy) * sz; te[9] = (yz - wx) * sz; te[10] = (1 - (xx + yy)) * sz; te[11] = 0; te[12] = srcTranslation[0]; te[13] = srcTranslation[1]; te[14] = srcTranslation[2]; te[15] = 1; return te; } } function equalsRef(refA, refB) { if (!!refA !== !!refB) return false; const a = refA.getChild(); const b = refB.getChild(); return a === b || a.equals(b); } function equalsRefSet(refSetA, refSetB) { if (!!refSetA !== !!refSetB) return false; const refValuesA = refSetA.values(); const refValuesB = refSetB.values(); if (refValuesA.length !== refValuesB.length) return false; for (let i = 0; i < refValuesA.length; i++) { const a = refValuesA[i]; const b = refValuesB[i]; if (a.getChild() === b.getChild()) continue; if (!a.getChild().equals(b.getChild())) return false; } return true; } function equalsRefMap(refMapA, refMapB) { if (!!refMapA !== !!refMapB) return false; const keysA = refMapA.keys(); const keysB = refMapB.keys(); if (keysA.length !== keysB.length) return false; for (const key of keysA) { const refA = refMapA.get(key); const refB = refMapB.get(key); if (!!refA !== !!refB) return false; const a = refA.getChild(); const b = refB.getChild(); if (a === b) continue; if (!a.equals(b)) return false; } return true; } function equalsArray(a, b) { if (a === b) return true; if (!!a !== !!b || !a || !b) 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; } function equalsObject(_a, _b) { if (_a === _b) return true; if (!!_a !== !!_b) return false; if (!isPlainObject(_a) || !isPlainObject(_b)) { return _a === _b; } const a = _a; const b = _b; let numKeysA = 0; let numKeysB = 0; let key; for (key in a) numKeysA++; for (key in b) numKeysB++; if (numKeysA !== numKeysB) return false; for (key in a) { const valueA = a[key]; const valueB = b[key]; if (isArray(valueA) && isArray(valueB)) { if (!equalsArray(valueA, valueB)) return false; } else if (isPlainObject(valueA) && isPlainObject(valueB)) { if (!equalsObject(valueA, valueB)) return false; } else { if (valueA !== valueB) return false; } } return true; } function isArray(value) { return Array.isArray(value) || ArrayBuffer.isView(value); } const ALPHABET = '23456789abdegjkmnpqrvwxyzABDEGJKMNPQRVWXYZ'; const UNIQUE_RETRIES = 999; const ID_LENGTH = 6; const previousIDs = new Set(); const generateOne = function () { let rtn = ''; for (let i = 0; i < ID_LENGTH; i++) { rtn += ALPHABET.charAt(Math.floor(Math.random() * ALPHABET.length)); } return rtn; }; /** * Short ID generator. * * Generated IDs are short, easy to type, and unique for the duration of the program's execution. * Uniqueness across multiple program executions, or on other devices, is not guaranteed. Based on * [Short ID Generation in JavaScript](https://tomspencer.dev/blog/2014/11/16/short-id-generation-in-javascript/), * with alterations. * * @category Utilities * @hidden */ const uuid = function () { for (let retries = 0; retries < UNIQUE_RETRIES; retries++) { const id = generateOne(); if (!previousIDs.has(id)) { previousIDs.add(id); return id; } } return ''; }; // Need a placeholder domain to construct a URL from a relative path. We only // access `url.pathname`, so the domain doesn't matter. const NULL_DOMAIN = 'https://null.example'; /** * *Utility class for working with URLs.* * * @category Utilities */ class HTTPUtils { static dirname(path) { const index = path.lastIndexOf('/'); if (index === -1) return './'; return path.substring(0, index + 1); } /** * Extracts the basename from a URL, e.g. "folder/model.glb" -> "model". * See: {@link FileUtils.basename} */ static basename(uri) { return FileUtils.basename(new URL(uri, NULL_DOMAIN).pathname); } /** * Extracts the extension from a URL, e.g. "folder/model.glb" -> "glb". * See: {@link FileUtils.extension} */ static extension(uri) { return FileUtils.extension(new URL(uri, NULL_DOMAIN).pathname); } static resolve(base, path) { if (!this.isRelativePath(path)) return path; const stack = base.split('/'); const parts = path.split('/'); stack.pop(); for (let i = 0; i < parts.length; i++) { if (parts[i] === '.') continue; if (parts[i] === '..') { stack.pop(); } else { stack.push(parts[i]); } } return stack.join('/'); } /** * Returns true for URLs containing a protocol, and false for both * absolute and relative paths. */ static isAbsoluteURL(path) { return this.PROTOCOL_REGEXP.test(path); } /** * Returns true for paths that are declared relative to some unknown base * path. For example, "foo/bar/" is relative both "/foo/bar/" is not. */ static isRelativePath(path) { return !/^(?:[a-zA-Z]+:)?\//.test(path); } } HTTPUtils.DEFAULT_INIT = {}; HTTPUtils.PROTOCOL_REGEXP = /^[a-zA-Z]+:\/\//; const COPY_IDENTITY = t => t; const EMPTY_SET = new Set(); /** * *Properties represent distinct resources in a glTF asset, referenced by other properties.* * * For example, each material and texture is a property, with material properties holding * references to the textures. All properties are created with factory methods on the * {@link Document} in which they should be constructed. Properties are destroyed by calling * {@link Property.dispose}(). * * Usage: * * ```ts * const texture = doc.createTexture('myTexture'); * doc.listTextures(); // → [texture x 1] * * // Attach a texture to a material. * material.setBaseColorTexture(texture); * material.getBaseColortexture(); // → texture * * // Detaching a texture removes any references to it, except from the doc. * texture.detach(); * material.getBaseColorTexture(); // → null * doc.listTextures(); // → [texture x 1] * * // Disposing a texture removes all references to it, and its own references. * texture.dispose(); * doc.listTextures(); // → [] * ``` * * Reference: * - [glTF → Concepts](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#concepts) * * @category Properties */ class Property extends propertyGraph.GraphNode { /** @hidden */ constructor(graph, name) { if (name === void 0) { name = ''; } super(graph); this[propertyGraph.$attributes]['name'] = name; this.init(); this.dispatchEvent({ type: 'create' }); } /** * Returns the Graph associated with this Property. For internal use. * @hidden * @experimental */ getGraph() { return this.graph; } /** * Returns default attributes for the property. Empty lists and maps should be initialized * to empty arrays and objects. Always invoke `super.getDefaults()` and extend the result. */ getDefaults() { return Object.assign(super.getDefaults(), { name: '', extras: {} }); } /** @hidden */ set(attribute, value) { if (Array.isArray(value)) value = value.slice(); // copy vector, quat, color … return super.set(attribute, value); } /********************************************************************************************** * Name. */ /** * Returns the name of this property. While names are not required to be unique, this is * encouraged, and non-unique names will be overwritten in some tools. For custom data about * a property, prefer to use Extras. */ getName() { return this.get('name'); } /** * Sets the name of this property. While names are not required to be unique, this is * encouraged, and non-unique names will be overwritten in some tools. For custom data about * a property, prefer to use Extras. */ setName(name) { return this.set('name', name); } /********************************************************************************************** * Extras. */ /** * Returns a reference to the Extras object, containing application-specific data for this * Property. Extras should be an Object, not a primitive value, for best portability. */ getExtras() { return this.get('extras'); } /** * Updates the Extras object, containing application-specific data for this Property. Extras * should be an Object, not a primitive value, for best portability. */ setExtras(extras) { return this.set('extras', extras); } /********************************************************************************************** * Graph state. */ /** * Makes a copy of this property, with the same resources (by reference) as the original. */ clone() { const PropertyClass = this.constructor; return new PropertyClass(this.graph).copy(this, COPY_IDENTITY); } /** * Copies all data from another property to this one. Child properties are copied by reference, * unless a 'resolve' function is given to override that. * @param other Property to copy references from. * @param resolve Function to resolve each Property being transferred. Default is identity. */ copy(other, resolve) { if (resolve === void 0) { resolve = COPY_IDENTITY; } // Remove previous references. for (const key in this[propertyGraph.$attributes]) { const value = this[propertyGraph.$attributes][key]; if (value instanceof propertyGraph.GraphEdge) { if (!this[propertyGraph.$immutableKeys].has(key)) { value.dispose(); } } else if (value instanceof propertyGraph.RefList || value instanceof propertyGraph.RefSet) { for (const ref of value.values()) { ref.dispose(); } } else if (value instanceof propertyGraph.RefMap) { for (const ref of value.values()) { ref.dispose(); } } } // Add new references. for (const key in other[propertyGraph.$attributes]) { const thisValue = this[propertyGraph.$attributes][key]; const otherValue = other[propertyGraph.$attributes][key]; if (otherValue instanceof propertyGraph.GraphEdge) { if (this[propertyGraph.$immutableKeys].has(key)) { const ref = thisValue; ref.getChild().copy(resolve(otherValue.getChild()), resolve); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.setRef(key, resolve(otherValue.getChild()), otherValue.getAttributes()); } } else if (otherValue instanceof propertyGraph.RefSet || otherValue instanceof propertyGraph.RefList) { for (const ref of otherValue.values()) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this.addRef(key, resolve(ref.getChild()), ref.getAttributes()); } } else if (otherValue instanceof propertyGraph.RefMap) { for (const subkey of otherValue.keys()) { const ref = otherValue.get(subkey); // eslint-disable-next-line @typescript-eslint/no-explicit-any this.setRefMap(key, subkey, resolve(ref.getChild()), ref.getAttributes()); } } else if (isPlainObject(otherValue)) { this[propertyGraph.$attributes][key] = JSON.parse(JSON.stringify(otherValue)); } else if (Array.isArray(otherValue) || otherValue instanceof ArrayBuffer || ArrayBuffer.isView(otherValue)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any this[propertyGraph.$attributes][key] = otherValue.slice(); } else { this[propertyGraph.$attributes][key] = otherValue; } } return this; } /** * Returns true if two properties are deeply equivalent, recursively comparing the attributes * of the properties. Optionally, a 'skip' set may be included, specifying attributes whose * values should not be considered in the comparison. * * Example: Two {@link Primitive Primitives} are equivalent if they have accessors and * materials with equivalent content — but not necessarily the same specific accessors * and materials. */ equals(other, skip) { if (skip === void 0) { skip = EMPTY_SET; } if (this === other) return true; if (this.propertyType !== other.propertyType) return false; for (const key in this[propertyGraph.$attributes]) { if (skip.has(key)) continue; const a = this[propertyGraph.$attributes][key]; const b = other[propertyGraph.$attributes][key]; if (a instanceof propertyGraph.GraphEdge || b instanceof propertyGraph.GraphEdge) { if (!equalsRef(a, b)) { return false; } } else if (a instanceof propertyGraph.RefSet || b instanceof propertyGraph.RefSet || a instanceof propertyGraph.RefList || b instanceof propertyGraph.RefList) { if (!equalsRefSet(a, b)) { return false; } } else if (a instanceof propertyGraph.RefMap || b instanceof propertyGraph.RefMap) { if (!equalsRefMap(a, b)) { return false; } } else if (isPlainObject(a) || isPlainObject(b)) { if (!equalsObject(a, b)) return false; } else if (isArray(a) || isArray(b)) { if (!equalsArray(a, b)) return false; } else { // Literal. if (a !== b) return false; } } return true; } detach() { // Detaching should keep properties in the same Document, and attached to its root. this.graph.disconnectParents(this, n => n.propertyType !== 'Root'); return this; } /** * Returns a list of all properties that hold a reference to this property. For example, a * material may hold references to various textures, but a texture does not hold references * to the materials that use it. * * It is often necessary to filter the results for a particular type: some resources, like * {@link Accessor}s, may be referenced by different types of properties. Most properties * include the {@link Root} as a parent, which is usually not of interest. * * Usage: * * ```ts * const materials = texture * .listParents() * .filter((p) => p instanceof Material) * ``` */ listParents() { return this.graph.listParents(this); } } /** * *A {@link Property} that can have {@link ExtensionProperty} instances attached.* * * Most properties are extensible. See the {@link Extension} documentation for information about * how to use extensions. * * @category Properties */ class ExtensibleProperty extends Property { getDefaults() { return Object.assign(super.getDefaults(), { extensions: new propertyGraph.RefMap() }); } /** Returns an {@link ExtensionProperty} attached to this Property, if any. */ getExtension(name) { return this.getRefMap('extensions', name); } /** * Attaches the given {@link ExtensionProperty} to this Property. For a given extension, only * one ExtensionProperty may be attached to any one Property at a time. */ setExtension(name, extensionProperty) { if (extensionProperty) extensionProperty._validateParent(this); return this.setRefMap('extensions', name, extensionProperty); } /** Lists all {@link ExtensionProperty} instances attached to this Property. */ listExtensions() { return this.listRefMapValues('extensions'); } } /** * *Accessors store lists of numeric, vector, or matrix elements in a typed array.* * * All large data for {@link Mesh}, {@link Skin}, and {@link Animation} properties is stored in * {@link Accessor}s, organized into one or more {@link Buffer}s. Each accessor provides data in * typed arrays, with two abstractions: * * *Elements* are the logical divisions of the data into useful types: `"SCALAR"`, `"VEC2"`, * `"VEC3"`, `"VEC4"`, `"MAT3"`, or `"MAT4"`. The element type can be determined with the * {@link Accessor.getType getType}() method, and the number of elements in the accessor determine its * {@link Accessor.getCount getCount}(). The number of components in an element — e.g. 9 for `"MAT3"` — are its * {@link Accessor.getElementSize getElementSize}(). See {@link Accessor.Type}. * * *Components* are the numeric values within an element — e.g. `.x` and `.y` for `"VEC2"`. Various * component types are available: `BYTE`, `UNSIGNED_BYTE`, `SHORT`, `UNSIGNED_SHORT`, * `UNSIGNED_INT`, and `FLOAT`. The component type can be determined with the * {@link Accessor.getComponentType getComponentType} method, and the number of bytes in each component determine its * {@link Accessor.getComponentSize getComponentSize}. See {@link Accessor.ComponentType}. * * Usage: * * ```typescript * const accessor = doc.createAccessor('myData') * .setArray(new Float32Array([1,2,3,4,5,6,7,8,9,10,11,12])) * .setType(Accessor.Type.VEC3) * .setBuffer(doc.getRoot().listBuffers()[0]); * * accessor.getCount(); // → 4 * accessor.getElementSize(); // → 3 * accessor.getByteLength(); // → 48 * accessor.getElement(1, []); // → [4, 5, 6] * * accessor.setElement(0, [10, 20, 30]); * ``` * * Data access through the {@link Accessor.getElement getElement} and {@link Accessor.setElement setElement} * methods reads or overwrites the content of the underlying typed array. These methods use * element arrays intended to be compatible with the [gl-matrix](https://github.com/toji/gl-matrix) * library, or with the `toArray`/`fromArray` methods of libraries like three.js and babylon.js. * * Each Accessor must be assigned to a {@link Buffer}, which determines where the accessor's data * is stored in the final file. Assigning Accessors to different Buffers allows the data to be * written to different `.bin` files. * * glTF Transform does not expose many details of sparse, normalized, or interleaved accessors * through its API. It reads files using those techniques, presents a simplified view of the data * for editing, and attempts to write data back out with optimizations. For example, vertex * attributes will typically be interleaved by default, regardless of the input file. * * References: * - [glTF → Accessors](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#accessors) * * @category Properties */ class Accessor extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.ACCESSOR; } getDefaults() { return Object.assign(super.getDefaults(), { array: null, type: Accessor.Type.SCALAR, componentType: Accessor.ComponentType.FLOAT, normalized: false, sparse: false, buffer: null }); } /********************************************************************************************** * Static. */ /** Returns size of a given element type, in components. */ static getElementSize(type) { switch (type) { case Accessor.Type.SCALAR: return 1; case Accessor.Type.VEC2: return 2; case Accessor.Type.VEC3: return 3; case Accessor.Type.VEC4: return 4; case Accessor.Type.MAT2: return 4; case Accessor.Type.MAT3: return 9; case Accessor.Type.MAT4: return 16; default: throw new Error('Unexpected type: ' + type); } } /** Returns size of a given component type, in bytes. */ static getComponentSize(componentType) { switch (componentType) { case Accessor.ComponentType.BYTE: return 1; case Accessor.ComponentType.UNSIGNED_BYTE: return 1; case Accessor.ComponentType.SHORT: return 2; case Accessor.ComponentType.UNSIGNED_SHORT: return 2; case Accessor.ComponentType.UNSIGNED_INT: return 4; case Accessor.ComponentType.FLOAT: return 4; default: throw new Error('Unexpected component type: ' + componentType); } } /********************************************************************************************** * Min/max bounds. */ /** * Minimum value of each component in this attribute. Unlike in a final glTF file, values * returned by this method will reflect the minimum accounting for {@link .normalized} * state. */ getMinNormalized(target) { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); this.getMin(target); if (normalized) { for (let j = 0; j < elementSize; j++) { target[j] = MathUtils.decodeNormalizedInt(target[j], componentType); } } return target; } /** * Minimum value of each component in this attribute. Values returned by this method do not * reflect normalization: use {@link .getMinNormalized} in that case. */ getMin(target) { const array = this.getArray(); const count = this.getCount(); const elementSize = this.getElementSize(); for (let j = 0; j < elementSize; j++) target[j] = Infinity; for (let i = 0; i < count * elementSize; i += elementSize) { for (let j = 0; j < elementSize; j++) { const value = array[i + j]; if (Number.isFinite(value)) { target[j] = Math.min(target[j], value); } } } return target; } /** * Maximum value of each component in this attribute. Unlike in a final glTF file, values * returned by this method will reflect the minimum accounting for {@link .normalized} * state. */ getMaxNormalized(target) { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); this.getMax(target); if (normalized) { for (let j = 0; j < elementSize; j++) { target[j] = MathUtils.decodeNormalizedInt(target[j], componentType); } } return target; } /** * Maximum value of each component in this attribute. Values returned by this method do not * reflect normalization: use {@link .getMinNormalized} in that case. */ getMax(target) { const array = this.get('array'); const count = this.getCount(); const elementSize = this.getElementSize(); for (let j = 0; j < elementSize; j++) target[j] = -Infinity; for (let i = 0; i < count * elementSize; i += elementSize) { for (let j = 0; j < elementSize; j++) { const value = array[i + j]; if (Number.isFinite(value)) { target[j] = Math.max(target[j], value); } } } return target; } /********************************************************************************************** * Layout. */ /** * Number of elements in the accessor. An array of length 30, containing 10 `VEC3` elements, * will have a count of 10. */ getCount() { const array = this.get('array'); return array ? array.length / this.getElementSize() : 0; } /** Type of element stored in the accessor. `VEC2`, `VEC3`, etc. */ getType() { return this.get('type'); } /** * Sets type of element stored in the accessor. `VEC2`, `VEC3`, etc. Array length must be a * multiple of the component size (`VEC2` = 2, `VEC3` = 3, ...) for the selected type. */ setType(type) { return this.set('type', type); } /** * Number of components in each element of the accessor. For example, the element size of a * `VEC2` accessor is 2. This value is determined automatically based on array length and * accessor type, specified with {@link Accessor.setType setType()}. */ getElementSize() { return Accessor.getElementSize(this.get('type')); } /** * Size of each component (a value in the raw array), in bytes. For example, the * `componentSize` of data backed by a `float32` array is 4 bytes. */ getComponentSize() { return this.get('array').BYTES_PER_ELEMENT; } /** * Component type (float32, uint16, etc.). This value is determined automatically, and can only * be modified by replacing the underlying array. */ getComponentType() { return this.get('componentType'); } /********************************************************************************************** * Normalization. */ /** * Specifies whether integer data values should be normalized (true) to [0, 1] (for unsigned * types) or [-1, 1] (for signed types), or converted directly (false) when they are accessed. * This property is defined only for accessors that contain vertex attributes or animation * output data. */ getNormalized() { return this.get('normalized'); } /** * Specifies whether integer data values should be normalized (true) to [0, 1] (for unsigned * types) or [-1, 1] (for signed types), or converted directly (false) when they are accessed. * This property is defined only for accessors that contain vertex attributes or animation * output data. */ setNormalized(normalized) { return this.set('normalized', normalized); } /********************************************************************************************** * Data access. */ /** * Returns the scalar element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, values are * decoded and returned in floating-point form. */ getScalar(index) { const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray(); if (this.getNormalized()) { return MathUtils.decodeNormalizedInt(array[index * elementSize], componentType); } return array[index * elementSize]; } /** * Assigns the scalar element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, "value" should be * given in floating-point form — it will be integer-encoded before writing * to the underlying array. */ setScalar(index, x) { const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray(); if (this.getNormalized()) { array[index * elementSize] = MathUtils.encodeNormalizedInt(x, componentType); } else { array[index * elementSize] = x; } return this; } /** * Returns the vector or matrix element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, values are * decoded and returned in floating-point form. * * Example: * * ```javascript * import { add } from 'gl-matrix/add'; * * const element = []; * const offset = [1, 1, 1]; * * for (let i = 0; i < accessor.getCount(); i++) { * accessor.getElement(i, element); * add(element, element, offset); * accessor.setElement(i, element); * } * ``` */ getElement(index, target) { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray(); for (let i = 0; i < elementSize; i++) { if (normalized) { target[i] = MathUtils.decodeNormalizedInt(array[index * elementSize + i], componentType); } else { target[i] = array[index * elementSize + i]; } } return target; } /** * Assigns the vector or matrix element value at the given index. For * {@link Accessor.getNormalized normalized} integer accessors, "value" should be * given in floating-point form — it will be integer-encoded before writing * to the underlying array. * * Example: * * ```javascript * import { add } from 'gl-matrix/add'; * * const element = []; * const offset = [1, 1, 1]; * * for (let i = 0; i < accessor.getCount(); i++) { * accessor.getElement(i, element); * add(element, element, offset); * accessor.setElement(i, element); * } * ``` */ setElement(index, value) { const normalized = this.getNormalized(); const elementSize = this.getElementSize(); const componentType = this.getComponentType(); const array = this.getArray(); for (let i = 0; i < elementSize; i++) { if (normalized) { array[index * elementSize + i] = MathUtils.encodeNormalizedInt(value[i], componentType); } else { array[index * elementSize + i] = value[i]; } } return this; } /********************************************************************************************** * Raw data storage. */ /** * Specifies whether the accessor should be stored sparsely. When written to a glTF file, sparse * accessors store only values that differ from base values. When loaded in glTF Transform (or most * runtimes) a sparse accessor can be treated like any other accessor. Currently, glTF Transform always * uses zeroes for the base values when writing files. * @experimental */ getSparse() { return this.get('sparse'); } /** * Specifies whether the accessor should be stored sparsely. When written to a glTF file, sparse * accessors store only values that differ from base values. When loaded in glTF Transform (or most * runtimes) a sparse accessor can be treated like any other accessor. Currently, glTF Transform always * uses zeroes for the base values when writing files. * @experimental */ setSparse(sparse) { return this.set('sparse', sparse); } /** Returns the {@link Buffer} into which this accessor will be organized. */ getBuffer() { return this.getRef('buffer'); } /** Assigns the {@link Buffer} into which this accessor will be organized. */ setBuffer(buffer) { return this.setRef('buffer', buffer); } /** Returns the raw typed array underlying this accessor. */ getArray() { return this.get('array'); } /** Assigns the raw typed array underlying this accessor. */ setArray(array) { this.set('componentType', array ? arrayToComponentType(array) : Accessor.ComponentType.FLOAT); this.set('array', array); return this; } /** Returns the total bytelength of this accessor, exclusive of padding. */ getByteLength() { const array = this.get('array'); return array ? array.byteLength : 0; } } /************************************************************************************************** * Accessor utilities. */ /** @internal */ /********************************************************************************************** * Constants. */ /** Element type contained by the accessor (SCALAR, VEC2, ...). */ Accessor.Type = { /** Scalar, having 1 value per element. */ SCALAR: 'SCALAR', /** 2-component vector, having 2 components per element. */ VEC2: 'VEC2', /** 3-component vector, having 3 components per element. */ VEC3: 'VEC3', /** 4-component vector, having 4 components per element. */ VEC4: 'VEC4', /** 2x2 matrix, having 4 components per element. */ MAT2: 'MAT2', /** 3x3 matrix, having 9 components per element. */ MAT3: 'MAT3', /** 4x3 matrix, having 16 components per element. */ MAT4: 'MAT4' }; /** Data type of the values composing each element in the accessor. */ Accessor.ComponentType = { /** * 1-byte signed integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int8Array Int8Array}. */ BYTE: 5120, /** * 1-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array Uint8Array}. */ UNSIGNED_BYTE: 5121, /** * 2-byte signed integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Int16Array Int16Array}. */ SHORT: 5122, /** * 2-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint16Array Uint16Array}. */ UNSIGNED_SHORT: 5123, /** * 4-byte unsigned integer, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint32Array Uint32Array}. */ UNSIGNED_INT: 5125, /** * 4-byte floating point number, stored as * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float32Array Float32Array}. */ FLOAT: 5126 }; function arrayToComponentType(array) { switch (array.constructor) { case Float32Array: return Accessor.ComponentType.FLOAT; case Uint32Array: return Accessor.ComponentType.UNSIGNED_INT; case Uint16Array: return Accessor.ComponentType.UNSIGNED_SHORT; case Uint8Array: return Accessor.ComponentType.UNSIGNED_BYTE; case Int16Array: return Accessor.ComponentType.SHORT; case Int8Array: return Accessor.ComponentType.BYTE; default: throw new Error('Unknown accessor componentType.'); } } /** * *Reusable collections of {@link AnimationChannel}s, together representing a discrete animation * clip.* * * One Animation represents one playable unit in an animation system. Each may contain channels * affecting multiple paths (`translation`, `rotation`, `scale`, or `weights`) on multiple * {@link Node}s. An Animation's channels must be played together, and do not have any meaning in * isolation. * * Multiple Animations _may_ be played together: for example, one character's _Walk_ animation * might play while another character's _Run_ animation plays. Or a single character might have * both an _Idle_ and a _Talk_ animation playing at the same time. However, glTF does not define * any particular relationship between top-level Animations, or any particular playback behavior * like looping or sequences of Animations. General-purpose viewers typically autoplay the first * animation and provide UI controls for choosing another. Game engines may have significantly * more advanced methods of playing and blending animations. * * For example, a very simple skinned {@link Mesh} might have two Animations, _Idle_ and _Walk_. * Each of those Animations might affect the rotations of two bones, _LegL_ and _LegR_, where the * keyframes for each target-path pair are stored in {@link AnimationChannel} instances. In total, * this model would contain two Animations and Four {@link AnimationChannel}s. * * Usage: * * ```ts * const animation = doc.createAnimation('machineRun') * .addChannel(rotateCog1) * .addChannel(rotateCog2) * .addChannel(rotateCog3); * ``` * * Reference * - [glTF → Animations](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#animations) * * @category Properties */ class Animation extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.ANIMATION; } getDefaults() { return Object.assign(super.getDefaults(), { channels: new propertyGraph.RefSet(), samplers: new propertyGraph.RefSet() }); } /** Adds an {@link AnimationChannel} to this Animation. */ addChannel(channel) { return this.addRef('channels', channel); } /** Removes an {@link AnimationChannel} from this Animation. */ removeChannel(channel) { return this.removeRef('channels', channel); } /** Lists {@link AnimationChannel}s in this Animation. */ listChannels() { return this.listRefs('channels'); } /** Adds an {@link AnimationSampler} to this Animation. */ addSampler(sampler) { return this.addRef('samplers', sampler); } /** Removes an {@link AnimationSampler} from this Animation. */ removeSampler(sampler) { return this.removeRef('samplers', sampler); } /** Lists {@link AnimationSampler}s in this Animation. */ listSamplers() { return this.listRefs('samplers'); } } /** * *A target-path pair within a larger {@link Animation}, which refers to an * {@link AnimationSampler} storing the keyframe data for that pair.* * * A _target_ is always a {@link Node}, in the core glTF spec. A _path_ is any property of that * Node that can be affected by animation: `translation`, `rotation`, `scale`, or `weights`. An * {@link Animation} affecting the positions and rotations of several {@link Node}s would contain * one channel for each Node-position or Node-rotation pair. The keyframe data for an * AnimationChannel is stored in an {@link AnimationSampler}, which must be attached to the same * {@link Animation}. * * Usage: * * ```ts * const node = doc.getRoot() * .listNodes() * .find((node) => node.getName() === 'Cog'); * * const channel = doc.createAnimationChannel('cogRotation') * .setTargetPath('rotation') * .setTargetNode(node) * .setSampler(rotateSampler); * ``` * * Reference * - [glTF → Animations](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#animations) * * @category Properties */ class AnimationChannel extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.ANIMATION_CHANNEL; } getDefaults() { return Object.assign(super.getDefaults(), { targetPath: null, targetNode: null, sampler: null }); } /********************************************************************************************** * Properties. */ /** * Path (property) animated on the target {@link Node}. Supported values include: * `translation`, `rotation`, `scale`, or `weights`. */ getTargetPath() { return this.get('targetPath'); } /** * Path (property) animated on the target {@link Node}. Supported values include: * `translation`, `rotation`, `scale`, or `weights`. */ setTargetPath(targetPath) { return this.set('targetPath', targetPath); } /** Target {@link Node} animated by the channel. */ getTargetNode() { return this.getRef('targetNode'); } /** Target {@link Node} animated by the channel. */ setTargetNode(targetNode) { return this.setRef('targetNode', targetNode); } /** * Keyframe data input/output values for the channel. Must be attached to the same * {@link Animation}. */ getSampler() { return this.getRef('sampler'); } /** * Keyframe data input/output values for the channel. Must be attached to the same * {@link Animation}. */ setSampler(sampler) { return this.setRef('sampler', sampler); } } /********************************************************************************************** * Constants. */ /** Name of the property to be modified by an animation channel. */ AnimationChannel.TargetPath = { /** Channel targets {@link Node.setTranslation}. */ TRANSLATION: 'translation', /** Channel targets {@link Node.setRotation}. */ ROTATION: 'rotation', /** Channel targets {@link Node.setScale}. */ SCALE: 'scale', /** Channel targets {@link Node.setWeights}, affecting {@link PrimitiveTarget} weights. */ WEIGHTS: 'weights' }; /** * *Reusable collection of keyframes affecting particular property of an object.* * * Each AnimationSampler refers to an input and an output {@link Accessor}. Input contains times * (in seconds) for each keyframe. Output contains values (of any {@link Accessor.Type}) for the * animated property at each keyframe. Samplers using `CUBICSPLINE` interpolation will also contain * in/out tangents in the output, with the layout: * * in1, value1, out1, * in2, value2, out2, * in3, value3, out3, ... * * Usage: * * ```ts * // Create accessor containing input times, in seconds. * const input = doc.createAccessor('bounceTimes') * .setArray(new Float32Array([0, 1, 2])) * .setType(Accessor.Type.SCALAR); * * // Create accessor containing output values, in local units. * const output = doc.createAccessor('bounceValues') * .setArray(new Float32Array([ * 0, 0, 0, // y = 0 * 0, 1, 0, // y = 1 * 0, 0, 0, // y = 0 * ])) * .setType(Accessor.Type.VEC3); * * // Create sampler. * const sampler = doc.createAnimationSampler('bounce') * .setInput(input) * .setOutput(output) * .setInterpolation('LINEAR'); * ``` * * Reference * - [glTF → Animations](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#animations) * * @category Properties */ class AnimationSampler extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.ANIMATION_SAMPLER; } getDefaultAttributes() { return Object.assign(super.getDefaults(), { interpolation: AnimationSampler.Interpolation.LINEAR, input: null, output: null }); } /********************************************************************************************** * Static. */ /** Interpolation mode: `STEP`, `LINEAR`, or `CUBICSPLINE`. */ getInterpolation() { return this.get('interpolation'); } /** Interpolation mode: `STEP`, `LINEAR`, or `CUBICSPLINE`. */ setInterpolation(interpolation) { return this.set('interpolation', interpolation); } /** Times for each keyframe, in seconds. */ getInput() { return this.getRef('input'); } /** Times for each keyframe, in seconds. */ setInput(input) { return this.setRef('input', input, { usage: BufferViewUsage$1.OTHER }); } /** * Values for each keyframe. For `CUBICSPLINE` interpolation, output also contains in/out * tangents. */ getOutput() { return this.getRef('output'); } /** * Values for each keyframe. For `CUBICSPLINE` interpolation, output also contains in/out * tangents. */ setOutput(output) { return this.setRef('output', output, { usage: BufferViewUsage$1.OTHER }); } } /********************************************************************************************** * Constants. */ /** Interpolation method. */ AnimationSampler.Interpolation = { /** Animated values are linearly interpolated between keyframes. */ LINEAR: 'LINEAR', /** Animated values remain constant from one keyframe until the next keyframe. */ STEP: 'STEP', /** Animated values are interpolated according to given cubic spline tangents. */ CUBICSPLINE: 'CUBICSPLINE' }; /** * *Buffers are low-level storage units for binary data.* * * glTF 2.0 has three concepts relevant to binary storage: accessors, buffer views, and buffers. * In glTF Transform, an {@link Accessor} is referenced by any property that requires numeric typed * array data. Meshes, Primitives, and Animations all reference Accessors. Buffers define how that * data is organized into transmitted file(s). A `.glb` file has only a single Buffer, and when * exporting to `.glb` your resources should be grouped accordingly. A `.gltf` file may reference * one or more `.bin` files — each `.bin` is a Buffer — and grouping Accessors under different * Buffers allow you to specify that structure. * * For engines that can dynamically load portions of a glTF file, splitting data into separate * buffers can allow you to avoid loading data until it is needed. For example, you might put * binary data for specific meshes into a different `.bin` buffer, or put each animation's binary * payload into its own `.bin`. * * Buffer Views define how Accessors are organized within a given Buffer. glTF Transform creates an * efficient Buffer View layout automatically at export: there is no Buffer View property exposed * by the glTF Transform API, simplifying data management. * * Usage: * * ```ts * // Create two buffers with custom filenames. * const buffer1 = doc.createBuffer('buffer1') * .setURI('part1.bin'); * const buffer2 = doc.createBuffer('buffer2') * .setURI('part2.bin'); * * // Assign the attributes of two meshes to different buffers. If the meshes * // had indices or morph target attributes, you would also want to relocate * // those accessors. * mesh1 * .listPrimitives() * .forEach((primitive) => primitive.listAttributes() * .forEach((attribute) => attribute.setBuffer(buffer1))); * mesh2 * .listPrimitives() * .forEach((primitive) => primitive.listAttributes() * .forEach((attribute) => attribute.setBuffer(buffer2))); * * // Write to disk. Each mesh's binary data will be in a separate binary file; * // any remaining accessors will be in a third (default) buffer. * await new NodeIO().write('scene.gltf', doc); * // → scene.gltf, part1.bin, part2.bin * ``` * * References: * - [glTF → Buffers and Buffer Views](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#buffers-and-buffer-views) * - [glTF → Accessors](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#accessors) * * @category Properties */ class Buffer$1 extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.BUFFER; } getDefaults() { return Object.assign(super.getDefaults(), { uri: '' }); } /** * Returns the URI (or filename) of this buffer (e.g. 'myBuffer.bin'). URIs are strongly * encouraged to be relative paths, rather than absolute. Use of a protocol (like `file://`) * is possible for custom applications, but will limit the compatibility of the asset with most * tools. * * Buffers commonly use the extension `.bin`, though this is not required. */ getURI() { return this.get('uri'); } /** * Sets the URI (or filename) of this buffer (e.g. 'myBuffer.bin'). URIs are strongly * encouraged to be relative paths, rather than absolute. Use of a protocol (like `file://`) * is possible for custom applications, but will limit the compatibility of the asset with most * tools. * * Buffers commonly use the extension `.bin`, though this is not required. */ setURI(uri) { return this.set('uri', uri); } } /** * *Cameras are perspectives through which the {@link Scene} may be viewed.* * * Projection can be perspective or orthographic. Cameras are contained in nodes and thus can be * transformed. The camera is defined such that the local +X axis is to the right, the lens looks * towards the local -Z axis, and the top of the camera is aligned with the local +Y axis. If no * transformation is specified, the location of the camera is at the origin. * * Usage: * * ```typescript * const camera = doc.createCamera('myCamera') * .setType(GLTF.CameraType.PERSPECTIVE) * .setZNear(0.1) * .setZFar(100) * .setYFov(Math.PI / 4) * .setAspectRatio(1.5); * * node.setCamera(camera); * ``` * * References: * - [glTF → Cameras](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#cameras) * * @category Properties */ class Camera extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.CAMERA; } getDefaults() { return Object.assign(super.getDefaults(), { // Common. type: Camera.Type.PERSPECTIVE, znear: 0.1, zfar: 100, // Perspective. aspectRatio: null, yfov: Math.PI * 2 * 50 / 360, // 50º // Orthographic. xmag: 1, ymag: 1 }); } /********************************************************************************************** * Common. */ /** Specifies if the camera uses a perspective or orthographic projection. */ getType() { return this.get('type'); } /** Specifies if the camera uses a perspective or orthographic projection. */ setType(type) { return this.set('type', type); } /** Floating-point distance to the near clipping plane. */ getZNear() { return this.get('znear'); } /** Floating-point distance to the near clipping plane. */ setZNear(znear) { return this.set('znear', znear); } /** * Floating-point distance to the far clipping plane. When defined, zfar must be greater than * znear. If zfar is undefined, runtime must use infinite projection matrix. */ getZFar() { return this.get('zfar'); } /** * Floating-point distance to the far clipping plane. When defined, zfar must be greater than * znear. If zfar is undefined, runtime must use infinite projection matrix. */ setZFar(zfar) { return this.set('zfar', zfar); } /********************************************************************************************** * Perspective. */ /** * Floating-point aspect ratio of the field of view. When undefined, the aspect ratio of the * canvas is used. */ getAspectRatio() { return this.get('aspectRatio'); } /** * Floating-point aspect ratio of the field of view. When undefined, the aspect ratio of the * canvas is used. */ setAspectRatio(aspectRatio) { return this.set('aspectRatio', aspectRatio); } /** Floating-point vertical field of view in radians. */ getYFov() { return this.get('yfov'); } /** Floating-point vertical field of view in radians. */ setYFov(yfov) { return this.set('yfov', yfov); } /********************************************************************************************** * Orthographic. */ /** * Floating-point horizontal magnification of the view, and half the view's width * in world units. */ getXMag() { return this.get('xmag'); } /** * Floating-point horizontal magnification of the view, and half the view's width * in world units. */ setXMag(xmag) { return this.set('xmag', xmag); } /** * Floating-point vertical magnification of the view, and half the view's height * in world units. */ getYMag() { return this.get('ymag'); } /** * Floating-point vertical magnification of the view, and half the view's height * in world units. */ setYMag(ymag) { return this.set('ymag', ymag); } } /********************************************************************************************** * Constants. */ Camera.Type = { /** A perspective camera representing a perspective projection matrix. */ PERSPECTIVE: 'perspective', /** An orthographic camera representing an orthographic projection matrix. */ ORTHOGRAPHIC: 'orthographic' }; /** * *Base class for all {@link Property} types that can be attached by an {@link Extension}.* * * After an {@link Extension} is attached to a glTF {@link Document}, the Extension may be used to * construct ExtensionProperty instances, to be referenced throughout the document as prescribed by * the Extension. For example, the `KHR_materials_clearcoat` Extension defines a `Clearcoat` * ExtensionProperty, which is referenced by {@link Material} Properties in the Document, and may * contain references to {@link Texture} properties of its own. * * For more information on available extensions and their usage, see [Extensions](/extensions). * * Reference: * - [glTF → Extensions](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#specifying-extensions) * * @category Properties */ class ExtensionProperty extends Property { /** @hidden */ _validateParent(parent) { if (!this.parentTypes.includes(parent.propertyType)) { throw new Error(`Parent "${parent.propertyType}" invalid for child "${this.propertyType}".`); } } } ExtensionProperty.EXTENSION_NAME = void 0; /** * *Settings associated with a particular use of a {@link Texture}.* * * Different materials may reuse the same texture but with different texture coordinates, * minFilter/magFilter, or wrapS/wrapT settings. The TextureInfo class contains settings * derived from both the "TextureInfo" and "Sampler" properties in the glTF specification, * consolidated here for simplicity. * * TextureInfo properties cannot be directly created. For any material texture slot, such as * baseColorTexture, there will be a corresponding method to obtain the TextureInfo for that slot. * For example, see {@link Material.getBaseColorTextureInfo}. * * References: * - [glTF → Texture Info](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#reference-textureinfo) * * @category Properties */ class TextureInfo extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.TEXTURE_INFO; } getDefaults() { return Object.assign(super.getDefaults(), { texCoord: 0, magFilter: null, minFilter: null, wrapS: TextureInfo.WrapMode.REPEAT, wrapT: TextureInfo.WrapMode.REPEAT }); } /********************************************************************************************** * Texture coordinates. */ /** Returns the texture coordinate (UV set) index for the texture. */ getTexCoord() { return this.get('texCoord'); } /** Sets the texture coordinate (UV set) index for the texture. */ setTexCoord(texCoord) { return this.set('texCoord', texCoord); } /********************************************************************************************** * Min/mag filter. */ /** Returns the magnification filter applied to the texture. */ getMagFilter() { return this.get('magFilter'); } /** Sets the magnification filter applied to the texture. */ setMagFilter(magFilter) { return this.set('magFilter', magFilter); } /** Sets the minification filter applied to the texture. */ getMinFilter() { return this.get('minFilter'); } /** Returns the minification filter applied to the texture. */ setMinFilter(minFilter) { return this.set('minFilter', minFilter); } /********************************************************************************************** * UV wrapping. */ /** Returns the S (U) wrapping mode for UVs used by the texture. */ getWrapS() { return this.get('wrapS'); } /** Sets the S (U) wrapping mode for UVs used by the texture. */ setWrapS(wrapS) { return this.set('wrapS', wrapS); } /** Returns the T (V) wrapping mode for UVs used by the texture. */ getWrapT() { return this.get('wrapT'); } /** Sets the T (V) wrapping mode for UVs used by the texture. */ setWrapT(wrapT) { return this.set('wrapT', wrapT); } } /********************************************************************************************** * Constants. */ /** UV wrapping mode. Values correspond to WebGL enums. */ TextureInfo.WrapMode = { /** */ CLAMP_TO_EDGE: 33071, /** */ MIRRORED_REPEAT: 33648, /** */ REPEAT: 10497 }; /** Magnification filter. Values correspond to WebGL enums. */ TextureInfo.MagFilter = { /** */ NEAREST: 9728, /** */ LINEAR: 9729 }; /** Minification filter. Values correspond to WebGL enums. */ TextureInfo.MinFilter = { /** */ NEAREST: 9728, /** */ LINEAR: 9729, /** */ NEAREST_MIPMAP_NEAREST: 9984, /** */ LINEAR_MIPMAP_NEAREST: 9985, /** */ NEAREST_MIPMAP_LINEAR: 9986, /** */ LINEAR_MIPMAP_LINEAR: 9987 }; const { R, G, B, A } = exports.TextureChannel; /** * *Materials describe a surface's appearance and response to light.* * * Each {@link Primitive} within a {@link Mesh} may be assigned a single Material. The number of * GPU draw calls typically increases with both the numbers of Primitives and of Materials in an * asset; Materials should be reused wherever possible. Techniques like texture atlasing and vertex * colors allow objects to have varied appearances while technically sharing a single Material. * * Material properties are modified by both scalars (like `baseColorFactor`) and textures (like * `baseColorTexture`). When both are available, factors are considered linear multipliers against * textures of the same name. In the case of base color, vertex colors (`COLOR_0` attributes) are * also multiplied. * * Textures containing color data (`baseColorTexture`, `emissiveTexture`) are sRGB. All other * textures are linear. Like other resources, textures should be reused when possible. * * Usage: * * ```typescript * const material = doc.createMaterial('myMaterial') * .setBaseColorFactor([1, 0.5, 0.5, 1]) // RGBA * .setOcclusionTexture(aoTexture) * .setOcclusionStrength(0.5); * * mesh.listPrimitives() * .forEach((prim) => prim.setMaterial(material)); * ``` * * @category Properties */ class Material extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.MATERIAL; } getDefaults() { return Object.assign(super.getDefaults(), { alphaMode: Material.AlphaMode.OPAQUE, alphaCutoff: 0.5, doubleSided: false, baseColorFactor: [1, 1, 1, 1], baseColorTexture: null, baseColorTextureInfo: new TextureInfo(this.graph, 'baseColorTextureInfo'), emissiveFactor: [0, 0, 0], emissiveTexture: null, emissiveTextureInfo: new TextureInfo(this.graph, 'emissiveTextureInfo'), normalScale: 1, normalTexture: null, normalTextureInfo: new TextureInfo(this.graph, 'normalTextureInfo'), occlusionStrength: 1, occlusionTexture: null, occlusionTextureInfo: new TextureInfo(this.graph, 'occlusionTextureInfo'), roughnessFactor: 1, metallicFactor: 1, metallicRoughnessTexture: null, metallicRoughnessTextureInfo: new TextureInfo(this.graph, 'metallicRoughnessTextureInfo') }); } /********************************************************************************************** * Double-sided / culling. */ /** Returns true when both sides of triangles should be rendered. May impact performance. */ getDoubleSided() { return this.get('doubleSided'); } /** Sets whether to render both sides of triangles. May impact performance. */ setDoubleSided(doubleSided) { return this.set('doubleSided', doubleSided); } /********************************************************************************************** * Alpha. */ /** Returns material alpha, equivalent to baseColorFactor[3]. */ getAlpha() { return this.get('baseColorFactor')[3]; } /** Sets material alpha, equivalent to baseColorFactor[3]. */ setAlpha(alpha) { const baseColorFactor = this.get('baseColorFactor').slice(); baseColorFactor[3] = alpha; return this.set('baseColorFactor', baseColorFactor); } /** * Returns the mode of the material's alpha channels, which are provided by `baseColorFactor` * and `baseColorTexture`. * * - `OPAQUE`: Alpha value is ignored and the rendered output is fully opaque. * - `BLEND`: Alpha value is used to determine the transparency each pixel on a surface, and * the fraction of surface vs. background color in the final result. Alpha blending creates * significant edge cases in realtime renderers, and some care when structuring the model is * necessary for good results. In particular, transparent geometry should be kept in separate * meshes or primitives from opaque geometry. The `depthWrite` or `zWrite` settings in engines * should usually be disabled on transparent materials. * - `MASK`: Alpha value is compared against `alphaCutoff` threshold for each pixel on a * surface, and the pixel is either fully visible or fully discarded based on that cutoff. * This technique is useful for things like leafs/foliage, grass, fabric meshes, and other * surfaces where no semitransparency is needed. With a good choice of `alphaCutoff`, surfaces * that don't require semitransparency can avoid the performance penalties and visual issues * involved with `BLEND` transparency. * * Reference: * - [glTF → material.alphaMode](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#materialalphamode) */ getAlphaMode() { return this.get('alphaMode'); } /** Sets the mode of the material's alpha channels. See {@link Material.getAlphaMode getAlphaMode} for details. */ setAlphaMode(alphaMode) { return this.set('alphaMode', alphaMode); } /** Returns the visibility threshold; applied only when `.alphaMode='MASK'`. */ getAlphaCutoff() { return this.get('alphaCutoff'); } /** Sets the visibility threshold; applied only when `.alphaMode='MASK'`. */ setAlphaCutoff(alphaCutoff) { return this.set('alphaCutoff', alphaCutoff); } /********************************************************************************************** * Base color. */ /** * Base color / albedo factor; Linear-sRGB components. * See {@link Material.getBaseColorTexture getBaseColorTexture}. */ getBaseColorFactor() { return this.get('baseColorFactor'); } /** * Base color / albedo factor; Linear-sRGB components. * See {@link Material.getBaseColorTexture getBaseColorTexture}. */ setBaseColorFactor(baseColorFactor) { return this.set('baseColorFactor', baseColorFactor); } /** * Base color / albedo. The visible color of a non-metallic surface under constant ambient * light would be a linear combination (multiplication) of its vertex colors, base color * factor, and base color texture. Lighting, and reflections in metallic or smooth surfaces, * also effect the final color. The alpha (`.a`) channel of base color factors and textures * will have varying effects, based on the setting of {@link Material.getAlphaMode getAlphaMode}. * * Reference: * - [glTF → material.pbrMetallicRoughness.baseColorFactor](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#pbrmetallicroughnessbasecolorfactor) */ getBaseColorTexture() { return this.getRef('baseColorTexture'); } /** * Settings affecting the material's use of its base color texture. If no texture is attached, * {@link TextureInfo} is `null`. */ getBaseColorTextureInfo() { return this.getRef('baseColorTexture') ? this.getRef('baseColorTextureInfo') : null; } /** Sets base color / albedo texture. See {@link Material.getBaseColorTexture getBaseColorTexture}. */ setBaseColorTexture(texture) { return this.setRef('baseColorTexture', texture, { channels: R | G | B | A, isColor: true }); } /********************************************************************************************** * Emissive. */ /** Emissive color; Linear-sRGB components. See {@link Material.getEmissiveTexture getEmissiveTexture}. */ getEmissiveFactor() { return this.get('emissiveFactor'); } /** Emissive color; Linear-sRGB components. See {@link Material.getEmissiveTexture getEmissiveTexture}. */ setEmissiveFactor(emissiveFactor) { return this.set('emissiveFactor', emissiveFactor); } /** * Emissive texture. Emissive color is added to any base color of the material, after any * lighting/shadowing are applied. An emissive color does not inherently "glow", or affect * objects around it at all. To create that effect, most viewers must also enable a * post-processing effect called "bloom". * * Reference: * - [glTF → material.emissiveTexture](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#materialemissivetexture) */ getEmissiveTexture() { return this.getRef('emissiveTexture'); } /** * Settings affecting the material's use of its emissive texture. If no texture is attached, * {@link TextureInfo} is `null`. */ getEmissiveTextureInfo() { return this.getRef('emissiveTexture') ? this.getRef('emissiveTextureInfo') : null; } /** Sets emissive texture. See {@link Material.getEmissiveTexture getEmissiveTexture}. */ setEmissiveTexture(texture) { return this.setRef('emissiveTexture', texture, { channels: R | G | B, isColor: true }); } /********************************************************************************************** * Normal. */ /** Normal (surface detail) factor; linear multiplier. Affects `.normalTexture`. */ getNormalScale() { return this.get('normalScale'); } /** Normal (surface detail) factor; linear multiplier. Affects `.normalTexture`. */ setNormalScale(scale) { return this.set('normalScale', scale); } /** * Normal (surface detail) texture. * * A tangent space normal map. The texture contains RGB components. Each texel represents the * XYZ components of a normal vector in tangent space. Red [0 to 255] maps to X [-1 to 1]. * Green [0 to 255] maps to Y [-1 to 1]. Blue [128 to 255] maps to Z [1/255 to 1]. The normal * vectors use OpenGL conventions where +X is right and +Y is up. +Z points toward the viewer. * * Reference: * - [glTF → material.normalTexture](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#materialnormaltexture) */ getNormalTexture() { return this.getRef('normalTexture'); } /** * Settings affecting the material's use of its normal texture. If no texture is attached, * {@link TextureInfo} is `null`. */ getNormalTextureInfo() { return this.getRef('normalTexture') ? this.getRef('normalTextureInfo') : null; } /** Sets normal (surface detail) texture. See {@link Material.getNormalTexture getNormalTexture}. */ setNormalTexture(texture) { return this.setRef('normalTexture', texture, { channels: R | G | B }); } /********************************************************************************************** * Occlusion. */ /** (Ambient) Occlusion factor; linear multiplier. Affects `.occlusionTexture`. */ getOcclusionStrength() { return this.get('occlusionStrength'); } /** Sets (ambient) occlusion factor; linear multiplier. Affects `.occlusionTexture`. */ setOcclusionStrength(strength) { return this.set('occlusionStrength', strength); } /** * (Ambient) Occlusion texture, generally used for subtle 'baked' shadowing effects that are * independent of an object's position, such as shading in inset areas and corners. Direct * lighting is not affected by occlusion, so at least one indirect light source must be present * in the scene for occlusion effects to be visible. * * The occlusion values are sampled from the R channel. Higher values indicate areas that * should receive full indirect lighting and lower values indicate no indirect lighting. * * Reference: * - [glTF → material.occlusionTexture](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#materialocclusiontexture) */ getOcclusionTexture() { return this.getRef('occlusionTexture'); } /** * Settings affecting the material's use of its occlusion texture. If no texture is attached, * {@link TextureInfo} is `null`. */ getOcclusionTextureInfo() { return this.getRef('occlusionTexture') ? this.getRef('occlusionTextureInfo') : null; } /** Sets (ambient) occlusion texture. See {@link Material.getOcclusionTexture getOcclusionTexture}. */ setOcclusionTexture(texture) { return this.setRef('occlusionTexture', texture, { channels: R }); } /********************************************************************************************** * Metallic / roughness. */ /** * Roughness factor; linear multiplier. Affects roughness channel of * `metallicRoughnessTexture`. See {@link Material.getMetallicRoughnessTexture getMetallicRoughnessTexture}. */ getRoughnessFactor() { return this.get('roughnessFactor'); } /** * Sets roughness factor; linear multiplier. Affects roughness channel of * `metallicRoughnessTexture`. See {@link Material.getMetallicRoughnessTexture getMetallicRoughnessTexture}. */ setRoughnessFactor(factor) { return this.set('roughnessFactor', factor); } /** * Metallic factor; linear multiplier. Affects roughness channel of * `metallicRoughnessTexture`. See {@link Material.getMetallicRoughnessTexture getMetallicRoughnessTexture}. */ getMetallicFactor() { return this.get('metallicFactor'); } /** * Sets metallic factor; linear multiplier. Affects roughness channel of * `metallicRoughnessTexture`. See {@link Material.getMetallicRoughnessTexture getMetallicRoughnessTexture}. */ setMetallicFactor(factor) { return this.set('metallicFactor', factor); } /** * Metallic roughness texture. The metalness values are sampled from the B channel. The * roughness values are sampled from the G channel. When a material is fully metallic, * or nearly so, it may require image-based lighting (i.e. an environment map) or global * illumination to appear well-lit. * * Reference: * - [glTF → material.pbrMetallicRoughness.metallicRoughnessTexture](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#pbrmetallicroughnessmetallicroughnesstexture) */ getMetallicRoughnessTexture() { return this.getRef('metallicRoughnessTexture'); } /** * Settings affecting the material's use of its metallic/roughness texture. If no texture is * attached, {@link TextureInfo} is `null`. */ getMetallicRoughnessTextureInfo() { return this.getRef('metallicRoughnessTexture') ? this.getRef('metallicRoughnessTextureInfo') : null; } /** * Sets metallic/roughness texture. * See {@link Material.getMetallicRoughnessTexture getMetallicRoughnessTexture}. */ setMetallicRoughnessTexture(texture) { return this.setRef('metallicRoughnessTexture', texture, { channels: G | B }); } } /********************************************************************************************** * Constants. */ Material.AlphaMode = { /** * The alpha value is ignored and the rendered output is fully opaque */ OPAQUE: 'OPAQUE', /** * The rendered output is either fully opaque or fully transparent depending on the alpha * value and the specified alpha cutoff value */ MASK: 'MASK', /** * The alpha value is used to composite the source and destination areas. The rendered * output is combined with the background using the normal painting operation (i.e. the * Porter and Duff over operator) */ BLEND: 'BLEND' }; /** * *Meshes define reusable geometry (triangles, lines, or points) and are instantiated by * {@link Node}s.* * * Each draw call required to render a mesh is represented as a {@link Primitive}. Meshes typically * have only a single {@link Primitive}, but may have more for various reasons. A mesh manages only * a list of primitives — materials, morph targets, and other properties are managed on a per- * primitive basis. * * When the same geometry and material should be rendered at multiple places in the scene, reuse * the same Mesh instance and attach it to multiple nodes for better efficiency. Where the geometry * is shared but the material is not, reusing {@link Accessor}s under different meshes and * primitives can similarly improve transmission efficiency, although some rendering efficiency is * lost as the number of materials in a scene increases. * * Usage: * * ```ts * const primitive = doc.createPrimitive() * .setAttribute('POSITION', positionAccessor) * .setAttribute('TEXCOORD_0', uvAccessor); * const mesh = doc.createMesh('myMesh') * .addPrimitive(primitive); * node.setMesh(mesh); * ``` * * References: * - [glTF → Geometry](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#geometry) * * @category Properties */ class Mesh extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.MESH; } getDefaults() { return Object.assign(super.getDefaults(), { weights: [], primitives: new propertyGraph.RefSet() }); } /** Adds a {@link Primitive} to the mesh's draw call list. */ addPrimitive(primitive) { return this.addRef('primitives', primitive); } /** Removes a {@link Primitive} from the mesh's draw call list. */ removePrimitive(primitive) { return this.removeRef('primitives', primitive); } /** Lists {@link Primitive} draw calls of the mesh. */ listPrimitives() { return this.listRefs('primitives'); } /** * Initial weights of each {@link PrimitiveTarget} on this mesh. Each {@link Primitive} must * have the same number of targets. Most engines only support 4-8 active morph targets at a * time. */ getWeights() { return this.get('weights'); } /** * Initial weights of each {@link PrimitiveTarget} on this mesh. Each {@link Primitive} must * have the same number of targets. Most engines only support 4-8 active morph targets at a * time. */ setWeights(weights) { return this.set('weights', weights); } } /** * *Nodes are the objects that comprise a {@link Scene}.* * * Each Node may have one or more children, and a transform (position, rotation, and scale) that * applies to all of its descendants. A Node may also reference (or "instantiate") other resources * at its location, including {@link Mesh}, Camera, Light, and Skin properties. A Node cannot be * part of more than one {@link Scene}. * * A Node's local transform is represented with array-like objects, intended to be compatible with * [gl-matrix](https://github.com/toji/gl-matrix), or with the `toArray`/`fromArray` methods of * libraries like three.js and babylon.js. * * Usage: * * ```ts * const node = doc.createNode('myNode') * .setMesh(mesh) * .setTranslation([0, 0, 0]) * .addChild(otherNode); * ``` * * References: * - [glTF → Nodes and Hierarchy](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#nodes-and-hierarchy) * * @category Properties */ class Node extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.NODE; } getDefaults() { return Object.assign(super.getDefaults(), { translation: [0, 0, 0], rotation: [0, 0, 0, 1], scale: [1, 1, 1], weights: [], camera: null, mesh: null, skin: null, children: new propertyGraph.RefSet() }); } copy(other, resolve) { if (resolve === void 0) { resolve = COPY_IDENTITY; } // Node cannot be copied, only cloned. Copying is shallow, but Nodes cannot have more than // one parent. Rather than leaving one of the two Nodes without children, throw an error here. if (resolve === COPY_IDENTITY) throw new Error('Node cannot be copied.'); return super.copy(other, resolve); } /********************************************************************************************** * Local transform. */ /** Returns the translation (position) of this Node in local space. */ getTranslation() { return this.get('translation'); } /** Returns the rotation (quaternion) of this Node in local space. */ getRotation() { return this.get('rotation'); } /** Returns the scale of this Node in local space. */ getScale() { return this.get('scale'); } /** Sets the translation (position) of this Node in local space. */ setTranslation(translation) { return this.set('translation', translation); } /** Sets the rotation (quaternion) of this Node in local space. */ setRotation(rotation) { return this.set('rotation', rotation); } /** Sets the scale of this Node in local space. */ setScale(scale) { return this.set('scale', scale); } /** Returns the local matrix of this Node. */ getMatrix() { return MathUtils.compose(this.get('translation'), this.get('rotation'), this.get('scale'), []); } /** Sets the local matrix of this Node. Matrix will be decomposed to TRS properties. */ setMatrix(matrix) { const translation = this.get('translation').slice(); const rotation = this.get('rotation').slice(); const scale = this.get('scale').slice(); MathUtils.decompose(matrix, translation, rotation, scale); return this.set('translation', translation).set('rotation', rotation).set('scale', scale); } /********************************************************************************************** * World transform. */ /** Returns the translation (position) of this Node in world space. */ getWorldTranslation() { const t = [0, 0, 0]; MathUtils.decompose(this.getWorldMatrix(), t, [0, 0, 0, 1], [1, 1, 1]); return t; } /** Returns the rotation (quaternion) of this Node in world space. */ getWorldRotation() { const r = [0, 0, 0, 1]; MathUtils.decompose(this.getWorldMatrix(), [0, 0, 0], r, [1, 1, 1]); return r; } /** Returns the scale of this Node in world space. */ getWorldScale() { const s = [1, 1, 1]; MathUtils.decompose(this.getWorldMatrix(), [0, 0, 0], [0, 0, 0, 1], s); return s; } /** Returns the world matrix of this Node. */ getWorldMatrix() { // Build ancestor chain. const ancestors = []; for (let node = this; node != null; node = node.getParentNode()) { ancestors.push(node); } // Compute world matrix. let ancestor; const worldMatrix = ancestors.pop().getMatrix(); while (ancestor = ancestors.pop()) { multiply(worldMatrix, worldMatrix, ancestor.getMatrix()); } return worldMatrix; } /********************************************************************************************** * Scene hierarchy. */ /** * Adds the given Node as a child of this Node. * * Requirements: * * 1. Nodes MAY be root children of multiple {@link Scene Scenes} * 2. Nodes MUST NOT be children of >1 Node * 3. Nodes MUST NOT be children of both Nodes and {@link Scene Scenes} * * The `addChild` method enforces these restrictions automatically, and will * remove the new child from previous parents where needed. This behavior * may change in future major releases of the library. */ addChild(child) { // Remove existing parents. const parentNode = child.getParentNode(); if (parentNode) parentNode.removeChild(child); for (const parent of child.listParents()) { if (parent.propertyType === exports.PropertyType.SCENE) { parent.removeChild(child); } } return this.addRef('children', child); } /** Removes a Node from this Node's child Node list. */ removeChild(child) { return this.removeRef('children', child); } /** Lists all child Nodes of this Node. */ listChildren() { return this.listRefs('children'); } /** * Returns the Node's unique parent Node within the scene graph. If the * Node has no parents, or is a direct child of the {@link Scene} * ("root node"), this method returns null. * * Unrelated to {@link Property.listParents}, which lists all resource * references from properties of any type ({@link Skin}, {@link Root}, ...). */ getParentNode() { for (const parent of this.listParents()) { if (parent.propertyType === exports.PropertyType.NODE) { return parent; } } return null; } /********************************************************************************************** * Attachments. */ /** Returns the {@link Mesh}, if any, instantiated at this Node. */ getMesh() { return this.getRef('mesh'); } /** * Sets a {@link Mesh} to be instantiated at this Node. A single mesh may be instatiated by * multiple Nodes; reuse of this sort is strongly encouraged. */ setMesh(mesh) { return this.setRef('mesh', mesh); } /** Returns the {@link Camera}, if any, instantiated at this Node. */ getCamera() { return this.getRef('camera'); } /** Sets a {@link Camera} to be instantiated at this Node. */ setCamera(camera) { return this.setRef('camera', camera); } /** Returns the {@link Skin}, if any, instantiated at this Node. */ getSkin() { return this.getRef('skin'); } /** Sets a {@link Skin} to be instantiated at this Node. */ setSkin(skin) { return this.setRef('skin', skin); } /** * Initial weights of each {@link PrimitiveTarget} for the mesh instance at this Node. * Most engines only support 4-8 active morph targets at a time. */ getWeights() { return this.get('weights'); } /** * Initial weights of each {@link PrimitiveTarget} for the mesh instance at this Node. * Most engines only support 4-8 active morph targets at a time. */ setWeights(weights) { return this.set('weights', weights); } /********************************************************************************************** * Helpers. */ /** Visits this {@link Node} and its descendants, top-down. */ traverse(fn) { fn(this); for (const child of this.listChildren()) child.traverse(fn); return this; } } /** * *Primitives are individual GPU draw calls comprising a {@link Mesh}.* * * Meshes typically have only a single Primitive, although various cases may require more. Each * primitive may be assigned vertex attributes, morph target attributes, and a material. Any of * these properties should be reused among multiple primitives where feasible. * * Primitives cannot be moved independently of other primitives within the same mesh, except * through the use of morph targets and skinning. If independent movement or other runtime * behavior is necessary (like raycasting or collisions) prefer to assign each primitive to a * different mesh. The number of GPU draw calls is typically not affected by grouping or * ungrouping primitives to a mesh. * * Each primitive may optionally be deformed by one or more morph targets, stored in a * {@link PrimitiveTarget}. * * Usage: * * ```ts * const primitive = doc.createPrimitive() * .setAttribute('POSITION', positionAccessor) * .setAttribute('TEXCOORD_0', uvAccessor) * .setMaterial(material); * mesh.addPrimitive(primitive); * node.setMesh(mesh); * ``` * * References: * - [glTF → Geometry](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#geometry) * * @category Properties */ class Primitive extends ExtensibleProperty { /********************************************************************************************** * Instance. */ init() { this.propertyType = exports.PropertyType.PRIMITIVE; } getDefaults() { return Object.assign(super.getDefaults(), { mode: Primitive.Mode.TRIANGLES, material: null, indices: null, attributes: new propertyGraph.RefMap(), targets: new propertyGraph.RefSet() }); } /********************************************************************************************** * Primitive data. */ /** Returns an {@link Accessor} with indices of vertices to be drawn. */ getIndices() { return this.getRef('indices'); } /** * Sets an {@link Accessor} with indices of vertices to be drawn. In `TRIANGLES` draw mode, * each set of three indices define a triangle. The front face has a counter-clockwise (CCW) * winding order. */ setIndices(indices) { return this.setRef('indices', indices, { usage: BufferViewUsage$1.ELEMENT_ARRAY_BUFFER }); } /** Returns a vertex attribute as an {@link Accessor}. */ getAttribute(semantic) { return this.getRefMap('attributes', semantic); } /** * Sets a vertex attribute to an {@link Accessor}. All attributes must have the same vertex * count. */ setAttribute(semantic, accessor) { return this.setRefMap('attributes', semantic, accessor, { usage: BufferViewUsage$1.ARRAY_BUFFER }); } /** * Lists all vertex attribute {@link Accessor}s associated with the primitive, excluding any * attributes used for morph targets. For example, `[positionAccessor, normalAccessor, * uvAccessor]`. Order will be consistent with the order returned by {@link .listSemantics}(). */ listAttributes() { return this.listRefMapValues('attributes'); } /** * Lists all vertex attribute semantics associated with the primitive, excluding any semantics * used for morph targets. For example, `['POSITION', 'NORMAL', 'TEXCOORD_0']`. Order will be * consistent with the order returned by {@link .listAttributes}(). */ listSemantics() { return this.listRefMapKeys('attributes'); } /** Returns the material used to render the primitive. */ getMaterial() { return this.getRef('material'); } /** Sets the material used to render the primitive. */ setMaterial(material) { return this.setRef('material', material); } /********************************************************************************************** * Mode. */ /** * Returns the GPU draw mode (`TRIANGLES`, `LINES`, `POINTS`...) as a WebGL enum value. * * Reference: * - [glTF → `primitive.mode`](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#primitivemode) */ getMode() { return this.get('mode'); } /** * Sets the GPU draw mode (`TRIANGLES`, `LINES`, `POINTS`...) as a WebGL enum value. * * Reference: * - [glTF → `primitive.mode`](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#primitivemode) */ setMode(mode) { return this.set('mode', mode); } /********************************************************************************************** * Morph targets. */ /** Lists all morph targets associated with the primitive. */ listTargets() { return this.listRefs('targets'); } /** * Adds a morph target to the primitive. All primitives in the same mesh must have the same * number of targets. */ addTarget(target) { return this.addRef('targets', target); } /** * Removes a morph target from the primitive. All primitives in the same mesh must have the same * number of targets. */ removeTarget(target) { return this.removeRef('targets', target); } } /********************************************************************************************** * Constants. */ /** Type of primitives to render. All valid values correspond to WebGL enums. */ Primitive.Mode = { /** Draw single points. */ POINTS: 0, /** Draw lines. Each vertex connects to the one after it. */ LINES: 1, /** * Draw lines. Each set of two vertices is treated as a separate line segment. * @deprecated See {@link https://github.com/KhronosGroup/glTF/issues/1883 KhronosGroup/glTF#1883}. */ LINE_LOOP: 2, /** Draw a connected group of line segments from the first vertex to the last, */ LINE_STRIP: 3, /** Draw triangles. Each set of three vertices creates a separate triangle. */ TRIANGLES: 4, /** Draw a connected strip of triangles. */ TRIANGLE_STRIP: 5, /** * Draw a connected group of triangles. Each vertex connects to the previous and the first * vertex in the fan. * @deprecated See {@link https://github.com/KhronosGroup/glTF/issues/1883 KhronosGroup/glTF#1883}. */ TRIANGLE_FAN: 6 }; /** * *Morph target or shape key used to deform one {@link Primitive} in a {@link Mesh}.* * * A PrimitiveTarget contains a `POSITION` attribute (and optionally `NORMAL` and `TANGENT`) that * can additively deform the base attributes on a {@link Mesh} {@link Primitive}. Vertex values * of `0, 0, 0` in the target will have no effect, whereas a value of `0, 1, 0` would offset that * vertex in the base geometry by y+=1. Morph targets can be fully or partially applied: their * default state is controlled by {@link Mesh.getWeights}, which can also be overridden for a * particular instantiation of a {@link Mesh}, using {@link Node.getWeights}. * * Reference: * - [glTF → Morph Targets](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#morph-targets) * * @category Properties */ class PrimitiveTarget extends Property { init() { this.propertyType = exports.PropertyType.PRIMITIVE_TARGET; } getDefaults() { return Object.assign(super.getDefaults(), { attributes: new propertyGraph.RefMap() }); } /** Returns a morph target vertex attribute as an {@link Accessor}. */ getAttribute(semantic) { return this.getRefMap('attributes', semantic); } /** * Sets a morph target vertex attribute to an {@link Accessor}. */ setAttribute(semantic, accessor) { return this.setRefMap('attributes', semantic, accessor, { usage: BufferViewUsage$1.ARRAY_BUFFER }); } /** * Lists all morph target vertex attribute {@link Accessor}s associated. Order will be * consistent with the order returned by {@link .listSemantics}(). */ listAttributes() { return this.listRefMapValues('attributes'); } /** * Lists all morph target vertex attribute semantics associated. Order will be * consistent with the order returned by {@link .listAttributes}(). */ listSemantics() { return this.listRefMapKeys('attributes'); } } /** * *Scenes represent a set of visual objects to render.* * * Typically a glTF file contains only a single Scene, although more are allowed and useful in some * cases. No particular meaning is associated with additional Scenes, except as defined by the * application. Scenes reference {@link Node}s, and a single Node cannot be a member of more than * one Scene. * * References: * - [glTF → Scenes](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#scenes) * - [glTF → Coordinate System and Units](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#coordinate-system-and-units) * * @category Properties */ class Scene extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.SCENE; } getDefaults() { return Object.assign(super.getDefaults(), { children: new propertyGraph.RefSet() }); } copy(other, resolve) { if (resolve === void 0) { resolve = COPY_IDENTITY; } // Scene cannot be copied, only cloned. Copying is shallow, but nodes cannot have more than // one parent. Rather than leaving one of the two Scenes without children, throw an error here. if (resolve === COPY_IDENTITY) throw new Error('Scene cannot be copied.'); return super.copy(other, resolve); } /** * Adds a {@link Node} to the Scene. * * Requirements: * * 1. Nodes MAY be root children of multiple {@link Scene Scenes} * 2. Nodes MUST NOT be children of >1 Node * 3. Nodes MUST NOT be children of both Nodes and {@link Scene Scenes} * * The `addChild` method enforces these restrictions automatically, and will * remove the new child from previous parents where needed. This behavior * may change in future major releases of the library. */ addChild(node) { // Remove existing parent. const parentNode = node.getParentNode(); if (parentNode) parentNode.removeChild(node); return this.addRef('children', node); } /** Removes a {@link Node} from the Scene. */ removeChild(node) { return this.removeRef('children', node); } /** * Lists all direct child {@link Node Nodes} in the Scene. Indirect * descendants (children of children) are not returned, but may be * reached recursively or with {@link Scene.traverse} instead. */ listChildren() { return this.listRefs('children'); } /** Visits each {@link Node} in the Scene, including descendants, top-down. */ traverse(fn) { for (const node of this.listChildren()) node.traverse(fn); return this; } } /** * *Collection of {@link Node} joints and inverse bind matrices used with skinned {@link Mesh} * instances.* * * Reference * - [glTF → Skins](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#skins) * * @category Properties */ class Skin extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.SKIN; } getDefaults() { return Object.assign(super.getDefaults(), { skeleton: null, inverseBindMatrices: null, joints: new propertyGraph.RefSet() }); } /** * {@link Node} used as a skeleton root. The node must be the closest common root of the joints * hierarchy or a direct or indirect parent node of the closest common root. */ getSkeleton() { return this.getRef('skeleton'); } /** * {@link Node} used as a skeleton root. The node must be the closest common root of the joints * hierarchy or a direct or indirect parent node of the closest common root. */ setSkeleton(skeleton) { return this.setRef('skeleton', skeleton); } /** * {@link Accessor} containing the floating-point 4x4 inverse-bind matrices. The default is * that each matrix is a 4x4 identity matrix, which implies that inverse-bind matrices were * pre-applied. */ getInverseBindMatrices() { return this.getRef('inverseBindMatrices'); } /** * {@link Accessor} containing the floating-point 4x4 inverse-bind matrices. The default is * that each matrix is a 4x4 identity matrix, which implies that inverse-bind matrices were * pre-applied. */ setInverseBindMatrices(inverseBindMatrices) { return this.setRef('inverseBindMatrices', inverseBindMatrices, { usage: BufferViewUsage$1.INVERSE_BIND_MATRICES }); } /** Adds a joint {@link Node} to this {@link Skin}. */ addJoint(joint) { return this.addRef('joints', joint); } /** Removes a joint {@link Node} from this {@link Skin}. */ removeJoint(joint) { return this.removeRef('joints', joint); } /** Lists joints ({@link Node}s used as joints or bones) in this {@link Skin}. */ listJoints() { return this.listRefs('joints'); } } /** * *Texture, or images, referenced by {@link Material} properties.* * * Textures in glTF Transform are a combination of glTF's `texture` and `image` properties, and * should be unique within a document, such that no other texture contains the same * {@link Texture.getImage getImage()} data. Where duplicates may already exist, the `dedup({textures: true})` * transform can remove them. A {@link Document} with N texture properties will be exported to a * glTF file with N `image` properties, and the minimum number of `texture` properties necessary * for the materials that use it. * * For properties associated with a particular _use_ of a texture, see {@link TextureInfo}. * * Reference: * - [glTF → Textures](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#textures) * - [glTF → Images](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#images) * * @category Properties */ class Texture extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.TEXTURE; } getDefaults() { return Object.assign(super.getDefaults(), { image: null, mimeType: '', uri: '' }); } /********************************************************************************************** * MIME type / format. */ /** Returns the MIME type for this texture ('image/jpeg' or 'image/png'). */ getMimeType() { return this.get('mimeType') || ImageUtils.extensionToMimeType(FileUtils.extension(this.get('uri'))); } /** * Sets the MIME type for this texture ('image/jpeg' or 'image/png'). If the texture does not * have a URI, a MIME type is required for correct export. */ setMimeType(mimeType) { return this.set('mimeType', mimeType); } /********************************************************************************************** * URI / filename. */ /** Returns the URI (e.g. 'path/to/file.png') for this texture. */ getURI() { return this.get('uri'); } /** * Sets the URI (e.g. 'path/to/file.png') for this texture. If the texture does not have a MIME * type, a URI is required for correct export. */ setURI(uri) { this.set('uri', uri); const mimeType = ImageUtils.extensionToMimeType(FileUtils.extension(uri)); if (mimeType) this.set('mimeType', mimeType); return this; } /********************************************************************************************** * Image data. */ /** Returns the raw image data for this texture. */ getImage() { return this.get('image'); } /** Sets the raw image data for this texture. */ setImage(image) { return this.set('image', BufferUtils.assertView(image)); } /** Returns the size, in pixels, of this texture. */ getSize() { const image = this.get('image'); if (!image) return null; return ImageUtils.getSize(image, this.getMimeType()); } } /** * *Root property of a glTF asset.* * * Any properties to be exported with a particular asset must be referenced (directly or * indirectly) by the root. Metadata about the asset's license, generator, and glTF specification * version are stored in the asset, accessible with {@link Root.getAsset}. * * Properties are added to the root with factory methods on its {@link Document}, and removed by * calling {@link Property.dispose}() on the resource. Any properties that have been created but * not disposed will be included when calling the various `root.list*()` methods. * * A document's root cannot be removed, and no other root may be created. Unlike other * {@link Property} types, the `.dispose()`, `.detach()` methods have no useful function on a * Root property. * * Usage: * * ```ts * const root = document.getRoot(); * const scene = document.createScene('myScene'); * const node = document.createNode('myNode'); * scene.addChild(node); * * console.log(root.listScenes()); // → [scene x 1] * ``` * * Reference: [glTF → Concepts](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#concepts) * * @category Properties */ class Root extends ExtensibleProperty { init() { this.propertyType = exports.PropertyType.ROOT; } getDefaults() { return Object.assign(super.getDefaults(), { asset: { generator: `glTF-Transform ${VERSION}`, version: '2.0' }, defaultScene: null, accessors: new propertyGraph.RefSet(), animations: new propertyGraph.RefSet(), buffers: new propertyGraph.RefSet(), cameras: new propertyGraph.RefSet(), materials: new propertyGraph.RefSet(), meshes: new propertyGraph.RefSet(), nodes: new propertyGraph.RefSet(), scenes: new propertyGraph.RefSet(), skins: new propertyGraph.RefSet(), textures: new propertyGraph.RefSet() }); } /** @internal */ constructor(graph) { super(graph); this._extensions = new Set(); graph.addEventListener('node:create', event => { this._addChildOfRoot(event.target); }); } clone() { throw new Error('Root cannot be cloned.'); } copy(other, resolve) { if (resolve === void 0) { resolve = COPY_IDENTITY; } // Root cannot be cloned in isolation: only with its Document. Extensions are managed by // the Document during cloning. The Root, and only the Root, should keep existing // references while copying to avoid overwriting during a merge. if (resolve === COPY_IDENTITY) throw new Error('Root cannot be copied.'); // IMPORTANT: Root cannot call super.copy(), which removes existing references. this.set('asset', { ...other.get('asset') }); this.setName(other.getName()); this.setExtras({ ...other.getExtras() }); this.setDefaultScene(other.getDefaultScene() ? resolve(other.getDefaultScene()) : null); for (const extensionName of other.listRefMapKeys('extensions')) { const otherExtension = other.getExtension(extensionName); this.setExtension(extensionName, resolve(otherExtension)); } return this; } _addChildOfRoot(child) { if (child instanceof Scene) { this.addRef('scenes', child); } else if (child instanceof Node) { this.addRef('nodes', child); } else if (child instanceof Camera) { this.addRef('cameras', child); } else if (child instanceof Skin) { this.addRef('skins', child); } else if (child instanceof Mesh) { this.addRef('meshes', child); } else if (child instanceof Material) { this.addRef('materials', child); } else if (child instanceof Texture) { this.addRef('textures', child); } else if (child instanceof Animation) { this.addRef('animations', child); } else if (child instanceof Accessor) { this.addRef('accessors', child); } else if (child instanceof Buffer$1) { this.addRef('buffers', child); } // No error for untracked property types. return this; } /** * Returns the `asset` object, which specifies the target glTF version of the asset. Additional * metadata can be stored in optional properties such as `generator` or `copyright`. * * Reference: [glTF → Asset](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#asset) */ getAsset() { return this.get('asset'); } /********************************************************************************************** * Extensions. */ /** Lists all {@link Extension Extensions} enabled for this root. */ listExtensionsUsed() { return Array.from(this._extensions); } /** Lists all {@link Extension Extensions} enabled and required for this root. */ listExtensionsRequired() { return this.listExtensionsUsed().filter(extension => extension.isRequired()); } /** @internal */ _enableExtension(extension) { this._extensions.add(extension); return this; } /** @internal */ _disableExtension(extension) { this._extensions.delete(extension); return this; } /********************************************************************************************** * Properties. */ /** Lists all {@link Scene} properties associated with this root. */ listScenes() { return this.listRefs('scenes'); } /** Default {@link Scene} associated with this root. */ setDefaultScene(defaultScene) { return this.setRef('defaultScene', defaultScene); } /** Default {@link Scene} associated with this root. */ getDefaultScene() { return this.getRef('defaultScene'); } /** Lists all {@link Node} properties associated with this root. */ listNodes() { return this.listRefs('nodes'); } /** Lists all {@link Camera} properties associated with this root. */ listCameras() { return this.listRefs('cameras'); } /** Lists all {@link Skin} properties associated with this root. */ listSkins() { return this.listRefs('skins'); } /** Lists all {@link Mesh} properties associated with this root. */ listMeshes() { return this.listRefs('meshes'); } /** Lists all {@link Material} properties associated with this root. */ listMaterials() { return this.listRefs('materials'); } /** Lists all {@link Texture} properties associated with this root. */ listTextures() { return this.listRefs('textures'); } /** Lists all {@link Animation} properties associated with this root. */ listAnimations() { return this.listRefs('animations'); } /** Lists all {@link Accessor} properties associated with this root. */ listAccessors() { return this.listRefs('accessors'); } /** Lists all {@link Buffer} properties associated with this root. */ listBuffers() { return this.listRefs('buffers'); } } /** * *Wraps a glTF asset and its resources for easier modification.* * * Documents manage glTF assets and the relationships among dependencies. The document wrapper * allow tools to read and write changes without dealing with array indices or byte offsets, which * would otherwise require careful management over the course of a file modification. An internal * graph structure allows any property in the glTF file to maintain references to its dependencies, * and makes it easy to determine where a particular property dependency is being used. For * example, finding a list of materials that use a particular texture is as simple as calling * {@link Texture.listParents}(). * * A new resource {@link Property} (e.g. a {@link Mesh} or {@link Material}) is created by calling * 'create' methods on the document. Resources are destroyed by calling {@link Property.dispose}(). * * ```ts * import fs from 'fs/promises'; * import { Document } from '@gltf-transform/core'; * import { dedup } from '@gltf-transform/functions'; * * const document = new Document(); * * const texture1 = document.createTexture('myTexture') * .setImage(await fs.readFile('path/to/image.png')) * .setMimeType('image/png'); * const texture2 = document.createTexture('myTexture2') * .setImage(await fs.readFile('path/to/image2.png')) * .setMimeType('image/png'); * * // Document containing duplicate copies of the same texture. * document.getRoot().listTextures(); // → [texture x 2] * * await document.transform( * dedup({textures: true}), * // ... * ); * * // Document with duplicate textures removed. * document.getRoot().listTextures(); // → [texture x 1] * ``` * * Reference: * - [glTF → Basics](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#gltf-basics) * - [glTF → Concepts](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#concepts) * * @category Documents */ const _iteratorSymbol$1 = typeof Symbol !== "undefined" ? Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator")) : "@@iterator"; function _settle$2(pact, state, value) { if (!pact.s) { if (value instanceof _Pact$2) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle$2.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle$2.bind(null, pact, state), _settle$2.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; const observer = pact.o; if (observer) { observer(pact); } } } const _Pact$2 = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { const result = new _Pact(); const state = this.s; if (state) { const callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle$2(result, 1, callback(this.v)); } catch (e) { _settle$2(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { const value = _this.v; if (_this.s & 1) { _settle$2(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle$2(result, 1, onRejected(value)); } else { _settle$2(result, 2, value); } } catch (e) { _settle$2(result, 2, e); } }; return result; }; return _Pact; }(); function _isSettledPact$1(thenable) { return thenable instanceof _Pact$2 && thenable.s & 1; } function _forTo$1(array, body, check) { var i = -1, pact, reject; function _cycle(result) { try { while (++i < array.length && (!check || !check())) { result = body(i); if (result && result.then) { if (_isSettledPact$1(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$2.bind(null, pact = new _Pact$2(), 2))); return; } } } if (pact) { _settle$2(pact, 1, result); } else { pact = result; } } catch (e) { _settle$2(pact || (pact = new _Pact$2()), 2, e); } } _cycle(); return pact; } function _forOf$1(target, body, check) { if (typeof target[_iteratorSymbol$1] === "function") { var iterator = target[_iteratorSymbol$1](), step, pact, reject; function _cycle(result) { try { while (!(step = iterator.next()).done && (!check || !check())) { result = body(step.value); if (result && result.then) { if (_isSettledPact$1(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$2.bind(null, pact = new _Pact$2(), 2))); return; } } } if (pact) { _settle$2(pact, 1, result); } else { pact = result; } } catch (e) { _settle$2(pact || (pact = new _Pact$2()), 2, e); } } _cycle(); if (iterator.return) { var _fixup = function (value) { try { if (!step.done) { iterator.return(); } } catch (e) {} return value; }; if (pact && pact.then) { return pact.then(_fixup, function (e) { throw _fixup(e); }); } _fixup(); } return pact; } // No support for Symbol.iterator if (!("length" in target)) { throw new TypeError("Object is not iterable"); } // Handle live collections properly var values = []; for (var i = 0; i < target.length; i++) { values.push(target[i]); } return _forTo$1(values, function (i) { return body(values[i]); }, check); } class Document { /** * Returns the Document associated with a given Graph, if any. * @hidden * @experimental */ static fromGraph(graph) { return Document._GRAPH_DOCUMENTS.get(graph) || null; } /** Creates a new Document, representing an empty glTF asset. */ constructor() { this._graph = new propertyGraph.Graph(); this._root = new Root(this._graph); this._logger = Logger.DEFAULT_INSTANCE; Document._GRAPH_DOCUMENTS.set(this._graph, this); } /** Returns the glTF {@link Root} property. */ getRoot() { return this._root; } /** * Returns the {@link Graph} representing connectivity of resources within this document. * @hidden */ getGraph() { return this._graph; } /** Returns the {@link Logger} instance used for any operations performed on this document. */ getLogger() { return this._logger; } /** * Overrides the {@link Logger} instance used for any operations performed on this document. * * Usage: * * ```ts * doc * .setLogger(new Logger(Logger.Verbosity.SILENT)) * .transform(dedup(), weld()); * ``` */ setLogger(logger) { this._logger = logger; return this; } /** * Clones this Document, copying all resources within it. * @deprecated Use 'cloneDocument(document)' from '@gltf-transform/functions'. * @hidden * @internal */ clone() { throw new Error(`Use 'cloneDocument(source)' from '@gltf-transform/functions'.`); } /** * Merges the content of another Document into this one, without affecting the original. * @deprecated Use 'mergeDocuments(target, source)' from '@gltf-transform/functions'. * @hidden * @internal */ merge(_other) { throw new Error(`Use 'mergeDocuments(target, source)' from '@gltf-transform/functions'.`); } /** * Applies a series of modifications to this document. Each transformation is asynchronous, * takes the {@link Document} as input, and returns nothing. Transforms are applied in the * order given, which may affect the final result. * * Usage: * * ```ts * await doc.transform( * dedup(), * prune() * ); * ``` * * @param transforms List of synchronous transformation functions to apply. */ transform() { try { const _arguments = arguments, _this = this; var transforms = [].slice.call(_arguments); const stack = transforms.map(fn => fn.name); const _temp = _forOf$1(transforms, function (transform) { return Promise.resolve(transform(_this, { stack })).then(function () {}); }); return Promise.resolve(_temp && _temp.then ? _temp.then(function () { return _this; }) : _this); } catch (e) { return Promise.reject(e); } } /********************************************************************************************** * Extension factory method. */ /** * Creates a new {@link Extension}, for the extension type of the given constructor. If the * extension is already enabled for this Document, the previous Extension reference is reused. */ createExtension(ctor) { const extensionName = ctor.EXTENSION_NAME; const prevExtension = this.getRoot().listExtensionsUsed().find(ext => ext.extensionName === extensionName); return prevExtension || new ctor(this); } /********************************************************************************************** * Property factory methods. */ /** Creates a new {@link Scene} attached to this document's {@link Root}. */ createScene(name) { if (name === void 0) { name = ''; } return new Scene(this._graph, name); } /** Creates a new {@link Node} attached to this document's {@link Root}. */ createNode(name) { if (name === void 0) { name = ''; } return new Node(this._graph, name); } /** Creates a new {@link Camera} attached to this document's {@link Root}. */ createCamera(name) { if (name === void 0) { name = ''; } return new Camera(this._graph, name); } /** Creates a new {@link Skin} attached to this document's {@link Root}. */ createSkin(name) { if (name === void 0) { name = ''; } return new Skin(this._graph, name); } /** Creates a new {@link Mesh} attached to this document's {@link Root}. */ createMesh(name) { if (name === void 0) { name = ''; } return new Mesh(this._graph, name); } /** * Creates a new {@link Primitive}. Primitives must be attached to a {@link Mesh} * for use and export; they are not otherwise associated with a {@link Root}. */ createPrimitive() { return new Primitive(this._graph); } /** * Creates a new {@link PrimitiveTarget}, or morph target. Targets must be attached to a * {@link Primitive} for use and export; they are not otherwise associated with a {@link Root}. */ createPrimitiveTarget(name) { if (name === void 0) { name = ''; } return new PrimitiveTarget(this._graph, name); } /** Creates a new {@link Material} attached to this document's {@link Root}. */ createMaterial(name) { if (name === void 0) { name = ''; } return new Material(this._graph, name); } /** Creates a new {@link Texture} attached to this document's {@link Root}. */ createTexture(name) { if (name === void 0) { name = ''; } return new Texture(this._graph, name); } /** Creates a new {@link Animation} attached to this document's {@link Root}. */ createAnimation(name) { if (name === void 0) { name = ''; } return new Animation(this._graph, name); } /** * Creates a new {@link AnimationChannel}. Channels must be attached to an {@link Animation} * for use and export; they are not otherwise associated with a {@link Root}. */ createAnimationChannel(name) { if (name === void 0) { name = ''; } return new AnimationChannel(this._graph, name); } /** * Creates a new {@link AnimationSampler}. Samplers must be attached to an {@link Animation} * for use and export; they are not otherwise associated with a {@link Root}. */ createAnimationSampler(name) { if (name === void 0) { name = ''; } return new AnimationSampler(this._graph, name); } /** Creates a new {@link Accessor} attached to this document's {@link Root}. */ createAccessor(name, buffer) { if (name === void 0) { name = ''; } if (buffer === void 0) { buffer = null; } if (!buffer) { buffer = this.getRoot().listBuffers()[0]; } return new Accessor(this._graph, name).setBuffer(buffer); } /** Creates a new {@link Buffer} attached to this document's {@link Root}. */ createBuffer(name) { if (name === void 0) { name = ''; } return new Buffer$1(this._graph, name); } } /** * Enables lookup of a Document from its Graph. For internal use, only. * @internal * @experimental */ Document._GRAPH_DOCUMENTS = new WeakMap(); /** * *Base class for all Extensions.* * * Extensions enhance a glTF {@link Document} with additional features and schema, beyond the core * glTF specification. Common extensions may be imported from the `@gltf-transform/extensions` * package, or custom extensions may be created by extending this base class. * * An extension is added to a Document by calling {@link Document.createExtension} with the * extension constructor. The extension object may then be used to construct * {@link ExtensionProperty} instances, which are attached to properties throughout the Document * as prescribed by the extension itself. * * For more information on available extensions and their usage, see [Extensions](/extensions). * * Reference: * - [glTF → Extensions](https://github.com/KhronosGroup/gltf/blob/main/specification/2.0/README.md#specifying-extensions) * - [glTF Extension Registry](https://github.com/KhronosGroup/gltf/blob/main/extensions) * * @category Extensions */ class Extension { /** @hidden */ constructor(document) { /** Official name of the extension. */ this.extensionName = ''; /** * Before reading, extension should be called for these {@link Property} types. *Most * extensions don't need to implement this.* * @hidden */ this.prereadTypes = []; /** * Before writing, extension should be called for these {@link Property} types. *Most * extensions don't need to implement this.* * @hidden */ this.prewriteTypes = []; /** @hidden Dependency IDs needed to read this extension, to be installed before I/O. */ this.readDependencies = []; /** @hidden Dependency IDs needed to write this extension, to be installed before I/O. */ this.writeDependencies = []; /** @hidden */ this.document = void 0; /** @hidden */ this.required = false; /** @hidden */ this.properties = new Set(); /** @hidden */ this._listener = void 0; this.document = document; document.getRoot()._enableExtension(this); this._listener = _event => { const event = _event; const target = event.target; if (target instanceof ExtensionProperty && target.extensionName === this.extensionName) { if (event.type === 'node:create') this._addExtensionProperty(target); if (event.type === 'node:dispose') this._removeExtensionProperty(target); } }; const graph = document.getGraph(); graph.addEventListener('node:create', this._listener); graph.addEventListener('node:dispose', this._listener); } /** Disables and removes the extension from the Document. */ dispose() { this.document.getRoot()._disableExtension(this); const graph = this.document.getGraph(); graph.removeEventListener('node:create', this._listener); graph.removeEventListener('node:dispose', this._listener); for (const property of this.properties) { property.dispose(); } } /** @hidden Performs first-time setup for the extension. Must be idempotent. */ static register() {} /** * Indicates to the client whether it is OK to load the asset when this extension is not * recognized. Optional extensions are generally preferred, if there is not a good reason * to require a client to completely fail when an extension isn't known. */ isRequired() { return this.required; } /** * Indicates to the client whether it is OK to load the asset when this extension is not * recognized. Optional extensions are generally preferred, if there is not a good reason * to require a client to completely fail when an extension isn't known. */ setRequired(required) { this.required = required; return this; } /** * Lists all {@link ExtensionProperty} instances associated with, or created by, this * extension. Includes only instances that are attached to the Document's graph; detached * instances will be excluded. */ listProperties() { return Array.from(this.properties); } /********************************************************************************************** * ExtensionProperty management. */ /** @internal */ _addExtensionProperty(property) { this.properties.add(property); return this; } /** @internal */ _removeExtensionProperty(property) { this.properties.delete(property); return this; } /********************************************************************************************** * I/O implementation. */ /** @hidden Installs dependencies required by the extension. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars install(key, dependency) { return this; } /** * Used by the {@link PlatformIO} utilities when reading a glTF asset. This method may * optionally be implemented by an extension, and should then support any property type * declared by the Extension's {@link Extension.prereadTypes} list. The Extension will * be given a ReaderContext instance, and is expected to update either the context or its * {@link JSONDocument} with resources known to the Extension. *Most extensions don't need to * implement this.* * @hidden */ preread(_readerContext, _propertyType) { return this; } /** * Used by the {@link PlatformIO} utilities when writing a glTF asset. This method may * optionally be implemented by an extension, and should then support any property type * declared by the Extension's {@link Extension.prewriteTypes} list. The Extension will * be given a WriterContext instance, and is expected to update either the context or its * {@link JSONDocument} with resources known to the Extension. *Most extensions don't need to * implement this.* * @hidden */ prewrite(_writerContext, _propertyType) { return this; } } /** Official name of the extension. */ Extension.EXTENSION_NAME = void 0; /** * Model class providing glTF Transform objects representing each definition in the glTF file, used * by a {@link GLTFReader} and its {@link Extension} implementations. Indices of all properties will be * consistent with the glTF file. * * @hidden */ class ReaderContext { constructor(jsonDoc) { this.jsonDoc = void 0; this.buffers = []; this.bufferViews = []; this.bufferViewBuffers = []; this.accessors = []; this.textures = []; this.textureInfos = new Map(); this.materials = []; this.meshes = []; this.cameras = []; this.nodes = []; this.skins = []; this.animations = []; this.scenes = []; this.jsonDoc = jsonDoc; } setTextureInfo(textureInfo, textureInfoDef) { this.textureInfos.set(textureInfo, textureInfoDef); if (textureInfoDef.texCoord !== undefined) { textureInfo.setTexCoord(textureInfoDef.texCoord); } if (textureInfoDef.extras !== undefined) { textureInfo.setExtras(textureInfoDef.extras); } const textureDef = this.jsonDoc.json.textures[textureInfoDef.index]; if (textureDef.sampler === undefined) return; const samplerDef = this.jsonDoc.json.samplers[textureDef.sampler]; if (samplerDef.magFilter !== undefined) { textureInfo.setMagFilter(samplerDef.magFilter); } if (samplerDef.minFilter !== undefined) { textureInfo.setMinFilter(samplerDef.minFilter); } if (samplerDef.wrapS !== undefined) { textureInfo.setWrapS(samplerDef.wrapS); } if (samplerDef.wrapT !== undefined) { textureInfo.setWrapT(samplerDef.wrapT); } } } const DEFAULT_OPTIONS = { logger: Logger.DEFAULT_INSTANCE, extensions: [], dependencies: {} }; const SUPPORTED_PREREAD_TYPES = new Set([exports.PropertyType.BUFFER, exports.PropertyType.TEXTURE, exports.PropertyType.MATERIAL, exports.PropertyType.MESH, exports.PropertyType.PRIMITIVE, exports.PropertyType.NODE, exports.PropertyType.SCENE]); /** @internal */ class GLTFReader { static read(jsonDoc, _options) { if (_options === void 0) { _options = DEFAULT_OPTIONS; } const options = { ...DEFAULT_OPTIONS, ..._options }; const { json } = jsonDoc; const document = new Document().setLogger(options.logger); this.validate(jsonDoc, options); /* Reader context. */ const context = new ReaderContext(jsonDoc); /** Asset. */ const assetDef = json.asset; const asset = document.getRoot().getAsset(); if (assetDef.copyright) asset.copyright = assetDef.copyright; if (assetDef.extras) asset.extras = assetDef.extras; if (json.extras !== undefined) { document.getRoot().setExtras({ ...json.extras }); } /** Extensions (1/2). */ const extensionsUsed = json.extensionsUsed || []; const extensionsRequired = json.extensionsRequired || []; options.extensions.sort((a, b) => a.EXTENSION_NAME > b.EXTENSION_NAME ? 1 : -1); for (const Extension of options.extensions) { if (extensionsUsed.includes(Extension.EXTENSION_NAME)) { // Create extension. const extension = document.createExtension(Extension).setRequired(extensionsRequired.includes(Extension.EXTENSION_NAME)); // Warn on unsupported preread hooks. const unsupportedHooks = extension.prereadTypes.filter(type => !SUPPORTED_PREREAD_TYPES.has(type)); if (unsupportedHooks.length) { options.logger.warn(`Preread hooks for some types (${unsupportedHooks.join()}), requested by extension ` + `${extension.extensionName}, are unsupported. Please file an issue or a PR.`); } // Install dependencies. for (const key of extension.readDependencies) { extension.install(key, options.dependencies[key]); } } } /** Buffers. */ const bufferDefs = json.buffers || []; document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.BUFFER)).forEach(extension => extension.preread(context, exports.PropertyType.BUFFER)); context.buffers = bufferDefs.map(bufferDef => { const buffer = document.createBuffer(bufferDef.name); if (bufferDef.extras) buffer.setExtras(bufferDef.extras); if (bufferDef.uri && bufferDef.uri.indexOf('__') !== 0) { buffer.setURI(bufferDef.uri); } return buffer; }); /** Buffer views. */ const bufferViewDefs = json.bufferViews || []; context.bufferViewBuffers = bufferViewDefs.map((bufferViewDef, index) => { if (!context.bufferViews[index]) { const bufferDef = jsonDoc.json.buffers[bufferViewDef.buffer]; const resource = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER]; const byteOffset = bufferViewDef.byteOffset || 0; context.bufferViews[index] = BufferUtils.toView(resource, byteOffset, bufferViewDef.byteLength); } return context.buffers[bufferViewDef.buffer]; }); /** Accessors. */ // Accessor .count and .componentType properties are inferred dynamically. const accessorDefs = json.accessors || []; context.accessors = accessorDefs.map(accessorDef => { const buffer = context.bufferViewBuffers[accessorDef.bufferView]; const accessor = document.createAccessor(accessorDef.name, buffer).setType(accessorDef.type); if (accessorDef.extras) accessor.setExtras(accessorDef.extras); if (accessorDef.normalized !== undefined) { accessor.setNormalized(accessorDef.normalized); } // Sparse accessors, KHR_draco_mesh_compression, and EXT_meshopt_compression. if (accessorDef.bufferView === undefined) return accessor; // NOTICE: We mark sparse accessors at the end of the I/O reading process. Consider an // accessor to be 'sparse' if it (A) includes sparse value overrides, or (B) does not // define .bufferView _and_ no extension provides that data. accessor.setArray(getAccessorArray(accessorDef, context)); return accessor; }); /** Textures. */ // glTF Transform's "Texture" properties correspond 1:1 with glTF "Image" properties, and // with image files. The glTF file may contain more one texture per image, where images // are reused with different sampler properties. const imageDefs = json.images || []; const textureDefs = json.textures || []; document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.TEXTURE)).forEach(extension => extension.preread(context, exports.PropertyType.TEXTURE)); context.textures = imageDefs.map(imageDef => { const texture = document.createTexture(imageDef.name); // glTF Image corresponds 1:1 with glTF Transform Texture. See `writer.ts`. if (imageDef.extras) texture.setExtras(imageDef.extras); if (imageDef.bufferView !== undefined) { const bufferViewDef = json.bufferViews[imageDef.bufferView]; const bufferDef = jsonDoc.json.buffers[bufferViewDef.buffer]; const bufferData = bufferDef.uri ? jsonDoc.resources[bufferDef.uri] : jsonDoc.resources[GLB_BUFFER]; const byteOffset = bufferViewDef.byteOffset || 0; const byteLength = bufferViewDef.byteLength; const imageData = bufferData.slice(byteOffset, byteOffset + byteLength); texture.setImage(imageData); } else if (imageDef.uri !== undefined) { texture.setImage(jsonDoc.resources[imageDef.uri]); if (imageDef.uri.indexOf('__') !== 0) { texture.setURI(imageDef.uri); } } if (imageDef.mimeType !== undefined) { texture.setMimeType(imageDef.mimeType); } else if (imageDef.uri) { const extension = FileUtils.extension(imageDef.uri); texture.setMimeType(ImageUtils.extensionToMimeType(extension)); } return texture; }); /** Materials. */ document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.MATERIAL)).forEach(extension => extension.preread(context, exports.PropertyType.MATERIAL)); const materialDefs = json.materials || []; context.materials = materialDefs.map(materialDef => { const material = document.createMaterial(materialDef.name); if (materialDef.extras) material.setExtras(materialDef.extras); // Program state & blending. if (materialDef.alphaMode !== undefined) { material.setAlphaMode(materialDef.alphaMode); } if (materialDef.alphaCutoff !== undefined) { material.setAlphaCutoff(materialDef.alphaCutoff); } if (materialDef.doubleSided !== undefined) { material.setDoubleSided(materialDef.doubleSided); } // Factors. const pbrDef = materialDef.pbrMetallicRoughness || {}; if (pbrDef.baseColorFactor !== undefined) { material.setBaseColorFactor(pbrDef.baseColorFactor); } if (materialDef.emissiveFactor !== undefined) { material.setEmissiveFactor(materialDef.emissiveFactor); } if (pbrDef.metallicFactor !== undefined) { material.setMetallicFactor(pbrDef.metallicFactor); } if (pbrDef.roughnessFactor !== undefined) { material.setRoughnessFactor(pbrDef.roughnessFactor); } // Textures. if (pbrDef.baseColorTexture !== undefined) { const textureInfoDef = pbrDef.baseColorTexture; const texture = context.textures[textureDefs[textureInfoDef.index].source]; material.setBaseColorTexture(texture); context.setTextureInfo(material.getBaseColorTextureInfo(), textureInfoDef); } if (materialDef.emissiveTexture !== undefined) { const textureInfoDef = materialDef.emissiveTexture; const texture = context.textures[textureDefs[textureInfoDef.index].source]; material.setEmissiveTexture(texture); context.setTextureInfo(material.getEmissiveTextureInfo(), textureInfoDef); } if (materialDef.normalTexture !== undefined) { const textureInfoDef = materialDef.normalTexture; const texture = context.textures[textureDefs[textureInfoDef.index].source]; material.setNormalTexture(texture); context.setTextureInfo(material.getNormalTextureInfo(), textureInfoDef); if (materialDef.normalTexture.scale !== undefined) { material.setNormalScale(materialDef.normalTexture.scale); } } if (materialDef.occlusionTexture !== undefined) { const textureInfoDef = materialDef.occlusionTexture; const texture = context.textures[textureDefs[textureInfoDef.index].source]; material.setOcclusionTexture(texture); context.setTextureInfo(material.getOcclusionTextureInfo(), textureInfoDef); if (materialDef.occlusionTexture.strength !== undefined) { material.setOcclusionStrength(materialDef.occlusionTexture.strength); } } if (pbrDef.metallicRoughnessTexture !== undefined) { const textureInfoDef = pbrDef.metallicRoughnessTexture; const texture = context.textures[textureDefs[textureInfoDef.index].source]; material.setMetallicRoughnessTexture(texture); context.setTextureInfo(material.getMetallicRoughnessTextureInfo(), textureInfoDef); } return material; }); /** Meshes. */ document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.MESH)).forEach(extension => extension.preread(context, exports.PropertyType.MESH)); const meshDefs = json.meshes || []; document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.PRIMITIVE)).forEach(extension => extension.preread(context, exports.PropertyType.PRIMITIVE)); context.meshes = meshDefs.map(meshDef => { const mesh = document.createMesh(meshDef.name); if (meshDef.extras) mesh.setExtras(meshDef.extras); if (meshDef.weights !== undefined) { mesh.setWeights(meshDef.weights); } const primitiveDefs = meshDef.primitives || []; primitiveDefs.forEach(primitiveDef => { const primitive = document.createPrimitive(); if (primitiveDef.extras) primitive.setExtras(primitiveDef.extras); if (primitiveDef.material !== undefined) { primitive.setMaterial(context.materials[primitiveDef.material]); } if (primitiveDef.mode !== undefined) { primitive.setMode(primitiveDef.mode); } for (const [semantic, index] of Object.entries(primitiveDef.attributes || {})) { primitive.setAttribute(semantic, context.accessors[index]); } if (primitiveDef.indices !== undefined) { primitive.setIndices(context.accessors[primitiveDef.indices]); } const targetNames = meshDef.extras && meshDef.extras.targetNames || []; const targetDefs = primitiveDef.targets || []; targetDefs.forEach((targetDef, targetIndex) => { const targetName = targetNames[targetIndex] || targetIndex.toString(); const target = document.createPrimitiveTarget(targetName); for (const [semantic, accessorIndex] of Object.entries(targetDef)) { target.setAttribute(semantic, context.accessors[accessorIndex]); } primitive.addTarget(target); }); mesh.addPrimitive(primitive); }); return mesh; }); /** Cameras. */ const cameraDefs = json.cameras || []; context.cameras = cameraDefs.map(cameraDef => { const camera = document.createCamera(cameraDef.name).setType(cameraDef.type); if (cameraDef.extras) camera.setExtras(cameraDef.extras); if (cameraDef.type === Camera.Type.PERSPECTIVE) { const perspectiveDef = cameraDef.perspective; camera.setYFov(perspectiveDef.yfov); camera.setZNear(perspectiveDef.znear); if (perspectiveDef.zfar !== undefined) { camera.setZFar(perspectiveDef.zfar); } if (perspectiveDef.aspectRatio !== undefined) { camera.setAspectRatio(perspectiveDef.aspectRatio); } } else { const orthoDef = cameraDef.orthographic; camera.setZNear(orthoDef.znear).setZFar(orthoDef.zfar).setXMag(orthoDef.xmag).setYMag(orthoDef.ymag); } return camera; }); /** Nodes. */ const nodeDefs = json.nodes || []; document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.NODE)).forEach(extension => extension.preread(context, exports.PropertyType.NODE)); context.nodes = nodeDefs.map(nodeDef => { const node = document.createNode(nodeDef.name); if (nodeDef.extras) node.setExtras(nodeDef.extras); if (nodeDef.translation !== undefined) { node.setTranslation(nodeDef.translation); } if (nodeDef.rotation !== undefined) { node.setRotation(nodeDef.rotation); } if (nodeDef.scale !== undefined) { node.setScale(nodeDef.scale); } if (nodeDef.matrix !== undefined) { const translation = [0, 0, 0]; const rotation = [0, 0, 0, 1]; const scale = [1, 1, 1]; MathUtils.decompose(nodeDef.matrix, translation, rotation, scale); node.setTranslation(translation); node.setRotation(rotation); node.setScale(scale); } if (nodeDef.weights !== undefined) { node.setWeights(nodeDef.weights); } // Attachments (mesh, camera, skin) defined later in reading process. return node; }); /** Skins. */ const skinDefs = json.skins || []; context.skins = skinDefs.map(skinDef => { const skin = document.createSkin(skinDef.name); if (skinDef.extras) skin.setExtras(skinDef.extras); if (skinDef.inverseBindMatrices !== undefined) { skin.setInverseBindMatrices(context.accessors[skinDef.inverseBindMatrices]); } if (skinDef.skeleton !== undefined) { skin.setSkeleton(context.nodes[skinDef.skeleton]); } for (const nodeIndex of skinDef.joints) { skin.addJoint(context.nodes[nodeIndex]); } return skin; }); /** Node attachments. */ nodeDefs.map((nodeDef, nodeIndex) => { const node = context.nodes[nodeIndex]; const children = nodeDef.children || []; children.forEach(childIndex => node.addChild(context.nodes[childIndex])); if (nodeDef.mesh !== undefined) node.setMesh(context.meshes[nodeDef.mesh]); if (nodeDef.camera !== undefined) node.setCamera(context.cameras[nodeDef.camera]); if (nodeDef.skin !== undefined) node.setSkin(context.skins[nodeDef.skin]); }); /** Animations. */ const animationDefs = json.animations || []; context.animations = animationDefs.map(animationDef => { const animation = document.createAnimation(animationDef.name); if (animationDef.extras) animation.setExtras(animationDef.extras); const samplerDefs = animationDef.samplers || []; const samplers = samplerDefs.map(samplerDef => { const sampler = document.createAnimationSampler().setInput(context.accessors[samplerDef.input]).setOutput(context.accessors[samplerDef.output]).setInterpolation(samplerDef.interpolation || AnimationSampler.Interpolation.LINEAR); if (samplerDef.extras) sampler.setExtras(samplerDef.extras); animation.addSampler(sampler); return sampler; }); const channels = animationDef.channels || []; channels.forEach(channelDef => { const channel = document.createAnimationChannel().setSampler(samplers[channelDef.sampler]).setTargetPath(channelDef.target.path); if (channelDef.target.node !== undefined) channel.setTargetNode(context.nodes[channelDef.target.node]); if (channelDef.extras) channel.setExtras(channelDef.extras); animation.addChannel(channel); }); return animation; }); /** Scenes. */ const sceneDefs = json.scenes || []; document.getRoot().listExtensionsUsed().filter(extension => extension.prereadTypes.includes(exports.PropertyType.SCENE)).forEach(extension => extension.preread(context, exports.PropertyType.SCENE)); context.scenes = sceneDefs.map(sceneDef => { const scene = document.createScene(sceneDef.name); if (sceneDef.extras) scene.setExtras(sceneDef.extras); const children = sceneDef.nodes || []; children.map(nodeIndex => context.nodes[nodeIndex]).forEach(node => scene.addChild(node)); return scene; }); if (json.scene !== undefined) { document.getRoot().setDefaultScene(context.scenes[json.scene]); } /** Extensions (2/2). */ document.getRoot().listExtensionsUsed().forEach(extension => extension.read(context)); /** Post-processing. */ // Consider an accessor to be 'sparse' if it (A) includes sparse value overrides, // or (B) does not define .bufferView _and_ no extension provides that data. Case // (B) represents a zero-filled accessor. accessorDefs.forEach((accessorDef, index) => { const accessor = context.accessors[index]; const hasSparseValues = !!accessorDef.sparse; const isZeroFilled = !accessorDef.bufferView && !accessor.getArray(); if (hasSparseValues || isZeroFilled) { accessor.setSparse(true).setArray(getSparseArray(accessorDef, context)); } }); return document; } static validate(jsonDoc, options) { const json = jsonDoc.json; if (json.asset.version !== '2.0') { throw new Error(`Unsupported glTF version, "${json.asset.version}".`); } if (json.extensionsRequired) { for (const extensionName of json.extensionsRequired) { if (!options.extensions.find(extension => extension.EXTENSION_NAME === extensionName)) { throw new Error(`Missing required extension, "${extensionName}".`); } } } if (json.extensionsUsed) { for (const extensionName of json.extensionsUsed) { if (!options.extensions.find(extension => extension.EXTENSION_NAME === extensionName)) { options.logger.warn(`Missing optional extension, "${extensionName}".`); } } } } } /** * Returns the contents of an interleaved accessor, as a typed array. * @internal */ function getInterleavedArray(accessorDef, context) { const jsonDoc = context.jsonDoc; const bufferView = context.bufferViews[accessorDef.bufferView]; const bufferViewDef = jsonDoc.json.bufferViews[accessorDef.bufferView]; const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType]; const elementSize = Accessor.getElementSize(accessorDef.type); const componentSize = TypedArray.BYTES_PER_ELEMENT; const accessorByteOffset = accessorDef.byteOffset || 0; const array = new TypedArray(accessorDef.count * elementSize); const view = new DataView(bufferView.buffer, bufferView.byteOffset, bufferView.byteLength); const byteStride = bufferViewDef.byteStride; for (let i = 0; i < accessorDef.count; i++) { for (let j = 0; j < elementSize; j++) { const byteOffset = accessorByteOffset + i * byteStride + j * componentSize; let value; switch (accessorDef.componentType) { case Accessor.ComponentType.FLOAT: value = view.getFloat32(byteOffset, true); break; case Accessor.ComponentType.UNSIGNED_INT: value = view.getUint32(byteOffset, true); break; case Accessor.ComponentType.UNSIGNED_SHORT: value = view.getUint16(byteOffset, true); break; case Accessor.ComponentType.UNSIGNED_BYTE: value = view.getUint8(byteOffset); break; case Accessor.ComponentType.SHORT: value = view.getInt16(byteOffset, true); break; case Accessor.ComponentType.BYTE: value = view.getInt8(byteOffset); break; default: throw new Error(`Unexpected componentType "${accessorDef.componentType}".`); } array[i * elementSize + j] = value; } } return array; } /** * Returns the contents of an accessor, as a typed array. * @internal */ function getAccessorArray(accessorDef, context) { const jsonDoc = context.jsonDoc; const bufferView = context.bufferViews[accessorDef.bufferView]; const bufferViewDef = jsonDoc.json.bufferViews[accessorDef.bufferView]; const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType]; const elementSize = Accessor.getElementSize(accessorDef.type); const componentSize = TypedArray.BYTES_PER_ELEMENT; const elementStride = elementSize * componentSize; // Interleaved buffer view. if (bufferViewDef.byteStride !== undefined && bufferViewDef.byteStride !== elementStride) { return getInterleavedArray(accessorDef, context); } const byteOffset = bufferView.byteOffset + (accessorDef.byteOffset || 0); const byteLength = accessorDef.count * elementSize * componentSize; // Might optimize this to avoid deep copy later, but it's useful for now and not a known // bottleneck. See https://github.com/donmccurdy/glTF-Transform/issues/256. return new TypedArray(bufferView.buffer.slice(byteOffset, byteOffset + byteLength)); } /** * Returns the contents of a sparse accessor, as a typed array. * @internal */ function getSparseArray(accessorDef, context) { const TypedArray = ComponentTypeToTypedArray[accessorDef.componentType]; const elementSize = Accessor.getElementSize(accessorDef.type); let array; if (accessorDef.bufferView !== undefined) { array = getAccessorArray(accessorDef, context); } else { array = new TypedArray(accessorDef.count * elementSize); } const sparseDef = accessorDef.sparse; if (!sparseDef) return array; // Zero-filled accessor. const count = sparseDef.count; const indicesDef = { ...accessorDef, ...sparseDef.indices, count, type: 'SCALAR' }; const valuesDef = { ...accessorDef, ...sparseDef.values, count }; const indices = getAccessorArray(indicesDef, context); const values = getAccessorArray(valuesDef, context); // Override indices given in the sparse data. for (let i = 0; i < indicesDef.count; i++) { for (let j = 0; j < elementSize; j++) { array[indices[i] * elementSize + j] = values[i * elementSize + j]; } } return array; } var BufferViewTarget; (function (BufferViewTarget) { BufferViewTarget[BufferViewTarget["ARRAY_BUFFER"] = 34962] = "ARRAY_BUFFER"; BufferViewTarget[BufferViewTarget["ELEMENT_ARRAY_BUFFER"] = 34963] = "ELEMENT_ARRAY_BUFFER"; })(BufferViewTarget || (BufferViewTarget = {})); /** * Model class providing writing state to a {@link GLTFWriter} and its {@link Extension} * implementations. * * @hidden */ class WriterContext { constructor(_doc, jsonDoc, options) { this._doc = void 0; this.jsonDoc = void 0; this.options = void 0; this.accessorIndexMap = new Map(); this.animationIndexMap = new Map(); this.bufferIndexMap = new Map(); this.cameraIndexMap = new Map(); this.skinIndexMap = new Map(); this.materialIndexMap = new Map(); this.meshIndexMap = new Map(); this.nodeIndexMap = new Map(); this.imageIndexMap = new Map(); this.textureDefIndexMap = new Map(); // textureDef JSON -> index this.textureInfoDefMap = new Map(); this.samplerDefIndexMap = new Map(); // samplerDef JSON -> index this.sceneIndexMap = new Map(); this.imageBufferViews = []; this.otherBufferViews = new Map(); this.otherBufferViewsIndexMap = new Map(); this.extensionData = {}; this.bufferURIGenerator = void 0; this.imageURIGenerator = void 0; this.logger = void 0; this._accessorUsageMap = new Map(); this.accessorUsageGroupedByParent = new Set(['ARRAY_BUFFER']); this.accessorParents = new Map(); this._doc = _doc; this.jsonDoc = jsonDoc; this.options = options; const root = _doc.getRoot(); const numBuffers = root.listBuffers().length; const numImages = root.listTextures().length; this.bufferURIGenerator = new UniqueURIGenerator(numBuffers > 1, () => options.basename || 'buffer'); this.imageURIGenerator = new UniqueURIGenerator(numImages > 1, texture => getSlot(_doc, texture) || options.basename || 'texture'); this.logger = _doc.getLogger(); } /** * Creates a TextureInfo definition, and any Texture or Sampler definitions it requires. If * possible, Texture and Sampler definitions are shared. */ createTextureInfoDef(texture, textureInfo) { const samplerDef = { magFilter: textureInfo.getMagFilter() || undefined, minFilter: textureInfo.getMinFilter() || undefined, wrapS: textureInfo.getWrapS(), wrapT: textureInfo.getWrapT() }; const samplerKey = JSON.stringify(samplerDef); if (!this.samplerDefIndexMap.has(samplerKey)) { this.samplerDefIndexMap.set(samplerKey, this.jsonDoc.json.samplers.length); this.jsonDoc.json.samplers.push(samplerDef); } const textureDef = { source: this.imageIndexMap.get(texture), sampler: this.samplerDefIndexMap.get(samplerKey) }; const textureKey = JSON.stringify(textureDef); if (!this.textureDefIndexMap.has(textureKey)) { this.textureDefIndexMap.set(textureKey, this.jsonDoc.json.textures.length); this.jsonDoc.json.textures.push(textureDef); } const textureInfoDef = { index: this.textureDefIndexMap.get(textureKey) }; if (textureInfo.getTexCoord() !== 0) { textureInfoDef.texCoord = textureInfo.getTexCoord(); } if (Object.keys(textureInfo.getExtras()).length > 0) { textureInfoDef.extras = textureInfo.getExtras(); } this.textureInfoDefMap.set(textureInfo, textureInfoDef); return textureInfoDef; } createPropertyDef(property) { const def = {}; if (property.getName()) { def.name = property.getName(); } if (Object.keys(property.getExtras()).length > 0) { def.extras = property.getExtras(); } return def; } createAccessorDef(accessor) { const accessorDef = this.createPropertyDef(accessor); accessorDef.type = accessor.getType(); accessorDef.componentType = accessor.getComponentType(); accessorDef.count = accessor.getCount(); const needsBounds = this._doc.getGraph().listParentEdges(accessor).some(edge => edge.getName() === 'attributes' && edge.getAttributes().key === 'POSITION' || edge.getName() === 'input'); if (needsBounds) { accessorDef.max = accessor.getMax([]).map(Math.fround); accessorDef.min = accessor.getMin([]).map(Math.fround); } if (accessor.getNormalized()) { accessorDef.normalized = accessor.getNormalized(); } return accessorDef; } createImageData(imageDef, data, texture) { if (this.options.format === exports.Format.GLB) { this.imageBufferViews.push(data); imageDef.bufferView = this.jsonDoc.json.bufferViews.length; this.jsonDoc.json.bufferViews.push({ buffer: 0, byteOffset: -1, // determined while iterating buffers, in Writer.ts. byteLength: data.byteLength }); } else { const extension = ImageUtils.mimeTypeToExtension(texture.getMimeType()); imageDef.uri = this.imageURIGenerator.createURI(texture, extension); this.assignResourceURI(imageDef.uri, data, false); } } assignResourceURI(uri, data, throwOnConflict) { const resources = this.jsonDoc.resources; // https://github.com/KhronosGroup/glTF/issues/2446 if (!(uri in resources)) { resources[uri] = data; return; } if (data === resources[uri]) { this.logger.warn(`Duplicate resource URI, "${uri}".`); return; } const conflictMessage = `Resource URI "${uri}" already assigned to different data.`; if (!throwOnConflict) { this.logger.warn(conflictMessage); return; } throw new Error(conflictMessage); } /** * Returns implicit usage type of the given accessor, related to grouping accessors into * buffer views. Usage is a superset of buffer view target, including ARRAY_BUFFER and * ELEMENT_ARRAY_BUFFER, but also usages that do not match GPU buffer view targets such as * IBMs. Additional usages are defined by extensions, like `EXT_mesh_gpu_instancing`. */ getAccessorUsage(accessor) { const cachedUsage = this._accessorUsageMap.get(accessor); if (cachedUsage) return cachedUsage; if (accessor.getSparse()) return BufferViewUsage$1.SPARSE; for (const edge of this._doc.getGraph().listParentEdges(accessor)) { const { usage } = edge.getAttributes(); if (usage) return usage; if (edge.getParent().propertyType !== exports.PropertyType.ROOT) { this.logger.warn(`Missing attribute ".usage" on edge, "${edge.getName()}".`); } } // Group accessors with no specified usage into a miscellaneous buffer view. return BufferViewUsage$1.OTHER; } /** * Sets usage for the given accessor. Some accessor types must be grouped into * buffer views with like accessors. This includes the specified buffer view "targets", but * also implicit usage like IBMs or instanced mesh attributes. If unspecified, an accessor * will be grouped with other accessors of unspecified usage. */ addAccessorToUsageGroup(accessor, usage) { const prevUsage = this._accessorUsageMap.get(accessor); if (prevUsage && prevUsage !== usage) { throw new Error(`Accessor with usage "${prevUsage}" cannot be reused as "${usage}".`); } this._accessorUsageMap.set(accessor, usage); return this; } } /** Explicit buffer view targets defined by glTF specification. */ WriterContext.BufferViewTarget = BufferViewTarget; /** * Implicit buffer view usage, not required by glTF specification, but nonetheless useful for * proper grouping of accessors into buffer views. Additional usages are defined by extensions, * like `EXT_mesh_gpu_instancing`. */ WriterContext.BufferViewUsage = BufferViewUsage$1; /** Maps usage type to buffer view target. Usages not mapped have undefined targets. */ WriterContext.USAGE_TO_TARGET = { [BufferViewUsage$1.ARRAY_BUFFER]: BufferViewTarget.ARRAY_BUFFER, [BufferViewUsage$1.ELEMENT_ARRAY_BUFFER]: BufferViewTarget.ELEMENT_ARRAY_BUFFER }; class UniqueURIGenerator { constructor(multiple, basename) { this.multiple = void 0; this.basename = void 0; this.counter = {}; this.multiple = multiple; this.basename = basename; } createURI(object, extension) { if (object.getURI()) { return object.getURI(); } else if (!this.multiple) { return `${this.basename(object)}.${extension}`; } else { const basename = this.basename(object); this.counter[basename] = this.counter[basename] || 1; return `${basename}_${this.counter[basename]++}.${extension}`; } } } /** Returns the first slot (by name) to which the texture is assigned. */ function getSlot(document, texture) { const edge = document.getGraph().listParentEdges(texture).find(edge => edge.getParent() !== document.getRoot()); return edge ? edge.getName().replace(/texture$/i, '') : ''; } const { BufferViewUsage } = WriterContext; const { UNSIGNED_INT, UNSIGNED_SHORT, UNSIGNED_BYTE } = Accessor.ComponentType; const SUPPORTED_PREWRITE_TYPES = new Set([exports.PropertyType.ACCESSOR, exports.PropertyType.BUFFER, exports.PropertyType.MATERIAL, exports.PropertyType.MESH]); /** * @internal * @hidden */ class GLTFWriter { static write(doc, options) { const graph = doc.getGraph(); const root = doc.getRoot(); const json = { asset: { generator: `glTF-Transform ${VERSION}`, ...root.getAsset() }, extras: { ...root.getExtras() } }; const jsonDoc = { json, resources: {} }; const context = new WriterContext(doc, jsonDoc, options); const logger = options.logger || Logger.DEFAULT_INSTANCE; /* Extensions (1/2). */ // Extensions present on the Document are not written unless they are also registered with // the I/O class. This ensures that setup in `extension.register()` is completed, and // allows a Document to be written with specific extensions disabled. const extensionsRegistered = new Set(options.extensions.map(ext => ext.EXTENSION_NAME)); const extensionsUsed = doc.getRoot().listExtensionsUsed().filter(ext => extensionsRegistered.has(ext.extensionName)).sort((a, b) => a.extensionName > b.extensionName ? 1 : -1); const extensionsRequired = doc.getRoot().listExtensionsRequired().filter(ext => extensionsRegistered.has(ext.extensionName)).sort((a, b) => a.extensionName > b.extensionName ? 1 : -1); if (extensionsUsed.length < doc.getRoot().listExtensionsUsed().length) { logger.warn('Some extensions were not registered for I/O, and will not be written.'); } for (const extension of extensionsUsed) { // Warn on unsupported prewrite hooks. const unsupportedHooks = extension.prewriteTypes.filter(type => !SUPPORTED_PREWRITE_TYPES.has(type)); if (unsupportedHooks.length) { logger.warn(`Prewrite hooks for some types (${unsupportedHooks.join()}), requested by extension ` + `${extension.extensionName}, are unsupported. Please file an issue or a PR.`); } // Install dependencies. for (const key of extension.writeDependencies) { extension.install(key, options.dependencies[key]); } } /** * Pack a group of accessors into a sequential buffer view. Appends accessor and buffer view * definitions to the root JSON lists. * * @param accessors Accessors to be included. * @param bufferIndex Buffer to write to. * @param bufferByteOffset Current offset into the buffer, accounting for other buffer views. * @param bufferViewTarget (Optional) target use of the buffer view. */ function concatAccessors(accessors, bufferIndex, bufferByteOffset, bufferViewTarget) { const buffers = []; let byteLength = 0; // Create accessor definitions, determining size of final buffer view. for (const accessor of accessors) { const accessorDef = context.createAccessorDef(accessor); accessorDef.bufferView = json.bufferViews.length; const accessorArray = accessor.getArray(); const data = BufferUtils.pad(BufferUtils.toView(accessorArray)); accessorDef.byteOffset = byteLength; byteLength += data.byteLength; buffers.push(data); context.accessorIndexMap.set(accessor, json.accessors.length); json.accessors.push(accessorDef); } // Create buffer view definition. const bufferViewData = BufferUtils.concat(buffers); const bufferViewDef = { buffer: bufferIndex, byteOffset: bufferByteOffset, byteLength: bufferViewData.byteLength }; if (bufferViewTarget) bufferViewDef.target = bufferViewTarget; json.bufferViews.push(bufferViewDef); return { buffers, byteLength }; } /** * Pack a group of accessors into an interleaved buffer view. Appends accessor and buffer * view definitions to the root JSON lists. Buffer view target is implicitly attribute data. * * References: * - [Apple • Best Practices for Working with Vertex Data](https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html) * - [Khronos • Vertex Specification Best Practices](https://www.khronos.org/opengl/wiki/Vertex_Specification_Best_Practices) * * @param accessors Accessors to be included. * @param bufferIndex Buffer to write to. * @param bufferByteOffset Offset into the buffer, accounting for other buffer views. */ function interleaveAccessors(accessors, bufferIndex, bufferByteOffset) { const vertexCount = accessors[0].getCount(); let byteStride = 0; // Create accessor definitions, determining size and stride of final buffer view. for (const accessor of accessors) { const accessorDef = context.createAccessorDef(accessor); accessorDef.bufferView = json.bufferViews.length; accessorDef.byteOffset = byteStride; const elementSize = accessor.getElementSize(); const componentSize = accessor.getComponentSize(); byteStride += BufferUtils.padNumber(elementSize * componentSize); context.accessorIndexMap.set(accessor, json.accessors.length); json.accessors.push(accessorDef); } // Allocate interleaved buffer view. const byteLength = vertexCount * byteStride; const buffer = new ArrayBuffer(byteLength); const view = new DataView(buffer); // Write interleaved accessor data to the buffer view. for (let i = 0; i < vertexCount; i++) { let vertexByteOffset = 0; for (const accessor of accessors) { const elementSize = accessor.getElementSize(); const componentSize = accessor.getComponentSize(); const componentType = accessor.getComponentType(); const array = accessor.getArray(); for (let j = 0; j < elementSize; j++) { const viewByteOffset = i * byteStride + vertexByteOffset + j * componentSize; const value = array[i * elementSize + j]; switch (componentType) { case Accessor.ComponentType.FLOAT: view.setFloat32(viewByteOffset, value, true); break; case Accessor.ComponentType.BYTE: view.setInt8(viewByteOffset, value); break; case Accessor.ComponentType.SHORT: view.setInt16(viewByteOffset, value, true); break; case Accessor.ComponentType.UNSIGNED_BYTE: view.setUint8(viewByteOffset, value); break; case Accessor.ComponentType.UNSIGNED_SHORT: view.setUint16(viewByteOffset, value, true); break; case Accessor.ComponentType.UNSIGNED_INT: view.setUint32(viewByteOffset, value, true); break; default: throw new Error('Unexpected component type: ' + componentType); } } vertexByteOffset += BufferUtils.padNumber(elementSize * componentSize); } } // Create buffer view definition. const bufferViewDef = { buffer: bufferIndex, byteOffset: bufferByteOffset, byteLength: byteLength, byteStride: byteStride, target: WriterContext.BufferViewTarget.ARRAY_BUFFER }; json.bufferViews.push(bufferViewDef); return { byteLength, buffers: [new Uint8Array(buffer)] }; } /** * Pack a group of sparse accessors. Appends accessor and buffer view * definitions to the root JSON lists. * * @param accessors Accessors to be included. * @param bufferIndex Buffer to write to. * @param bufferByteOffset Current offset into the buffer, accounting for other buffer views. */ function concatSparseAccessors(accessors, bufferIndex, bufferByteOffset) { const buffers = []; let byteLength = 0; const sparseData = new Map(); let maxIndex = -Infinity; let needSparseWarning = false; // (1) Write accessor definitions, gathering indices and values. for (const accessor of accessors) { const accessorDef = context.createAccessorDef(accessor); json.accessors.push(accessorDef); context.accessorIndexMap.set(accessor, json.accessors.length - 1); const indices = []; const values = []; const el = []; const base = new Array(accessor.getElementSize()).fill(0); for (let i = 0, il = accessor.getCount(); i < il; i++) { accessor.getElement(i, el); if (MathUtils.eq(el, base, 0)) continue; maxIndex = Math.max(i, maxIndex); indices.push(i); for (let j = 0; j < el.length; j++) values.push(el[j]); } const count = indices.length; const data = { accessorDef, count }; sparseData.set(accessor, data); if (count === 0) continue; if (count > accessor.getCount() / 2) { needSparseWarning = true; } const ValueArray = ComponentTypeToTypedArray[accessor.getComponentType()]; data.indices = indices; data.values = new ValueArray(values); } // (2) Early exit if all sparse accessors are just zero-filled arrays. if (!Number.isFinite(maxIndex)) { return { buffers, byteLength }; } if (needSparseWarning) { logger.warn(`Some sparse accessors have >50% non-zero elements, which may increase file size.`); } // (3) Write index buffer view. const IndexArray = maxIndex < 255 ? Uint8Array : maxIndex < 65535 ? Uint16Array : Uint32Array; const IndexComponentType = maxIndex < 255 ? UNSIGNED_BYTE : maxIndex < 65535 ? UNSIGNED_SHORT : UNSIGNED_INT; const indicesBufferViewDef = { buffer: bufferIndex, byteOffset: bufferByteOffset + byteLength, byteLength: 0 }; for (const accessor of accessors) { const data = sparseData.get(accessor); if (data.count === 0) continue; data.indicesByteOffset = indicesBufferViewDef.byteLength; const buffer = BufferUtils.pad(BufferUtils.toView(new IndexArray(data.indices))); buffers.push(buffer); byteLength += buffer.byteLength; indicesBufferViewDef.byteLength += buffer.byteLength; } json.bufferViews.push(indicesBufferViewDef); const indicesBufferViewIndex = json.bufferViews.length - 1; // (4) Write value buffer view. const valuesBufferViewDef = { buffer: bufferIndex, byteOffset: bufferByteOffset + byteLength, byteLength: 0 }; for (const accessor of accessors) { const data = sparseData.get(accessor); if (data.count === 0) continue; data.valuesByteOffset = valuesBufferViewDef.byteLength; const buffer = BufferUtils.pad(BufferUtils.toView(data.values)); buffers.push(buffer); byteLength += buffer.byteLength; valuesBufferViewDef.byteLength += buffer.byteLength; } json.bufferViews.push(valuesBufferViewDef); const valuesBufferViewIndex = json.bufferViews.length - 1; // (5) Write accessor sparse entries. for (const accessor of accessors) { const data = sparseData.get(accessor); if (data.count === 0) continue; data.accessorDef.sparse = { count: data.count, indices: { bufferView: indicesBufferViewIndex, byteOffset: data.indicesByteOffset, componentType: IndexComponentType }, values: { bufferView: valuesBufferViewIndex, byteOffset: data.valuesByteOffset } }; } return { buffers, byteLength }; } json.accessors = []; json.bufferViews = []; /* Textures. */ // glTF Transform's "Texture" properties correspond 1:1 with glTF "Image" properties, and // with image files. The glTF file may contain more one texture per image, where images // are reused with different sampler properties. json.samplers = []; json.textures = []; json.images = root.listTextures().map((texture, textureIndex) => { const imageDef = context.createPropertyDef(texture); if (texture.getMimeType()) { imageDef.mimeType = texture.getMimeType(); } const image = texture.getImage(); if (image) { context.createImageData(imageDef, image, texture); } context.imageIndexMap.set(texture, textureIndex); return imageDef; }); /* Accessors. */ extensionsUsed.filter(extension => extension.prewriteTypes.includes(exports.PropertyType.ACCESSOR)).forEach(extension => extension.prewrite(context, exports.PropertyType.ACCESSOR)); root.listAccessors().forEach(accessor => { // Attributes are grouped and interleaved in one buffer view per mesh primitive. // Indices for all primitives are grouped into a single buffer view. IBMs are grouped // into a single buffer view. Other usage (if specified by extensions) also goes into // a dedicated buffer view. Everything else goes into a miscellaneous buffer view. // Certain accessor usage should group data into buffer views by the accessor parent. // The `accessorParents` map uses the first parent of each accessor for this purpose. const groupByParent = context.accessorUsageGroupedByParent; const accessorParents = context.accessorParents; // Skip if already written by an extension. if (context.accessorIndexMap.has(accessor)) return; // Assign usage for core accessor usage types (explicit targets and implicit usage). const usage = context.getAccessorUsage(accessor); context.addAccessorToUsageGroup(accessor, usage); // For accessor usage that requires grouping by parent (vertex and instance // attributes) organize buffer views accordingly. if (groupByParent.has(usage)) { const parent = graph.listParents(accessor).find(parent => parent.propertyType !== exports.PropertyType.ROOT); accessorParents.set(accessor, parent); } }); /* Buffers, buffer views. */ extensionsUsed.filter(extension => extension.prewriteTypes.includes(exports.PropertyType.BUFFER)).forEach(extension => extension.prewrite(context, exports.PropertyType.BUFFER)); const needsBuffer = root.listAccessors().length > 0 || context.otherBufferViews.size > 0 || root.listTextures().length > 0 && options.format === exports.Format.GLB; if (needsBuffer && root.listBuffers().length === 0) { throw new Error('Buffer required for Document resources, but none was found.'); } json.buffers = []; root.listBuffers().forEach((buffer, index) => { const bufferDef = context.createPropertyDef(buffer); const groupByParent = context.accessorUsageGroupedByParent; const accessors = buffer.listParents().filter(property => property instanceof Accessor); const uniqueParents = new Set(accessors.map(accessor => context.accessorParents.get(accessor))); const parentToIndex = new Map(Array.from(uniqueParents).map((parent, index) => [parent, index])); const accessorGroups = {}; for (const accessor of accessors) { var _key; // Skip if already written by an extension. if (context.accessorIndexMap.has(accessor)) continue; const usage = context.getAccessorUsage(accessor); let key = usage; if (groupByParent.has(usage)) { const parent = context.accessorParents.get(accessor); key += `:${parentToIndex.get(parent)}`; } accessorGroups[_key = key] || (accessorGroups[_key] = { usage, accessors: [] }); accessorGroups[key].accessors.push(accessor); } // Write accessor groups to buffer views. const buffers = []; const bufferIndex = json.buffers.length; let bufferByteLength = 0; for (const { usage, accessors: groupAccessors } of Object.values(accessorGroups)) { if (usage === BufferViewUsage.ARRAY_BUFFER && options.vertexLayout === exports.VertexLayout.INTERLEAVED) { // (1) Interleaved vertex attributes. const result = interleaveAccessors(groupAccessors, bufferIndex, bufferByteLength); bufferByteLength += result.byteLength; buffers.push(...result.buffers); } else if (usage === BufferViewUsage.ARRAY_BUFFER) { // (2) Non-interleaved vertex attributes. for (const accessor of groupAccessors) { // We 'interleave' a single accessor because the method pads to // 4-byte boundaries, which concatAccessors() does not. const result = interleaveAccessors([accessor], bufferIndex, bufferByteLength); bufferByteLength += result.byteLength; buffers.push(...result.buffers); } } else if (usage === BufferViewUsage.SPARSE) { // (3) Sparse accessors. const result = concatSparseAccessors(groupAccessors, bufferIndex, bufferByteLength); bufferByteLength += result.byteLength; buffers.push(...result.buffers); } else if (usage === BufferViewUsage.ELEMENT_ARRAY_BUFFER) { // (4) Indices. const target = WriterContext.BufferViewTarget.ELEMENT_ARRAY_BUFFER; const result = concatAccessors(groupAccessors, bufferIndex, bufferByteLength, target); bufferByteLength += result.byteLength; buffers.push(...result.buffers); } else { // (5) Other. const result = concatAccessors(groupAccessors, bufferIndex, bufferByteLength); bufferByteLength += result.byteLength; buffers.push(...result.buffers); } } // We only support embedded images in GLB, where the embedded buffer must be the first. // Additional buffers are currently left empty (see EXT_meshopt_compression fallback). if (context.imageBufferViews.length && index === 0) { for (let i = 0; i < context.imageBufferViews.length; i++) { json.bufferViews[json.images[i].bufferView].byteOffset = bufferByteLength; bufferByteLength += context.imageBufferViews[i].byteLength; buffers.push(context.imageBufferViews[i]); if (bufferByteLength % 8) { // See: https://github.com/KhronosGroup/glTF/issues/1935 const imagePadding = 8 - bufferByteLength % 8; bufferByteLength += imagePadding; buffers.push(new Uint8Array(imagePadding)); } } } if (context.otherBufferViews.has(buffer)) { for (const data of context.otherBufferViews.get(buffer)) { json.bufferViews.push({ buffer: bufferIndex, byteOffset: bufferByteLength, byteLength: data.byteLength }); context.otherBufferViewsIndexMap.set(data, json.bufferViews.length - 1); bufferByteLength += data.byteLength; buffers.push(data); } } if (bufferByteLength) { // Assign buffer URI. let uri; if (options.format === exports.Format.GLB) { uri = GLB_BUFFER; } else { uri = context.bufferURIGenerator.createURI(buffer, 'bin'); bufferDef.uri = uri; } // Write buffer views to buffer. bufferDef.byteLength = bufferByteLength; context.assignResourceURI(uri, BufferUtils.concat(buffers), true); } json.buffers.push(bufferDef); context.bufferIndexMap.set(buffer, index); }); if (root.listAccessors().find(a => !a.getBuffer())) { logger.warn('Skipped writing one or more Accessors: no Buffer assigned.'); } /* Materials. */ extensionsUsed.filter(extension => extension.prewriteTypes.includes(exports.PropertyType.MATERIAL)).forEach(extension => extension.prewrite(context, exports.PropertyType.MATERIAL)); json.materials = root.listMaterials().map((material, index) => { const materialDef = context.createPropertyDef(material); // Program state & blending. if (material.getAlphaMode() !== Material.AlphaMode.OPAQUE) { materialDef.alphaMode = material.getAlphaMode(); } if (material.getAlphaMode() === Material.AlphaMode.MASK) { materialDef.alphaCutoff = material.getAlphaCutoff(); } if (material.getDoubleSided()) materialDef.doubleSided = true; // Factors. materialDef.pbrMetallicRoughness = {}; if (!MathUtils.eq(material.getBaseColorFactor(), [1, 1, 1, 1])) { materialDef.pbrMetallicRoughness.baseColorFactor = material.getBaseColorFactor(); } if (!MathUtils.eq(material.getEmissiveFactor(), [0, 0, 0])) { materialDef.emissiveFactor = material.getEmissiveFactor(); } if (material.getRoughnessFactor() !== 1) { materialDef.pbrMetallicRoughness.roughnessFactor = material.getRoughnessFactor(); } if (material.getMetallicFactor() !== 1) { materialDef.pbrMetallicRoughness.metallicFactor = material.getMetallicFactor(); } // Textures. if (material.getBaseColorTexture()) { const texture = material.getBaseColorTexture(); const textureInfo = material.getBaseColorTextureInfo(); materialDef.pbrMetallicRoughness.baseColorTexture = context.createTextureInfoDef(texture, textureInfo); } if (material.getEmissiveTexture()) { const texture = material.getEmissiveTexture(); const textureInfo = material.getEmissiveTextureInfo(); materialDef.emissiveTexture = context.createTextureInfoDef(texture, textureInfo); } if (material.getNormalTexture()) { const texture = material.getNormalTexture(); const textureInfo = material.getNormalTextureInfo(); const textureInfoDef = context.createTextureInfoDef(texture, textureInfo); if (material.getNormalScale() !== 1) { textureInfoDef.scale = material.getNormalScale(); } materialDef.normalTexture = textureInfoDef; } if (material.getOcclusionTexture()) { const texture = material.getOcclusionTexture(); const textureInfo = material.getOcclusionTextureInfo(); const textureInfoDef = context.createTextureInfoDef(texture, textureInfo); if (material.getOcclusionStrength() !== 1) { textureInfoDef.strength = material.getOcclusionStrength(); } materialDef.occlusionTexture = textureInfoDef; } if (material.getMetallicRoughnessTexture()) { const texture = material.getMetallicRoughnessTexture(); const textureInfo = material.getMetallicRoughnessTextureInfo(); materialDef.pbrMetallicRoughness.metallicRoughnessTexture = context.createTextureInfoDef(texture, textureInfo); } context.materialIndexMap.set(material, index); return materialDef; }); /* Meshes. */ extensionsUsed.filter(extension => extension.prewriteTypes.includes(exports.PropertyType.MESH)).forEach(extension => extension.prewrite(context, exports.PropertyType.MESH)); json.meshes = root.listMeshes().map((mesh, index) => { const meshDef = context.createPropertyDef(mesh); let targetNames = null; meshDef.primitives = mesh.listPrimitives().map(primitive => { const primitiveDef = { attributes: {} }; primitiveDef.mode = primitive.getMode(); const material = primitive.getMaterial(); if (material) { primitiveDef.material = context.materialIndexMap.get(material); } if (Object.keys(primitive.getExtras()).length) { primitiveDef.extras = primitive.getExtras(); } const indices = primitive.getIndices(); if (indices) { primitiveDef.indices = context.accessorIndexMap.get(indices); } for (const semantic of primitive.listSemantics()) { primitiveDef.attributes[semantic] = context.accessorIndexMap.get(primitive.getAttribute(semantic)); } for (const target of primitive.listTargets()) { const targetDef = {}; for (const semantic of target.listSemantics()) { targetDef[semantic] = context.accessorIndexMap.get(target.getAttribute(semantic)); } primitiveDef.targets = primitiveDef.targets || []; primitiveDef.targets.push(targetDef); } if (primitive.listTargets().length && !targetNames) { targetNames = primitive.listTargets().map(target => target.getName()); } return primitiveDef; }); if (mesh.getWeights().length) { meshDef.weights = mesh.getWeights(); } if (targetNames) { meshDef.extras = meshDef.extras || {}; meshDef.extras['targetNames'] = targetNames; } context.meshIndexMap.set(mesh, index); return meshDef; }); /** Cameras. */ json.cameras = root.listCameras().map((camera, index) => { const cameraDef = context.createPropertyDef(camera); cameraDef.type = camera.getType(); if (cameraDef.type === Camera.Type.PERSPECTIVE) { cameraDef.perspective = { znear: camera.getZNear(), zfar: camera.getZFar(), yfov: camera.getYFov() }; const aspectRatio = camera.getAspectRatio(); if (aspectRatio !== null) { cameraDef.perspective.aspectRatio = aspectRatio; } } else { cameraDef.orthographic = { znear: camera.getZNear(), zfar: camera.getZFar(), xmag: camera.getXMag(), ymag: camera.getYMag() }; } context.cameraIndexMap.set(camera, index); return cameraDef; }); /* Nodes. */ json.nodes = root.listNodes().map((node, index) => { const nodeDef = context.createPropertyDef(node); if (!MathUtils.eq(node.getTranslation(), [0, 0, 0])) { nodeDef.translation = node.getTranslation(); } if (!MathUtils.eq(node.getRotation(), [0, 0, 0, 1])) { nodeDef.rotation = node.getRotation(); } if (!MathUtils.eq(node.getScale(), [1, 1, 1])) { nodeDef.scale = node.getScale(); } if (node.getWeights().length) { nodeDef.weights = node.getWeights(); } // Attachments (mesh, camera, skin) defined later in writing process. context.nodeIndexMap.set(node, index); return nodeDef; }); /** Skins. */ json.skins = root.listSkins().map((skin, index) => { const skinDef = context.createPropertyDef(skin); const inverseBindMatrices = skin.getInverseBindMatrices(); if (inverseBindMatrices) { skinDef.inverseBindMatrices = context.accessorIndexMap.get(inverseBindMatrices); } const skeleton = skin.getSkeleton(); if (skeleton) { skinDef.skeleton = context.nodeIndexMap.get(skeleton); } skinDef.joints = skin.listJoints().map(joint => context.nodeIndexMap.get(joint)); context.skinIndexMap.set(skin, index); return skinDef; }); /** Node attachments. */ root.listNodes().forEach((node, index) => { const nodeDef = json.nodes[index]; const mesh = node.getMesh(); if (mesh) { nodeDef.mesh = context.meshIndexMap.get(mesh); } const camera = node.getCamera(); if (camera) { nodeDef.camera = context.cameraIndexMap.get(camera); } const skin = node.getSkin(); if (skin) { nodeDef.skin = context.skinIndexMap.get(skin); } if (node.listChildren().length > 0) { nodeDef.children = node.listChildren().map(node => context.nodeIndexMap.get(node)); } }); /** Animations. */ json.animations = root.listAnimations().map((animation, index) => { const animationDef = context.createPropertyDef(animation); const samplerIndexMap = new Map(); animationDef.samplers = animation.listSamplers().map((sampler, samplerIndex) => { const samplerDef = context.createPropertyDef(sampler); samplerDef.input = context.accessorIndexMap.get(sampler.getInput()); samplerDef.output = context.accessorIndexMap.get(sampler.getOutput()); samplerDef.interpolation = sampler.getInterpolation(); samplerIndexMap.set(sampler, samplerIndex); return samplerDef; }); animationDef.channels = animation.listChannels().map(channel => { const channelDef = context.createPropertyDef(channel); channelDef.sampler = samplerIndexMap.get(channel.getSampler()); channelDef.target = { node: context.nodeIndexMap.get(channel.getTargetNode()), path: channel.getTargetPath() }; return channelDef; }); context.animationIndexMap.set(animation, index); return animationDef; }); /* Scenes. */ json.scenes = root.listScenes().map((scene, index) => { const sceneDef = context.createPropertyDef(scene); sceneDef.nodes = scene.listChildren().map(node => context.nodeIndexMap.get(node)); context.sceneIndexMap.set(scene, index); return sceneDef; }); const defaultScene = root.getDefaultScene(); if (defaultScene) { json.scene = root.listScenes().indexOf(defaultScene); } /* Extensions (2/2). */ json.extensionsUsed = extensionsUsed.map(ext => ext.extensionName); json.extensionsRequired = extensionsRequired.map(ext => ext.extensionName); extensionsUsed.forEach(extension => extension.write(context)); // clean(json); return jsonDoc; } } /** * Removes empty and null values from an object. * @param object * @internal */ function clean(object) { const unused = []; for (const key in object) { const value = object[key]; if (Array.isArray(value) && value.length === 0) { unused.push(key); } else if (value === null || value === '') { unused.push(key); } else if (value && typeof value === 'object' && Object.keys(value).length === 0) { unused.push(key); } } for (const key of unused) { delete object[key]; } } var ChunkType; (function (ChunkType) { ChunkType[ChunkType["JSON"] = 1313821514] = "JSON"; ChunkType[ChunkType["BIN"] = 5130562] = "BIN"; })(ChunkType || (ChunkType = {})); /** * *Abstract I/O service.* * * The most common use of the I/O service is to read/write a {@link Document} with a given path. * Methods are also available for converting in-memory representations of raw glTF files, both * binary (*Uint8Array*) and JSON ({@link JSONDocument}). * * For platform-specific implementations, see {@link NodeIO}, {@link WebIO}, and {@link DenoIO}. * * @category I/O */ class PlatformIO { constructor() { this._logger = Logger.DEFAULT_INSTANCE; this._extensions = new Set(); this._dependencies = {}; this._vertexLayout = exports.VertexLayout.INTERLEAVED; /** @hidden */ this.lastReadBytes = 0; /** @hidden */ this.lastWriteBytes = 0; } /** Sets the {@link Logger} used by this I/O instance. Defaults to Logger.DEFAULT_INSTANCE. */ setLogger(logger) { this._logger = logger; return this; } /** Registers extensions, enabling I/O class to read and write glTF assets requiring them. */ registerExtensions(extensions) { for (const extension of extensions) { this._extensions.add(extension); extension.register(); } return this; } /** Registers dependencies used (e.g. by extensions) in the I/O process. */ registerDependencies(dependencies) { Object.assign(this._dependencies, dependencies); return this; } /** * Sets the vertex layout method used by this I/O instance. Defaults to * VertexLayout.INTERLEAVED. */ setVertexLayout(layout) { this._vertexLayout = layout; return this; } /********************************************************************************************** * Public Read API. */ /** Reads a {@link Document} from the given URI. */ read(uri) { try { const _this = this; const _readJSON = _this.readJSON; return Promise.resolve(_this.readAsJSON(uri)).then(function (_this$readAsJSON) { return Promise.resolve(_readJSON.call(_this, _this$readAsJSON)); }); } catch (e) { return Promise.reject(e); } } /** Loads a URI and returns a {@link JSONDocument} struct, without parsing. */ readAsJSON(uri) { try { const _this2 = this; return Promise.resolve(_this2.readURI(uri, 'view')).then(function (view) { _this2.lastReadBytes = view.byteLength; const jsonDoc = isGLB(view) ? _this2._binaryToJSON(view) : { json: JSON.parse(BufferUtils.decodeText(view)), resources: {} }; // Read external resources first, before Data URIs are replaced. return Promise.resolve(_this2._readResourcesExternal(jsonDoc, _this2.dirname(uri))).then(function () { _this2._readResourcesInternal(jsonDoc); return jsonDoc; }); }); } catch (e) { return Promise.reject(e); } } /** Converts glTF-formatted JSON and a resource map to a {@link Document}. */ readJSON(jsonDoc) { try { const _this3 = this; jsonDoc = _this3._copyJSON(jsonDoc); _this3._readResourcesInternal(jsonDoc); return Promise.resolve(GLTFReader.read(jsonDoc, { extensions: Array.from(_this3._extensions), dependencies: _this3._dependencies, logger: _this3._logger })); } catch (e) { return Promise.reject(e); } } /** Converts a GLB-formatted Uint8Array to a {@link JSONDocument}. */ binaryToJSON(glb) { try { const _this4 = this; const jsonDoc = _this4._binaryToJSON(BufferUtils.assertView(glb)); _this4._readResourcesInternal(jsonDoc); const json = jsonDoc.json; // Check for external references, which can't be resolved by this method. if (json.buffers && json.buffers.some(bufferDef => isExternalBuffer(jsonDoc, bufferDef))) { throw new Error('Cannot resolve external buffers with binaryToJSON().'); } else if (json.images && json.images.some(imageDef => isExternalImage(jsonDoc, imageDef))) { throw new Error('Cannot resolve external images with binaryToJSON().'); } return Promise.resolve(jsonDoc); } catch (e) { return Promise.reject(e); } } /** Converts a GLB-formatted Uint8Array to a {@link Document}. */ readBinary(glb) { try { const _this5 = this; const _readJSON2 = _this5.readJSON; return Promise.resolve(_this5.binaryToJSON(BufferUtils.assertView(glb))).then(function (_this5$binaryToJSON) { return _readJSON2.call(_this5, _this5$binaryToJSON); }); } catch (e) { return Promise.reject(e); } } /********************************************************************************************** * Public Write API. */ /** Converts a {@link Document} to glTF-formatted JSON and a resource map. */ writeJSON(doc, _options) { if (_options === void 0) { _options = {}; } try { const _this6 = this; if (_options.format === exports.Format.GLB && doc.getRoot().listBuffers().length > 1) { throw new Error('GLB must have 0–1 buffers.'); } return Promise.resolve(GLTFWriter.write(doc, { format: _options.format || exports.Format.GLTF, basename: _options.basename || '', logger: _this6._logger, vertexLayout: _this6._vertexLayout, dependencies: { ..._this6._dependencies }, extensions: Array.from(_this6._extensions) })); } catch (e) { return Promise.reject(e); } } /** Converts a {@link Document} to a GLB-formatted Uint8Array. */ writeBinary(doc) { try { const _this7 = this; return Promise.resolve(_this7.writeJSON(doc, { format: exports.Format.GLB })).then(function (_ref) { let { json, resources } = _ref; const header = new Uint32Array([0x46546c67, 2, 12]); const jsonText = JSON.stringify(json); const jsonChunkData = BufferUtils.pad(BufferUtils.encodeText(jsonText), 0x20); const jsonChunkHeader = BufferUtils.toView(new Uint32Array([jsonChunkData.byteLength, 0x4e4f534a])); const jsonChunk = BufferUtils.concat([jsonChunkHeader, jsonChunkData]); header[header.length - 1] += jsonChunk.byteLength; const binBuffer = Object.values(resources)[0]; if (!binBuffer || !binBuffer.byteLength) { return BufferUtils.concat([BufferUtils.toView(header), jsonChunk]); } const binChunkData = BufferUtils.pad(binBuffer, 0x00); const binChunkHeader = BufferUtils.toView(new Uint32Array([binChunkData.byteLength, 0x004e4942])); const binChunk = BufferUtils.concat([binChunkHeader, binChunkData]); header[header.length - 1] += binChunk.byteLength; return BufferUtils.concat([BufferUtils.toView(header), jsonChunk, binChunk]); }); } catch (e) { return Promise.reject(e); } } /********************************************************************************************** * Internal. */ _readResourcesExternal(jsonDoc, base) { try { const _this8 = this; const images = jsonDoc.json.images || []; const buffers = jsonDoc.json.buffers || []; const pendingResources = [...images, ...buffers].map(function (resource) { try { const uri = resource.uri; if (!uri || uri.match(/data:/)) return Promise.resolve(); return Promise.resolve(_this8.readURI(_this8.resolve(base, uri), 'view')).then(function (_this8$readURI) { jsonDoc.resources[uri] = _this8$readURI; _this8.lastReadBytes += jsonDoc.resources[uri].byteLength; }); } catch (e) { return Promise.reject(e); } }); return Promise.resolve(Promise.all(pendingResources)).then(function () {}); } catch (e) { return Promise.reject(e); } } _readResourcesInternal(jsonDoc) { // NOTICE: This method may be called more than once during the loading // process (e.g. WebIO.read) and should handle that safely. function resolveResource(resource) { if (!resource.uri) return; if (resource.uri in jsonDoc.resources) { BufferUtils.assertView(jsonDoc.resources[resource.uri]); return; } if (resource.uri.match(/data:/)) { // Rewrite Data URIs to something short and unique. const resourceUUID = `__${uuid()}.${FileUtils.extension(resource.uri)}`; jsonDoc.resources[resourceUUID] = BufferUtils.createBufferFromDataURI(resource.uri); resource.uri = resourceUUID; } } // Unpack images. const images = jsonDoc.json.images || []; images.forEach(image => { if (image.bufferView === undefined && image.uri === undefined) { throw new Error('Missing resource URI or buffer view.'); } resolveResource(image); }); // Unpack buffers. const buffers = jsonDoc.json.buffers || []; buffers.forEach(resolveResource); } /** * Creates a shallow copy of glTF-formatted {@link JSONDocument}. * * Images, Buffers, and Resources objects are deep copies so that PlatformIO can safely * modify them during the parsing process. Other properties are shallow copies, and buffers * are passed by reference. */ _copyJSON(jsonDoc) { const { images, buffers } = jsonDoc.json; jsonDoc = { json: { ...jsonDoc.json }, resources: { ...jsonDoc.resources } }; if (images) { jsonDoc.json.images = images.map(image => ({ ...image })); } if (buffers) { jsonDoc.json.buffers = buffers.map(buffer => ({ ...buffer })); } return jsonDoc; } /** Internal version of binaryToJSON; does not warn about external resources. */ _binaryToJSON(glb) { // Decode and verify GLB header. if (!isGLB(glb)) { throw new Error('Invalid glTF 2.0 binary.'); } // Decode JSON chunk. const jsonChunkHeader = new Uint32Array(glb.buffer, glb.byteOffset + 12, 2); if (jsonChunkHeader[1] !== ChunkType.JSON) { throw new Error('Missing required GLB JSON chunk.'); } const jsonByteOffset = 20; const jsonByteLength = jsonChunkHeader[0]; const jsonText = BufferUtils.decodeText(BufferUtils.toView(glb, jsonByteOffset, jsonByteLength)); const json = JSON.parse(jsonText); // Decode BIN chunk. const binByteOffset = jsonByteOffset + jsonByteLength; if (glb.byteLength <= binByteOffset) { return { json, resources: {} }; } const binChunkHeader = new Uint32Array(glb.buffer, glb.byteOffset + binByteOffset, 2); if (binChunkHeader[1] !== ChunkType.BIN) { // Allow GLB files without BIN chunk, but with unknown chunk // Spec: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#chunks-overview return { json, resources: {} }; } const binByteLength = binChunkHeader[0]; const binBuffer = BufferUtils.toView(glb, binByteOffset + 8, binByteLength); return { json, resources: { [GLB_BUFFER]: binBuffer } }; } } function isExternalBuffer(jsonDocument, bufferDef) { return bufferDef.uri !== undefined && !(bufferDef.uri in jsonDocument.resources); } function isExternalImage(jsonDocument, imageDef) { return imageDef.uri !== undefined && !(imageDef.uri in jsonDocument.resources) && imageDef.bufferView === undefined; } function isGLB(view) { if (view.byteLength < 3 * Uint32Array.BYTES_PER_ELEMENT) return false; const header = new Uint32Array(view.buffer, view.byteOffset, 3); return header[0] === 0x46546c67 && header[1] === 2; } /** * *I/O service for Node.js.* * * The most common use of the I/O service is to read/write a {@link Document} with a given path. * Methods are also available for converting in-memory representations of raw glTF files, both * binary (*Uint8Array*) and JSON ({@link JSONDocument}). * * Usage: * * ```typescript * import { NodeIO } from '@gltf-transform/core'; * * const io = new NodeIO(); * * // Read. * let document; * document = await io.read('model.glb'); // → Document * document = await io.readBinary(glb); // Uint8Array → Document * * // Write. * await io.write('model.glb', document); // → void * const glb = await io.writeBinary(document); // Document → Uint8Array * ``` * * By default, NodeIO can only read/write paths on disk. To enable network requests, provide a Fetch * API implementation (global [`fetch()`](https://nodejs.org/api/globals.html#fetch) is stable in * Node.js v21+, or [`node-fetch`](https://www.npmjs.com/package/node-fetch) may be installed) and enable * {@link NodeIO.setAllowNetwork setAllowNetwork}. Network requests may optionally be configured with * [RequestInit](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters) parameters. * * ```typescript * const io = new NodeIO(fetch, {headers: {...}}).setAllowNetwork(true); * * const document = await io.read('https://example.com/path/to/model.glb'); * ``` * * @category I/O */ function _settle$1(pact, state, value) { if (!pact.s) { if (value instanceof _Pact$1) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle$1.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle$1.bind(null, pact, state), _settle$1.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; const observer = pact.o; if (observer) { observer(pact); } } } /** Divides a flat input array into batches of size `batchSize`. */ const _Pact$1 = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { const result = new _Pact(); const state = this.s; if (state) { const callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle$1(result, 1, callback(this.v)); } catch (e) { _settle$1(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { const value = _this.v; if (_this.s & 1) { _settle$1(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle$1(result, 1, onRejected(value)); } else { _settle$1(result, 2, value); } } catch (e) { _settle$1(result, 2, e); } }; return result; }; return _Pact; }(); function _switch$1(discriminant, cases) { var dispatchIndex = -1; var awaitBody; outer: { for (var i = 0; i < cases.length; i++) { var test = cases[i][0]; if (test) { var testValue = test(); if (testValue && testValue.then) { break outer; } if (testValue === discriminant) { dispatchIndex = i; break; } } else { // Found the default case, set it as the pending dispatch case dispatchIndex = i; } } if (dispatchIndex !== -1) { do { var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } var result = body(); if (result && result.then) { awaitBody = true; break outer; } var fallthroughCheck = cases[dispatchIndex][2]; dispatchIndex++; } while (fallthroughCheck && !fallthroughCheck()); return result; } } const pact = new _Pact$1(); const reject = _settle$1.bind(null, pact, 2); (awaitBody ? result.then(_resumeAfterBody) : testValue.then(_resumeAfterTest)).then(void 0, reject); return pact; function _resumeAfterTest(value) { for (;;) { if (value === discriminant) { dispatchIndex = i; break; } if (++i === cases.length) { if (dispatchIndex !== -1) { break; } else { _settle$1(pact, 1, result); return; } } test = cases[i][0]; if (test) { value = test(); if (value && value.then) { value.then(_resumeAfterTest).then(void 0, reject); return; } } else { dispatchIndex = i; } } do { var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } var result = body(); if (result && result.then) { result.then(_resumeAfterBody).then(void 0, reject); return; } var fallthroughCheck = cases[dispatchIndex][2]; dispatchIndex++; } while (fallthroughCheck && !fallthroughCheck()); _settle$1(pact, 1, result); } function _resumeAfterBody(result) { for (;;) { var fallthroughCheck = cases[dispatchIndex][2]; if (!fallthroughCheck || fallthroughCheck()) { break; } dispatchIndex++; var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } result = body(); if (result && result.then) { result.then(_resumeAfterBody).then(void 0, reject); return; } } _settle$1(pact, 1, result); } } const _iteratorSymbol = typeof Symbol !== "undefined" ? Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator")) : "@@iterator"; function _isSettledPact(thenable) { return thenable instanceof _Pact$1 && thenable.s & 1; } function _forTo(array, body, check) { var i = -1, pact, reject; function _cycle(result) { try { while (++i < array.length && (!check || !check())) { result = body(i); if (result && result.then) { if (_isSettledPact(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$1.bind(null, pact = new _Pact$1(), 2))); return; } } } if (pact) { _settle$1(pact, 1, result); } else { pact = result; } } catch (e) { _settle$1(pact || (pact = new _Pact$1()), 2, e); } } _cycle(); return pact; } function _forOf(target, body, check) { if (typeof target[_iteratorSymbol] === "function") { var iterator = target[_iteratorSymbol](), step, pact, reject; function _cycle(result) { try { while (!(step = iterator.next()).done && (!check || !check())) { result = body(step.value); if (result && result.then) { if (_isSettledPact(result)) { result = result.v; } else { result.then(_cycle, reject || (reject = _settle$1.bind(null, pact = new _Pact$1(), 2))); return; } } } if (pact) { _settle$1(pact, 1, result); } else { pact = result; } } catch (e) { _settle$1(pact || (pact = new _Pact$1()), 2, e); } } _cycle(); if (iterator.return) { var _fixup = function (value) { try { if (!step.done) { iterator.return(); } } catch (e) {} return value; }; if (pact && pact.then) { return pact.then(_fixup, function (e) { throw _fixup(e); }); } _fixup(); } return pact; } // No support for Symbol.iterator if (!("length" in target)) { throw new TypeError("Object is not iterable"); } // Handle live collections properly var values = []; for (var i = 0; i < target.length; i++) { values.push(target[i]); } return _forTo(values, function (i) { return body(values[i]); }, check); } class NodeIO extends PlatformIO { /** * Constructs a new NodeIO service. Instances are reusable. By default, only NodeIO can only * read/write paths on disk. To enable HTTP requests, provide a Fetch API implementation and * enable {@link NodeIO.setAllowNetwork setAllowNetwork}. * * @param fetch Implementation of Fetch API. * @param fetchConfig Configuration object for Fetch API. */ constructor(_fetch, _fetchConfig) { if (_fetch === void 0) { _fetch = null; } if (_fetchConfig === void 0) { _fetchConfig = HTTPUtils.DEFAULT_INIT; } super(); this._fetch = void 0; this._fetchConfig = void 0; this._init = void 0; this._fetchEnabled = false; this._fetch = _fetch; this._fetchConfig = _fetchConfig; this._init = this.init(); } init() { try { const _this = this; if (_this._init) return Promise.resolve(_this._init); return Promise.all([Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('fs')); }), Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('path')); })]).then(_ref => { let [fs, path] = _ref; _this._fs = fs.promises; _this._path = path; }); } catch (e) { return Promise.reject(e); } } setAllowNetwork(allow) { if (allow && !this._fetch) { throw new Error('NodeIO requires a Fetch API implementation for HTTP requests.'); } this._fetchEnabled = allow; return this; } readURI(uri, type) { try { const _this2 = this; return Promise.resolve(_this2.init()).then(function () { return function () { if (HTTPUtils.isAbsoluteURL(uri)) { if (!_this2._fetchEnabled || !_this2._fetch) { throw new Error('Network request blocked. Allow HTTP requests explicitly, if needed.'); } return Promise.resolve(_this2._fetch(uri, _this2._fetchConfig)).then(function (response) { return _switch$1(type, [[function () { return 'view'; }, function () { return Promise.resolve(response.arrayBuffer()).then(function (_response$arrayBuffer) { return new Uint8Array(_response$arrayBuffer); }); }], [function () { return 'text'; }, function () { return response.text(); }]]); }); } else { switch (type) { case 'view': return _this2._fs.readFile(uri); case 'text': return _this2._fs.readFile(uri, 'utf8'); } } }(); }); } catch (e) { return Promise.reject(e); } } resolve(base, path) { if (HTTPUtils.isAbsoluteURL(base) || HTTPUtils.isAbsoluteURL(path)) { return HTTPUtils.resolve(base, path); } // https://github.com/KhronosGroup/glTF/issues/1449 // https://stackoverflow.com/a/27278490/1314762 return this._path.resolve(base, decodeURIComponent(path)); } dirname(uri) { if (HTTPUtils.isAbsoluteURL(uri)) { return HTTPUtils.dirname(uri); } return this._path.dirname(uri); } /********************************************************************************************** * Public. */ /** Writes a {@link Document} instance to a local path. */ write(uri, doc) { try { const _this3 = this; return Promise.resolve(_this3.init()).then(function () { const isGLB = !!uri.match(/\.glb$/); return Promise.resolve(isGLB ? _this3._writeGLB(uri, doc) : _this3._writeGLTF(uri, doc)).then(function () {}); }); } catch (e) { return Promise.reject(e); } } /********************************************************************************************** * Private. */ /** @internal */ _writeGLTF(uri, doc) { try { const _this4 = this; _this4.lastWriteBytes = 0; return Promise.resolve(_this4.writeJSON(doc, { format: exports.Format.GLTF, basename: FileUtils.basename(uri) })).then(function (_ref2) { let { json, resources } = _ref2; const { _fs: fs, _path: path } = _this4; const dir = path.dirname(uri); // write json const jsonContent = JSON.stringify(json, null, 2); return Promise.resolve(fs.writeFile(uri, jsonContent)).then(function () { _this4.lastWriteBytes += jsonContent.length; // write resources const _temp = _forOf(listBatches(Object.keys(resources), 10), function (batch) { return Promise.resolve(Promise.all(batch.map(function (resourceURI) { try { if (HTTPUtils.isAbsoluteURL(resourceURI)) { if (HTTPUtils.extension(resourceURI) === 'bin') { throw new Error(`Cannot write buffer to path "${resourceURI}".`); } return Promise.resolve(); } const resourcePath = path.join(dir, decodeURIComponent(resourceURI)); return Promise.resolve(fs.mkdir(path.dirname(resourcePath), { recursive: true })).then(function () { return Promise.resolve(fs.writeFile(resourcePath, resources[resourceURI])).then(function () { _this4.lastWriteBytes += resources[resourceURI].byteLength; }); }); } catch (e) { return Promise.reject(e); } }))).then(function () {}); }); if (_temp && _temp.then) return _temp.then(function () {}); }); }); } catch (e) { return Promise.reject(e); } } /** @internal */ _writeGLB(uri, doc) { try { const _this5 = this; return Promise.resolve(_this5.writeBinary(doc)).then(function (buffer) { return Promise.resolve(_this5._fs.writeFile(uri, buffer)).then(function () { _this5.lastWriteBytes = buffer.byteLength; }); }); } catch (e) { return Promise.reject(e); } } } function listBatches(array, batchSize) { const batches = []; for (let i = 0, il = array.length; i < il; i += batchSize) { const batch = []; for (let j = 0; j < batchSize && i + j < il; j++) { batch.push(array[i + j]); } batches.push(batch); } return batches; } /** * *I/O service for [Deno](https://deno.land/).* * * The most common use of the I/O service is to read/write a {@link Document} with a given path. * Methods are also available for converting in-memory representations of raw glTF files, both * binary (*Uint8Array*) and JSON ({@link JSONDocument}). * * _*NOTICE:* Support for the Deno environment is currently experimental. See * [glTF-Transform#457](https://github.com/donmccurdy/glTF-Transform/issues/457)._ * * Usage: * * ```typescript * import { DenoIO } from 'https://esm.sh/@gltf-transform/core'; * import * as path from 'https://deno.land/std/path/mod.ts'; * * const io = new DenoIO(path); * * // Read. * let document; * document = io.read('model.glb'); // → Document * document = io.readBinary(glb); // Uint8Array → Document * * // Write. * const glb = io.writeBinary(document); // Document → Uint8Array * ``` * * @category I/O */ class DenoIO extends PlatformIO { constructor(path) { super(); this._path = void 0; this._path = path; } readURI(uri, type) { try { switch (type) { case 'view': return Promise.resolve(Deno.readFile(uri)); case 'text': return Promise.resolve(Deno.readTextFile(uri)); } return Promise.resolve(); } catch (e) { return Promise.reject(e); } } resolve(base, path) { // https://github.com/KhronosGroup/glTF/issues/1449 // https://stackoverflow.com/a/27278490/1314762 return this._path.resolve(base, decodeURIComponent(path)); } dirname(uri) { return this._path.dirname(uri); } } /** * *I/O service for Web.* * * The most common use of the I/O service is to read/write a {@link Document} with a given path. * Methods are also available for converting in-memory representations of raw glTF files, both * binary (*Uint8Array*) and JSON ({@link JSONDocument}). * * Usage: * * ```typescript * import { WebIO } from '@gltf-transform/core'; * * const io = new WebIO({credentials: 'include'}); * * // Read. * let document; * document = await io.read('model.glb'); // → Document * document = await io.readBinary(glb); // Uint8Array → Document * * // Write. * const glb = await io.writeBinary(document); // Document → Uint8Array * ``` * * @category I/O */ function _settle(pact, state, value) { if (!pact.s) { if (value instanceof _Pact) { if (value.s) { if (state & 1) { state = value.s; } value = value.v; } else { value.o = _settle.bind(null, pact, state); return; } } if (value && value.then) { value.then(_settle.bind(null, pact, state), _settle.bind(null, pact, 2)); return; } pact.s = state; pact.v = value; const observer = pact.o; if (observer) { observer(pact); } } } const _Pact = /*#__PURE__*/function () { function _Pact() {} _Pact.prototype.then = function (onFulfilled, onRejected) { const result = new _Pact(); const state = this.s; if (state) { const callback = state & 1 ? onFulfilled : onRejected; if (callback) { try { _settle(result, 1, callback(this.v)); } catch (e) { _settle(result, 2, e); } return result; } else { return this; } } this.o = function (_this) { try { const value = _this.v; if (_this.s & 1) { _settle(result, 1, onFulfilled ? onFulfilled(value) : value); } else if (onRejected) { _settle(result, 1, onRejected(value)); } else { _settle(result, 2, value); } } catch (e) { _settle(result, 2, e); } }; return result; }; return _Pact; }(); function _switch(discriminant, cases) { var dispatchIndex = -1; var awaitBody; outer: { for (var i = 0; i < cases.length; i++) { var test = cases[i][0]; if (test) { var testValue = test(); if (testValue && testValue.then) { break outer; } if (testValue === discriminant) { dispatchIndex = i; break; } } else { // Found the default case, set it as the pending dispatch case dispatchIndex = i; } } if (dispatchIndex !== -1) { do { var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } var result = body(); if (result && result.then) { awaitBody = true; break outer; } var fallthroughCheck = cases[dispatchIndex][2]; dispatchIndex++; } while (fallthroughCheck && !fallthroughCheck()); return result; } } const pact = new _Pact(); const reject = _settle.bind(null, pact, 2); (awaitBody ? result.then(_resumeAfterBody) : testValue.then(_resumeAfterTest)).then(void 0, reject); return pact; function _resumeAfterTest(value) { for (;;) { if (value === discriminant) { dispatchIndex = i; break; } if (++i === cases.length) { if (dispatchIndex !== -1) { break; } else { _settle(pact, 1, result); return; } } test = cases[i][0]; if (test) { value = test(); if (value && value.then) { value.then(_resumeAfterTest).then(void 0, reject); return; } } else { dispatchIndex = i; } } do { var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } var result = body(); if (result && result.then) { result.then(_resumeAfterBody).then(void 0, reject); return; } var fallthroughCheck = cases[dispatchIndex][2]; dispatchIndex++; } while (fallthroughCheck && !fallthroughCheck()); _settle(pact, 1, result); } function _resumeAfterBody(result) { for (;;) { var fallthroughCheck = cases[dispatchIndex][2]; if (!fallthroughCheck || fallthroughCheck()) { break; } dispatchIndex++; var body = cases[dispatchIndex][1]; while (!body) { dispatchIndex++; body = cases[dispatchIndex][1]; } result = body(); if (result && result.then) { result.then(_resumeAfterBody).then(void 0, reject); return; } } _settle(pact, 1, result); } } class WebIO extends PlatformIO { /** * Constructs a new WebIO service. Instances are reusable. * @param fetchConfig Configuration object for Fetch API. */ constructor(fetchConfig) { if (fetchConfig === void 0) { fetchConfig = HTTPUtils.DEFAULT_INIT; } super(); this._fetchConfig = void 0; this._fetchConfig = fetchConfig; } readURI(uri, type) { try { const _this = this; return Promise.resolve(fetch(uri, _this._fetchConfig)).then(function (response) { return _switch(type, [[function () { return 'view'; }, function () { return Promise.resolve(response.arrayBuffer()).then(function (_response$arrayBuffer) { return new Uint8Array(_response$arrayBuffer); }); }], [function () { return 'text'; }, function () { return response.text(); }]]); }); } catch (e) { return Promise.reject(e); } } resolve(base, path) { return HTTPUtils.resolve(base, path); } dirname(uri) { return HTTPUtils.dirname(uri); } } Object.defineProperty(exports, 'Graph', { enumerable: true, get: function () { return propertyGraph.Graph; } }); Object.defineProperty(exports, 'GraphEdge', { enumerable: true, get: function () { return propertyGraph.GraphEdge; } }); Object.defineProperty(exports, 'RefList', { enumerable: true, get: function () { return propertyGraph.RefList; } }); Object.defineProperty(exports, 'RefMap', { enumerable: true, get: function () { return propertyGraph.RefMap; } }); Object.defineProperty(exports, 'RefSet', { enumerable: true, get: function () { return propertyGraph.RefSet; } }); exports.Accessor = Accessor; exports.Animation = Animation; exports.AnimationChannel = AnimationChannel; exports.AnimationSampler = AnimationSampler; exports.Buffer = Buffer$1; exports.BufferUtils = BufferUtils; exports.COPY_IDENTITY = COPY_IDENTITY; exports.Camera = Camera; exports.ColorUtils = ColorUtils; exports.ComponentTypeToTypedArray = ComponentTypeToTypedArray; exports.DenoIO = DenoIO; exports.Document = Document; exports.ExtensibleProperty = ExtensibleProperty; exports.Extension = Extension; exports.ExtensionProperty = ExtensionProperty; exports.FileUtils = FileUtils; exports.GLB_BUFFER = GLB_BUFFER; exports.HTTPUtils = HTTPUtils; exports.ImageUtils = ImageUtils; exports.Logger = Logger; exports.Material = Material; exports.MathUtils = MathUtils; exports.Mesh = Mesh; exports.Node = Node; exports.NodeIO = NodeIO; exports.PlatformIO = PlatformIO; exports.Primitive = Primitive; exports.PrimitiveTarget = PrimitiveTarget; exports.Property = Property; exports.ReaderContext = ReaderContext; exports.Root = Root; exports.Scene = Scene; exports.Skin = Skin; exports.Texture = Texture; exports.TextureInfo = TextureInfo; exports.VERSION = VERSION; exports.WebIO = WebIO; exports.WriterContext = WriterContext; exports.getBounds = getBounds; exports.uuid = uuid; //# sourceMappingURL=index.cjs.map