var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { PropertyBinding, InterpolateLinear, Color, Vector3, CompressedTexture, Texture, MathUtils, RGBAFormat, DoubleSide, BufferAttribute, InterpolateDiscrete, Matrix4, Scene, PlaneGeometry, ShaderMaterial, Uniform, Mesh, PerspectiveCamera, WebGLRenderer, NearestFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearFilter, LinearMipmapNearestFilter, LinearMipmapLinearFilter, ClampToEdgeWrapping, RepeatWrapping, MirroredRepeatWrapping } from "three"; import { version } from "../_polyfill/constants.js"; async function readAsDataURL(blob) { const buffer = await blob.arrayBuffer(); const data = btoa(String.fromCharCode(...new Uint8Array(buffer))); return `data:${blob.type || ""};base64,${data}`; } let _renderer; let fullscreenQuadGeometry; let fullscreenQuadMaterial; let fullscreenQuad; function decompress(texture, maxTextureSize = Infinity, renderer = null) { if (!fullscreenQuadGeometry) fullscreenQuadGeometry = new PlaneGeometry(2, 2, 1, 1); if (!fullscreenQuadMaterial) fullscreenQuadMaterial = new ShaderMaterial({ uniforms: { blitTexture: new Uniform(texture) }, vertexShader: ( /* glsl */ ` varying vec2 vUv; void main(){ vUv = uv; gl_Position = vec4(position.xy * 1.0,0.,.999999); } ` ), fragmentShader: ( /* glsl */ ` uniform sampler2D blitTexture; varying vec2 vUv; void main(){ gl_FragColor = vec4(vUv.xy, 0, 1); #ifdef IS_SRGB gl_FragColor = LinearTosRGB( texture2D( blitTexture, vUv) ); #else gl_FragColor = texture2D( blitTexture, vUv); #endif } ` ) }); fullscreenQuadMaterial.uniforms.blitTexture.value = texture; fullscreenQuadMaterial.defines.IS_SRGB = "colorSpace" in texture ? texture.colorSpace === "srgb" : texture.encoding === 3001; fullscreenQuadMaterial.needsUpdate = true; if (!fullscreenQuad) { fullscreenQuad = new Mesh(fullscreenQuadGeometry, fullscreenQuadMaterial); fullscreenQuad.frustrumCulled = false; } const _camera = new PerspectiveCamera(); const _scene = new Scene(); _scene.add(fullscreenQuad); if (!renderer) { renderer = _renderer = new WebGLRenderer({ antialias: false }); } renderer.setSize(Math.min(texture.image.width, maxTextureSize), Math.min(texture.image.height, maxTextureSize)); renderer.clear(); renderer.render(_scene, _camera); const readableTexture = new Texture(renderer.domElement); readableTexture.minFilter = texture.minFilter; readableTexture.magFilter = texture.magFilter; readableTexture.wrapS = texture.wrapS; readableTexture.wrapT = texture.wrapT; readableTexture.name = texture.name; if (_renderer) { _renderer.dispose(); _renderer = null; } return readableTexture; } const KHR_mesh_quantization_ExtraAttrTypes = { POSITION: [ "byte", "byte normalized", "unsigned byte", "unsigned byte normalized", "short", "short normalized", "unsigned short", "unsigned short normalized" ], NORMAL: ["byte normalized", "short normalized"], TANGENT: ["byte normalized", "short normalized"], TEXCOORD: ["byte", "byte normalized", "unsigned byte", "short", "short normalized", "unsigned short"] }; const GLTFExporter = /* @__PURE__ */ (() => { class GLTFExporter2 { constructor() { this.pluginCallbacks = []; this.register(function(writer) { return new GLTFLightExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsUnlitExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsTransmissionExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsVolumeExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsIorExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsSpecularExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsClearcoatExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsIridescenceExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsSheenExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsAnisotropyExtension(writer); }); this.register(function(writer) { return new GLTFMaterialsEmissiveStrengthExtension(writer); }); } register(callback) { if (this.pluginCallbacks.indexOf(callback) === -1) { this.pluginCallbacks.push(callback); } return this; } unregister(callback) { if (this.pluginCallbacks.indexOf(callback) !== -1) { this.pluginCallbacks.splice(this.pluginCallbacks.indexOf(callback), 1); } return this; } /** * Parse scenes and generate GLTF output * @param {Scene or [THREE.Scenes]} input Scene or Array of THREE.Scenes * @param {Function} onDone Callback on completed * @param {Function} onError Callback on errors * @param {Object} options options */ parse(input, onDone, onError, options) { const writer = new GLTFWriter(); const plugins = []; for (let i = 0, il = this.pluginCallbacks.length; i < il; i++) { plugins.push(this.pluginCallbacks[i](writer)); } writer.setPlugins(plugins); writer.write(input, onDone, options).catch(onError); } parseAsync(input, options) { const scope = this; return new Promise(function(resolve, reject) { scope.parse(input, resolve, reject, options); }); } } /** * Static utility functions */ __publicField(GLTFExporter2, "Utils", { insertKeyframe: function(track, time) { const tolerance = 1e-3; const valueSize = track.getValueSize(); const times = new track.TimeBufferType(track.times.length + 1); const values = new track.ValueBufferType(track.values.length + valueSize); const interpolant = track.createInterpolant(new track.ValueBufferType(valueSize)); let index; if (track.times.length === 0) { times[0] = time; for (let i = 0; i < valueSize; i++) { values[i] = 0; } index = 0; } else if (time < track.times[0]) { if (Math.abs(track.times[0] - time) < tolerance) return 0; times[0] = time; times.set(track.times, 1); values.set(interpolant.evaluate(time), 0); values.set(track.values, valueSize); index = 0; } else if (time > track.times[track.times.length - 1]) { if (Math.abs(track.times[track.times.length - 1] - time) < tolerance) { return track.times.length - 1; } times[times.length - 1] = time; times.set(track.times, 0); values.set(track.values, 0); values.set(interpolant.evaluate(time), track.values.length); index = times.length - 1; } else { for (let i = 0; i < track.times.length; i++) { if (Math.abs(track.times[i] - time) < tolerance) return i; if (track.times[i] < time && track.times[i + 1] > time) { times.set(track.times.slice(0, i + 1), 0); times[i + 1] = time; times.set(track.times.slice(i + 1), i + 2); values.set(track.values.slice(0, (i + 1) * valueSize), 0); values.set(interpolant.evaluate(time), (i + 1) * valueSize); values.set(track.values.slice((i + 1) * valueSize), (i + 2) * valueSize); index = i + 1; break; } } } track.times = times; track.values = values; return index; }, mergeMorphTargetTracks: function(clip, root) { const tracks = []; const mergedTracks = {}; const sourceTracks = clip.tracks; for (let i = 0; i < sourceTracks.length; ++i) { let sourceTrack = sourceTracks[i]; const sourceTrackBinding = PropertyBinding.parseTrackName(sourceTrack.name); const sourceTrackNode = PropertyBinding.findNode(root, sourceTrackBinding.nodeName); if (sourceTrackBinding.propertyName !== "morphTargetInfluences" || sourceTrackBinding.propertyIndex === void 0) { tracks.push(sourceTrack); continue; } if (sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodDiscrete && sourceTrack.createInterpolant !== sourceTrack.InterpolantFactoryMethodLinear) { if (sourceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline) { throw new Error("THREE.GLTFExporter: Cannot merge tracks with glTF CUBICSPLINE interpolation."); } console.warn("THREE.GLTFExporter: Morph target interpolation mode not yet supported. Using LINEAR instead."); sourceTrack = sourceTrack.clone(); sourceTrack.setInterpolation(InterpolateLinear); } const targetCount = sourceTrackNode.morphTargetInfluences.length; const targetIndex = sourceTrackNode.morphTargetDictionary[sourceTrackBinding.propertyIndex]; if (targetIndex === void 0) { throw new Error("THREE.GLTFExporter: Morph target name not found: " + sourceTrackBinding.propertyIndex); } let mergedTrack; if (mergedTracks[sourceTrackNode.uuid] === void 0) { mergedTrack = sourceTrack.clone(); const values = new mergedTrack.ValueBufferType(targetCount * mergedTrack.times.length); for (let j = 0; j < mergedTrack.times.length; j++) { values[j * targetCount + targetIndex] = mergedTrack.values[j]; } mergedTrack.name = (sourceTrackBinding.nodeName || "") + ".morphTargetInfluences"; mergedTrack.values = values; mergedTracks[sourceTrackNode.uuid] = mergedTrack; tracks.push(mergedTrack); continue; } const sourceInterpolant = sourceTrack.createInterpolant(new sourceTrack.ValueBufferType(1)); mergedTrack = mergedTracks[sourceTrackNode.uuid]; for (let j = 0; j < mergedTrack.times.length; j++) { mergedTrack.values[j * targetCount + targetIndex] = sourceInterpolant.evaluate(mergedTrack.times[j]); } for (let j = 0; j < sourceTrack.times.length; j++) { const keyframeIndex = this.insertKeyframe(mergedTrack, sourceTrack.times[j]); mergedTrack.values[keyframeIndex * targetCount + targetIndex] = sourceTrack.values[j]; } } clip.tracks = tracks; return clip; } }); return GLTFExporter2; })(); const WEBGL_CONSTANTS = { POINTS: 0, LINES: 1, LINE_LOOP: 2, LINE_STRIP: 3, TRIANGLES: 4, TRIANGLE_STRIP: 5, TRIANGLE_FAN: 6, BYTE: 5120, UNSIGNED_BYTE: 5121, SHORT: 5122, UNSIGNED_SHORT: 5123, INT: 5124, UNSIGNED_INT: 5125, FLOAT: 5126, ARRAY_BUFFER: 34962, ELEMENT_ARRAY_BUFFER: 34963, NEAREST: 9728, LINEAR: 9729, NEAREST_MIPMAP_NEAREST: 9984, LINEAR_MIPMAP_NEAREST: 9985, NEAREST_MIPMAP_LINEAR: 9986, LINEAR_MIPMAP_LINEAR: 9987, CLAMP_TO_EDGE: 33071, MIRRORED_REPEAT: 33648, REPEAT: 10497 }; const KHR_MESH_QUANTIZATION = "KHR_mesh_quantization"; const THREE_TO_WEBGL = {}; THREE_TO_WEBGL[NearestFilter] = WEBGL_CONSTANTS.NEAREST; THREE_TO_WEBGL[NearestMipmapNearestFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST; THREE_TO_WEBGL[NearestMipmapLinearFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR; THREE_TO_WEBGL[LinearFilter] = WEBGL_CONSTANTS.LINEAR; THREE_TO_WEBGL[LinearMipmapNearestFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST; THREE_TO_WEBGL[LinearMipmapLinearFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR; THREE_TO_WEBGL[ClampToEdgeWrapping] = WEBGL_CONSTANTS.CLAMP_TO_EDGE; THREE_TO_WEBGL[RepeatWrapping] = WEBGL_CONSTANTS.REPEAT; THREE_TO_WEBGL[MirroredRepeatWrapping] = WEBGL_CONSTANTS.MIRRORED_REPEAT; const PATH_PROPERTIES = { scale: "scale", position: "translation", quaternion: "rotation", morphTargetInfluences: "weights" }; const DEFAULT_SPECULAR_COLOR = /* @__PURE__ */ new Color(); const GLB_HEADER_BYTES = 12; const GLB_HEADER_MAGIC = 1179937895; const GLB_VERSION = 2; const GLB_CHUNK_PREFIX_BYTES = 8; const GLB_CHUNK_TYPE_JSON = 1313821514; const GLB_CHUNK_TYPE_BIN = 5130562; function equalArray(array1, array2) { return array1.length === array2.length && array1.every(function(element, index) { return element === array2[index]; }); } function stringToArrayBuffer(text) { return new TextEncoder().encode(text).buffer; } function isIdentityMatrix(matrix) { return equalArray(matrix.elements, [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]); } function getMinMax(attribute, start, count) { const output = { min: new Array(attribute.itemSize).fill(Number.POSITIVE_INFINITY), max: new Array(attribute.itemSize).fill(Number.NEGATIVE_INFINITY) }; for (let i = start; i < start + count; i++) { for (let a = 0; a < attribute.itemSize; a++) { let value; if (attribute.itemSize > 4) { value = attribute.array[i * attribute.itemSize + a]; } else { if (a === 0) value = attribute.getX(i); else if (a === 1) value = attribute.getY(i); else if (a === 2) value = attribute.getZ(i); else if (a === 3) value = attribute.getW(i); if (attribute.normalized === true) { value = MathUtils.normalize(value, attribute.array); } } output.min[a] = Math.min(output.min[a], value); output.max[a] = Math.max(output.max[a], value); } } return output; } function getPaddedBufferSize(bufferSize) { return Math.ceil(bufferSize / 4) * 4; } function getPaddedArrayBuffer(arrayBuffer, paddingByte = 0) { const paddedLength = getPaddedBufferSize(arrayBuffer.byteLength); if (paddedLength !== arrayBuffer.byteLength) { const array = new Uint8Array(paddedLength); array.set(new Uint8Array(arrayBuffer)); if (paddingByte !== 0) { for (let i = arrayBuffer.byteLength; i < paddedLength; i++) { array[i] = paddingByte; } } return array.buffer; } return arrayBuffer; } function getCanvas() { if (typeof document === "undefined" && typeof OffscreenCanvas !== "undefined") { return new OffscreenCanvas(1, 1); } return document.createElement("canvas"); } function getToBlobPromise(canvas, mimeType) { if (canvas.toBlob !== void 0) { return new Promise((resolve) => canvas.toBlob(resolve, mimeType)); } let quality; if (mimeType === "image/jpeg") { quality = 0.92; } else if (mimeType === "image/webp") { quality = 0.8; } return canvas.convertToBlob({ type: mimeType, quality }); } class GLTFWriter { constructor() { this.plugins = []; this.options = {}; this.pending = []; this.buffers = []; this.byteOffset = 0; this.buffers = []; this.nodeMap = /* @__PURE__ */ new Map(); this.skins = []; this.extensionsUsed = {}; this.extensionsRequired = {}; this.uids = /* @__PURE__ */ new Map(); this.uid = 0; this.json = { asset: { version: "2.0", generator: "THREE.GLTFExporter" } }; this.cache = { meshes: /* @__PURE__ */ new Map(), attributes: /* @__PURE__ */ new Map(), attributesNormalized: /* @__PURE__ */ new Map(), materials: /* @__PURE__ */ new Map(), textures: /* @__PURE__ */ new Map(), images: /* @__PURE__ */ new Map() }; } setPlugins(plugins) { this.plugins = plugins; } /** * Parse scenes and generate GLTF output * @param {Scene or [THREE.Scenes]} input Scene or Array of THREE.Scenes * @param {Function} onDone Callback on completed * @param {Object} options options */ async write(input, onDone, options = {}) { this.options = Object.assign( { // default options binary: false, trs: false, onlyVisible: true, maxTextureSize: Infinity, animations: [], includeCustomExtensions: false }, options ); if (this.options.animations.length > 0) { this.options.trs = true; } this.processInput(input); await Promise.all(this.pending); const writer = this; const buffers = writer.buffers; const json = writer.json; options = writer.options; const extensionsUsed = writer.extensionsUsed; const extensionsRequired = writer.extensionsRequired; const blob = new Blob(buffers, { type: "application/octet-stream" }); const extensionsUsedList = Object.keys(extensionsUsed); const extensionsRequiredList = Object.keys(extensionsRequired); if (extensionsUsedList.length > 0) json.extensionsUsed = extensionsUsedList; if (extensionsRequiredList.length > 0) json.extensionsRequired = extensionsRequiredList; if (json.buffers && json.buffers.length > 0) json.buffers[0].byteLength = blob.size; if (options.binary === true) { blob.arrayBuffer().then((result) => { const binaryChunk = getPaddedArrayBuffer(result); const binaryChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)); binaryChunkPrefix.setUint32(0, binaryChunk.byteLength, true); binaryChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_BIN, true); const jsonChunk = getPaddedArrayBuffer(stringToArrayBuffer(JSON.stringify(json)), 32); const jsonChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)); jsonChunkPrefix.setUint32(0, jsonChunk.byteLength, true); jsonChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_JSON, true); const header = new ArrayBuffer(GLB_HEADER_BYTES); const headerView = new DataView(header); headerView.setUint32(0, GLB_HEADER_MAGIC, true); headerView.setUint32(4, GLB_VERSION, true); const totalByteLength = GLB_HEADER_BYTES + jsonChunkPrefix.byteLength + jsonChunk.byteLength + binaryChunkPrefix.byteLength + binaryChunk.byteLength; headerView.setUint32(8, totalByteLength, true); const glbBlob = new Blob([header, jsonChunkPrefix, jsonChunk, binaryChunkPrefix, binaryChunk], { type: "application/octet-stream" }); glbBlob.arrayBuffer().then(onDone); }); } else { if (json.buffers && json.buffers.length > 0) { readAsDataURL(blob).then((uri) => { json.buffers[0].uri = uri; onDone(json); }); } else { onDone(json); } } } /** * Serializes a userData. * * @param {THREE.Object3D|THREE.Material} object * @param {Object} objectDef */ serializeUserData(object, objectDef) { if (Object.keys(object.userData).length === 0) return; const options = this.options; const extensionsUsed = this.extensionsUsed; try { const json = JSON.parse(JSON.stringify(object.userData)); if (options.includeCustomExtensions && json.gltfExtensions) { if (objectDef.extensions === void 0) objectDef.extensions = {}; for (const extensionName in json.gltfExtensions) { objectDef.extensions[extensionName] = json.gltfExtensions[extensionName]; extensionsUsed[extensionName] = true; } delete json.gltfExtensions; } if (Object.keys(json).length > 0) objectDef.extras = json; } catch (error) { console.warn( "THREE.GLTFExporter: userData of '" + object.name + "' won't be serialized because of JSON.stringify error - " + error.message ); } } /** * Returns ids for buffer attributes. * @param {Object} object * @return {Integer} */ getUID(attribute, isRelativeCopy = false) { if (this.uids.has(attribute) === false) { const uids2 = /* @__PURE__ */ new Map(); uids2.set(true, this.uid++); uids2.set(false, this.uid++); this.uids.set(attribute, uids2); } const uids = this.uids.get(attribute); return uids.get(isRelativeCopy); } /** * Checks if normal attribute values are normalized. * * @param {BufferAttribute} normal * @returns {Boolean} */ isNormalizedNormalAttribute(normal) { const cache = this.cache; if (cache.attributesNormalized.has(normal)) return false; const v = new Vector3(); for (let i = 0, il = normal.count; i < il; i++) { if (Math.abs(v.fromBufferAttribute(normal, i).length() - 1) > 5e-4) return false; } return true; } /** * Creates normalized normal buffer attribute. * * @param {BufferAttribute} normal * @returns {BufferAttribute} * */ createNormalizedNormalAttribute(normal) { const cache = this.cache; if (cache.attributesNormalized.has(normal)) return cache.attributesNormalized.get(normal); const attribute = normal.clone(); const v = new Vector3(); for (let i = 0, il = attribute.count; i < il; i++) { v.fromBufferAttribute(attribute, i); if (v.x === 0 && v.y === 0 && v.z === 0) { v.setX(1); } else { v.normalize(); } attribute.setXYZ(i, v.x, v.y, v.z); } cache.attributesNormalized.set(normal, attribute); return attribute; } /** * Applies a texture transform, if present, to the map definition. Requires * the KHR_texture_transform extension. * * @param {Object} mapDef * @param {THREE.Texture} texture */ applyTextureTransform(mapDef, texture) { let didTransform = false; const transformDef = {}; if (texture.offset.x !== 0 || texture.offset.y !== 0) { transformDef.offset = texture.offset.toArray(); didTransform = true; } if (texture.rotation !== 0) { transformDef.rotation = texture.rotation; didTransform = true; } if (texture.repeat.x !== 1 || texture.repeat.y !== 1) { transformDef.scale = texture.repeat.toArray(); didTransform = true; } if (didTransform) { mapDef.extensions = mapDef.extensions || {}; mapDef.extensions["KHR_texture_transform"] = transformDef; this.extensionsUsed["KHR_texture_transform"] = true; } } buildMetalRoughTexture(metalnessMap, roughnessMap) { if (metalnessMap === roughnessMap) return metalnessMap; function getEncodingConversion(map) { if ("colorSpace" in map ? map.colorSpace === "srgb" : map.encoding === 3001) { return function SRGBToLinear(c) { return c < 0.04045 ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4); }; } return function LinearToLinear(c) { return c; }; } console.warn("THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures."); if (metalnessMap instanceof CompressedTexture) { metalnessMap = decompress(metalnessMap); } if (roughnessMap instanceof CompressedTexture) { roughnessMap = decompress(roughnessMap); } const metalness = metalnessMap ? metalnessMap.image : null; const roughness = roughnessMap ? roughnessMap.image : null; const width = Math.max(metalness ? metalness.width : 0, roughness ? roughness.width : 0); const height = Math.max(metalness ? metalness.height : 0, roughness ? roughness.height : 0); const canvas = getCanvas(); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); context.fillStyle = "#00ffff"; context.fillRect(0, 0, width, height); const composite = context.getImageData(0, 0, width, height); if (metalness) { context.drawImage(metalness, 0, 0, width, height); const convert = getEncodingConversion(metalnessMap); const data = context.getImageData(0, 0, width, height).data; for (let i = 2; i < data.length; i += 4) { composite.data[i] = convert(data[i] / 256) * 256; } } if (roughness) { context.drawImage(roughness, 0, 0, width, height); const convert = getEncodingConversion(roughnessMap); const data = context.getImageData(0, 0, width, height).data; for (let i = 1; i < data.length; i += 4) { composite.data[i] = convert(data[i] / 256) * 256; } } context.putImageData(composite, 0, 0); const reference = metalnessMap || roughnessMap; const texture = reference.clone(); texture.source = new Texture(canvas).source; if ("colorSpace" in texture) texture.colorSpace = ""; else texture.encoding = 3e3; texture.channel = (metalnessMap || roughnessMap).channel; if (metalnessMap && roughnessMap && metalnessMap.channel !== roughnessMap.channel) { console.warn("THREE.GLTFExporter: UV channels for metalnessMap and roughnessMap textures must match."); } return texture; } /** * Process a buffer to append to the default one. * @param {ArrayBuffer} buffer * @return {Integer} */ processBuffer(buffer) { const json = this.json; const buffers = this.buffers; if (!json.buffers) json.buffers = [{ byteLength: 0 }]; buffers.push(buffer); return 0; } /** * Process and generate a BufferView * @param {BufferAttribute} attribute * @param {number} componentType * @param {number} start * @param {number} count * @param {number} target (Optional) Target usage of the BufferView * @return {Object} */ processBufferView(attribute, componentType, start, count, target) { const json = this.json; if (!json.bufferViews) json.bufferViews = []; let componentSize; switch (componentType) { case WEBGL_CONSTANTS.BYTE: case WEBGL_CONSTANTS.UNSIGNED_BYTE: componentSize = 1; break; case WEBGL_CONSTANTS.SHORT: case WEBGL_CONSTANTS.UNSIGNED_SHORT: componentSize = 2; break; default: componentSize = 4; } let byteStride = attribute.itemSize * componentSize; if (target === WEBGL_CONSTANTS.ARRAY_BUFFER) { byteStride = Math.ceil(byteStride / 4) * 4; } const byteLength = getPaddedBufferSize(count * byteStride); const dataView = new DataView(new ArrayBuffer(byteLength)); let offset = 0; for (let i = start; i < start + count; i++) { for (let a = 0; a < attribute.itemSize; a++) { let value; if (attribute.itemSize > 4) { value = attribute.array[i * attribute.itemSize + a]; } else { if (a === 0) value = attribute.getX(i); else if (a === 1) value = attribute.getY(i); else if (a === 2) value = attribute.getZ(i); else if (a === 3) value = attribute.getW(i); if (attribute.normalized === true) { value = MathUtils.normalize(value, attribute.array); } } if (componentType === WEBGL_CONSTANTS.FLOAT) { dataView.setFloat32(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.INT) { dataView.setInt32(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_INT) { dataView.setUint32(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.SHORT) { dataView.setInt16(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT) { dataView.setUint16(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.BYTE) { dataView.setInt8(offset, value); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE) { dataView.setUint8(offset, value); } offset += componentSize; } if (offset % byteStride !== 0) { offset += byteStride - offset % byteStride; } } const bufferViewDef = { buffer: this.processBuffer(dataView.buffer), byteOffset: this.byteOffset, byteLength }; if (target !== void 0) bufferViewDef.target = target; if (target === WEBGL_CONSTANTS.ARRAY_BUFFER) { bufferViewDef.byteStride = byteStride; } this.byteOffset += byteLength; json.bufferViews.push(bufferViewDef); const output = { id: json.bufferViews.length - 1, byteLength: 0 }; return output; } /** * Process and generate a BufferView from an image Blob. * @param {Blob} blob * @return {Promise} */ processBufferViewImage(blob) { const writer = this; const json = writer.json; if (!json.bufferViews) json.bufferViews = []; return blob.arrayBuffer().then((result) => { const buffer = getPaddedArrayBuffer(result); const bufferViewDef = { buffer: writer.processBuffer(buffer), byteOffset: writer.byteOffset, byteLength: buffer.byteLength }; writer.byteOffset += buffer.byteLength; return json.bufferViews.push(bufferViewDef) - 1; }); } /** * Process attribute to generate an accessor * @param {BufferAttribute} attribute Attribute to process * @param {THREE.BufferGeometry} geometry (Optional) Geometry used for truncated draw range * @param {Integer} start (Optional) * @param {Integer} count (Optional) * @return {Integer|null} Index of the processed accessor on the "accessors" array */ processAccessor(attribute, geometry, start, count) { const json = this.json; const types = { 1: "SCALAR", 2: "VEC2", 3: "VEC3", 4: "VEC4", 9: "MAT3", 16: "MAT4" }; let componentType; if (attribute.array.constructor === Float32Array) { componentType = WEBGL_CONSTANTS.FLOAT; } else if (attribute.array.constructor === Int32Array) { componentType = WEBGL_CONSTANTS.INT; } else if (attribute.array.constructor === Uint32Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_INT; } else if (attribute.array.constructor === Int16Array) { componentType = WEBGL_CONSTANTS.SHORT; } else if (attribute.array.constructor === Uint16Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT; } else if (attribute.array.constructor === Int8Array) { componentType = WEBGL_CONSTANTS.BYTE; } else if (attribute.array.constructor === Uint8Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE; } else { throw new Error( "THREE.GLTFExporter: Unsupported bufferAttribute component type: " + attribute.array.constructor.name ); } if (start === void 0) start = 0; if (count === void 0) count = attribute.count; if (count === 0) return null; const minMax = getMinMax(attribute, start, count); let bufferViewTarget; if (geometry !== void 0) { bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER; } const bufferView = this.processBufferView(attribute, componentType, start, count, bufferViewTarget); const accessorDef = { bufferView: bufferView.id, byteOffset: bufferView.byteOffset, componentType, count, max: minMax.max, min: minMax.min, type: types[attribute.itemSize] }; if (attribute.normalized === true) accessorDef.normalized = true; if (!json.accessors) json.accessors = []; return json.accessors.push(accessorDef) - 1; } /** * Process image * @param {Image} image to process * @param {Integer} format of the image (RGBAFormat) * @param {Boolean} flipY before writing out the image * @param {String} mimeType export format * @return {Integer} Index of the processed texture in the "images" array */ processImage(image, format, flipY, mimeType = "image/png") { if (image !== null) { const writer = this; const cache = writer.cache; const json = writer.json; const options = writer.options; const pending = writer.pending; if (!cache.images.has(image)) cache.images.set(image, {}); const cachedImages = cache.images.get(image); const key = mimeType + ":flipY/" + flipY.toString(); if (cachedImages[key] !== void 0) return cachedImages[key]; if (!json.images) json.images = []; const imageDef = { mimeType }; const canvas = getCanvas(); canvas.width = Math.min(image.width, options.maxTextureSize); canvas.height = Math.min(image.height, options.maxTextureSize); const ctx = canvas.getContext("2d"); if (flipY === true) { ctx.translate(0, canvas.height); ctx.scale(1, -1); } if (image.data !== void 0) { if (format !== RGBAFormat) { console.error("GLTFExporter: Only RGBAFormat is supported.", format); } if (image.width > options.maxTextureSize || image.height > options.maxTextureSize) { console.warn("GLTFExporter: Image size is bigger than maxTextureSize", image); } const data = new Uint8ClampedArray(image.height * image.width * 4); for (let i = 0; i < data.length; i += 4) { data[i + 0] = image.data[i + 0]; data[i + 1] = image.data[i + 1]; data[i + 2] = image.data[i + 2]; data[i + 3] = image.data[i + 3]; } ctx.putImageData(new ImageData(data, image.width, image.height), 0, 0); } else { ctx.drawImage(image, 0, 0, canvas.width, canvas.height); } if (options.binary === true) { pending.push( getToBlobPromise(canvas, mimeType).then((blob) => writer.processBufferViewImage(blob)).then((bufferViewIndex) => { imageDef.bufferView = bufferViewIndex; }) ); } else { if (canvas.toDataURL !== void 0) { imageDef.uri = canvas.toDataURL(mimeType); } else { pending.push( getToBlobPromise(canvas, mimeType).then(readAsDataURL).then((uri) => { imageDef.uri = uri; }) ); } } const index = json.images.push(imageDef) - 1; cachedImages[key] = index; return index; } else { throw new Error("THREE.GLTFExporter: No valid image data found. Unable to process texture."); } } /** * Process sampler * @param {Texture} map Texture to process * @return {Integer} Index of the processed texture in the "samplers" array */ processSampler(map) { const json = this.json; if (!json.samplers) json.samplers = []; const samplerDef = { magFilter: THREE_TO_WEBGL[map.magFilter], minFilter: THREE_TO_WEBGL[map.minFilter], wrapS: THREE_TO_WEBGL[map.wrapS], wrapT: THREE_TO_WEBGL[map.wrapT] }; return json.samplers.push(samplerDef) - 1; } /** * Process texture * @param {Texture} map Map to process * @return {Integer} Index of the processed texture in the "textures" array */ processTexture(map) { const writer = this; const options = writer.options; const cache = this.cache; const json = this.json; if (cache.textures.has(map)) return cache.textures.get(map); if (!json.textures) json.textures = []; if (map instanceof CompressedTexture) { map = decompress(map, options.maxTextureSize); } let mimeType = map.userData.mimeType; if (mimeType === "image/webp") mimeType = "image/png"; const textureDef = { sampler: this.processSampler(map), source: this.processImage(map.image, map.format, map.flipY, mimeType) }; if (map.name) textureDef.name = map.name; this._invokeAll(function(ext) { ext.writeTexture && ext.writeTexture(map, textureDef); }); const index = json.textures.push(textureDef) - 1; cache.textures.set(map, index); return index; } /** * Process material * @param {THREE.Material} material Material to process * @return {Integer|null} Index of the processed material in the "materials" array */ processMaterial(material) { const cache = this.cache; const json = this.json; if (cache.materials.has(material)) return cache.materials.get(material); if (material.isShaderMaterial) { console.warn("GLTFExporter: THREE.ShaderMaterial not supported."); return null; } if (!json.materials) json.materials = []; const materialDef = { pbrMetallicRoughness: {} }; if (material.isMeshStandardMaterial !== true && material.isMeshBasicMaterial !== true) { console.warn("GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results."); } const color = material.color.toArray().concat([material.opacity]); if (!equalArray(color, [1, 1, 1, 1])) { materialDef.pbrMetallicRoughness.baseColorFactor = color; } if (material.isMeshStandardMaterial) { materialDef.pbrMetallicRoughness.metallicFactor = material.metalness; materialDef.pbrMetallicRoughness.roughnessFactor = material.roughness; } else { materialDef.pbrMetallicRoughness.metallicFactor = 0.5; materialDef.pbrMetallicRoughness.roughnessFactor = 0.5; } if (material.metalnessMap || material.roughnessMap) { const metalRoughTexture = this.buildMetalRoughTexture(material.metalnessMap, material.roughnessMap); const metalRoughMapDef = { index: this.processTexture(metalRoughTexture), channel: metalRoughTexture.channel }; this.applyTextureTransform(metalRoughMapDef, metalRoughTexture); materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef; } if (material.map) { const baseColorMapDef = { index: this.processTexture(material.map), texCoord: material.map.channel }; this.applyTextureTransform(baseColorMapDef, material.map); materialDef.pbrMetallicRoughness.baseColorTexture = baseColorMapDef; } if (material.emissive) { const emissive = material.emissive; const maxEmissiveComponent = Math.max(emissive.r, emissive.g, emissive.b); if (maxEmissiveComponent > 0) { materialDef.emissiveFactor = material.emissive.toArray(); } if (material.emissiveMap) { const emissiveMapDef = { index: this.processTexture(material.emissiveMap), texCoord: material.emissiveMap.channel }; this.applyTextureTransform(emissiveMapDef, material.emissiveMap); materialDef.emissiveTexture = emissiveMapDef; } } if (material.normalMap) { const normalMapDef = { index: this.processTexture(material.normalMap), texCoord: material.normalMap.channel }; if (material.normalScale && material.normalScale.x !== 1) { normalMapDef.scale = material.normalScale.x; } this.applyTextureTransform(normalMapDef, material.normalMap); materialDef.normalTexture = normalMapDef; } if (material.aoMap) { const occlusionMapDef = { index: this.processTexture(material.aoMap), texCoord: material.aoMap.channel }; if (material.aoMapIntensity !== 1) { occlusionMapDef.strength = material.aoMapIntensity; } this.applyTextureTransform(occlusionMapDef, material.aoMap); materialDef.occlusionTexture = occlusionMapDef; } if (material.transparent) { materialDef.alphaMode = "BLEND"; } else { if (material.alphaTest > 0) { materialDef.alphaMode = "MASK"; materialDef.alphaCutoff = material.alphaTest; } } if (material.side === DoubleSide) materialDef.doubleSided = true; if (material.name !== "") materialDef.name = material.name; this.serializeUserData(material, materialDef); this._invokeAll(function(ext) { ext.writeMaterial && ext.writeMaterial(material, materialDef); }); const index = json.materials.push(materialDef) - 1; cache.materials.set(material, index); return index; } /** * Process mesh * @param {THREE.Mesh} mesh Mesh to process * @return {Integer|null} Index of the processed mesh in the "meshes" array */ processMesh(mesh) { const cache = this.cache; const json = this.json; const meshCacheKeyParts = [mesh.geometry.uuid]; if (Array.isArray(mesh.material)) { for (let i = 0, l = mesh.material.length; i < l; i++) { meshCacheKeyParts.push(mesh.material[i].uuid); } } else { meshCacheKeyParts.push(mesh.material.uuid); } const meshCacheKey = meshCacheKeyParts.join(":"); if (cache.meshes.has(meshCacheKey)) return cache.meshes.get(meshCacheKey); const geometry = mesh.geometry; let mode; if (mesh.isLineSegments) { mode = WEBGL_CONSTANTS.LINES; } else if (mesh.isLineLoop) { mode = WEBGL_CONSTANTS.LINE_LOOP; } else if (mesh.isLine) { mode = WEBGL_CONSTANTS.LINE_STRIP; } else if (mesh.isPoints) { mode = WEBGL_CONSTANTS.POINTS; } else { mode = mesh.material.wireframe ? WEBGL_CONSTANTS.LINES : WEBGL_CONSTANTS.TRIANGLES; } const meshDef = {}; const attributes = {}; const primitives = []; const targets = []; const nameConversion = { ...version >= 152 ? { uv: "TEXCOORD_0", uv1: "TEXCOORD_1", uv2: "TEXCOORD_2", uv3: "TEXCOORD_3" } : { uv: "TEXCOORD_0", uv2: "TEXCOORD_1" }, color: "COLOR_0", skinWeight: "WEIGHTS_0", skinIndex: "JOINTS_0" }; const originalNormal = geometry.getAttribute("normal"); if (originalNormal !== void 0 && !this.isNormalizedNormalAttribute(originalNormal)) { console.warn("THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one."); geometry.setAttribute("normal", this.createNormalizedNormalAttribute(originalNormal)); } let modifiedAttribute = null; for (let attributeName in geometry.attributes) { if (attributeName.slice(0, 5) === "morph") continue; const attribute = geometry.attributes[attributeName]; attributeName = nameConversion[attributeName] || attributeName.toUpperCase(); const validVertexAttributes = /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/; if (!validVertexAttributes.test(attributeName)) attributeName = "_" + attributeName; if (cache.attributes.has(this.getUID(attribute))) { attributes[attributeName] = cache.attributes.get(this.getUID(attribute)); continue; } modifiedAttribute = null; const array = attribute.array; if (attributeName === "JOINTS_0" && !(array instanceof Uint16Array) && !(array instanceof Uint8Array)) { console.warn('GLTFExporter: Attribute "skinIndex" converted to type UNSIGNED_SHORT.'); modifiedAttribute = new BufferAttribute(new Uint16Array(array), attribute.itemSize, attribute.normalized); } const accessor = this.processAccessor(modifiedAttribute || attribute, geometry); if (accessor !== null) { if (!attributeName.startsWith("_")) { this.detectMeshQuantization(attributeName, attribute); } attributes[attributeName] = accessor; cache.attributes.set(this.getUID(attribute), accessor); } } if (originalNormal !== void 0) geometry.setAttribute("normal", originalNormal); if (Object.keys(attributes).length === 0) return null; if (mesh.morphTargetInfluences !== void 0 && mesh.morphTargetInfluences.length > 0) { const weights = []; const targetNames = []; const reverseDictionary = {}; if (mesh.morphTargetDictionary !== void 0) { for (const key in mesh.morphTargetDictionary) { reverseDictionary[mesh.morphTargetDictionary[key]] = key; } } for (let i = 0; i < mesh.morphTargetInfluences.length; ++i) { const target = {}; let warned = false; for (const attributeName in geometry.morphAttributes) { if (attributeName !== "position" && attributeName !== "normal") { if (!warned) { console.warn("GLTFExporter: Only POSITION and NORMAL morph are supported."); warned = true; } continue; } const attribute = geometry.morphAttributes[attributeName][i]; const gltfAttributeName = attributeName.toUpperCase(); const baseAttribute = geometry.attributes[attributeName]; if (cache.attributes.has(this.getUID(attribute, true))) { target[gltfAttributeName] = cache.attributes.get(this.getUID(attribute, true)); continue; } const relativeAttribute = attribute.clone(); if (!geometry.morphTargetsRelative) { for (let j = 0, jl = attribute.count; j < jl; j++) { for (let a = 0; a < attribute.itemSize; a++) { if (a === 0) relativeAttribute.setX(j, attribute.getX(j) - baseAttribute.getX(j)); if (a === 1) relativeAttribute.setY(j, attribute.getY(j) - baseAttribute.getY(j)); if (a === 2) relativeAttribute.setZ(j, attribute.getZ(j) - baseAttribute.getZ(j)); if (a === 3) relativeAttribute.setW(j, attribute.getW(j) - baseAttribute.getW(j)); } } } target[gltfAttributeName] = this.processAccessor(relativeAttribute, geometry); cache.attributes.set(this.getUID(baseAttribute, true), target[gltfAttributeName]); } targets.push(target); weights.push(mesh.morphTargetInfluences[i]); if (mesh.morphTargetDictionary !== void 0) targetNames.push(reverseDictionary[i]); } meshDef.weights = weights; if (targetNames.length > 0) { meshDef.extras = {}; meshDef.extras.targetNames = targetNames; } } const isMultiMaterial = Array.isArray(mesh.material); if (isMultiMaterial && geometry.groups.length === 0) return null; const materials = isMultiMaterial ? mesh.material : [mesh.material]; const groups = isMultiMaterial ? geometry.groups : [{ materialIndex: 0, start: void 0, count: void 0 }]; for (let i = 0, il = groups.length; i < il; i++) { const primitive = { mode, attributes }; this.serializeUserData(geometry, primitive); if (targets.length > 0) primitive.targets = targets; if (geometry.index !== null) { let cacheKey = this.getUID(geometry.index); if (groups[i].start !== void 0 || groups[i].count !== void 0) { cacheKey += ":" + groups[i].start + ":" + groups[i].count; } if (cache.attributes.has(cacheKey)) { primitive.indices = cache.attributes.get(cacheKey); } else { primitive.indices = this.processAccessor(geometry.index, geometry, groups[i].start, groups[i].count); cache.attributes.set(cacheKey, primitive.indices); } if (primitive.indices === null) delete primitive.indices; } const material = this.processMaterial(materials[groups[i].materialIndex]); if (material !== null) primitive.material = material; primitives.push(primitive); } meshDef.primitives = primitives; if (!json.meshes) json.meshes = []; this._invokeAll(function(ext) { ext.writeMesh && ext.writeMesh(mesh, meshDef); }); const index = json.meshes.push(meshDef) - 1; cache.meshes.set(meshCacheKey, index); return index; } /** * If a vertex attribute with a * [non-standard data type](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview) * is used, it is checked whether it is a valid data type according to the * [KHR_mesh_quantization](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md) * extension. * In this case the extension is automatically added to the list of used extensions. * * @param {string} attributeName * @param {THREE.BufferAttribute} attribute */ detectMeshQuantization(attributeName, attribute) { if (this.extensionsUsed[KHR_MESH_QUANTIZATION]) return; let attrType = void 0; switch (attribute.array.constructor) { case Int8Array: attrType = "byte"; break; case Uint8Array: attrType = "unsigned byte"; break; case Int16Array: attrType = "short"; break; case Uint16Array: attrType = "unsigned short"; break; default: return; } if (attribute.normalized) attrType += " normalized"; const attrNamePrefix = attributeName.split("_", 1)[0]; if (KHR_mesh_quantization_ExtraAttrTypes[attrNamePrefix] && KHR_mesh_quantization_ExtraAttrTypes[attrNamePrefix].includes(attrType)) { this.extensionsUsed[KHR_MESH_QUANTIZATION] = true; this.extensionsRequired[KHR_MESH_QUANTIZATION] = true; } } /** * Process camera * @param {THREE.Camera} camera Camera to process * @return {Integer} Index of the processed mesh in the "camera" array */ processCamera(camera) { const json = this.json; if (!json.cameras) json.cameras = []; const isOrtho = camera.isOrthographicCamera; const cameraDef = { type: isOrtho ? "orthographic" : "perspective" }; if (isOrtho) { cameraDef.orthographic = { xmag: camera.right * 2, ymag: camera.top * 2, zfar: camera.far <= 0 ? 1e-3 : camera.far, znear: camera.near < 0 ? 0 : camera.near }; } else { cameraDef.perspective = { aspectRatio: camera.aspect, yfov: MathUtils.degToRad(camera.fov), zfar: camera.far <= 0 ? 1e-3 : camera.far, znear: camera.near < 0 ? 0 : camera.near }; } if (camera.name !== "") cameraDef.name = camera.type; return json.cameras.push(cameraDef) - 1; } /** * Creates glTF animation entry from AnimationClip object. * * Status: * - Only properties listed in PATH_PROPERTIES may be animated. * * @param {THREE.AnimationClip} clip * @param {THREE.Object3D} root * @return {number|null} */ processAnimation(clip, root) { const json = this.json; const nodeMap = this.nodeMap; if (!json.animations) json.animations = []; clip = GLTFExporter.Utils.mergeMorphTargetTracks(clip.clone(), root); const tracks = clip.tracks; const channels = []; const samplers = []; for (let i = 0; i < tracks.length; ++i) { const track = tracks[i]; const trackBinding = PropertyBinding.parseTrackName(track.name); let trackNode = PropertyBinding.findNode(root, trackBinding.nodeName); const trackProperty = PATH_PROPERTIES[trackBinding.propertyName]; if (trackBinding.objectName === "bones") { if (trackNode.isSkinnedMesh === true) { trackNode = trackNode.skeleton.getBoneByName(trackBinding.objectIndex); } else { trackNode = void 0; } } if (!trackNode || !trackProperty) { console.warn('THREE.GLTFExporter: Could not export animation track "%s".', track.name); return null; } const inputItemSize = 1; let outputItemSize = track.values.length / track.times.length; if (trackProperty === PATH_PROPERTIES.morphTargetInfluences) { outputItemSize /= trackNode.morphTargetInfluences.length; } let interpolation; if (track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true) { interpolation = "CUBICSPLINE"; outputItemSize /= 3; } else if (track.getInterpolation() === InterpolateDiscrete) { interpolation = "STEP"; } else { interpolation = "LINEAR"; } samplers.push({ input: this.processAccessor(new BufferAttribute(track.times, inputItemSize)), output: this.processAccessor(new BufferAttribute(track.values, outputItemSize)), interpolation }); channels.push({ sampler: samplers.length - 1, target: { node: nodeMap.get(trackNode), path: trackProperty } }); } json.animations.push({ name: clip.name || "clip_" + json.animations.length, samplers, channels }); return json.animations.length - 1; } /** * @param {THREE.Object3D} object * @return {number|null} */ processSkin(object) { const json = this.json; const nodeMap = this.nodeMap; const node = json.nodes[nodeMap.get(object)]; const skeleton = object.skeleton; if (skeleton === void 0) return null; const rootJoint = object.skeleton.bones[0]; if (rootJoint === void 0) return null; const joints = []; const inverseBindMatrices = new Float32Array(skeleton.bones.length * 16); const temporaryBoneInverse = new Matrix4(); for (let i = 0; i < skeleton.bones.length; ++i) { joints.push(nodeMap.get(skeleton.bones[i])); temporaryBoneInverse.copy(skeleton.boneInverses[i]); temporaryBoneInverse.multiply(object.bindMatrix).toArray(inverseBindMatrices, i * 16); } if (json.skins === void 0) json.skins = []; json.skins.push({ inverseBindMatrices: this.processAccessor(new BufferAttribute(inverseBindMatrices, 16)), joints, skeleton: nodeMap.get(rootJoint) }); const skinIndex = node.skin = json.skins.length - 1; return skinIndex; } /** * Process Object3D node * @param {THREE.Object3D} node Object3D to processNode * @return {Integer} Index of the node in the nodes list */ processNode(object) { const json = this.json; const options = this.options; const nodeMap = this.nodeMap; if (!json.nodes) json.nodes = []; const nodeDef = {}; if (options.trs) { const rotation = object.quaternion.toArray(); const position = object.position.toArray(); const scale = object.scale.toArray(); if (!equalArray(rotation, [0, 0, 0, 1])) { nodeDef.rotation = rotation; } if (!equalArray(position, [0, 0, 0])) { nodeDef.translation = position; } if (!equalArray(scale, [1, 1, 1])) { nodeDef.scale = scale; } } else { if (object.matrixAutoUpdate) { object.updateMatrix(); } if (isIdentityMatrix(object.matrix) === false) { nodeDef.matrix = object.matrix.elements; } } if (object.name !== "") nodeDef.name = String(object.name); this.serializeUserData(object, nodeDef); if (object.isMesh || object.isLine || object.isPoints) { const meshIndex = this.processMesh(object); if (meshIndex !== null) nodeDef.mesh = meshIndex; } else if (object.isCamera) { nodeDef.camera = this.processCamera(object); } if (object.isSkinnedMesh) this.skins.push(object); if (object.children.length > 0) { const children = []; for (let i = 0, l = object.children.length; i < l; i++) { const child = object.children[i]; if (child.visible || options.onlyVisible === false) { const nodeIndex2 = this.processNode(child); if (nodeIndex2 !== null) children.push(nodeIndex2); } } if (children.length > 0) nodeDef.children = children; } this._invokeAll(function(ext) { ext.writeNode && ext.writeNode(object, nodeDef); }); const nodeIndex = json.nodes.push(nodeDef) - 1; nodeMap.set(object, nodeIndex); return nodeIndex; } /** * Process Scene * @param {Scene} node Scene to process */ processScene(scene) { const json = this.json; const options = this.options; if (!json.scenes) { json.scenes = []; json.scene = 0; } const sceneDef = {}; if (scene.name !== "") sceneDef.name = scene.name; json.scenes.push(sceneDef); const nodes = []; for (let i = 0, l = scene.children.length; i < l; i++) { const child = scene.children[i]; if (child.visible || options.onlyVisible === false) { const nodeIndex = this.processNode(child); if (nodeIndex !== null) nodes.push(nodeIndex); } } if (nodes.length > 0) sceneDef.nodes = nodes; this.serializeUserData(scene, sceneDef); } /** * Creates a Scene to hold a list of objects and parse it * @param {Array} objects List of objects to process */ processObjects(objects) { const scene = new Scene(); scene.name = "AuxScene"; for (let i = 0; i < objects.length; i++) { scene.children.push(objects[i]); } this.processScene(scene); } /** * @param {THREE.Object3D|Array} input */ processInput(input) { const options = this.options; input = input instanceof Array ? input : [input]; this._invokeAll(function(ext) { ext.beforeParse && ext.beforeParse(input); }); const objectsWithoutScene = []; for (let i = 0; i < input.length; i++) { if (input[i] instanceof Scene) { this.processScene(input[i]); } else { objectsWithoutScene.push(input[i]); } } if (objectsWithoutScene.length > 0) this.processObjects(objectsWithoutScene); for (let i = 0; i < this.skins.length; ++i) { this.processSkin(this.skins[i]); } for (let i = 0; i < options.animations.length; ++i) { this.processAnimation(options.animations[i], input[0]); } this._invokeAll(function(ext) { ext.afterParse && ext.afterParse(input); }); } _invokeAll(func) { for (let i = 0, il = this.plugins.length; i < il; i++) { func(this.plugins[i]); } } } class GLTFLightExtension { constructor(writer) { this.writer = writer; this.name = "KHR_lights_punctual"; } writeNode(light, nodeDef) { if (!light.isLight) return; if (!light.isDirectionalLight && !light.isPointLight && !light.isSpotLight) { console.warn("THREE.GLTFExporter: Only directional, point, and spot lights are supported.", light); return; } const writer = this.writer; const json = writer.json; const extensionsUsed = writer.extensionsUsed; const lightDef = {}; if (light.name) lightDef.name = light.name; lightDef.color = light.color.toArray(); lightDef.intensity = light.intensity; if (light.isDirectionalLight) { lightDef.type = "directional"; } else if (light.isPointLight) { lightDef.type = "point"; if (light.distance > 0) lightDef.range = light.distance; } else if (light.isSpotLight) { lightDef.type = "spot"; if (light.distance > 0) lightDef.range = light.distance; lightDef.spot = {}; lightDef.spot.innerConeAngle = (light.penumbra - 1) * light.angle * -1; lightDef.spot.outerConeAngle = light.angle; } if (light.decay !== void 0 && light.decay !== 2) { console.warn( "THREE.GLTFExporter: Light decay may be lost. glTF is physically-based, and expects light.decay=2." ); } if (light.target && (light.target.parent !== light || light.target.position.x !== 0 || light.target.position.y !== 0 || light.target.position.z !== -1)) { console.warn( "THREE.GLTFExporter: Light direction may be lost. For best results, make light.target a child of the light with position 0,0,-1." ); } if (!extensionsUsed[this.name]) { json.extensions = json.extensions || {}; json.extensions[this.name] = { lights: [] }; extensionsUsed[this.name] = true; } const lights = json.extensions[this.name].lights; lights.push(lightDef); nodeDef.extensions = nodeDef.extensions || {}; nodeDef.extensions[this.name] = { light: lights.length - 1 }; } } class GLTFMaterialsUnlitExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_unlit"; } writeMaterial(material, materialDef) { if (!material.isMeshBasicMaterial) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = {}; extensionsUsed[this.name] = true; materialDef.pbrMetallicRoughness.metallicFactor = 0; materialDef.pbrMetallicRoughness.roughnessFactor = 0.9; } } class GLTFMaterialsClearcoatExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_clearcoat"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.clearcoat === 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.clearcoatFactor = material.clearcoat; if (material.clearcoatMap) { const clearcoatMapDef = { index: writer.processTexture(material.clearcoatMap), texCoord: material.clearcoatMap.channel }; writer.applyTextureTransform(clearcoatMapDef, material.clearcoatMap); extensionDef.clearcoatTexture = clearcoatMapDef; } extensionDef.clearcoatRoughnessFactor = material.clearcoatRoughness; if (material.clearcoatRoughnessMap) { const clearcoatRoughnessMapDef = { index: writer.processTexture(material.clearcoatRoughnessMap), texCoord: material.clearcoatRoughnessMap.channel }; writer.applyTextureTransform(clearcoatRoughnessMapDef, material.clearcoatRoughnessMap); extensionDef.clearcoatRoughnessTexture = clearcoatRoughnessMapDef; } if (material.clearcoatNormalMap) { const clearcoatNormalMapDef = { index: writer.processTexture(material.clearcoatNormalMap), texCoord: material.clearcoatNormalMap.channel }; writer.applyTextureTransform(clearcoatNormalMapDef, material.clearcoatNormalMap); extensionDef.clearcoatNormalTexture = clearcoatNormalMapDef; } materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsIridescenceExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_iridescence"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.iridescence === 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.iridescenceFactor = material.iridescence; if (material.iridescenceMap) { const iridescenceMapDef = { index: writer.processTexture(material.iridescenceMap), texCoord: material.iridescenceMap.channel }; writer.applyTextureTransform(iridescenceMapDef, material.iridescenceMap); extensionDef.iridescenceTexture = iridescenceMapDef; } extensionDef.iridescenceIor = material.iridescenceIOR; extensionDef.iridescenceThicknessMinimum = material.iridescenceThicknessRange[0]; extensionDef.iridescenceThicknessMaximum = material.iridescenceThicknessRange[1]; if (material.iridescenceThicknessMap) { const iridescenceThicknessMapDef = { index: writer.processTexture(material.iridescenceThicknessMap), texCoord: material.iridescenceThicknessMap.channel }; writer.applyTextureTransform(iridescenceThicknessMapDef, material.iridescenceThicknessMap); extensionDef.iridescenceThicknessTexture = iridescenceThicknessMapDef; } materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsTransmissionExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_transmission"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.transmission === 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.transmissionFactor = material.transmission; if (material.transmissionMap) { const transmissionMapDef = { index: writer.processTexture(material.transmissionMap), texCoord: material.transmissionMap.channel }; writer.applyTextureTransform(transmissionMapDef, material.transmissionMap); extensionDef.transmissionTexture = transmissionMapDef; } materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsVolumeExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_volume"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.transmission === 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.thicknessFactor = material.thickness; if (material.thicknessMap) { const thicknessMapDef = { index: writer.processTexture(material.thicknessMap), texCoord: material.thicknessMap.channel }; writer.applyTextureTransform(thicknessMapDef, material.thicknessMap); extensionDef.thicknessTexture = thicknessMapDef; } extensionDef.attenuationDistance = material.attenuationDistance; extensionDef.attenuationColor = material.attenuationColor.toArray(); materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsIorExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_ior"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.ior === 1.5) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.ior = material.ior; materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsSpecularExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_specular"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.specularIntensity === 1 && material.specularColor.equals(DEFAULT_SPECULAR_COLOR) && !material.specularIntensityMap && !material.specularColorTexture) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; if (material.specularIntensityMap) { const specularIntensityMapDef = { index: writer.processTexture(material.specularIntensityMap), texCoord: material.specularIntensityMap.channel }; writer.applyTextureTransform(specularIntensityMapDef, material.specularIntensityMap); extensionDef.specularTexture = specularIntensityMapDef; } if (material.specularColorMap) { const specularColorMapDef = { index: writer.processTexture(material.specularColorMap), texCoord: material.specularColorMap.channel }; writer.applyTextureTransform(specularColorMapDef, material.specularColorMap); extensionDef.specularColorTexture = specularColorMapDef; } extensionDef.specularFactor = material.specularIntensity; extensionDef.specularColorFactor = material.specularColor.toArray(); materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsSheenExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_sheen"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.sheen == 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; if (material.sheenRoughnessMap) { const sheenRoughnessMapDef = { index: writer.processTexture(material.sheenRoughnessMap), texCoord: material.sheenRoughnessMap.channel }; writer.applyTextureTransform(sheenRoughnessMapDef, material.sheenRoughnessMap); extensionDef.sheenRoughnessTexture = sheenRoughnessMapDef; } if (material.sheenColorMap) { const sheenColorMapDef = { index: writer.processTexture(material.sheenColorMap), texCoord: material.sheenColorMap.channel }; writer.applyTextureTransform(sheenColorMapDef, material.sheenColorMap); extensionDef.sheenColorTexture = sheenColorMapDef; } extensionDef.sheenRoughnessFactor = material.sheenRoughness; extensionDef.sheenColorFactor = material.sheenColor.toArray(); materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsAnisotropyExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_anisotropy"; } writeMaterial(material, materialDef) { if (!material.isMeshPhysicalMaterial || material.anisotropy == 0) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; if (material.anisotropyMap) { const anisotropyMapDef = { index: writer.processTexture(material.anisotropyMap) }; writer.applyTextureTransform(anisotropyMapDef, material.anisotropyMap); extensionDef.anisotropyTexture = anisotropyMapDef; } extensionDef.anisotropyStrength = material.anisotropy; extensionDef.anisotropyRotation = material.anisotropyRotation; materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } class GLTFMaterialsEmissiveStrengthExtension { constructor(writer) { this.writer = writer; this.name = "KHR_materials_emissive_strength"; } writeMaterial(material, materialDef) { if (!material.isMeshStandardMaterial || material.emissiveIntensity === 1) return; const writer = this.writer; const extensionsUsed = writer.extensionsUsed; const extensionDef = {}; extensionDef.emissiveStrength = material.emissiveIntensity; materialDef.extensions = materialDef.extensions || {}; materialDef.extensions[this.name] = extensionDef; extensionsUsed[this.name] = true; } } export { GLTFExporter }; //# sourceMappingURL=GLTFExporter.js.map