3dtiles-tool
Version:
高效易用的3DTiles工具库,各种功能持续更新中
1,466 lines (1,196 loc) • 75.6 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three')) :
typeof define === 'function' && define.amd ? define(['exports', 'three'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.bundle = {}, global.three));
})(this, (function (exports, THREE) { 'use strict';
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n["default"] = e;
return Object.freeze(n);
}
var THREE__namespace = /*#__PURE__*/_interopNamespace(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__namespace.NearestFilter] = WEBGL_CONSTANTS.NEAREST;
THREE_TO_WEBGL[THREE__namespace.NearestMipmapNearestFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_NEAREST;
THREE_TO_WEBGL[THREE__namespace.NearestMipmapLinearFilter] = WEBGL_CONSTANTS.NEAREST_MIPMAP_LINEAR;
THREE_TO_WEBGL[THREE__namespace.LinearFilter] = WEBGL_CONSTANTS.LINEAR;
THREE_TO_WEBGL[THREE__namespace.LinearMipmapNearestFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_NEAREST;
THREE_TO_WEBGL[THREE__namespace.LinearMipmapLinearFilter] = WEBGL_CONSTANTS.LINEAR_MIPMAP_LINEAR;
THREE_TO_WEBGL[THREE__namespace.ClampToEdgeWrapping] = WEBGL_CONSTANTS.CLAMP_TO_EDGE;
THREE_TO_WEBGL[THREE__namespace.RepeatWrapping] = WEBGL_CONSTANTS.REPEAT;
THREE_TO_WEBGL[THREE__namespace.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__namespace.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__namespace.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__namespace.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__namespace.RGBAFormat && format !== THREE__namespace.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__namespace.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__namespace.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__namespace.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__namespace.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__namespace.PropertyBinding.parseTrackName(track.name);
var trackNode = THREE__namespace.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__namespace.InterpolateDiscrete) {
interpolation = 'STEP';
} else {
interpolation = 'LINEAR';
}
samplers.push({
input: processAccessor(new THREE__namespace.BufferAttribute(track.times, inputItemSize)),
output: processAccessor(new THREE__namespace.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__namespace.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__namespace.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
* @