UNPKG

3dtiles-tool

Version:

高效易用的3DTiles工具库,各种功能持续更新中

2,221 lines (1,354 loc) 53.6 kB
import * as THREE from "three" //------------------------------------------------------------------------------ // Constants //------------------------------------------------------------------------------ var WEBGL_CONSTANTS = { POINTS: 0x0000, LINES: 0x0001, LINE_LOOP: 0x0002, LINE_STRIP: 0x0003, TRIANGLES: 0x0004, TRIANGLE_STRIP: 0x0005, TRIANGLE_FAN: 0x0006, UNSIGNED_BYTE: 0x1401, UNSIGNED_SHORT: 0x1403, FLOAT: 0x1406, UNSIGNED_INT: 0x1405, ARRAY_BUFFER: 0x8892, ELEMENT_ARRAY_BUFFER: 0x8893, NEAREST: 0x2600, LINEAR: 0x2601, NEAREST_MIPMAP_NEAREST: 0x2700, LINEAR_MIPMAP_NEAREST: 0x2701, NEAREST_MIPMAP_LINEAR: 0x2702, LINEAR_MIPMAP_LINEAR: 0x2703, CLAMP_TO_EDGE: 33071, MIRRORED_REPEAT: 33648, REPEAT: 10497 }; var identityArray = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; var THREE_TO_WEBGL = {}; THREE_TO_WEBGL[THREE.NearestFilter] = WEBGL_CONSTANTS.NEAREST; THREE_TO_WEBGL[THREE.NearestMipmapNearestFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST; THREE_TO_WEBGL[THREE.NearestMipmapLinearFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR; THREE_TO_WEBGL[THREE.LinearFilter] = WEBGL_CONSTANTS.LINEAR; THREE_TO_WEBGL[THREE.LinearMipmapNearestFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST; THREE_TO_WEBGL[THREE.LinearMipmapLinearFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR; THREE_TO_WEBGL[THREE.ClampToEdgeWrapping] = WEBGL_CONSTANTS.CLAMP_TO_EDGE; THREE_TO_WEBGL[THREE.RepeatWrapping] = WEBGL_CONSTANTS.REPEAT; THREE_TO_WEBGL[THREE.MirroredRepeatWrapping] = WEBGL_CONSTANTS.MIRRORED_REPEAT; var PATH_PROPERTIES = { scale: 'scale', position: 'translation', quaternion: 'rotation', morphTargetInfluences: 'weights' }; //------------------------------------------------------------------------------ // GLTF Exporter //------------------------------------------------------------------------------ const GLTFExporter = function () { }; GLTFExporter.prototype = { constructor: GLTFExporter, /** * Parse scenes and generate GLTF output * @param {THREE.Scene or [THREE.Scenes]} input THREE.Scene or Array of THREE.Scenes * @param {Function} onDone Callback on completed * @param {Object} options options */ parse: function (input, onDone, options) { var DEFAULT_OPTIONS = { binary: true, trs: false, onlyVisible: true, truncateDrawRange: true, embedImages: false, maxTextureSize: Infinity, animations: [], includeCustomExtensions: false, textureUrlWithoutDomainName: false, customizeTextureUrlMap: null, }; options = Object.assign({}, DEFAULT_OPTIONS, options); if (options.animations.length > 0) { // Only TRS properties, and not matrices, may be targeted by animation. options.trs = true; } var outputJSON = { asset: { version: '2.0', generator: 'THREE.GLTFExporter' } }; var byteOffset = 0; var buffers = []; var pending = []; var nodeMap = new Map(); var skins = []; var extensionsUsed = {}; var cachedData = { meshes: new Map(), attributes: new Map(), attributesNormalized: new Map(), materials: new Map(), textures: new Map(), images: new Map() }; var cachedCanvas; var uids = new Map(); var uid = 0; /** * Assign and return a temporal unique id for an object * especially which doesn't have .uuid * @param {Object} object * @return {Integer} */ function getUID(object) { if (!uids.has(object)) uids.set(object, uid++); return uids.get(object); } /** * Compare two arrays * @param {Array} array1 Array 1 to compare * @param {Array} array2 Array 2 to compare * @return {Boolean} Returns true if both arrays are equal */ function equalArray(array1, array2) { return (array1.length === array2.length) && array1.every(function (element, index) { return element === array2[index]; }); } /** * Is identity matrix * * @param {THREE.Matrix4} matrix * @returns {Boolean} Returns true, if parameter is identity matrix */ function isIdentityMatrix(matrix) { return equalArray(matrix.elements, identityArray); } /** * Converts a string to an ArrayBuffer. * @param {string} text * @return {ArrayBuffer} */ function stringToArrayBuffer(text) { var array = new Uint8Array(new ArrayBuffer(text.length)); for (var i = 0, il = text.length; i < il; i++) { var value = text.charCodeAt(i); // Replacing multi-byte character with space(0x20). array[i] = value > 0xFF ? 0x20 : value; } return array.buffer; } /** * Get the min and max vectors from the given attribute * @param {THREE.BufferAttribute} attribute Attribute to find the min/max in range from start to start + count * @param {Integer} start * @param {Integer} count * @return {Object} Object containing the `min` and `max` values (As an array of attribute.itemSize components) */ function getMinMax(attribute, start, count) { var output = { min: new Array(attribute.itemSize).fill(Number.POSITIVE_INFINITY), max: new Array(attribute.itemSize).fill(Number.NEGATIVE_INFINITY) }; for (var i = start; i < start + count; i++) { for (var a = 0; a < attribute.itemSize; a++) { var value; if (attribute.itemSize > 4) { // no support for interleaved data for 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); } output.min[a] = Math.min(output.min[a], value); output.max[a] = Math.max(output.max[a], value); } } return output; } /** * Checks if normal attribute values are normalized. * * @param {THREE.BufferAttribute} normal * @returns {Boolean} * */ function isNormalizedNormalAttribute(normal) { if (cachedData.attributesNormalized.has(normal)) { return false; } var v = new THREE.Vector3(); for (var i = 0, il = normal.count; i < il; i++) { // 0.0005 is from glTF-validator if (Math.abs(v.fromBufferAttribute(normal, i).length() - 1.0) > 0.0005) return false; } return true; } /** * Creates normalized normal buffer attribute. * * @param {THREE.BufferAttribute} normal * @returns {THREE.BufferAttribute} * */ function createNormalizedNormalAttribute(normal) { if (cachedData.attributesNormalized.has(normal)) { return cachedData.attributesNormalized.get(normal); } var attribute = normal.clone(); var v = new THREE.Vector3(); for (var i = 0, il = attribute.count; i < il; i++) { v.fromBufferAttribute(attribute, i); if (v.x === 0 && v.y === 0 && v.z === 0) { // if values can't be normalized set (1, 0, 0) v.setX(1.0); } else { v.normalize(); } attribute.setXYZ(i, v.x, v.y, v.z); } cachedData.attributesNormalized.set(normal, attribute); return attribute; } /** * Get the required size + padding for a buffer, rounded to the next 4-byte boundary. * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment * * @param {Integer} bufferSize The size the original buffer. * @returns {Integer} new buffer size with required padding. * */ function getPaddedBufferSize(bufferSize) { return Math.ceil(bufferSize / 4) * 4; } /** * Returns a buffer aligned to 4-byte boundary. * * @param {ArrayBuffer} arrayBuffer Buffer to pad * @param {Integer} paddingByte (Optional) * @returns {ArrayBuffer} The same buffer if it's already aligned to 4-byte boundary or a new buffer */ function getPaddedArrayBuffer(arrayBuffer, paddingByte) { paddingByte = paddingByte || 0; var paddedLength = getPaddedBufferSize(arrayBuffer.byteLength); if (paddedLength !== arrayBuffer.byteLength) { var array = new Uint8Array(paddedLength); array.set(new Uint8Array(arrayBuffer)); if (paddingByte !== 0) { for (var i = arrayBuffer.byteLength; i < paddedLength; i++) { array[i] = paddingByte; } } return array.buffer; } return arrayBuffer; } /** * Serializes a userData. * * @param {THREE.Object3D|THREE.Material} object * @param {Object} gltfProperty */ function serializeUserData(object, gltfProperty) { if (Object.keys(object.userData).length === 0) { return; } try { var json = JSON.parse(JSON.stringify(object.userData)); if (options.includeCustomExtensions && json.gltfExtensions) { if (gltfProperty.extensions === undefined) { gltfProperty.extensions = {}; } for (var extensionName in json.gltfExtensions) { gltfProperty.extensions[extensionName] = json.gltfExtensions[extensionName]; extensionsUsed[extensionName] = true; } delete json.gltfExtensions; } if (Object.keys(json).length > 0) { gltfProperty.extras = json; } } catch (error) { console.warn('THREE.GLTFExporter: userData of \'' + object.name + '\' ' + 'won\'t be serialized because of JSON.stringify error - ' + error.message); } } /** * Applies a texture transform, if present, to the map definition. Requires * the KHR_texture_transform extension. */ function applyTextureTransform(mapDef, texture) { var didTransform = false; var 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; extensionsUsed['KHR_texture_transform'] = true; } } /** * Process a buffer to append to the default one. * @param {ArrayBuffer} buffer * @return {Integer} */ function processBuffer(buffer) { if (!outputJSON.buffers) { outputJSON.buffers = [{ byteLength: 0 }]; } // All buffers are merged before export. buffers.push(buffer); return 0; } /** * Process and generate a BufferView * @param {THREE.BufferAttribute} attribute * @param {number} componentType * @param {number} start * @param {number} count * @param {number} target (Optional) Target usage of the BufferView * @return {Object} */ function processBufferView(attribute, componentType, start, count, target) { if (!outputJSON.bufferViews) { outputJSON.bufferViews = []; } // Create a new dataview and dump the attribute's array into it var componentSize; if (componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE) { componentSize = 1; } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT) { componentSize = 2; } else { componentSize = 4; } var byteLength = getPaddedBufferSize(count * attribute.itemSize * componentSize); var dataView = new DataView(new ArrayBuffer(byteLength)); var offset = 0; for (var i = start; i < start + count; i++) { for (var a = 0; a < attribute.itemSize; a++) { var value; if (attribute.itemSize > 4) { // no support for interleaved data for 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 (componentType === WEBGL_CONSTANTS.FLOAT) { dataView.setFloat32(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_INT) { dataView.setUint32(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_SHORT) { dataView.setUint16(offset, value, true); } else if (componentType === WEBGL_CONSTANTS.UNSIGNED_BYTE) { dataView.setUint8(offset, value); } offset += componentSize; } } var gltfBufferView = { buffer: processBuffer(dataView.buffer), byteOffset: byteOffset, byteLength: byteLength }; if (target !== undefined) gltfBufferView.target = target; if (target === WEBGL_CONSTANTS.ARRAY_BUFFER) { // Only define byteStride for vertex attributes. gltfBufferView.byteStride = attribute.itemSize * componentSize; } byteOffset += byteLength; outputJSON.bufferViews.push(gltfBufferView); // @TODO Merge bufferViews where possible. var output = { id: outputJSON.bufferViews.length - 1, byteLength: 0 }; return output; } /** * Process and generate a BufferView from an image Blob. * @param {Blob} blob * @return {Promise<Integer>} */ function processBufferViewImage(blob) { if (!outputJSON.bufferViews) { outputJSON.bufferViews = []; } return new Promise(function (resolve) { var reader = new FileReader(); reader.readAsArrayBuffer(blob); reader.onloadend = function () { var buffer = getPaddedArrayBuffer(reader.result); var bufferView = { buffer: processBuffer(buffer), byteOffset: byteOffset, byteLength: buffer.byteLength }; byteOffset += buffer.byteLength; outputJSON.bufferViews.push(bufferView); resolve(outputJSON.bufferViews.length - 1); }; }); } /** * Process attribute to generate an accessor * @param {THREE.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} Index of the processed accessor on the "accessors" array */ function processAccessor(attribute, geometry, start, count) { var types = { 1: 'SCALAR', 2: 'VEC2', 3: 'VEC3', 4: 'VEC4', 16: 'MAT4' }; var componentType; // Detect the component type of the attribute array (float, uint or ushort) if (attribute.array.constructor === Float32Array) { componentType = WEBGL_CONSTANTS.FLOAT; } else if (attribute.array.constructor === Uint32Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_INT; } else if (attribute.array.constructor === Uint16Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_SHORT; } else if (attribute.array.constructor === Uint8Array) { componentType = WEBGL_CONSTANTS.UNSIGNED_BYTE; } else { throw new Error('THREE.GLTFExporter: Unsupported bufferAttribute component type.'); } if (start === undefined) start = 0; if (count === undefined) count = attribute.count; // @TODO Indexed buffer geometry with drawRange not supported yet if (options.truncateDrawRange && geometry !== undefined && geometry.index === null) { var end = start + count; var end2 = geometry.drawRange.count === Infinity ? attribute.count : geometry.drawRange.start + geometry.drawRange.count; start = Math.max(start, geometry.drawRange.start); count = Math.min(end, end2) - start; if (count < 0) count = 0; } // Skip creating an accessor if the attribute doesn't have data to export if (count === 0) { return null; } var minMax = getMinMax(attribute, start, count); var bufferViewTarget; // If geometry isn't provided, don't infer the target usage of the bufferView. For // animation samplers, target must not be set. if (geometry !== undefined) { bufferViewTarget = attribute === geometry.index ? WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER : WEBGL_CONSTANTS.ARRAY_BUFFER; } var bufferView = processBufferView(attribute, componentType, start, count, bufferViewTarget); var gltfAccessor = { bufferView: bufferView.id, byteOffset: bufferView.byteOffset, componentType: componentType, count: count, max: minMax.max, min: minMax.min, type: types[attribute.itemSize] }; if (attribute.normalized === true) { gltfAccessor.normalized = true; } if (!outputJSON.accessors) { outputJSON.accessors = []; } outputJSON.accessors.push(gltfAccessor); return outputJSON.accessors.length - 1; } /** * Process image * @param {Image} image to process * @param {Integer} format of the image (e.g. THREE.RGBFormat, THREE.RGBAFormat etc) * @param {Boolean} flipY before writing out the image * @return {Integer} Index of the processed texture in the "images" array */ function processImage(image, format, flipY) { if (!cachedData.images.has(image)) { cachedData.images.set(image, {}); } var cachedImages = cachedData.images.get(image); var mimeType = format === THREE.RGBAFormat ? 'image/png' : 'image/jpeg'; var key = mimeType + ':flipY/' + flipY.toString(); if (cachedImages[key] !== undefined) { return cachedImages[key]; } if (!outputJSON.images) { outputJSON.images = []; } var gltfImage = { mimeType: mimeType }; if (options.embedImages) { var canvas = cachedCanvas = cachedCanvas || document.createElement('canvas'); canvas.width = Math.min(image.width, options.maxTextureSize); canvas.height = Math.min(image.height, options.maxTextureSize); var ctx = canvas.getContext('2d'); if (flipY === true) { ctx.translate(0, canvas.height); ctx.scale(1, - 1); } if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) || (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) || (typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) || (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap)) { ctx.drawImage(image, 0, 0, canvas.width, canvas.height); } else { if (format !== THREE.RGBAFormat && format !== THREE.RGBFormat) { console.error('GLTFExporter: Only RGB and RGBA formats are supported.'); } if (image.width > options.maxTextureSize || image.height > options.maxTextureSize) { console.warn('GLTFExporter: Image size is bigger than maxTextureSize', image); } let data = image.data; if (format === THREE.RGBFormat) { data = new Uint8ClampedArray(image.height * image.width * 4); for (var i = 0, j = 0; i < data.length; i += 4, j += 3) { data[i + 0] = image.data[j + 0]; data[i + 1] = image.data[j + 1]; data[i + 2] = image.data[j + 2]; data[i + 3] = 255; } } ctx.putImageData(new ImageData(data, image.width, image.height), 0, 0); } if (options.binary === true) { pending.push(new Promise(function (resolve) { canvas.toBlob(function (blob) { processBufferViewImage(blob).then(function (bufferViewIndex) { gltfImage.bufferView = bufferViewIndex; resolve(); }); }, mimeType); })); } else { gltfImage.uri = canvas.toDataURL(mimeType); } } else { if (options.customizeTextureUrlMap instanceof Map && options.customizeTextureUrlMap.has(image.src)) { gltfImage.uri = options.customizeTextureUrlMap.get(image.src) } else if (options.textureUrlWithoutDomainName) { gltfImage.uri = image.src.replace(/^(http|https):\/\/[^/]+/, ""); } else { gltfImage.uri = image.src; } } outputJSON.images.push(gltfImage); var index = outputJSON.images.length - 1; cachedImages[key] = index; return index; } /** * Process sampler * @param {Texture} map Texture to process * @return {Integer} Index of the processed texture in the "samplers" array */ function processSampler(map) { if (!outputJSON.samplers) { outputJSON.samplers = []; } var gltfSampler = { magFilter: THREE_TO_WEBGL[map.magFilter], minFilter: THREE_TO_WEBGL[map.minFilter], wrapS: THREE_TO_WEBGL[map.wrapS], wrapT: THREE_TO_WEBGL[map.wrapT] }; outputJSON.samplers.push(gltfSampler); return outputJSON.samplers.length - 1; } /** * Process texture * @param {Texture} map Map to process * @return {Integer} Index of the processed texture in the "textures" array */ function processTexture(map) { if (cachedData.textures.has(map)) { return cachedData.textures.get(map); } if (!outputJSON.textures) { outputJSON.textures = []; } var gltfTexture = { sampler: processSampler(map), source: processImage(map.image, map.format, map.flipY) }; if (map.name) { gltfTexture.name = map.name; } outputJSON.textures.push(gltfTexture); var index = outputJSON.textures.length - 1; cachedData.textures.set(map, index); return index; } /** * Process material * @param {THREE.Material} material Material to process * @return {Integer} Index of the processed material in the "materials" array */ function processMaterial(material) { if (cachedData.materials.has(material)) { return cachedData.materials.get(material); } if (material.isShaderMaterial) { console.warn('GLTFExporter: THREE.ShaderMaterial not supported.'); return null; } if (!outputJSON.materials) { outputJSON.materials = []; } // @QUESTION Should we avoid including any attribute that has the default value? var gltfMaterial = { pbrMetallicRoughness: {} }; if (material.isMeshBasicMaterial) { gltfMaterial.extensions = { KHR_materials_unlit: {} }; extensionsUsed['KHR_materials_unlit'] = true; } else if (material.isGLTFSpecularGlossinessMaterial) { gltfMaterial.extensions = { KHR_materials_pbrSpecularGlossiness: {} }; extensionsUsed['KHR_materials_pbrSpecularGlossiness'] = true; } else if (!material.isMeshStandardMaterial) { console.warn('GLTFExporter: Use MeshStandardMaterial or MeshBasicMaterial for best results.'); } // pbrMetallicRoughness.baseColorFactor var color = material.color.toArray().concat([material.opacity]); if (!equalArray(color, [1, 1, 1, 1])) { gltfMaterial.pbrMetallicRoughness.baseColorFactor = color; } if (material.isMeshStandardMaterial) { gltfMaterial.pbrMetallicRoughness.metallicFactor = material.metalness; gltfMaterial.pbrMetallicRoughness.roughnessFactor = material.roughness; } else if (material.isMeshBasicMaterial) { gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.0; gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.9; } else { gltfMaterial.pbrMetallicRoughness.metallicFactor = 0.5; gltfMaterial.pbrMetallicRoughness.roughnessFactor = 0.5; } // pbrSpecularGlossiness diffuse, specular and glossiness factor if (material.isGLTFSpecularGlossinessMaterial) { if (gltfMaterial.pbrMetallicRoughness.baseColorFactor) { gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness.diffuseFactor = gltfMaterial.pbrMetallicRoughness.baseColorFactor; } var specularFactor = [1, 1, 1]; material.specular.toArray(specularFactor, 0); gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness.specularFactor = specularFactor; gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness.glossinessFactor = material.glossiness; } // pbrMetallicRoughness.metallicRoughnessTexture if (material.metalnessMap || material.roughnessMap) { if (material.metalnessMap === material.roughnessMap) { var metalRoughMapDef = { index: processTexture(material.metalnessMap) }; applyTextureTransform(metalRoughMapDef, material.metalnessMap); gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef; } else { console.warn('THREE.GLTFExporter: Ignoring metalnessMap and roughnessMap because they are not the same Texture.'); } } // pbrMetallicRoughness.baseColorTexture or pbrSpecularGlossiness diffuseTexture if (material.map) { var baseColorMapDef = { index: processTexture(material.map) }; applyTextureTransform(baseColorMapDef, material.map); if (material.isGLTFSpecularGlossinessMaterial) { gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness.diffuseTexture = baseColorMapDef; } gltfMaterial.pbrMetallicRoughness.baseColorTexture = baseColorMapDef; } // pbrSpecularGlossiness specular map if (material.isGLTFSpecularGlossinessMaterial && material.specularMap) { var specularMapDef = { index: processTexture(material.specularMap) }; applyTextureTransform(specularMapDef, material.specularMap); gltfMaterial.extensions.KHR_materials_pbrSpecularGlossiness.specularGlossinessTexture = specularMapDef; } if (material.emissive) { // emissiveFactor var emissive = material.emissive.clone().multiplyScalar(material.emissiveIntensity).toArray(); if (!equalArray(emissive, [0, 0, 0])) { gltfMaterial.emissiveFactor = emissive; } // emissiveTexture if (material.emissiveMap) { var emissiveMapDef = { index: processTexture(material.emissiveMap) }; applyTextureTransform(emissiveMapDef, material.emissiveMap); gltfMaterial.emissiveTexture = emissiveMapDef; } } // normalTexture if (material.normalMap) { var normalMapDef = { index: processTexture(material.normalMap) }; if (material.normalScale && material.normalScale.x !== - 1) { if (material.normalScale.x !== material.normalScale.y) { console.warn('THREE.GLTFExporter: Normal scale components are different, ignoring Y and exporting X.'); } normalMapDef.scale = material.normalScale.x; } applyTextureTransform(normalMapDef, material.normalMap); gltfMaterial.normalTexture = normalMapDef; } // occlusionTexture if (material.aoMap) { var occlusionMapDef = { index: processTexture(material.aoMap), texCoord: 1 }; if (material.aoMapIntensity !== 1.0) { occlusionMapDef.strength = material.aoMapIntensity; } applyTextureTransform(occlusionMapDef, material.aoMap); gltfMaterial.occlusionTexture = occlusionMapDef; } // alphaMode if (material.transparent) { gltfMaterial.alphaMode = 'BLEND'; } else { if (material.alphaTest > 0.0) { gltfMaterial.alphaMode = 'MASK'; gltfMaterial.alphaCutoff = material.alphaTest; } } // doubleSided if (material.side === THREE.DoubleSide) { gltfMaterial.doubleSided = true; } if (material.name !== '') { gltfMaterial.name = material.name; } serializeUserData(material, gltfMaterial); outputJSON.materials.push(gltfMaterial); var index = outputJSON.materials.length - 1; cachedData.materials.set(material, index); return index; } /** * Process mesh * @param {THREE.Mesh} mesh Mesh to process * @return {Integer} Index of the processed mesh in the "meshes" array */ function processMesh(mesh) { var meshCacheKeyParts = [mesh.geometry.uuid]; if (Array.isArray(mesh.material)) { for (var i = 0, l = mesh.material.length; i < l; i++) { meshCacheKeyParts.push(mesh.material[i].uuid); } } else { meshCacheKeyParts.push(mesh.material.uuid); } var meshCacheKey = meshCacheKeyParts.join(':'); if (cachedData.meshes.has(meshCacheKey)) { return cachedData.meshes.get(meshCacheKey); } var geometry = mesh.geometry; var mode; // Use the correct 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; } if (geometry.isBufferGeometry !== true) { throw new Error('THREE.GLTFExporter: Geometry is not of type THREE.BufferGeometry.'); } var gltfMesh = {}; var attributes = {}; var primitives = []; var targets = []; // Conversion between attributes names in threejs and gltf spec var nameConversion = { uv: 'TEXCOORD_0', uv2: 'TEXCOORD_1', color: 'COLOR_0', skinWeight: 'WEIGHTS_0', skinIndex: 'JOINTS_0' }; var originalNormal = geometry.getAttribute('normal'); if (originalNormal !== undefined && !isNormalizedNormalAttribute(originalNormal)) { console.warn('THREE.GLTFExporter: Creating normalized normal attribute from the non-normalized one.'); geometry.setAttribute('normal', createNormalizedNormalAttribute(originalNormal)); } // @QUESTION Detect if .vertexColors = true? // For every attribute create an accessor var modifiedAttribute = null; for (var attributeName in geometry.attributes) { // Ignore morph target attributes, which are exported later. if (attributeName.substr(0, 5) === 'morph') continue; var attribute = geometry.attributes[attributeName]; attributeName = nameConversion[attributeName] || attributeName.toUpperCase(); // Prefix all geometry attributes except the ones specifically // listed in the spec; non-spec attributes are considered custom. var validVertexAttributes = /^(POSITION|NORMAL|TANGENT|TEXCOORD_\d+|COLOR_\d+|JOINTS_\d+|WEIGHTS_\d+)$/; if (!validVertexAttributes.test(attributeName)) { attributeName = '_' + attributeName; } if (cachedData.attributes.has(getUID(attribute))) { attributes[attributeName] = cachedData.attributes.get(getUID(attribute)); continue; } // JOINTS_0 must be UNSIGNED_BYTE or UNSIGNED_SHORT. modifiedAttribute = null; var 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 THREE.BufferAttribute(new Uint16Array(array), attribute.itemSize, attribute.normalized); } var accessor = processAccessor(modifiedAttribute || attribute, geometry); if (accessor !== null) { attributes[attributeName] = accessor; cachedData.attributes.set(getUID(attribute), accessor); } } if (originalNormal !== undefined) geometry.setAttribute('normal', originalNormal); // Skip if no exportable attributes found if (Object.keys(attributes).length === 0) { return null; } // Morph targets if (mesh.morphTargetInfluences !== undefined && mesh.morphTargetInfluences.length > 0) { var weights = []; var targetNames = []; var reverseDictionary = {}; if (mesh.morphTargetDictionary !== undefined) { for (var key in mesh.morphTargetDictionary) { reverseDictionary[mesh.morphTargetDictionary[key]] = key; } } for (var i = 0; i < mesh.morphTargetInfluences.length; ++i) { var target = {}; var warned = false; for (var attributeName in geometry.morphAttributes) { // glTF 2.0 morph supports only POSITION/NORMAL/TANGENT. // Three.js doesn't support TANGENT yet. if (attributeName !== 'position' && attributeName !== 'normal') { if (!warned) { console.warn('GLTFExporter: Only POSITION and NORMAL morph are supported.'); warned = true; } continue; } var attribute = geometry.morphAttributes[attributeName][i]; var gltfAttributeName = attributeName.toUpperCase(); // Three.js morph attribute has absolute values while the one of glTF has relative values. // // glTF 2.0 Specification: // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#morph-targets var baseAttribute = geometry.attributes[attributeName]; if (cachedData.attributes.has(getUID(attribute))) { target[gltfAttributeName] = cachedData.attributes.get(getUID(attribute)); continue; } // Clones attribute not to override var relativeAttribute = attribute.clone(); if (!geometry.morphTargetsRelative) { for (var j = 0, jl = attribute.count; j < jl; j++) { relativeAttribute.setXYZ( j, attribute.getX(j) - baseAttribute.getX(j), attribute.getY(j) - baseAttribute.getY(j), attribute.getZ(j) - baseAttribute.getZ(j) ); } } target[gltfAttributeName] = processAccessor(relativeAttribute, geometry); cachedData.attributes.set(getUID(baseAttribute), target[gltfAttributeName]); } targets.push(target); weights.push(mesh.morphTargetInfluences[i]); if (mesh.morphTargetDictionary !== undefined) targetNames.push(reverseDictionary[i]); } gltfMesh.weights = weights; if (targetNames.length > 0) { gltfMesh.extras = {}; gltfMesh.extras.targetNames = targetNames; } } var isMultiMaterial = Array.isArray(mesh.material); if (isMultiMaterial && geometry.groups.length === 0) return null; var materials = isMultiMaterial ? mesh.material : [mesh.material]; var groups = isMultiMaterial ? geometry.groups : [{ materialIndex: 0, start: undefined, count: undefined }]; for (var i = 0, il = groups.length; i < il; i++) { var primitive = { mode: mode, attributes: attributes, }; serializeUserData(geometry, primitive); if (targets.length > 0) primitive.targets = targets; if (geometry.index !== null) { var cacheKey = getUID(geometry.index); if (groups[i].start !== undefined || groups[i].count !== undefined) { cacheKey += ':' + groups[i].start + ':' + groups[i].count; } if (cachedData.attributes.has(cacheKey)) { primitive.indices = cachedData.attributes.get(cacheKey); } else { primitive.indices = processAccessor(geometry.index, geometry, groups[i].start, groups[i].count); cachedData.attributes.set(cacheKey, primitive.indices); } if (primitive.indices === null) delete primitive.indices; } var material = processMaterial(materials[groups[i].materialIndex]); if (material !== null) { primitive.material = material; } primitives.push(primitive); } gltfMesh.primitives = primitives; if (!outputJSON.meshes) { outputJSON.meshes = []; } outputJSON.meshes.push(gltfMesh); var index = outputJSON.meshes.length - 1; cachedData.meshes.set(meshCacheKey, index); return index; } /** * Process camera * @param {THREE.Camera} camera Camera to process * @return {Integer} Index of the processed mesh in the "camera" array */ function processCamera(camera) { if (!outputJSON.cameras) { outputJSON.cameras = []; } var isOrtho = camera.isOrthographicCamera; var gltfCamera = { type: isOrtho ? 'orthographic' : 'perspective' }; if (isOrtho) { gltfCamera.orthographic = { xmag: camera.right * 2, ymag: camera.top * 2, zfar: camera.far <= 0 ? 0.001 : camera.far, znear: camera.near < 0 ? 0 : camera.near }; } else { gltfCamera.perspective = { aspectRatio: camera.aspect, yfov: THREE.MathUtils.degToRad(camera.fov), zfar: camera.far <= 0 ? 0.001 : camera.far, znear: camera.near < 0 ? 0 : camera.near }; } if (camera.name !== '') { gltfCamera.name = camera.type; } outputJSON.cameras.push(gltfCamera); return outputJSON.cameras.length - 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} */ function processAnimation(clip, root) { if (!outputJSON.animations) { outputJSON.animations = []; } clip = GLTFExporter.Utils.mergeMorphTargetTracks(clip.clone(), root); var tracks = clip.tracks; var channels = []; var samplers = []; for (var i = 0; i < tracks.length; ++i) { var track = tracks[i]; var trackBinding = THREE.PropertyBinding.parseTrackName(track.name); var trackNode = THREE.PropertyBinding.findNode(root, trackBinding.nodeName); var trackProperty = PATH_PROPERTIES[trackBinding.propertyName]; if (trackBinding.objectName === 'bones') { if (trackNode.isSkinnedMesh === true) { trackNode = trackNode.skeleton.getBoneByName(trackBinding.objectIndex); } else { trackNode = undefined; } } if (!trackNode || !trackProperty) { console.warn('THREE.GLTFExporter: Could not export animation track "%s".', track.name); return null; } var inputItemSize = 1; var outputItemSize = track.values.length / track.times.length; if (trackProperty === PATH_PROPERTIES.morphTargetInfluences) { outputItemSize /= trackNode.morphTargetInfluences.length; } var interpolation; // @TODO export CubicInterpolant(InterpolateSmooth) as CUBICSPLINE // Detecting glTF cubic spline interpolant by checking factory method's special property // GLTFCubicSplineInterpolant is a custom interpolant and track doesn't return // valid value from .getInterpolation(). if (track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline === true) { interpolation = 'CUBICSPLINE'; // itemSize of CUBICSPLINE keyframe is 9 // (VEC3 * 3: inTangent, splineVertex, and outTangent) // but needs to be stored as VEC3 so dividing by 3 here. outputItemSize /= 3; } else if (track.getInterpolation() === THREE.InterpolateDiscrete) { interpolation = 'STEP'; } else { interpolation = 'LINEAR'; } samplers.push({ input: processAccessor(new THREE.BufferAttribute(track.times, inputItemSize)), output: processAccessor(new THREE.BufferAttribute(track.values, outputItemSize)), interpolation: interpolation }); channels.push({ sampler: samplers.length - 1, target: { node: nodeMap.get(trackNode), path: trackProperty } }); } outputJSON.animations.push({ name: clip.name || 'clip_' + outputJSON.animations.length, samplers: samplers, channels: channels }); return outputJSON.animations.length - 1; } function processSkin(object) { var node = outputJSON.nodes[nodeMap.get(object)]; var skeleton = object.skeleton; if (skeleton === undefined) return null; var rootJoint = object.skeleton.bones[0]; if (rootJoint === undefined) return null; var joints = []; var inverseBindMatrices = new Float32Array(skeleton.bones.length * 16); var temporaryBoneInverse = new THREE.Matrix4(); for (var 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 (outputJSON.skins === undefined) { outputJSON.skins = []; } outputJSON.skins.push({ inverseBindMatrices: processAccessor(new THREE.BufferAttribute(inverseBindMatrices, 16)), joints: joints, skeleton: nodeMap.get(rootJoint) }); var skinIndex = node.skin = outputJSON.skins.length - 1; return skinIndex; } function processLight(light) { var 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.0) * light.angle * - 1.0; lightDef.spot.outerConeAngle = light.angle; } if (light.decay !== undefined && 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.'); } var lights = outputJSON.extensions['KHR_lights_punctual'].lights; lights.push(lightDef); return lights.length - 1; } /** * Process Object3D node * @param {THREE.Object3D} node Object3D to processNode * @return {Integer} Index of the node in the nodes list */ function processNode(object) { if (!outputJSON.nodes) { outputJSON.nodes = []; } var gltfNode = {}; if (options.trs) { var rotation = object.quaternion.toArray(); var position = object.position.toArray(); var scale = object.scale.toArray(); if (!equalArray(rotation, [0, 0, 0, 1])) { gltfNode.rotation = rotation; } if (!equalArray(position, [0, 0, 0])) { gltfNode.translation = position; } if (!equalArray(scale, [1, 1, 1])) { gltfNode.scale = scale; } } else { if (object.matrixAutoUpdate) { object.updateMatrix(); } if (isIdentityMatrix(object.matrix) === false) { gltfNode.matrix = object.matrix.elements; } } // We don't export empty strings name because it represents no-name in Three.js. if (object.name !== '') { gltfNode.name = String(object.name); } serializeUserData(object, gltfNode); if (object.isMesh || object.isLine || object.isPoints) { var mesh = processMesh(object); if (mesh !== null) { gltfNode.mesh = mesh; } } else if (object.isCamera) { gltfNode.camera = processCamera(object); } else if (object.isDirectionalLight || object.isPointLight || object.isSpotLight) { if (!extensionsUsed['KHR_lights_punctual']) { outputJSON.extensions = outputJSON.extensions || {}; outputJSON.extensions['KHR_lights_punctual'] = { lights: [] }; extensionsUsed['KHR_lights_punctual'] = true; } gltfNode.extensions = gltfNode.extensions || {}; gltfNode.extensions['KHR_lights_punctual'] = { light: processLight(object) }; } else if (object.isLight) { console.warn('THREE.GLTFExporter: Only directional, point, and spot lights are supported.', object); return null; } if (object.isSkinnedMesh) { skins.push(object); } if (object.children.length > 0) { var children = []; for (var i = 0, l = object.children.length; i < l; i++) { var child = object.children[i]; if (child.visible || options.onlyVisible === false) { var node = processNode(child); if (node !== null) { children.push(node); } } } if (children.length > 0) { gltfNode.children = children; } } outputJSON.nodes.push(gltfNode); var nodeIndex = outputJSON.nodes.length - 1; nodeMap.set(object, nodeIndex); return nodeIndex; } /** * Process Scene * @param {THREE.Scene} node Scene to process */ function processScene(scene) { if (!outputJSON.scenes) { outputJSON.scenes = []; outputJSON.scene = 0; } var gltfScene = {}; if (scene.name !== '') { gltfScene.name = scene.name; } outputJSON.scenes.push(gltfScene); var nodes = []; for (var i = 0, l = scene.children.length; i < l; i++) { var child = scene.children[i]; if (child.visible || options.onlyVisible === false) { var node = processNode(child); if (node !== null) { nodes.push(node); } } } if (nodes.length > 0) { gltfScene.nodes = nodes; } serializeUserData(scene, gltfScene); } /** * Creates a THREE.Scene to hold a list of objects and parse it * @param {Array} objects List of objects to process */ function processObjects(objects) { var scene = new THREE.Scene(); scene.name = 'AuxScene'; for (var i = 0; i < objects.length; i++) { // We push directly to children instead of calling `add` to prevent // modify the .parent and break its original scene and hierarchy scene.children.push(objects[i]); } processScene(scene); } function processInput(input) { input = input instanceof Array ? input : [input]; var objectsWithoutScene = []; for (var i = 0; i < input.length; i++) { if (input[i] instanceof THREE.Scene) { processScene(input[i]); } else { objectsWithoutScene.push(input[i]); } } if (objectsWithoutScene.length > 0) { processObjects(objectsWithoutScene); } for (var i = 0; i < skins.length; ++i) { processSkin(skins[i]); } for (var i = 0; i < options.animations.length; ++i) { processAnimation(options.animations[i], input[0]); } } processInput(input); Promise.all(pending).then(function () { // Merge buffers. const joinedBuffers = joinBuffers(...buffers) // Declare extensions. var extensionsUsedList = Object.keys(extensionsUsed); if (extensionsUsedList.length > 0) outputJSON.extensionsUsed = extensionsUsedList; // Update bytelength of the single buffer. if (outputJSON.buffers && outputJSON.buffers.length > 0) outputJSON.buffers[0].byteLength = joinedBuffers.byteLength; if (options.binary === true) { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification var GLB_HEADER_BYTES = 12; var GLB_HEADER_MAGIC = 0x46546C67; var GLB_VERSION = 2; var GLB_CHUNK_PREFIX_BYTES = 8; var GLB_CHUNK_TYPE_JSON = 0x4E4F534A; var GLB_CHUNK_TYPE_BIN = 0x004E4942; // Binary chunk. var binaryChunk = getPaddedArrayBuffer(joinedBuffers); var binaryChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)); binaryChunkPrefix.setUint32(0, binaryChunk.byteLength, true); binaryChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_BIN, true); // JSON chunk. var jsonChunk = getPaddedArrayBuffer(stringToArrayBuffer(JSON.stringify(outputJSON)), 0x20); var jsonChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)); jsonChunkPrefix.setUint32(0, jsonChunk.byteLength, true); jsonChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_JSON, true); // GLB header. var header = new ArrayBuffer(GLB_HEADER_BYTES); var headerView = new DataView(header); headerView.setUint32(0, GLB_HEADER_MAGIC, true); headerView.setUint32(4, GLB_VERSION, true); var totalByteLength = GLB_HEADER_BYTES + jsonChunkPrefix.byteLength + jsonChunk.byteLength + binaryChunkPrefix.byteLength + binaryChunk.byteLength; headerView.setUint32(8, totalByteLength, true); onDone(joinBuffers( header, jsonChunkPrefix, jsonChunk, binaryChunkPrefix, binaryChunk )) } else { if (outputJSON.buffers && outputJSON.buffers.length > 0) { var reader = new FileReader(); reader.readAsDataURL(blob); reader.onloadend = function () { var base64data = reader.result; outputJSON.buffers[0].uri = base64data; onDone(outputJSON); }; } else { onDone(outputJSON); } } }); } }; GLTFExporter.Utils = { insertKeyframe: function (track, time) { var tolerance = 0.001; // 1ms var valueSize = track.getValueSize(); var times = new track.TimeBufferType(track.times.length + 1); var values = new track.ValueBufferType(track.values.length + valueSize); var interpolant = track.createInterpolant(new track.ValueBufferType(valueSize)); var index; if (track.times.length === 0) { times[0] = time; for (var 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 (var 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) { var tracks = []; var mergedTracks = {}; var sourceTracks = clip.tracks; for (var i = 0; i < sourceTracks.length; ++i) { var sourceTrack = sourceTracks[i]; var sourceTrackBinding = THREE.PropertyBinding.parseTrackName(sourceTrack.name); var sourceTrackNode = THREE.PropertyBinding.findNode(root, sourceTrackBinding.