From a73b163e4ecd3ec676d62229d035dc608cd200cd Mon Sep 17 00:00:00 2001 From: Paul Graffam Date: Mon, 6 Jul 2020 17:24:10 -0400 Subject: [PATCH] Added gltf loader --- src/js/app/loaders/GLTFLoader.js | 2910 ++++++++++++++++++++++++++++++ 1 file changed, 2910 insertions(+) create mode 100644 src/js/app/loaders/GLTFLoader.js diff --git a/src/js/app/loaders/GLTFLoader.js b/src/js/app/loaders/GLTFLoader.js new file mode 100644 index 0000000..c9d162d --- /dev/null +++ b/src/js/app/loaders/GLTFLoader.js @@ -0,0 +1,2910 @@ +/** + * @author Rich Tibbett / https://github.com/richtr + * @author mrdoob / http://mrdoob.com/ + * @author Tony Parisi / http://www.tonyparisi.com/ + * @author Takahiro / https://github.com/takahirox + * @author Don McCurdy / https://www.donmccurdy.com + */ + +import { + AnimationClip, + Bone, + Box3, + BufferAttribute, + BufferGeometry, + ClampToEdgeWrapping, + Color, + DirectionalLight, + DoubleSide, + FileLoader, + FrontSide, + Group, + InterleavedBuffer, + InterleavedBufferAttribute, + Interpolant, + InterpolateDiscrete, + InterpolateLinear, + Line, + LineBasicMaterial, + LineLoop, + LineSegments, + LinearFilter, + LinearMipmapLinearFilter, + LinearMipmapNearestFilter, + Loader, + LoaderUtils, + Material, + MathUtils, + Matrix4, + Mesh, + MeshBasicMaterial, + MeshPhysicalMaterial, + MeshStandardMaterial, + MirroredRepeatWrapping, + NearestFilter, + NearestMipmapLinearFilter, + NearestMipmapNearestFilter, + NumberKeyframeTrack, + Object3D, + OrthographicCamera, + PerspectiveCamera, + PointLight, + Points, + PointsMaterial, + PropertyBinding, + QuaternionKeyframeTrack, + RGBAFormat, + RGBFormat, + RepeatWrapping, + Skeleton, + SkinnedMesh, + Sphere, + SpotLight, + TangentSpaceNormalMap, + TextureLoader, + TriangleFanDrawMode, + TriangleStripDrawMode, + Vector2, + Vector3, + VectorKeyframeTrack, + sRGBEncoding +} from 'three'; + +const GLTFLoader = (function () { + function GLTFLoader(manager) { + Loader.call(this, manager); + + this.dracoLoader = null; + this.ddsLoader = null; + } + + GLTFLoader.prototype = Object.assign(Object.create(Loader.prototype), { + constructor: GLTFLoader, + + load: function (url, onLoad, onProgress, onError) { + var scope = this; + + var resourcePath; + + if (this.resourcePath !== '') { + resourcePath = this.resourcePath; + } else if (this.path !== '') { + resourcePath = this.path; + } else { + resourcePath = LoaderUtils.extractUrlBase(url); + } + + // Tells the LoadingManager to track an extra item, which resolves after + // the model is fully loaded. This means the count of items loaded will + // be incorrect, but ensures manager.onLoad() does not fire early. + scope.manager.itemStart(url); + + var _onError = function (e) { + if (onError) { + onError(e); + } else { + console.error(e); + } + + scope.manager.itemError(url); + scope.manager.itemEnd(url); + }; + + var loader = new FileLoader(scope.manager); + + loader.setPath(this.path); + loader.setResponseType('arraybuffer'); + + if (scope.crossOrigin === 'use-credentials') { + loader.setWithCredentials(true); + } + + loader.load( + url, + function (data) { + try { + scope.parse( + data, + resourcePath, + function (gltf) { + onLoad(gltf); + + scope.manager.itemEnd(url); + }, + _onError + ); + } catch (e) { + _onError(e); + } + }, + onProgress, + _onError + ); + }, + + setDRACOLoader: function (dracoLoader) { + this.dracoLoader = dracoLoader; + return this; + }, + + setDDSLoader: function (ddsLoader) { + this.ddsLoader = ddsLoader; + return this; + }, + + parse: function (data, path, onLoad, onError) { + var content; + var extensions = {}; + + if (typeof data === 'string') { + content = data; + } else { + var magic = LoaderUtils.decodeText(new Uint8Array(data, 0, 4)); + + if (magic === BINARY_EXTENSION_HEADER_MAGIC) { + try { + extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data); + } catch (error) { + if (onError) onError(error); + return; + } + + content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content; + } else { + content = LoaderUtils.decodeText(new Uint8Array(data)); + } + } + + var json = JSON.parse(content); + + if (json.asset === undefined || json.asset.version[0] < 2) { + if (onError) + onError( + new Error('THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.') + ); + return; + } + + if (json.extensionsUsed) { + for (var i = 0; i < json.extensionsUsed.length; ++i) { + var extensionName = json.extensionsUsed[i]; + var extensionsRequired = json.extensionsRequired || []; + + switch (extensionName) { + case EXTENSIONS.KHR_LIGHTS_PUNCTUAL: + extensions[extensionName] = new GLTFLightsExtension(json); + break; + + case EXTENSIONS.KHR_MATERIALS_CLEARCOAT: + extensions[extensionName] = new GLTFMaterialsClearcoatExtension(); + break; + + case EXTENSIONS.KHR_MATERIALS_UNLIT: + extensions[extensionName] = new GLTFMaterialsUnlitExtension(); + break; + + case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: + extensions[extensionName] = new GLTFMaterialsPbrSpecularGlossinessExtension(); + break; + + case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION: + extensions[extensionName] = new GLTFDracoMeshCompressionExtension( + json, + this.dracoLoader + ); + break; + + case EXTENSIONS.MSFT_TEXTURE_DDS: + extensions[extensionName] = new GLTFTextureDDSExtension(this.ddsLoader); + break; + + case EXTENSIONS.KHR_TEXTURE_TRANSFORM: + extensions[extensionName] = new GLTFTextureTransformExtension(); + break; + + case EXTENSIONS.KHR_MESH_QUANTIZATION: + extensions[extensionName] = new GLTFMeshQuantizationExtension(); + break; + + default: + if (extensionsRequired.indexOf(extensionName) >= 0) { + console.warn('THREE.GLTFLoader: Unknown extension "' + extensionName + '".'); + } + } + } + } + + var parser = new GLTFParser(json, extensions, { + path: path || this.resourcePath || '', + crossOrigin: this.crossOrigin, + manager: this.manager + }); + + parser.parse(onLoad, onError); + }, + }); + + /* GLTFREGISTRY */ + + function GLTFRegistry() { + var objects = {}; + + return { + get: function (key) { + return objects[key]; + }, + + add: function (key, object) { + objects[key] = object; + }, + + remove: function (key) { + delete objects[key]; + }, + + removeAll: function () { + objects = {}; + }, + }; + } + + /*********************************/ + /********** EXTENSIONS ***********/ + /*********************************/ + + var EXTENSIONS = { + KHR_BINARY_GLTF: 'KHR_binary_glTF', + KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression', + KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual', + KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat', + KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness', + KHR_MATERIALS_UNLIT: 'KHR_materials_unlit', + KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform', + KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization', + MSFT_TEXTURE_DDS: 'MSFT_texture_dds' + }; + + /** + * DDS Texture Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds + * + */ + function GLTFTextureDDSExtension(ddsLoader) { + if (!ddsLoader) { + throw new Error( + 'THREE.GLTFLoader: Attempting to load .dds texture without importing DDSLoader' + ); + } + + this.name = EXTENSIONS.MSFT_TEXTURE_DDS; + this.ddsLoader = ddsLoader; + } + + /** + * Punctual Lights Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual + */ + function GLTFLightsExtension(json) { + this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL; + + var extension = (json.extensions && json.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL]) || {}; + this.lightDefs = extension.lights || []; + } + + GLTFLightsExtension.prototype.loadLight = function (lightIndex) { + var lightDef = this.lightDefs[lightIndex]; + var lightNode; + + var color = new Color(0xffffff); + if (lightDef.color !== undefined) color.fromArray(lightDef.color); + + var range = lightDef.range !== undefined ? lightDef.range : 0; + + switch (lightDef.type) { + case 'directional': + lightNode = new DirectionalLight(color); + lightNode.target.position.set(0, 0, -1); + lightNode.add(lightNode.target); + break; + + case 'point': + lightNode = new PointLight(color); + lightNode.distance = range; + break; + + case 'spot': + lightNode = new SpotLight(color); + lightNode.distance = range; + // Handle spotlight properties. + lightDef.spot = lightDef.spot || {}; + lightDef.spot.innerConeAngle = + lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0; + lightDef.spot.outerConeAngle = + lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0; + lightNode.angle = lightDef.spot.outerConeAngle; + lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle; + lightNode.target.position.set(0, 0, -1); + lightNode.add(lightNode.target); + break; + + default: + throw new Error('THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".'); + } + + // Some lights (e.g. spot) default to a position other than the origin. Reset the position + // here, because node-level parsing will only override position if explicitly specified. + lightNode.position.set(0, 0, 0); + + lightNode.decay = 2; + + if (lightDef.intensity !== undefined) lightNode.intensity = lightDef.intensity; + + lightNode.name = lightDef.name || 'light_' + lightIndex; + + return Promise.resolve(lightNode); + }; + + /** + * Unlit Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit + */ + function GLTFMaterialsUnlitExtension() { + this.name = EXTENSIONS.KHR_MATERIALS_UNLIT; + } + + GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () { + return MeshBasicMaterial; + }; + + GLTFMaterialsUnlitExtension.prototype.extendParams = function ( + materialParams, + materialDef, + parser + ) { + var pending = []; + + materialParams.color = new Color(1.0, 1.0, 1.0); + materialParams.opacity = 1.0; + + var metallicRoughness = materialDef.pbrMetallicRoughness; + + if (metallicRoughness) { + if (Array.isArray(metallicRoughness.baseColorFactor)) { + var array = metallicRoughness.baseColorFactor; + + materialParams.color.fromArray(array); + materialParams.opacity = array[3]; + } + + if (metallicRoughness.baseColorTexture !== undefined) { + pending.push( + parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture) + ); + } + } + + return Promise.all(pending); + }; + + /** + * Clearcoat Materials Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat + */ + function GLTFMaterialsClearcoatExtension() { + this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT; + } + + GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function () { + return MeshPhysicalMaterial; + }; + + GLTFMaterialsClearcoatExtension.prototype.extendParams = function ( + materialParams, + materialDef, + parser + ) { + var pending = []; + + var extension = materialDef.extensions[this.name]; + + if (extension.clearcoatFactor !== undefined) { + materialParams.clearcoat = extension.clearcoatFactor; + } + + if (extension.clearcoatTexture !== undefined) { + pending.push( + parser.assignTexture(materialParams, 'clearcoatMap', extension.clearcoatTexture) + ); + } + + if (extension.clearcoatRoughnessFactor !== undefined) { + materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor; + } + + if (extension.clearcoatRoughnessTexture !== undefined) { + pending.push( + parser.assignTexture( + materialParams, + 'clearcoatRoughnessMap', + extension.clearcoatRoughnessTexture + ) + ); + } + + if (extension.clearcoatNormalTexture !== undefined) { + pending.push( + parser.assignTexture( + materialParams, + 'clearcoatNormalMap', + extension.clearcoatNormalTexture + ) + ); + + if (extension.clearcoatNormalTexture.scale !== undefined) { + var scale = extension.clearcoatNormalTexture.scale; + + materialParams.clearcoatNormalScale = new Vector2(scale, scale); + } + } + + return Promise.all(pending); + }; + + /* BINARY EXTENSION */ + var BINARY_EXTENSION_HEADER_MAGIC = 'glTF'; + var BINARY_EXTENSION_HEADER_LENGTH = 12; + var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4e4f534a, BIN: 0x004e4942 }; + + function GLTFBinaryExtension(data) { + this.name = EXTENSIONS.KHR_BINARY_GLTF; + this.content = null; + this.body = null; + + var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH); + + this.header = { + magic: LoaderUtils.decodeText(new Uint8Array(data.slice(0, 4))), + version: headerView.getUint32(4, true), + length: headerView.getUint32(8, true) + }; + + if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) { + throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.'); + } else if (this.header.version < 2.0) { + throw new Error('THREE.GLTFLoader: Legacy binary file detected.'); + } + + var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH); + var chunkIndex = 0; + + while (chunkIndex < chunkView.byteLength) { + var chunkLength = chunkView.getUint32(chunkIndex, true); + chunkIndex += 4; + + var chunkType = chunkView.getUint32(chunkIndex, true); + chunkIndex += 4; + + if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) { + var contentArray = new Uint8Array( + data, + BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, + chunkLength + ); + this.content = LoaderUtils.decodeText(contentArray); + } else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) { + var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex; + this.body = data.slice(byteOffset, byteOffset + chunkLength); + } + + // Clients must ignore chunks with unknown types. + + chunkIndex += chunkLength; + } + + if (this.content === null) { + throw new Error('THREE.GLTFLoader: JSON content not found.'); + } + } + + /** + * DRACO Mesh Compression Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression + */ + function GLTFDracoMeshCompressionExtension(json, dracoLoader) { + if (!dracoLoader) { + throw new Error('THREE.GLTFLoader: No DRACOLoader instance provided.'); + } + + this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION; + this.json = json; + this.dracoLoader = dracoLoader; + this.dracoLoader.preload(); + } + + GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function (primitive, parser) { + var json = this.json; + var dracoLoader = this.dracoLoader; + var bufferViewIndex = primitive.extensions[this.name].bufferView; + var gltfAttributeMap = primitive.extensions[this.name].attributes; + var threeAttributeMap = {}; + var attributeNormalizedMap = {}; + var attributeTypeMap = {}; + + for (var attributeName in gltfAttributeMap) { + var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); + + threeAttributeMap[threeAttributeName] = gltfAttributeMap[attributeName]; + } + + for (attributeName in primitive.attributes) { + var threeAttributeName = ATTRIBUTES[attributeName] || attributeName.toLowerCase(); + + if (gltfAttributeMap[attributeName] !== undefined) { + var accessorDef = json.accessors[primitive.attributes[attributeName]]; + var componentType = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; + + attributeTypeMap[threeAttributeName] = componentType; + attributeNormalizedMap[threeAttributeName] = accessorDef.normalized === true; + } + } + + return parser.getDependency('bufferView', bufferViewIndex).then(function (bufferView) { + return new Promise(function (resolve) { + dracoLoader.decodeDracoFile( + bufferView, + function (geometry) { + for (var attributeName in geometry.attributes) { + var attribute = geometry.attributes[attributeName]; + var normalized = attributeNormalizedMap[attributeName]; + + if (normalized !== undefined) attribute.normalized = normalized; + } + + resolve(geometry); + }, + threeAttributeMap, + attributeTypeMap + ); + }); + }); + }; + + /** + * Texture Transform Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform + */ + function GLTFTextureTransformExtension() { + this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM; + } + + GLTFTextureTransformExtension.prototype.extendTexture = function (texture, transform) { + texture = texture.clone(); + + if (transform.offset !== undefined) { + texture.offset.fromArray(transform.offset); + } + + if (transform.rotation !== undefined) { + texture.rotation = transform.rotation; + } + + if (transform.scale !== undefined) { + texture.repeat.fromArray(transform.scale); + } + + if (transform.texCoord !== undefined) { + console.warn( + 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' + ); + } + + texture.needsUpdate = true; + + return texture; + }; + + /** + * Specular-Glossiness Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness + */ + + /** + * A sub class of StandardMaterial with some of the functionality + * changed via the `onBeforeCompile` callback + * @pailhead + */ + + function GLTFMeshStandardSGMaterial(params) { + MeshStandardMaterial.call(this); + + this.isGLTFSpecularGlossinessMaterial = true; + + //various chunks that need replacing + var specularMapParsFragmentChunk = [ + '#ifdef USE_SPECULARMAP', + ' uniform sampler2D specularMap;', + '#endif', + ].join('\n'); + + var glossinessMapParsFragmentChunk = [ + '#ifdef USE_GLOSSINESSMAP', + ' uniform sampler2D glossinessMap;', + '#endif', + ].join('\n'); + + var specularMapFragmentChunk = [ + 'vec3 specularFactor = specular;', + '#ifdef USE_SPECULARMAP', + ' vec4 texelSpecular = texture2D( specularMap, vUv );', + ' texelSpecular = sRGBToLinear( texelSpecular );', + ' // reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' specularFactor *= texelSpecular.rgb;', + '#endif', + ].join('\n'); + + var glossinessMapFragmentChunk = [ + 'float glossinessFactor = glossiness;', + '#ifdef USE_GLOSSINESSMAP', + ' vec4 texelGlossiness = texture2D( glossinessMap, vUv );', + ' // reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture', + ' glossinessFactor *= texelGlossiness.a;', + '#endif', + ].join('\n'); + + var lightPhysicalFragmentChunk = [ + 'PhysicalMaterial material;', + 'material.diffuseColor = diffuseColor.rgb;', + 'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );', + 'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );', + 'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 );// 0.0525 corresponds to the base mip of a 256 cubemap.', + 'material.specularRoughness += geometryRoughness;', + 'material.specularRoughness = min( material.specularRoughness, 1.0 );', + 'material.specularColor = specularFactor.rgb;', + ].join('\n'); + + var uniforms = { + specular: { value: new Color().setHex(0xffffff) }, + glossiness: { value: 1 }, + specularMap: { value: null }, + glossinessMap: { value: null }, + }; + + this._extraUniforms = uniforms; + + // please see #14031 or #13198 for an alternate approach + this.onBeforeCompile = function (shader) { + for (var uniformName in uniforms) { + shader.uniforms[uniformName] = uniforms[uniformName]; + } + + shader.fragmentShader = shader.fragmentShader.replace( + 'uniform float roughness;', + 'uniform vec3 specular;' + ); + shader.fragmentShader = shader.fragmentShader.replace( + 'uniform float metalness;', + 'uniform float glossiness;' + ); + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + specularMapParsFragmentChunk + ); + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + glossinessMapParsFragmentChunk + ); + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + specularMapFragmentChunk + ); + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + glossinessMapFragmentChunk + ); + shader.fragmentShader = shader.fragmentShader.replace( + '#include ', + lightPhysicalFragmentChunk + ); + }; + + /*eslint-disable*/ + Object.defineProperties(this, { + specular: { + get: function () { + return uniforms.specular.value; + }, + set: function (v) { + uniforms.specular.value = v; + }, + }, + specularMap: { + get: function () { + return uniforms.specularMap.value; + }, + set: function (v) { + uniforms.specularMap.value = v; + }, + }, + glossiness: { + get: function () { + return uniforms.glossiness.value; + }, + set: function (v) { + uniforms.glossiness.value = v; + }, + }, + glossinessMap: { + get: function () { + return uniforms.glossinessMap.value; + }, + set: function (v) { + uniforms.glossinessMap.value = v; + //how about something like this - @pailhead + if (v) { + this.defines.USE_GLOSSINESSMAP = ''; + // set USE_ROUGHNESSMAP to enable vUv + this.defines.USE_ROUGHNESSMAP = ''; + } else { + delete this.defines.USE_ROUGHNESSMAP; + delete this.defines.USE_GLOSSINESSMAP; + } + }, + }, + }); + + /*eslint-enable*/ + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; + + this.setValues(params); + } + + GLTFMeshStandardSGMaterial.prototype = Object.create(MeshStandardMaterial.prototype); + GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial; + + GLTFMeshStandardSGMaterial.prototype.copy = function (source) { + MeshStandardMaterial.prototype.copy.call(this, source); + this.specularMap = source.specularMap; + this.specular.copy(source.specular); + this.glossinessMap = source.glossinessMap; + this.glossiness = source.glossiness; + delete this.metalness; + delete this.roughness; + delete this.metalnessMap; + delete this.roughnessMap; + return this; + }; + + function GLTFMaterialsPbrSpecularGlossinessExtension() { + return { + name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS, + + specularGlossinessParams: [ + 'color', + 'map', + 'lightMap', + 'lightMapIntensity', + 'aoMap', + 'aoMapIntensity', + 'emissive', + 'emissiveIntensity', + 'emissiveMap', + 'bumpMap', + 'bumpScale', + 'normalMap', + 'normalMapType', + 'displacementMap', + 'displacementScale', + 'displacementBias', + 'specularMap', + 'specular', + 'glossinessMap', + 'glossiness', + 'alphaMap', + 'envMap', + 'envMapIntensity', + 'refractionRatio', + ], + + getMaterialType: function () { + return GLTFMeshStandardSGMaterial; + }, + + extendParams: function (materialParams, materialDef, parser) { + var pbrSpecularGlossiness = materialDef.extensions[this.name]; + + materialParams.color = new Color(1.0, 1.0, 1.0); + materialParams.opacity = 1.0; + + var pending = []; + + if (Array.isArray(pbrSpecularGlossiness.diffuseFactor)) { + var array = pbrSpecularGlossiness.diffuseFactor; + + materialParams.color.fromArray(array); + materialParams.opacity = array[3]; + } + + if (pbrSpecularGlossiness.diffuseTexture !== undefined) { + pending.push( + parser.assignTexture(materialParams, 'map', pbrSpecularGlossiness.diffuseTexture) + ); + } + + materialParams.emissive = new Color(0.0, 0.0, 0.0); + materialParams.glossiness = + pbrSpecularGlossiness.glossinessFactor !== undefined + ? pbrSpecularGlossiness.glossinessFactor + : 1.0; + materialParams.specular = new Color(1.0, 1.0, 1.0); + + if (Array.isArray(pbrSpecularGlossiness.specularFactor)) { + materialParams.specular.fromArray(pbrSpecularGlossiness.specularFactor); + } + + if (pbrSpecularGlossiness.specularGlossinessTexture !== undefined) { + var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture; + pending.push(parser.assignTexture(materialParams, 'glossinessMap', specGlossMapDef)); + pending.push(parser.assignTexture(materialParams, 'specularMap', specGlossMapDef)); + } + + return Promise.all(pending); + }, + + createMaterial: function (materialParams) { + var material = new GLTFMeshStandardSGMaterial(materialParams); + material.fog = true; + + material.color = materialParams.color; + + material.map = materialParams.map === undefined ? null : materialParams.map; + + material.lightMap = null; + material.lightMapIntensity = 1.0; + + material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap; + material.aoMapIntensity = 1.0; + + material.emissive = materialParams.emissive; + material.emissiveIntensity = 1.0; + material.emissiveMap = + materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap; + + material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap; + material.bumpScale = 1; + + material.normalMap = + materialParams.normalMap === undefined ? null : materialParams.normalMap; + material.normalMapType = TangentSpaceNormalMap; + + if (materialParams.normalScale) material.normalScale = materialParams.normalScale; + + material.displacementMap = null; + material.displacementScale = 1; + material.displacementBias = 0; + + material.specularMap = + materialParams.specularMap === undefined ? null : materialParams.specularMap; + material.specular = materialParams.specular; + + material.glossinessMap = + materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap; + material.glossiness = materialParams.glossiness; + + material.alphaMap = null; + + material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap; + material.envMapIntensity = 1.0; + + material.refractionRatio = 0.98; + + return material; + }, + }; + } + + /** + * Mesh Quantization Extension + * + * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization + */ + function GLTFMeshQuantizationExtension() { + this.name = EXTENSIONS.KHR_MESH_QUANTIZATION; + } + + /*********************************/ + /********** INTERPOLATION ********/ + /*********************************/ + + // Spline Interpolation + // Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation + function GLTFCubicSplineInterpolant(parameterPositions, sampleValues, sampleSize, resultBuffer) { + Interpolant.call(this, parameterPositions, sampleValues, sampleSize, resultBuffer); + } + + GLTFCubicSplineInterpolant.prototype = Object.create(Interpolant.prototype); + GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant; + + GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function (index) { + // Copies a sample value to the result buffer. See description of glTF + // CUBICSPLINE values layout in interpolate_() function below. + + var result = this.resultBuffer, + values = this.sampleValues, + valueSize = this.valueSize, + offset = index * valueSize * 3 + valueSize; + + for (var i = 0; i !== valueSize; i++) { + result[i] = values[offset + i]; + } + + return result; + }; + + GLTFCubicSplineInterpolant.prototype.beforeStart_ = + GLTFCubicSplineInterpolant.prototype.copySampleValue_; + + GLTFCubicSplineInterpolant.prototype.afterEnd_ = + GLTFCubicSplineInterpolant.prototype.copySampleValue_; + + GLTFCubicSplineInterpolant.prototype.interpolate_ = function (i1, t0, t, t1) { + var result = this.resultBuffer; + var values = this.sampleValues; + var stride = this.valueSize; + + var stride2 = stride * 2; + var stride3 = stride * 3; + + var td = t1 - t0; + + var p = (t - t0) / td; + var pp = p * p; + var ppp = pp * p; + + var offset1 = i1 * stride3; + var offset0 = offset1 - stride3; + + var s2 = -2 * ppp + 3 * pp; + var s3 = ppp - pp; + var s0 = 1 - s2; + var s1 = s3 - pp + p; + + // Layout of keyframe output values for CUBICSPLINE animations: + // [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ] + for (var i = 0; i !== stride; i++) { + var p0 = values[offset0 + i + stride]; // splineVertex_k + var m0 = values[offset0 + i + stride2] * td; // outTangent_k * (t_k+1 - t_k) + var p1 = values[offset1 + i + stride]; // splineVertex_k+1 + var m1 = values[offset1 + i] * td; // inTangent_k+1 * (t_k+1 - t_k) + + result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1; + } + + return result; + }; + + /*********************************/ + /********** INTERNALS ************/ + /*********************************/ + + /* CONSTANTS */ + + var WEBGL_CONSTANTS = { + FLOAT: 5126, + //FLOAT_MAT2: 35674, + FLOAT_MAT3: 35675, + FLOAT_MAT4: 35676, + FLOAT_VEC2: 35664, + FLOAT_VEC3: 35665, + FLOAT_VEC4: 35666, + LINEAR: 9729, + REPEAT: 10497, + SAMPLER_2D: 35678, + POINTS: 0, + LINES: 1, + LINE_LOOP: 2, + LINE_STRIP: 3, + TRIANGLES: 4, + TRIANGLE_STRIP: 5, + TRIANGLE_FAN: 6, + UNSIGNED_BYTE: 5121, + UNSIGNED_SHORT: 5123, + }; + + var WEBGL_COMPONENT_TYPES = { + 5120: Int8Array, + 5121: Uint8Array, + 5122: Int16Array, + 5123: Uint16Array, + 5125: Uint32Array, + 5126: Float32Array, + }; + + var WEBGL_FILTERS = { + 9728: NearestFilter, + 9729: LinearFilter, + 9984: NearestMipmapNearestFilter, + 9985: LinearMipmapNearestFilter, + 9986: NearestMipmapLinearFilter, + 9987: LinearMipmapLinearFilter, + }; + + var WEBGL_WRAPPINGS = { + 33071: ClampToEdgeWrapping, + 33648: MirroredRepeatWrapping, + 10497: RepeatWrapping, + }; + + var WEBGL_TYPE_SIZES = { + SCALAR: 1, + VEC2: 2, + VEC3: 3, + VEC4: 4, + MAT2: 4, + MAT3: 9, + MAT4: 16, + }; + + var ATTRIBUTES = { + POSITION: 'position', + NORMAL: 'normal', + TANGENT: 'tangent', + TEXCOORD_0: 'uv', + TEXCOORD_1: 'uv2', + COLOR_0: 'color', + WEIGHTS_0: 'skinWeight', + JOINTS_0: 'skinIndex', + }; + + var PATH_PROPERTIES = { + scale: 'scale', + translation: 'position', + rotation: 'quaternion', + weights: 'morphTargetInfluences', + }; + + var INTERPOLATION = { + CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each + // keyframe track will be initialized with a default interpolation type, then modified. + LINEAR: InterpolateLinear, + STEP: InterpolateDiscrete, + }; + + var ALPHA_MODES = { + OPAQUE: 'OPAQUE', + MASK: 'MASK', + BLEND: 'BLEND', + }; + + var MIME_TYPE_FORMATS = { + 'image/png': RGBAFormat, + 'image/jpeg': RGBFormat, + }; + + /* UTILITY FUNCTIONS */ + + function resolveURL(url, path) { + // Invalid URL + if (typeof url !== 'string' || url === '') return ''; + + // Host Relative URL + if (/^https?:\/\//i.test(path) && /^\//.test(url)) { + // eslint-disable-next-line no-useless-escape + path = path.replace(/(^https?:\/\/[^\/]+).*/i, '$1'); + } + + // Absolute URL http://,https://,// + if (/^(https?:)?\/\//i.test(url)) return url; + + // Data URI + if (/^data:.*,.*$/i.test(url)) return url; + + // Blob URL + if (/^blob:.*$/i.test(url)) return url; + + // Relative URL + return path + url; + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material + */ + function createDefaultMaterial(cache) { + if (cache['DefaultMaterial'] === undefined) { + cache['DefaultMaterial'] = new MeshStandardMaterial({ + color: 0xffffff, + emissive: 0x000000, + metalness: 1, + roughness: 1, + transparent: false, + depthTest: true, + side: FrontSide, + }); + } + + return cache['DefaultMaterial']; + } + + function addUnknownExtensionsToUserData(knownExtensions, object, objectDef) { + // Add unknown glTF extensions to an object's userData. + + for (var name in objectDef.extensions) { + if (knownExtensions[name] === undefined) { + object.userData.gltfExtensions = object.userData.gltfExtensions || {}; + object.userData.gltfExtensions[name] = objectDef.extensions[name]; + } + } + } + + /** + * @param {Object3D|Material|BufferGeometry} object + * @param {GLTF.definition} gltfDef + */ + function assignExtrasToUserData(object, gltfDef) { + if (gltfDef.extras !== undefined) { + if (typeof gltfDef.extras === 'object') { + Object.assign(object.userData, gltfDef.extras); + } else { + console.warn('THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras); + } + } + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets + * + * @param {BufferGeometry} geometry + * @param {Array} targets + * @param {GLTFParser} parser + * @return {Promise} + */ + function addMorphTargets(geometry, targets, parser) { + var hasMorphPosition = false; + var hasMorphNormal = false; + + for (var i = 0, il = targets.length; i < il; i++) { + var target = targets[i]; + + if (target.POSITION !== undefined) hasMorphPosition = true; + if (target.NORMAL !== undefined) hasMorphNormal = true; + + if (hasMorphPosition && hasMorphNormal) break; + } + + if (!hasMorphPosition && !hasMorphNormal) return Promise.resolve(geometry); + + var pendingPositionAccessors = []; + var pendingNormalAccessors = []; + + for (var i = 0, il = targets.length; i < il; i++) { + var target = targets[i]; + + if (hasMorphPosition) { + var pendingAccessor = + target.POSITION !== undefined + ? parser.getDependency('accessor', target.POSITION) + : geometry.attributes.position; + + pendingPositionAccessors.push(pendingAccessor); + } + + if (hasMorphNormal) { + var pendingAccessor = + target.NORMAL !== undefined + ? parser.getDependency('accessor', target.NORMAL) + : geometry.attributes.normal; + + pendingNormalAccessors.push(pendingAccessor); + } + } + + return Promise.all([ + Promise.all(pendingPositionAccessors), + Promise.all(pendingNormalAccessors), + ]).then(function (accessors) { + var morphPositions = accessors[0]; + var morphNormals = accessors[1]; + + if (hasMorphPosition) geometry.morphAttributes.position = morphPositions; + if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals; + geometry.morphTargetsRelative = true; + + return geometry; + }); + } + + /** + * @param {Mesh} mesh + * @param {GLTF.Mesh} meshDef + */ + function updateMorphTargets(mesh, meshDef) { + mesh.updateMorphTargets(); + + if (meshDef.weights !== undefined) { + for (var i = 0, il = meshDef.weights.length; i < il; i++) { + mesh.morphTargetInfluences[i] = meshDef.weights[i]; + } + } + + // .extras has user-defined data, so check that .extras.targetNames is an array. + if (meshDef.extras && Array.isArray(meshDef.extras.targetNames)) { + var targetNames = meshDef.extras.targetNames; + + if (mesh.morphTargetInfluences.length === targetNames.length) { + mesh.morphTargetDictionary = {}; + + for (var i = 0, il = targetNames.length; i < il; i++) { + mesh.morphTargetDictionary[targetNames[i]] = i; + } + } else { + console.warn('THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.'); + } + } + } + + function createPrimitiveKey(primitiveDef) { + var dracoExtension = + primitiveDef.extensions && primitiveDef.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]; + var geometryKey; + + if (dracoExtension) { + geometryKey = + 'draco:' + + dracoExtension.bufferView + + ':' + + dracoExtension.indices + + ':' + + createAttributesKey(dracoExtension.attributes); + } else { + geometryKey = + primitiveDef.indices + + ':' + + createAttributesKey(primitiveDef.attributes) + + ':' + + primitiveDef.mode; + } + + return geometryKey; + } + + function createAttributesKey(attributes) { + var attributesKey = ''; + + var keys = Object.keys(attributes).sort(); + + for (var i = 0, il = keys.length; i < il; i++) { + attributesKey += keys[i] + ':' + attributes[keys[i]] + ';'; + } + + return attributesKey; + } + + /* GLTF PARSER */ + + function GLTFParser(json, extensions, options) { + this.json = json || {}; + this.extensions = extensions || {}; + this.options = options || {}; + + // loader object cache + this.cache = new GLTFRegistry(); + + // BufferGeometry caching + this.primitiveCache = {}; + + this.textureLoader = new TextureLoader(this.options.manager); + this.textureLoader.setCrossOrigin(this.options.crossOrigin); + + this.fileLoader = new FileLoader(this.options.manager); + this.fileLoader.setResponseType('arraybuffer'); + + if (this.options.crossOrigin === 'use-credentials') { + this.fileLoader.setWithCredentials(true); + } + } + + GLTFParser.prototype.parse = function (onLoad, onError) { + var parser = this; + var json = this.json; + var extensions = this.extensions; + + // Clear the loader cache + this.cache.removeAll(); + + // Mark the special nodes/meshes in json for efficient parse + this.markDefs(); + + Promise.all([ + this.getDependencies('scene'), + this.getDependencies('animation'), + this.getDependencies('camera'), + ]) + .then(function (dependencies) { + var result = { + scene: dependencies[0][json.scene || 0], + scenes: dependencies[0], + animations: dependencies[1], + cameras: dependencies[2], + asset: json.asset, + parser: parser, + userData: {}, + }; + + addUnknownExtensionsToUserData(extensions, result, json); + + assignExtrasToUserData(result, json); + + onLoad(result); + }) + .catch(onError); + }; + + /** + * Marks the special nodes/meshes in json for efficient parse. + */ + GLTFParser.prototype.markDefs = function () { + var nodeDefs = this.json.nodes || []; + var skinDefs = this.json.skins || []; + var meshDefs = this.json.meshes || []; + + var meshReferences = {}; + var meshUses = {}; + + // Nothing in the node definition indicates whether it is a Bone or an + // Object3D. Use the skins' joint references to mark bones. + for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) { + var joints = skinDefs[skinIndex].joints; + + for (var i = 0, il = joints.length; i < il; i++) { + nodeDefs[joints[i]].isBone = true; + } + } + + // Meshes can (and should) be reused by multiple nodes in a glTF asset. To + // avoid having more than one Mesh with the same name, count + // references and rename instances below. + // + // Example: CesiumMilkTruck sample model reuses "Wheel" meshes. + for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) { + var nodeDef = nodeDefs[nodeIndex]; + + if (nodeDef.mesh !== undefined) { + if (meshReferences[nodeDef.mesh] === undefined) { + meshReferences[nodeDef.mesh] = meshUses[nodeDef.mesh] = 0; + } + + meshReferences[nodeDef.mesh]++; + + // Nothing in the mesh definition indicates whether it is + // a SkinnedMesh or Mesh. Use the node's mesh reference + // to mark SkinnedMesh if node has skin. + if (nodeDef.skin !== undefined) { + meshDefs[nodeDef.mesh].isSkinnedMesh = true; + } + } + } + + this.json.meshReferences = meshReferences; + this.json.meshUses = meshUses; + }; + + /** + * Requests the specified dependency asynchronously, with caching. + * @param {string} type + * @param {number} index + * @return {Promise} + */ + GLTFParser.prototype.getDependency = function (type, index) { + var cacheKey = type + ':' + index; + var dependency = this.cache.get(cacheKey); + + if (!dependency) { + switch (type) { + case 'scene': + dependency = this.loadScene(index); + break; + + case 'node': + dependency = this.loadNode(index); + break; + + case 'mesh': + dependency = this.loadMesh(index); + break; + + case 'accessor': + dependency = this.loadAccessor(index); + break; + + case 'bufferView': + dependency = this.loadBufferView(index); + break; + + case 'buffer': + dependency = this.loadBuffer(index); + break; + + case 'material': + dependency = this.loadMaterial(index); + break; + + case 'texture': + dependency = this.loadTexture(index); + break; + + case 'skin': + dependency = this.loadSkin(index); + break; + + case 'animation': + dependency = this.loadAnimation(index); + break; + + case 'camera': + dependency = this.loadCamera(index); + break; + + case 'light': + dependency = this.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].loadLight(index); + break; + + default: + throw new Error('Unknown type: ' + type); + } + + this.cache.add(cacheKey, dependency); + } + + return dependency; + }; + + /** + * Requests all dependencies of the specified type asynchronously, with caching. + * @param {string} type + * @return {Promise>} + */ + GLTFParser.prototype.getDependencies = function (type) { + var dependencies = this.cache.get(type); + + if (!dependencies) { + var parser = this; + var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || []; + + dependencies = Promise.all( + defs.map(function (def, index) { + return parser.getDependency(type, index); + }) + ); + + this.cache.add(type, dependencies); + } + + return dependencies; + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferIndex + * @return {Promise} + */ + GLTFParser.prototype.loadBuffer = function (bufferIndex) { + var bufferDef = this.json.buffers[bufferIndex]; + var loader = this.fileLoader; + + if (bufferDef.type && bufferDef.type !== 'arraybuffer') { + throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.'); + } + + // If present, GLB container is required to be the first buffer. + if (bufferDef.uri === undefined && bufferIndex === 0) { + return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body); + } + + var options = this.options; + + return new Promise(function (resolve, reject) { + loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () { + reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".')); + }); + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views + * @param {number} bufferViewIndex + * @return {Promise} + */ + GLTFParser.prototype.loadBufferView = function (bufferViewIndex) { + var bufferViewDef = this.json.bufferViews[bufferViewIndex]; + + return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) { + var byteLength = bufferViewDef.byteLength || 0; + var byteOffset = bufferViewDef.byteOffset || 0; + return buffer.slice(byteOffset, byteOffset + byteLength); + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors + * @param {number} accessorIndex + * @return {Promise} + */ + GLTFParser.prototype.loadAccessor = function (accessorIndex) { + var parser = this; + var json = this.json; + + var accessorDef = this.json.accessors[accessorIndex]; + + if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) { + // Ignore empty accessors, which may be used to declare runtime + // information about attributes coming from another source (e.g. Draco + // compression extension). + return Promise.resolve(null); + } + + var pendingBufferViews = []; + + if (accessorDef.bufferView !== undefined) { + pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView)); + } else { + pendingBufferViews.push(null); + } + + if (accessorDef.sparse !== undefined) { + pendingBufferViews.push( + this.getDependency('bufferView', accessorDef.sparse.indices.bufferView) + ); + pendingBufferViews.push( + this.getDependency('bufferView', accessorDef.sparse.values.bufferView) + ); + } + + return Promise.all(pendingBufferViews).then(function (bufferViews) { + var bufferView = bufferViews[0]; + + var itemSize = WEBGL_TYPE_SIZES[accessorDef.type]; + var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType]; + + // For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12. + var elementBytes = TypedArray.BYTES_PER_ELEMENT; + var itemBytes = elementBytes * itemSize; + var byteOffset = accessorDef.byteOffset || 0; + var byteStride = + accessorDef.bufferView !== undefined + ? json.bufferViews[accessorDef.bufferView].byteStride + : undefined; + var normalized = accessorDef.normalized === true; + var array, bufferAttribute; + + // The buffer is not interleaved if the stride is the item size in bytes. + if (byteStride && byteStride !== itemBytes) { + // Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer + // This makes sure that IBA.count reflects accessor.count properly + var ibSlice = Math.floor(byteOffset / byteStride); + var ibCacheKey = + 'InterleavedBuffer:' + + accessorDef.bufferView + + ':' + + accessorDef.componentType + + ':' + + ibSlice + + ':' + + accessorDef.count; + var ib = parser.cache.get(ibCacheKey); + + if (!ib) { + array = new TypedArray( + bufferView, + ibSlice * byteStride, + (accessorDef.count * byteStride) / elementBytes + ); + + // Integer parameters to IB/IBA are in array elements, not bytes. + ib = new InterleavedBuffer(array, byteStride / elementBytes); + + parser.cache.add(ibCacheKey, ib); + } + + bufferAttribute = new InterleavedBufferAttribute( + ib, + itemSize, + (byteOffset % byteStride) / elementBytes, + normalized + ); + } else { + if (bufferView === null) { + array = new TypedArray(accessorDef.count * itemSize); + } else { + array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize); + } + + bufferAttribute = new BufferAttribute(array, itemSize, normalized); + } + + // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors + if (accessorDef.sparse !== undefined) { + var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR; + var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType]; + + var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0; + var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0; + + var sparseIndices = new TypedArrayIndices( + bufferViews[1], + byteOffsetIndices, + accessorDef.sparse.count * itemSizeIndices + ); + var sparseValues = new TypedArray( + bufferViews[2], + byteOffsetValues, + accessorDef.sparse.count * itemSize + ); + + if (bufferView !== null) { + // Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes. + bufferAttribute = new BufferAttribute( + bufferAttribute.array.slice(), + bufferAttribute.itemSize, + bufferAttribute.normalized + ); + } + + for (var i = 0, il = sparseIndices.length; i < il; i++) { + var index = sparseIndices[i]; + + bufferAttribute.setX(index, sparseValues[i * itemSize]); + if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]); + if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]); + if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]); + if (itemSize >= 5) + throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.'); + } + } + + return bufferAttribute; + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures + * @param {number} textureIndex + * @return {Promise} + */ + GLTFParser.prototype.loadTexture = function (textureIndex) { + var parser = this; + var json = this.json; + var options = this.options; + var textureLoader = this.textureLoader; + + var URL = self.URL || self.webkitURL; + + var textureDef = json.textures[textureIndex]; + + var textureExtensions = textureDef.extensions || {}; + + var source; + + if (textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS]) { + source = json.images[textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS].source]; + } else { + source = json.images[textureDef.source]; + } + + var sourceURI = source.uri; + var isObjectURL = false; + + if (source.bufferView !== undefined) { + // Load binary image data from bufferView, if provided. + + sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) { + isObjectURL = true; + var blob = new Blob([bufferView], { type: source.mimeType }); + sourceURI = URL.createObjectURL(blob); + return sourceURI; + }); + } + + return Promise.resolve(sourceURI) + .then(function (sourceURI) { + // Load Texture resource. + + var loader = options.manager.getHandler(sourceURI); + + if (!loader) { + loader = textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS] + ? parser.extensions[EXTENSIONS.MSFT_TEXTURE_DDS].ddsLoader + : textureLoader; + } + + return new Promise(function (resolve, reject) { + loader.load(resolveURL(sourceURI, options.path), resolve, undefined, reject); + }); + }) + .then(function (texture) { + // Clean up resources and configure Texture. + + if (isObjectURL === true) { + URL.revokeObjectURL(sourceURI); + } + + texture.flipY = false; + + if (textureDef.name) texture.name = textureDef.name; + + // Ignore unknown mime types, like DDS files. + if (source.mimeType in MIME_TYPE_FORMATS) { + texture.format = MIME_TYPE_FORMATS[source.mimeType]; + } + + var samplers = json.samplers || {}; + var sampler = samplers[textureDef.sampler] || {}; + + texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || LinearFilter; + texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || LinearMipmapLinearFilter; + texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || RepeatWrapping; + texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || RepeatWrapping; + + return texture; + }); + }; + + /** + * Asynchronously assigns a texture to the given material parameters. + * @param {Object} materialParams + * @param {string} mapName + * @param {Object} mapDef + * @return {Promise} + */ + GLTFParser.prototype.assignTexture = function (materialParams, mapName, mapDef) { + var parser = this; + + return this.getDependency('texture', mapDef.index).then(function (texture) { + if (!texture.isCompressedTexture) { + switch (mapName) { + case 'aoMap': + case 'emissiveMap': + case 'metalnessMap': + case 'normalMap': + case 'roughnessMap': + texture.format = RGBFormat; + break; + } + } + + // Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured + // However, we will copy UV set 0 to UV set 1 on demand for aoMap + if ( + mapDef.texCoord !== undefined && + mapDef.texCoord != 0 && + !(mapName === 'aoMap' && mapDef.texCoord == 1) + ) { + console.warn( + 'THREE.GLTFLoader: Custom UV set ' + + mapDef.texCoord + + ' for texture ' + + mapName + + ' not yet supported.' + ); + } + + if (parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM]) { + var transform = + mapDef.extensions !== undefined + ? mapDef.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM] + : undefined; + + if (transform) { + texture = parser.extensions[EXTENSIONS.KHR_TEXTURE_TRANSFORM].extendTexture( + texture, + transform + ); + } + } + + materialParams[mapName] = texture; + }); + }; + + /** + * Assigns final material to a Mesh, Line, or Points instance. The instance + * already has a material (generated from the glTF material options alone) + * but reuse of the same glTF material may require multiple threejs materials + * to accomodate different primitive types, defines, etc. New materials will + * be created if necessary, and reused from a cache. + * @param {Object3D} mesh Mesh, Line, or Points instance. + */ + GLTFParser.prototype.assignFinalMaterial = function (mesh) { + var geometry = mesh.geometry; + var material = mesh.material; + + var useVertexTangents = geometry.attributes.tangent !== undefined; + var useVertexColors = geometry.attributes.color !== undefined; + var useFlatShading = geometry.attributes.normal === undefined; + var useSkinning = mesh.isSkinnedMesh === true; + var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0; + var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined; + + if (mesh.isPoints) { + var cacheKey = 'PointsMaterial:' + material.uuid; + + var pointsMaterial = this.cache.get(cacheKey); + + if (!pointsMaterial) { + pointsMaterial = new PointsMaterial(); + Material.prototype.copy.call(pointsMaterial, material); + pointsMaterial.color.copy(material.color); + pointsMaterial.map = material.map; + pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px + + this.cache.add(cacheKey, pointsMaterial); + } + + material = pointsMaterial; + } else if (mesh.isLine) { + var cacheKey = 'LineBasicMaterial:' + material.uuid; + + var lineMaterial = this.cache.get(cacheKey); + + if (!lineMaterial) { + lineMaterial = new LineBasicMaterial(); + Material.prototype.copy.call(lineMaterial, material); + lineMaterial.color.copy(material.color); + + this.cache.add(cacheKey, lineMaterial); + } + + material = lineMaterial; + } + + // Clone the material if it will be modified + if (useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets) { + var cacheKey = 'ClonedMaterial:' + material.uuid + ':'; + + if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:'; + if (useSkinning) cacheKey += 'skinning:'; + if (useVertexTangents) cacheKey += 'vertex-tangents:'; + if (useVertexColors) cacheKey += 'vertex-colors:'; + if (useFlatShading) cacheKey += 'flat-shading:'; + if (useMorphTargets) cacheKey += 'morph-targets:'; + if (useMorphNormals) cacheKey += 'morph-normals:'; + + var cachedMaterial = this.cache.get(cacheKey); + + if (!cachedMaterial) { + cachedMaterial = material.clone(); + + if (useSkinning) cachedMaterial.skinning = true; + if (useVertexTangents) cachedMaterial.vertexTangents = true; + if (useVertexColors) cachedMaterial.vertexColors = true; + if (useFlatShading) cachedMaterial.flatShading = true; + if (useMorphTargets) cachedMaterial.morphTargets = true; + if (useMorphNormals) cachedMaterial.morphNormals = true; + + this.cache.add(cacheKey, cachedMaterial); + } + + material = cachedMaterial; + } + + // workarounds for mesh and geometry + + if ( + material.aoMap && + geometry.attributes.uv2 === undefined && + geometry.attributes.uv !== undefined + ) { + geometry.setAttribute('uv2', geometry.attributes.uv); + } + + // https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995 + if (material.normalScale && !useVertexTangents) { + material.normalScale.y = -material.normalScale.y; + } + + if (material.clearcoatNormalScale && !useVertexTangents) { + material.clearcoatNormalScale.y = -material.clearcoatNormalScale.y; + } + + mesh.material = material; + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials + * @param {number} materialIndex + * @return {Promise} + */ + GLTFParser.prototype.loadMaterial = function (materialIndex) { + var parser = this; + var json = this.json; + var extensions = this.extensions; + var materialDef = json.materials[materialIndex]; + + var materialType; + var materialParams = {}; + var materialExtensions = materialDef.extensions || {}; + + var pending = []; + + if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) { + var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]; + materialType = sgExtension.getMaterialType(); + pending.push(sgExtension.extendParams(materialParams, materialDef, parser)); + } else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) { + var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT]; + materialType = kmuExtension.getMaterialType(); + pending.push(kmuExtension.extendParams(materialParams, materialDef, parser)); + } else { + // Specification: + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material + + materialType = MeshStandardMaterial; + + var metallicRoughness = materialDef.pbrMetallicRoughness || {}; + + materialParams.color = new Color(1.0, 1.0, 1.0); + materialParams.opacity = 1.0; + + if (Array.isArray(metallicRoughness.baseColorFactor)) { + var array = metallicRoughness.baseColorFactor; + + materialParams.color.fromArray(array); + materialParams.opacity = array[3]; + } + + if (metallicRoughness.baseColorTexture !== undefined) { + pending.push( + parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture) + ); + } + + materialParams.metalness = + metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0; + materialParams.roughness = + metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0; + + if (metallicRoughness.metallicRoughnessTexture !== undefined) { + pending.push( + parser.assignTexture( + materialParams, + 'metalnessMap', + metallicRoughness.metallicRoughnessTexture + ) + ); + pending.push( + parser.assignTexture( + materialParams, + 'roughnessMap', + metallicRoughness.metallicRoughnessTexture + ) + ); + } + } + + if (materialDef.doubleSided === true) { + materialParams.side = DoubleSide; + } + + var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE; + + if (alphaMode === ALPHA_MODES.BLEND) { + materialParams.transparent = true; + + // See: https://github.com/mrdoob/three.js/issues/17706 + materialParams.depthWrite = false; + } else { + materialParams.transparent = false; + + if (alphaMode === ALPHA_MODES.MASK) { + materialParams.alphaTest = + materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5; + } + } + + if (materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial) { + pending.push(parser.assignTexture(materialParams, 'normalMap', materialDef.normalTexture)); + + materialParams.normalScale = new Vector2(1, 1); + + if (materialDef.normalTexture.scale !== undefined) { + materialParams.normalScale.set( + materialDef.normalTexture.scale, + materialDef.normalTexture.scale + ); + } + } + + if (materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial) { + pending.push(parser.assignTexture(materialParams, 'aoMap', materialDef.occlusionTexture)); + + if (materialDef.occlusionTexture.strength !== undefined) { + materialParams.aoMapIntensity = materialDef.occlusionTexture.strength; + } + } + + if (materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial) { + materialParams.emissive = new Color().fromArray(materialDef.emissiveFactor); + } + + if (materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial) { + pending.push( + parser.assignTexture(materialParams, 'emissiveMap', materialDef.emissiveTexture) + ); + } + + if (materialExtensions[EXTENSIONS.KHR_MATERIALS_CLEARCOAT]) { + var clearcoatExtension = extensions[EXTENSIONS.KHR_MATERIALS_CLEARCOAT]; + materialType = clearcoatExtension.getMaterialType(); + pending.push( + clearcoatExtension.extendParams(materialParams, { extensions: materialExtensions }, parser) + ); + } + + return Promise.all(pending).then(function () { + var material; + + if (materialType === GLTFMeshStandardSGMaterial) { + material = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].createMaterial( + materialParams + ); + } else { + material = new materialType(materialParams); + } + + if (materialDef.name) material.name = materialDef.name; + + // baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding. + if (material.map) material.map.encoding = sRGBEncoding; + if (material.emissiveMap) material.emissiveMap.encoding = sRGBEncoding; + + assignExtrasToUserData(material, materialDef); + + if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef); + + return material; + }); + }; + + /** + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser + */ + function computeBounds(geometry, primitiveDef, parser) { + var attributes = primitiveDef.attributes; + + var box = new Box3(); + + if (attributes.POSITION !== undefined) { + var accessor = parser.json.accessors[attributes.POSITION]; + + var min = accessor.min; + var max = accessor.max; + + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + + if (min !== undefined && max !== undefined) { + box.set(new Vector3(min[0], min[1], min[2]), new Vector3(max[0], max[1], max[2])); + } else { + console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.'); + + return; + } + } else { + return; + } + + var targets = primitiveDef.targets; + + if (targets !== undefined) { + var maxDisplacement = new Vector3(); + var vector = new Vector3(); + + for (var i = 0, il = targets.length; i < il; i++) { + var target = targets[i]; + + if (target.POSITION !== undefined) { + var accessor = parser.json.accessors[target.POSITION]; + var min = accessor.min; + var max = accessor.max; + + // glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement. + + if (min !== undefined && max !== undefined) { + // we need to get max of absolute components because target weight is [-1,1] + vector.setX(Math.max(Math.abs(min[0]), Math.abs(max[0]))); + vector.setY(Math.max(Math.abs(min[1]), Math.abs(max[1]))); + vector.setZ(Math.max(Math.abs(min[2]), Math.abs(max[2]))); + + // Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative + // to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets + // are used to implement key-frame animations and as such only two are active at a time - this results in very large + // boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size. + maxDisplacement.max(vector); + } else { + console.warn('THREE.GLTFLoader: Missing min/max properties for accessor POSITION.'); + } + } + } + + // As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets. + box.expandByVector(maxDisplacement); + } + + geometry.boundingBox = box; + + var sphere = new Sphere(); + + box.getCenter(sphere.center); + sphere.radius = box.min.distanceTo(box.max) / 2; + + geometry.boundingSphere = sphere; + } + + /** + * @param {BufferGeometry} geometry + * @param {GLTF.Primitive} primitiveDef + * @param {GLTFParser} parser + * @return {Promise} + */ + function addPrimitiveAttributes(geometry, primitiveDef, parser) { + var attributes = primitiveDef.attributes; + + var pending = []; + + function assignAttributeAccessor(accessorIndex, attributeName) { + return parser.getDependency('accessor', accessorIndex).then(function (accessor) { + geometry.setAttribute(attributeName, accessor); + }); + } + + for (var gltfAttributeName in attributes) { + var threeAttributeName = ATTRIBUTES[gltfAttributeName] || gltfAttributeName.toLowerCase(); + + // Skip attributes already provided by e.g. Draco extension. + if (threeAttributeName in geometry.attributes) continue; + + pending.push(assignAttributeAccessor(attributes[gltfAttributeName], threeAttributeName)); + } + + if (primitiveDef.indices !== undefined && !geometry.index) { + var accessor = parser + .getDependency('accessor', primitiveDef.indices) + .then(function (accessor) { + geometry.setIndex(accessor); + }); + + pending.push(accessor); + } + + assignExtrasToUserData(geometry, primitiveDef); + + computeBounds(geometry, primitiveDef, parser); + + return Promise.all(pending).then(function () { + return primitiveDef.targets !== undefined + ? addMorphTargets(geometry, primitiveDef.targets, parser) + : geometry; + }); + } + + /** + * @param {BufferGeometry} geometry + * @param {Number} drawMode + * @return {BufferGeometry} + */ + function toTrianglesDrawMode(geometry, drawMode) { + var index = geometry.getIndex(); + + // generate index if not present + + if (index === null) { + var indices = []; + + var position = geometry.getAttribute('position'); + + if (position !== undefined) { + for (var i = 0; i < position.count; i++) { + indices.push(i); + } + + geometry.setIndex(indices); + index = geometry.getIndex(); + } else { + console.error( + 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' + ); + return geometry; + } + } + + // + + var numberOfTriangles = index.count - 2; + var newIndices = []; + + if (drawMode === TriangleFanDrawMode) { + // gl.TRIANGLE_FAN + + for (var i = 1; i <= numberOfTriangles; i++) { + newIndices.push(index.getX(0)); + newIndices.push(index.getX(i)); + newIndices.push(index.getX(i + 1)); + } + } else { + // gl.TRIANGLE_STRIP + + for (var i = 0; i < numberOfTriangles; i++) { + if (i % 2 === 0) { + newIndices.push(index.getX(i)); + newIndices.push(index.getX(i + 1)); + newIndices.push(index.getX(i + 2)); + } else { + newIndices.push(index.getX(i + 2)); + newIndices.push(index.getX(i + 1)); + newIndices.push(index.getX(i)); + } + } + } + + if (newIndices.length / 3 !== numberOfTriangles) { + console.error( + 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' + ); + } + + // build final geometry + + var newGeometry = geometry.clone(); + newGeometry.setIndex(newIndices); + + return newGeometry; + } + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry + * + * Creates BufferGeometries from primitives. + * + * @param {Array} primitives + * @return {Promise>} + */ + GLTFParser.prototype.loadGeometries = function (primitives) { + var parser = this; + var extensions = this.extensions; + var cache = this.primitiveCache; + + function createDracoPrimitive(primitive) { + return extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION] + .decodePrimitive(primitive, parser) + .then(function (geometry) { + return addPrimitiveAttributes(geometry, primitive, parser); + }); + } + + var pending = []; + + for (var i = 0, il = primitives.length; i < il; i++) { + var primitive = primitives[i]; + var cacheKey = createPrimitiveKey(primitive); + + // See if we've already created this geometry + var cached = cache[cacheKey]; + + if (cached) { + // Use the cached geometry if it exists + pending.push(cached.promise); + } else { + var geometryPromise; + + if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) { + // Use DRACO geometry if available + geometryPromise = createDracoPrimitive(primitive); + } else { + // Otherwise create a new geometry + geometryPromise = addPrimitiveAttributes(new BufferGeometry(), primitive, parser); + } + + // Cache this geometry + cache[cacheKey] = { primitive: primitive, promise: geometryPromise }; + + pending.push(geometryPromise); + } + } + + return Promise.all(pending); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes + * @param {number} meshIndex + * @return {Promise} + */ + GLTFParser.prototype.loadMesh = function (meshIndex) { + var parser = this; + var json = this.json; + + var meshDef = json.meshes[meshIndex]; + var primitives = meshDef.primitives; + + var pending = []; + + for (var i = 0, il = primitives.length; i < il; i++) { + var material = + primitives[i].material === undefined + ? createDefaultMaterial(this.cache) + : this.getDependency('material', primitives[i].material); + + pending.push(material); + } + + pending.push(parser.loadGeometries(primitives)); + + return Promise.all(pending).then(function (results) { + var materials = results.slice(0, results.length - 1); + var geometries = results[results.length - 1]; + + var meshes = []; + + for (var i = 0, il = geometries.length; i < il; i++) { + var geometry = geometries[i]; + var primitive = primitives[i]; + + // 1. create Mesh + + var mesh; + + var material = materials[i]; + + if ( + primitive.mode === WEBGL_CONSTANTS.TRIANGLES || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP || + primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN || + primitive.mode === undefined + ) { + // .isSkinnedMesh isn't in glTF spec. See .markDefs() + mesh = + meshDef.isSkinnedMesh === true + ? new SkinnedMesh(geometry, material) + : new Mesh(geometry, material); + + if (mesh.isSkinnedMesh === true && !mesh.geometry.attributes.skinWeight.normalized) { + // we normalize floating point skin weight array to fix malformed assets (see #15319) + // it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs + mesh.normalizeSkinWeights(); + } + + if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) { + mesh.geometry = toTrianglesDrawMode(mesh.geometry, TriangleStripDrawMode); + } else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) { + mesh.geometry = toTrianglesDrawMode(mesh.geometry, TriangleFanDrawMode); + } + } else if (primitive.mode === WEBGL_CONSTANTS.LINES) { + mesh = new LineSegments(geometry, material); + } else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) { + mesh = new Line(geometry, material); + } else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) { + mesh = new LineLoop(geometry, material); + } else if (primitive.mode === WEBGL_CONSTANTS.POINTS) { + mesh = new Points(geometry, material); + } else { + throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode); + } + + if (Object.keys(mesh.geometry.morphAttributes).length > 0) { + updateMorphTargets(mesh, meshDef); + } + + mesh.name = meshDef.name || 'mesh_' + meshIndex; + + if (geometries.length > 1) mesh.name += '_' + i; + + assignExtrasToUserData(mesh, meshDef); + + parser.assignFinalMaterial(mesh); + + meshes.push(mesh); + } + + if (meshes.length === 1) { + return meshes[0]; + } + + var group = new Group(); + + for (var i = 0, il = meshes.length; i < il; i++) { + group.add(meshes[i]); + } + + return group; + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras + * @param {number} cameraIndex + * @return {Promise} + */ + GLTFParser.prototype.loadCamera = function (cameraIndex) { + var camera; + var cameraDef = this.json.cameras[cameraIndex]; + var params = cameraDef[cameraDef.type]; + + if (!params) { + console.warn('THREE.GLTFLoader: Missing camera parameters.'); + return; + } + + if (cameraDef.type === 'perspective') { + camera = new PerspectiveCamera( + MathUtils.radToDeg(params.yfov), + params.aspectRatio || 1, + params.znear || 1, + params.zfar || 2e6 + ); + } else if (cameraDef.type === 'orthographic') { + camera = new OrthographicCamera( + -params.xmag, + params.xmag, + params.ymag, + -params.ymag, + params.znear, + params.zfar + ); + } + + if (cameraDef.name) camera.name = cameraDef.name; + + assignExtrasToUserData(camera, cameraDef); + + return Promise.resolve(camera); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins + * @param {number} skinIndex + * @return {Promise} + */ + GLTFParser.prototype.loadSkin = function (skinIndex) { + var skinDef = this.json.skins[skinIndex]; + + var skinEntry = { joints: skinDef.joints }; + + if (skinDef.inverseBindMatrices === undefined) { + return Promise.resolve(skinEntry); + } + + return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) { + skinEntry.inverseBindMatrices = accessor; + + return skinEntry; + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations + * @param {number} animationIndex + * @return {Promise} + */ + GLTFParser.prototype.loadAnimation = function (animationIndex) { + var json = this.json; + + var animationDef = json.animations[animationIndex]; + + var pendingNodes = []; + var pendingInputAccessors = []; + var pendingOutputAccessors = []; + var pendingSamplers = []; + var pendingTargets = []; + + for (var i = 0, il = animationDef.channels.length; i < il; i++) { + var channel = animationDef.channels[i]; + var sampler = animationDef.samplers[channel.sampler]; + var target = channel.target; + var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated. + var input = + animationDef.parameters !== undefined + ? animationDef.parameters[sampler.input] + : sampler.input; + var output = + animationDef.parameters !== undefined + ? animationDef.parameters[sampler.output] + : sampler.output; + + pendingNodes.push(this.getDependency('node', name)); + pendingInputAccessors.push(this.getDependency('accessor', input)); + pendingOutputAccessors.push(this.getDependency('accessor', output)); + pendingSamplers.push(sampler); + pendingTargets.push(target); + } + + return Promise.all([ + Promise.all(pendingNodes), + Promise.all(pendingInputAccessors), + Promise.all(pendingOutputAccessors), + Promise.all(pendingSamplers), + Promise.all(pendingTargets), + ]).then(function (dependencies) { + var nodes = dependencies[0]; + var inputAccessors = dependencies[1]; + var outputAccessors = dependencies[2]; + var samplers = dependencies[3]; + var targets = dependencies[4]; + + var tracks = []; + + for (var i = 0, il = nodes.length; i < il; i++) { + var node = nodes[i]; + var inputAccessor = inputAccessors[i]; + var outputAccessor = outputAccessors[i]; + var sampler = samplers[i]; + var target = targets[i]; + + if (node === undefined) continue; + + node.updateMatrix(); + node.matrixAutoUpdate = true; + + var TypedKeyframeTrack; + + switch (PATH_PROPERTIES[target.path]) { + case PATH_PROPERTIES.weights: + TypedKeyframeTrack = NumberKeyframeTrack; + break; + + case PATH_PROPERTIES.rotation: + TypedKeyframeTrack = QuaternionKeyframeTrack; + break; + + case PATH_PROPERTIES.position: + case PATH_PROPERTIES.scale: + default: + TypedKeyframeTrack = VectorKeyframeTrack; + break; + } + + var targetName = node.name ? node.name : node.uuid; + + var interpolation = + sampler.interpolation !== undefined + ? INTERPOLATION[sampler.interpolation] + : InterpolateLinear; + + var targetNames = []; + + if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) { + // Node may be a Group (glTF mesh with several primitives) or a Mesh. + node.traverse(function (object) { + if (object.isMesh === true && object.morphTargetInfluences) { + targetNames.push(object.name ? object.name : object.uuid); + } + }); + } else { + targetNames.push(targetName); + } + + var outputArray = outputAccessor.array; + + if (outputAccessor.normalized) { + var scale; + + if (outputArray.constructor === Int8Array) { + scale = 1 / 127; + } else if (outputArray.constructor === Uint8Array) { + scale = 1 / 255; + } else if (outputArray.constructor == Int16Array) { + scale = 1 / 32767; + } else if (outputArray.constructor === Uint16Array) { + scale = 1 / 65535; + } else { + throw new Error('THREE.GLTFLoader: Unsupported output accessor component type.'); + } + + var scaled = new Float32Array(outputArray.length); + + for (var j = 0, jl = outputArray.length; j < jl; j++) { + scaled[j] = outputArray[j] * scale; + } + + outputArray = scaled; + } + + for (var j = 0, jl = targetNames.length; j < jl; j++) { + var track = new TypedKeyframeTrack( + targetNames[j] + '.' + PATH_PROPERTIES[target.path], + inputAccessor.array, + outputArray, + interpolation + ); + + // Override interpolation with custom factory method. + if (sampler.interpolation === 'CUBICSPLINE') { + track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline(result) { + // A CUBICSPLINE keyframe in glTF has three output values for each input value, + // representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize() + // must be divided by three to get the interpolant's sampleSize argument. + + return new GLTFCubicSplineInterpolant( + this.times, + this.values, + this.getValueSize() / 3, + result + ); + }; + + // Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants. + track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true; + } + + tracks.push(track); + } + } + + var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex; + + return new AnimationClip(name, undefined, tracks); + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy + * @param {number} nodeIndex + * @return {Promise} + */ + GLTFParser.prototype.loadNode = function (nodeIndex) { + var json = this.json; + var extensions = this.extensions; + var parser = this; + + var meshReferences = json.meshReferences; + var meshUses = json.meshUses; + + var nodeDef = json.nodes[nodeIndex]; + + return (function () { + var pending = []; + + if (nodeDef.mesh !== undefined) { + pending.push( + parser.getDependency('mesh', nodeDef.mesh).then(function (mesh) { + var node; + + if (meshReferences[nodeDef.mesh] > 1) { + var instanceNum = meshUses[nodeDef.mesh]++; + + node = mesh.clone(); + node.name += '_instance_' + instanceNum; + } else { + node = mesh; + } + + // if weights are provided on the node, override weights on the mesh. + if (nodeDef.weights !== undefined) { + node.traverse(function (o) { + if (!o.isMesh) return; + + for (var i = 0, il = nodeDef.weights.length; i < il; i++) { + o.morphTargetInfluences[i] = nodeDef.weights[i]; + } + }); + } + + return node; + }) + ); + } + + if (nodeDef.camera !== undefined) { + pending.push(parser.getDependency('camera', nodeDef.camera)); + } + + if ( + nodeDef.extensions && + nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL] && + nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light !== undefined + ) { + pending.push( + parser.getDependency('light', nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light) + ); + } + + return Promise.all(pending); + })().then(function (objects) { + var node; + + // .isBone isn't in glTF spec. See .markDefs + if (nodeDef.isBone === true) { + node = new Bone(); + } else if (objects.length > 1) { + node = new Group(); + } else if (objects.length === 1) { + node = objects[0]; + } else { + node = new Object3D(); + } + + if (node !== objects[0]) { + for (var i = 0, il = objects.length; i < il; i++) { + node.add(objects[i]); + } + } + + if (nodeDef.name) { + node.userData.name = nodeDef.name; + node.name = PropertyBinding.sanitizeNodeName(nodeDef.name); + } + + assignExtrasToUserData(node, nodeDef); + + if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef); + + if (nodeDef.matrix !== undefined) { + var matrix = new Matrix4(); + matrix.fromArray(nodeDef.matrix); + node.applyMatrix4(matrix); + } else { + if (nodeDef.translation !== undefined) { + node.position.fromArray(nodeDef.translation); + } + + if (nodeDef.rotation !== undefined) { + node.quaternion.fromArray(nodeDef.rotation); + } + + if (nodeDef.scale !== undefined) { + node.scale.fromArray(nodeDef.scale); + } + } + + return node; + }); + }; + + /** + * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes + * @param {number} sceneIndex + * @return {Promise} + */ + GLTFParser.prototype.loadScene = (function () { + // scene node hierachy builder + + function buildNodeHierachy(nodeId, parentObject, json, parser) { + var nodeDef = json.nodes[nodeId]; + + return parser + .getDependency('node', nodeId) + .then(function (node) { + if (nodeDef.skin === undefined) return node; + + // build skeleton here as well + + var skinEntry; + + return parser + .getDependency('skin', nodeDef.skin) + .then(function (skin) { + skinEntry = skin; + + var pendingJoints = []; + + for (var i = 0, il = skinEntry.joints.length; i < il; i++) { + pendingJoints.push(parser.getDependency('node', skinEntry.joints[i])); + } + + return Promise.all(pendingJoints); + }) + .then(function (jointNodes) { + node.traverse(function (mesh) { + if (!mesh.isMesh) return; + + var bones = []; + var boneInverses = []; + + for (var j = 0, jl = jointNodes.length; j < jl; j++) { + var jointNode = jointNodes[j]; + + if (jointNode) { + bones.push(jointNode); + + var mat = new Matrix4(); + + if (skinEntry.inverseBindMatrices !== undefined) { + mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16); + } + + boneInverses.push(mat); + } else { + console.warn( + 'THREE.GLTFLoader: Joint "%s" could not be found.', + skinEntry.joints[j] + ); + } + } + + mesh.bind(new Skeleton(bones, boneInverses), mesh.matrixWorld); + }); + + return node; + }); + }) + .then(function (node) { + // build node hierachy + + parentObject.add(node); + + var pending = []; + + if (nodeDef.children) { + var children = nodeDef.children; + + for (var i = 0, il = children.length; i < il; i++) { + var child = children[i]; + pending.push(buildNodeHierachy(child, node, json, parser)); + } + } + + return Promise.all(pending); + }); + } + + return function loadScene(sceneIndex) { + var json = this.json; + var extensions = this.extensions; + var sceneDef = this.json.scenes[sceneIndex]; + var parser = this; + + // Loader returns Group, not Scene. + // See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172 + var scene = new Group(); + if (sceneDef.name) scene.name = sceneDef.name; + + assignExtrasToUserData(scene, sceneDef); + + if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef); + + var nodeIds = sceneDef.nodes || []; + + var pending = []; + + for (var i = 0, il = nodeIds.length; i < il; i++) { + pending.push(buildNodeHierachy(nodeIds[i], scene, json, parser)); + } + + return Promise.all(pending).then(function () { + return scene; + }); + }; + })(); + + return GLTFLoader; +})(); + +export { GLTFLoader };