diff --git a/src/assets/avatars/BotDefault_Avatar.glb b/src/assets/avatars/BotDefault_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..fea36f79201b45bc1cd4379c37b53f2de5557233
Binary files /dev/null and b/src/assets/avatars/BotDefault_Avatar.glb differ
diff --git a/src/assets/avatars/BotDefault_Avatar_Unlit.glb b/src/assets/avatars/BotDefault_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..6e4b52771581f36009f7c642666c90b7ca9a845d
Binary files /dev/null and b/src/assets/avatars/BotDefault_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/Bot_SkinnedWithAnim.glb b/src/assets/avatars/Bot_SkinnedWithAnim.glb
deleted file mode 100644
index 192bbed19788cb4c5cb71a3f23da428173766948..0000000000000000000000000000000000000000
Binary files a/src/assets/avatars/Bot_SkinnedWithAnim.glb and /dev/null differ
diff --git a/src/assets/environments/CliffVista_mesh.glb b/src/assets/environments/CliffVista_mesh.glb
index 0bfecdc65832c99b903d4f3bc40bf207bf3a867f..fbd377865a799bb52240155dbe34bf49d699bd97 100644
Binary files a/src/assets/environments/CliffVista_mesh.glb and b/src/assets/environments/CliffVista_mesh.glb differ
diff --git a/src/assets/environments/MeetingSpace1_mesh.glb b/src/assets/environments/MeetingSpace1_mesh.glb
index 431a21b5a3e034e3866fc6a6e480580375e35c46..e184d5800b35daa15754ba88db845144bcd53309 100644
Binary files a/src/assets/environments/MeetingSpace1_mesh.glb and b/src/assets/environments/MeetingSpace1_mesh.glb differ
diff --git a/src/assets/environments/OutdoorFacade_mesh.glb b/src/assets/environments/OutdoorFacade_mesh.glb
index 4a24a0667ab9f13a57b752d8b5ab1b1bbcd6599d..05ceb7b09786450daf59833ca10c20fa214ac362 100644
Binary files a/src/assets/environments/OutdoorFacade_mesh.glb and b/src/assets/environments/OutdoorFacade_mesh.glb differ
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index ee59e3a24ead224a4095c69c70d2ea0da1c9cbd5..8482dd7a78a14e4da287f0b0ad2de389f624821c 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -121,7 +121,18 @@ AFRAME.registerElement("a-gltf-entity", {
         // If the src attribute is a selector, get the url from the asset item.
         if (src.charAt(0) === "#") {
           const assetEl = document.getElementById(src.substring(1));
-          src = assetEl.getAttribute("src");
+
+          const fallbackSrc = assetEl.getAttribute("src");
+          const highSrc = assetEl.getAttribute("high-src");
+          const lowSrc = assetEl.getAttribute("low-src");
+
+          if (highSrc && AFRAME.quality === "high") {
+            src = highSrc;
+          } else if (lowSrc && AFRAME.quality === "low") {
+            src = lowSrc;
+          } else {
+            src = fallbackSrc;
+          }
         }
 
         const onLoad = gltfModel => {
diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js
new file mode 100644
index 0000000000000000000000000000000000000000..103a2d679f1b08c56c7355b5bef1bf226b601cd0
--- /dev/null
+++ b/src/elements/a-progressive-asset.js
@@ -0,0 +1,57 @@
+AFRAME.registerElement("a-progressive-asset", {
+  prototype: Object.create(AFRAME.ANode.prototype, {
+    createdCallback: {
+      value() {
+        this.data = null;
+        this.isAssetItem = true;
+        this.fileLoader = document.querySelector("a-assets").fileLoader;
+      }
+    },
+
+    attachedCallback: {
+      value() {
+        const self = this;
+        const fallbackSrc = this.getAttribute("src");
+        const highSrc = this.getAttribute("high-src");
+        const lowSrc = this.getAttribute("low-src");
+
+        let src = fallbackSrc;
+
+        if (AFRAME.quality === "high") {
+          src = highSrc;
+        } else if (AFRAME.quality === "low") {
+          src = lowSrc;
+        }
+
+        this.fileLoader.setResponseType(this.getAttribute("response-type") || inferResponseType(src));
+        this.fileLoader.load(
+          src,
+          function handleOnLoad(response) {
+            self.data = response;
+            /*
+            Workaround for a Chrome bug. If another XHR is sent to the same url before the
+            previous one closes, the second request never finishes.
+            setTimeout finishes the first request and lets the logic triggered by load open
+            subsequent requests.
+            setTimeout can be removed once the fix for the bug below ships:
+            https://bugs.chromium.org/p/chromium/issues/detail?id=633696&q=component%3ABlink%3ENetwork%3EXHR%20&colspec=ID%20Pri%20M%20Stars%20ReleaseBlock%20Component%20Status%20Owner%20Summary%20OS%20Modified
+          */
+            setTimeout(function load() {
+              AFRAME.ANode.prototype.load.call(self);
+            });
+          },
+          function handleOnProgress(xhr) {
+            self.emit("progress", {
+              loadedBytes: xhr.loaded,
+              totalBytes: xhr.total,
+              xhr: xhr
+            });
+          },
+          function handleOnError(xhr) {
+            self.emit("error", { xhr: xhr });
+          }
+        );
+      }
+    }
+  })
+});
diff --git a/src/room.html b/src/room.html
index 703874897968615d777c03353392ae90a8fff6a4..cb0d1520a342bf4ee40f6df829b6dc298cbe3525 100644
--- a/src/room.html
+++ b/src/room.html
@@ -26,7 +26,14 @@
         light="defaultLightsEnabled: false">
 
         <a-assets>
-            <a-asset-item id="bot-skinned-mesh" response-type="arraybuffer" src="./assets/avatars/Bot_SkinnedWithAnim.glb"></a-asset-item>
+            <a-progressive-asset
+                id="bot-skinned-mesh"
+                response-type="arraybuffer"
+                src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
+                high-src="./assets/avatars/BotDefault_Avatar.glb"
+                low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
+            ></a-progressive-asset>
+            
             <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item>
 
             <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item>
diff --git a/src/room.js b/src/room.js
index c3ed3d28e188cdf100e4ff4fb09b556a8e4efcd9..aad9727634daaf3d22152c6bb89fd7de03cc10dc 100644
--- a/src/room.js
+++ b/src/room.js
@@ -43,6 +43,18 @@ import "./systems/personal-space-bubble";
 
 import "./elements/a-gltf-entity";
 
+const qs = queryString.parse(location.search);
+const isMobile = AFRAME.utils.device.isMobile();
+
+if (qs.quality) {
+  console.log(qs.quality);
+  AFRAME.quality = qs.quality;
+} else {
+  AFRAME.quality = isMobile ? "low" : "high";
+}
+
+import "./elements/a-progressive-asset";
+
 import { promptForName, getCookie, parseJwt } from "./utils/identity";
 import registerNetworkSchemas from "./network-schemas";
 import { inGameActions, config } from "./input-mappings";
@@ -85,7 +97,6 @@ async function shareMedia(audio, video) {
 }
 
 async function onSceneLoad() {
-  const qs = queryString.parse(location.search);
   const scene = document.querySelector("a-scene");
 
   scene.setAttribute("networked-scene", {
@@ -97,7 +108,7 @@ async function onSceneLoad() {
     scene.setAttribute("stats", true);
   }
 
-  if (AFRAME.utils.device.isMobile() || qs.gamepad) {
+  if (isMobile || qs.mobile) {
     const playerRig = document.querySelector("#player-rig");
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js
index 90afbfd648c059f98aaa547c31fee06dbe100735..feac2a0af4b42ce35c4b6ce4a6074d28895ae9d5 100644
--- a/src/vendor/GLTFLoader.js
+++ b/src/vendor/GLTFLoader.js
@@ -1,3 +1,5 @@
+// https://github.com/mrdoob/three.js/blob/1e943ba79196737bc8505522e928595687c09425/examples/js/loaders/GLTFLoader.js
+
 /**
  * @author Rich Tibbett / https://github.com/richtr
  * @author mrdoob / http://mrdoob.com/
@@ -11,6 +13,7 @@ THREE.GLTFLoader = ( function () {
 	function GLTFLoader( manager ) {
 
 		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
+		this.dracoLoader = null;
 
 	}
 
@@ -24,7 +27,7 @@ THREE.GLTFLoader = ( function () {
 
 			var scope = this;
 
-			var path = this.path !== undefined ? this.path : THREE.Loader.prototype.extractUrlBase( url );
+			var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url );
 
 			var loader = new THREE.FileLoader( scope.manager );
 
@@ -57,12 +60,21 @@ THREE.GLTFLoader = ( function () {
 		setCrossOrigin: function ( value ) {
 
 			this.crossOrigin = value;
+			return this;
 
 		},
 
 		setPath: function ( value ) {
 
 			this.path = value;
+			return this;
+
+		},
+
+		setDRACOLoader: function ( dracoLoader ) {
+
+			this.dracoLoader = dracoLoader;
+			return this;
 
 		},
 
@@ -77,7 +89,7 @@ THREE.GLTFLoader = ( function () {
 
 			} else {
 
-				var magic = convertUint8ArrayToString( new Uint8Array( data, 0, 4 ) );
+				var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
 
 				if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {
 
@@ -96,7 +108,7 @@ THREE.GLTFLoader = ( function () {
 
 				} else {
 
-					content = convertUint8ArrayToString( new Uint8Array( data ) );
+					content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) );
 
 				}
 
@@ -106,7 +118,7 @@ THREE.GLTFLoader = ( function () {
 
 			if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {
 
-				if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) );
+				if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) );
 				return;
 
 			}
@@ -119,9 +131,9 @@ THREE.GLTFLoader = ( function () {
 
 				}
 
-				if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_COMMON ) >= 0 ) {
+				if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_MATERIALS_UNLIT ) >= 0 ) {
 
-					extensions[ EXTENSIONS.KHR_MATERIALS_COMMON ] = new GLTFMaterialsCommonExtension( json );
+					extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] = new GLTFMaterialsUnlitExtension( json );
 
 				}
 
@@ -131,6 +143,12 @@ THREE.GLTFLoader = ( function () {
 
 				}
 
+				if ( json.extensionsUsed.indexOf( EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ) >= 0 ) {
+
+					extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] = new GLTFDracoMeshCompressionExtension( this.dracoLoader );
+
+				}
+
 			}
 
 			console.time( 'GLTFLoader' );
@@ -143,7 +161,7 @@ THREE.GLTFLoader = ( function () {
 
 			} );
 
-			parser.parse( function ( scene, scenes, cameras, animations ) {
+			parser.parse( function ( scene, scenes, cameras, animations, asset ) {
 
 				console.timeEnd( 'GLTFLoader' );
 
@@ -151,7 +169,8 @@ THREE.GLTFLoader = ( function () {
 					scene: scene,
 					scenes: scenes,
 					cameras: cameras,
-					animations: animations
+					animations: animations,
+					asset: asset
 				};
 
 				onLoad( glTF );
@@ -204,10 +223,10 @@ THREE.GLTFLoader = ( function () {
 
 	var EXTENSIONS = {
 		KHR_BINARY_GLTF: 'KHR_binary_glTF',
+		KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
 		KHR_LIGHTS: 'KHR_lights',
-		KHR_MATERIALS_COMMON: 'KHR_materials_common',
 		KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
-		KHR_MATERIALS_CMN_CONSTANT: 'KHR_materials_cmnConstant'
+		KHR_MATERIALS_UNLIT: 'KHR_materials_unlit'
 	};
 
 	/**
@@ -295,99 +314,47 @@ THREE.GLTFLoader = ( function () {
 	}
 
 	/**
-	 * Common Materials Extension
+	 * Unlit Materials Extension (pending)
 	 *
-	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_common
+	 * PR: https://github.com/KhronosGroup/glTF/pull/1163
 	 */
-	function GLTFMaterialsCommonExtension( json ) {
+	function GLTFMaterialsUnlitExtension( json ) {
 
-		this.name = EXTENSIONS.KHR_MATERIALS_COMMON;
+		this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;
 
 	}
 
-	GLTFMaterialsCommonExtension.prototype.getMaterialType = function ( material ) {
-
-		var khrMaterial = material.extensions[ this.name ];
-
-		switch ( khrMaterial.type ) {
-
-			case 'commonBlinn' :
-			case 'commonPhong' :
-				return THREE.MeshPhongMaterial;
+	GLTFMaterialsUnlitExtension.prototype.getMaterialType = function ( material ) {
 
-			case 'commonLambert' :
-				return THREE.MeshLambertMaterial;
-
-			case 'commonConstant' :
-			default :
-				return THREE.MeshBasicMaterial;
-
-		}
+		return THREE.MeshBasicMaterial;
 
 	};
 
-	GLTFMaterialsCommonExtension.prototype.extendParams = function ( materialParams, material, parser ) {
-
-		var khrMaterial = material.extensions[ this.name ];
+	GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, material, parser ) {
 
 		var pending = [];
 
-		var keys = [];
-
-		// TODO: Currently ignored: 'ambientFactor', 'ambientTexture'
-		switch ( khrMaterial.type ) {
-
-			case 'commonBlinn' :
-			case 'commonPhong' :
-				keys.push( 'diffuseFactor', 'diffuseTexture', 'specularFactor', 'specularTexture', 'shininessFactor' );
-				break;
-
-			case 'commonLambert' :
-				keys.push( 'diffuseFactor', 'diffuseTexture' );
-				break;
-
-			case 'commonConstant' :
-			default :
-				break;
-
-		}
-
-		var materialValues = {};
-
-		keys.forEach( function ( v ) {
-
-			if ( khrMaterial[ v ] !== undefined ) materialValues[ v ] = khrMaterial[ v ];
-
-		} );
-
-		if ( materialValues.diffuseFactor !== undefined ) {
-
-			materialParams.color = new THREE.Color().fromArray( materialValues.diffuseFactor );
-			materialParams.opacity = materialValues.diffuseFactor[ 3 ];
-
-		}
-
-		if ( materialValues.diffuseTexture !== undefined ) {
+		materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
+		materialParams.opacity = 1.0;
 
-			pending.push( parser.assignTexture( materialParams, 'map', materialValues.diffuseTexture.index ) );
+		var metallicRoughness = material.pbrMetallicRoughness;
 
-		}
-
-		if ( materialValues.specularFactor !== undefined ) {
+		if ( metallicRoughness ) {
 
-			materialParams.specular = new THREE.Color().fromArray( materialValues.specularFactor );
+			if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {
 
-		}
+				var array = metallicRoughness.baseColorFactor;
 
-		if ( materialValues.specularTexture !== undefined ) {
+				materialParams.color.fromArray( array );
+				materialParams.opacity = array[ 3 ];
 
-			pending.push( parser.assignTexture( materialParams, 'specularMap', materialValues.specularTexture.index ) );
+			}
 
-		}
+			if ( metallicRoughness.baseColorTexture !== undefined ) {
 
-		if ( materialValues.shininessFactor !== undefined ) {
+				pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) );
 
-			materialParams.shininess = materialValues.shininessFactor;
+			}
 
 		}
 
@@ -411,7 +378,7 @@ THREE.GLTFLoader = ( function () {
 		var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );
 
 		this.header = {
-			magic: convertUint8ArrayToString( new Uint8Array( data.slice( 0, 4 ) ) ),
+			magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
 			version: headerView.getUint32( 4, true ),
 			length: headerView.getUint32( 8, true )
 		};
@@ -422,7 +389,7 @@ THREE.GLTFLoader = ( function () {
 
 		} else if ( this.header.version < 2.0 ) {
 
-			throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use GLTFLoader instead.' );
+			throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' );
 
 		}
 
@@ -440,7 +407,7 @@ THREE.GLTFLoader = ( function () {
 			if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {
 
 				var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
-				this.content = convertUint8ArrayToString( contentArray );
+				this.content = THREE.LoaderUtils.decodeText( contentArray );
 
 			} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {
 
@@ -463,10 +430,55 @@ THREE.GLTFLoader = ( function () {
 
 	}
 
+	/**
+	 * DRACO Mesh Compression Extension
+	 *
+	 * Specification: https://github.com/KhronosGroup/glTF/pull/874
+	 */
+	function GLTFDracoMeshCompressionExtension ( dracoLoader ) {
+
+		if ( ! dracoLoader ) {
+
+			throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' );
+
+		}
+
+		this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
+		this.dracoLoader = dracoLoader;
+
+	}
+
+	GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) {
+
+		var dracoLoader = this.dracoLoader;
+		var bufferViewIndex = primitive.extensions[ this.name ].bufferView;
+		var gltfAttributeMap = primitive.extensions[ this.name ].attributes;
+		var threeAttributeMap = {};
+
+		for ( var attributeName in gltfAttributeMap ) {
+
+			if ( !( attributeName in ATTRIBUTES ) ) continue;
+
+			threeAttributeMap[ ATTRIBUTES[ attributeName ] ] = gltfAttributeMap[ attributeName ];
+
+		}
+
+		return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) {
+
+			return new Promise( function ( resolve ) {
+
+				dracoLoader.decodeDracoFile( bufferView, resolve, threeAttributeMap );
+
+			} );
+
+		} );
+
+	};
+
 	/**
 	 * Specular-Glossiness Extension
 	 *
-	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_materials_pbrSpecularGlossiness
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
 	 */
 	function GLTFMaterialsPbrSpecularGlossinessExtension() {
 
@@ -530,6 +542,7 @@ THREE.GLTFLoader = ( function () {
 					'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'
@@ -698,7 +711,7 @@ THREE.GLTFLoader = ( function () {
 
 				var params = this.specularGlossinessParams;
 
-				for ( var i = 0; i < params.length; i ++ ) {
+				for ( var i = 0, il = params.length; i < il; i ++ ) {
 
 					target[ params[ i ] ] = source[ params[ i ] ];
 
@@ -711,6 +724,12 @@ THREE.GLTFLoader = ( function () {
 			// Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer.
 			refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) {
 
+				if ( material.isGLTFSpecularGlossinessMaterial !== true ) {
+
+					return;
+
+				}
+
 				var uniforms = material.uniforms;
 				var defines = material.defines;
 
@@ -855,6 +874,61 @@ THREE.GLTFLoader = ( function () {
 
 	}
 
+	/*********************************/
+	/********** 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 ) {
+
+		THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+	};
+
+	GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype );
+	GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant;
+
+	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 s0 = 2 * ppp - 3 * pp + 1;
+		var s1 = ppp - 2 * pp + p;
+		var s2 = - 2 * ppp + 3 * pp;
+		var s3 = ppp - pp;
+
+		// 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 ************/
 	/*********************************/
@@ -985,6 +1059,22 @@ THREE.GLTFLoader = ( function () {
 		'MAT4': 16
 	};
 
+	var ATTRIBUTES = {
+		POSITION: 'position',
+		NORMAL: 'normal',
+		TEXCOORD_0: 'uv',
+		TEXCOORD0: 'uv', // deprecated
+		TEXCOORD: 'uv', // deprecated
+		TEXCOORD_1: 'uv2',
+		COLOR_0: 'color',
+		COLOR0: 'color', // deprecated
+		COLOR: 'color', // deprecated
+		WEIGHTS_0: 'skinWeight',
+		WEIGHT: 'skinWeight', // deprecated
+		JOINTS_0: 'skinIndex',
+		JOINT: 'skinIndex' // deprecated
+	}
+
 	var PATH_PROPERTIES = {
 		scale: 'scale',
 		translation: 'position',
@@ -993,8 +1083,10 @@ THREE.GLTFLoader = ( function () {
 	};
 
 	var INTERPOLATION = {
-		CATMULLROMSPLINE: THREE.InterpolateSmooth,
-		CUBICSPLINE: THREE.InterpolateSmooth,
+		CUBICSPLINE: THREE.InterpolateSmooth, // We use custom interpolation GLTFCubicSplineInterpolation for CUBICSPLINE.
+		                                      // KeyframeTrack.optimize() can't handle glTF Cubic Spline output values layout,
+		                                      // using THREE.InterpolateSmooth for KeyframeTrack instantiation to prevent optimization.
+		                                      // See KeyframeTrack.optimize() for the detail.
 		LINEAR: THREE.InterpolateLinear,
 		STEP: THREE.InterpolateDiscrete
 	};
@@ -1016,148 +1108,25 @@ THREE.GLTFLoader = ( function () {
 
 	/* UTILITY FUNCTIONS */
 
-	function _each( object, callback, thisObj ) {
-
-		if ( ! object ) {
-
-			return Promise.resolve();
-
-		}
-
-		var results;
-		var fns = [];
-
-		if ( Object.prototype.toString.call( object ) === '[object Array]' ) {
-
-			results = [];
-
-			var length = object.length;
-
-			for ( var idx = 0; idx < length; idx ++ ) {
-
-				var value = callback.call( thisObj || this, object[ idx ], idx );
-
-				if ( value ) {
-
-					if ( value instanceof Promise ) {
-
-						value = value.then( function ( key, value ) {
-
-							results[ key ] = value;
-
-						}.bind( this, idx ) );
-
-					} else {
-
-						results[ idx ] = value;
-
-					}
-
-					fns.push( value );
-
-				}
-
-			}
-
-		} else {
-
-			results = {};
-
-			for ( var key in object ) {
-
-				if ( object.hasOwnProperty( key ) ) {
-
-					var value = callback.call( thisObj || this, object[ key ], key );
-
-					if ( value ) {
-
-						if ( value instanceof Promise ) {
-
-							value = value.then( function ( key, value ) {
-
-								results[ key ] = value;
-
-							}.bind( this, key ) );
-
-						} else {
-
-							results[ key ] = value;
-
-						}
-
-						fns.push( value );
-
-					}
-
-				}
-
-			}
-
-		}
-
-		return Promise.all( fns ).then( function () {
-
-			return results;
-
-		} );
-
-	}
-
 	function resolveURL( url, path ) {
 
 		// Invalid URL
-		if ( typeof url !== 'string' || url === '' )
-			return '';
+		if ( typeof url !== 'string' || url === '' ) return '';
 
 		// Absolute URL http://,https://,//
-		if ( /^(https?:)?\/\//i.test( url ) ) {
-
-			return url;
-
-		}
+		if ( /^(https?:)?\/\//i.test( url ) ) return url;
 
 		// Data URI
-		if ( /^data:.*,.*$/i.test( url ) ) {
-
-			return url;
-
-		}
+		if ( /^data:.*,.*$/i.test( url ) ) return url;
 
 		// Blob URL
-		if ( /^blob:.*$/i.test( url ) ) {
-
-			return url;
-
-		}
+		if ( /^blob:.*$/i.test( url ) ) return url;
 
 		// Relative URL
 		return path + url;
 
 	}
 
-	function convertUint8ArrayToString( array ) {
-
-		if ( window.TextDecoder !== undefined ) {
-
-			return new TextDecoder().decode( array );
-
-		}
-
-		// Avoid the String.fromCharCode.apply(null, array) shortcut, which
-		// throws a "maximum call stack size exceeded" error for large arrays.
-
-		var s = '';
-
-		for ( var i = 0, il = array.length; i < il; i ++ ) {
-
-			s += String.fromCharCode( array[ i ] );
-
-		}
-
-		return s;
-
-	}
-
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
 	 */
@@ -1178,14 +1147,12 @@ THREE.GLTFLoader = ( function () {
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
 	 *
-	 * TODO: Implement support for morph targets on TANGENT attribute.
-	 *
 	 * @param {THREE.Mesh} mesh
 	 * @param {GLTF.Mesh} meshDef
 	 * @param {GLTF.Primitive} primitiveDef
-	 * @param {Object} dependencies
+	 * @param {Array<THREE.BufferAttribute>} accessors
 	 */
-	function addMorphTargets( mesh, meshDef, primitiveDef, dependencies ) {
+	function addMorphTargets( mesh, meshDef, primitiveDef, accessors ) {
 
 		var geometry = mesh.geometry;
 		var material = mesh.material;
@@ -1221,7 +1188,7 @@ THREE.GLTFLoader = ( function () {
 				// So morphTarget value will depend on mesh's position, then cloning attribute
 				// for the case if attribute is shared among two or more meshes.
 
-				positionAttribute = dependencies.accessors[ target.POSITION ].clone();
+				positionAttribute = cloneBufferAttribute( accessors[ target.POSITION ] );
 				var position = geometry.attributes.position;
 
 				for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) {
@@ -1239,7 +1206,7 @@ THREE.GLTFLoader = ( function () {
 
 				// Copying the original position not to affect the final position.
 				// See the formula above.
-				positionAttribute = geometry.attributes.position.clone();
+				positionAttribute = cloneBufferAttribute( geometry.attributes.position );
 
 			}
 
@@ -1256,7 +1223,7 @@ THREE.GLTFLoader = ( function () {
 
 				// see target.POSITION's comment
 
-				normalAttribute = dependencies.accessors[ target.NORMAL ].clone();
+				normalAttribute = cloneBufferAttribute( accessors[ target.NORMAL ] );
 				var normal = geometry.attributes.normal;
 
 				for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) {
@@ -1272,7 +1239,7 @@ THREE.GLTFLoader = ( function () {
 
 			} else if ( geometry.attributes.normal !== undefined ) {
 
-				normalAttribute = geometry.attributes.normal.clone();
+				normalAttribute = cloneBufferAttribute( geometry.attributes.normal );
 
 			}
 
@@ -1297,109 +1264,231 @@ THREE.GLTFLoader = ( function () {
 
 		}
 
+		// .extras has user-defined data, so check that .extras.targetNames is an array.
+		if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) {
+
+			for ( var i = 0, il = meshDef.extras.targetNames.length; i < il; i ++ ) {
+
+				mesh.morphTargetDictionary[ meshDef.extras.targetNames[ i ] ] = i;
+
+			}
+
+		}
+
 	}
 
-	/* GLTF PARSER */
+	function isPrimitiveEqual( a, b ) {
 
-	function GLTFParser( json, extensions, options ) {
+		if ( a.indices !== b.indices ) {
 
-		this.json = json || {};
-		this.extensions = extensions || {};
-		this.options = options || {};
+			return false;
 
-		// loader object cache
-		this.cache = new GLTFRegistry();
+		}
 
-		this.textureLoader = new THREE.TextureLoader( this.options.manager );
-		this.textureLoader.setCrossOrigin( this.options.crossOrigin );
+		var attribA = a.attributes || {};
+		var attribB = b.attributes || {};
+		var keysA = Object.keys( attribA );
+		var keysB = Object.keys( attribB );
 
-		this.fileLoader = new THREE.FileLoader( this.options.manager );
-		this.fileLoader.setResponseType( 'arraybuffer' );
+		if ( keysA.length !== keysB.length ) {
+
+			return false;
+
+		}
+
+		for ( var i = 0, il = keysA.length; i < il; i ++ ) {
+
+			var key = keysA[ i ];
+
+			if ( attribA[ key ] !== attribB[ key ] ) {
+
+				return false;
+
+			}
+
+		}
+
+		return true;
 
 	}
 
-	GLTFParser.prototype._withDependencies = function ( dependencies ) {
+	function getCachedGeometry( cache, newPrimitive ) {
+
+		for ( var i = 0, il = cache.length; i < il; i ++ ) {
 
-		var _dependencies = {};
+			var cached = cache[ i ];
 
-		for ( var i = 0; i < dependencies.length; i ++ ) {
+			if ( isPrimitiveEqual( cached.primitive, newPrimitive ) ) {
 
-			var dependency = dependencies[ i ];
-			var fnName = 'load' + dependency.charAt( 0 ).toUpperCase() + dependency.slice( 1 );
+				return cached.promise;
 
-			var cached = this.cache.get( dependency );
+			}
+
+		}
+
+		return null;
 
-			if ( cached !== undefined ) {
+	}
+
+	function cloneBufferAttribute( attribute ) {
 
-				_dependencies[ dependency ] = cached;
+		if ( attribute.isInterleavedBufferAttribute ) {
 
-			} else if ( this[ fnName ] ) {
+			var count = attribute.count;
+			var itemSize = attribute.itemSize;
+			var array = attribute.array.slice( 0, count * itemSize );
 
-				var fn = this[ fnName ]();
-				this.cache.add( dependency, fn );
+			for ( var i = 0; i < count; ++ i ) {
 
-				_dependencies[ dependency ] = fn;
+				array[ i ] = attribute.getX( i );
+				if ( itemSize >= 2 ) array[ i + 1 ] = attribute.getY( i );
+				if ( itemSize >= 3 ) array[ i + 2 ] = attribute.getZ( i );
+				if ( itemSize >= 4 ) array[ i + 3 ] = attribute.getW( i );
 
 			}
 
+			return new THREE.BufferAttribute( array, itemSize, attribute.normalized );
+
 		}
 
-		return _each( _dependencies, function ( dependency ) {
+		return attribute.clone();
+
+	}
 
-			return dependency;
+	/* 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 THREE.TextureLoader( this.options.manager );
+		this.textureLoader.setCrossOrigin( this.options.crossOrigin );
+
+		this.fileLoader = new THREE.FileLoader( this.options.manager );
+		this.fileLoader.setResponseType( 'arraybuffer' );
+
+	}
 
 	GLTFParser.prototype.parse = function ( onLoad, onError ) {
 
 		var json = this.json;
-		var parser = this;
 
 		// Clear the loader cache
 		this.cache.removeAll();
 
+		// Mark the special nodes/meshes in json for efficient parse
+		this.markDefs();
+
 		// Fire the callback on complete
-		this._withDependencies( [
+		this.getMultiDependencies( [
 
-			'scenes',
-			'animations'
+			'scene',
+			'animation',
+			'camera'
 
 		] ).then( function ( dependencies ) {
 
 			var scenes = dependencies.scenes || [];
 			var scene = scenes[ json.scene || 0 ];
 			var animations = dependencies.animations || [];
+			var asset = json.asset;
+			var cameras = dependencies.cameras || [];
 
-			parser.getDependencies( 'camera' ).then( function ( cameras ) {
-
-				onLoad( scene, scenes, cameras, animations );
-
-			} ).catch( onError );
+			onLoad( scene, scenes, cameras, animations, asset );
 
 		} ).catch( onError );
 
 	};
 
 	/**
-	 * Requests the specified dependency asynchronously, with caching.
-	 * @param {string} type
-	 * @param {number} index
-	 * @return {Promise<Object>}
+	 * Marks the special nodes/meshes in json for efficient parse.
 	 */
-	GLTFParser.prototype.getDependency = function ( type, index ) {
+	GLTFParser.prototype.markDefs = function () {
 
-		var cacheKey = type + ':' + index;
-		var dependency = this.cache.get( cacheKey );
+		var nodeDefs = this.json.nodes || [];
+		var skinDefs = this.json.skins || [];
+		var meshDefs = this.json.meshes || [];
 
-		if ( ! dependency ) {
+		var meshReferences = {};
+		var meshUses = {};
 
-			var fnName = 'load' + type.charAt( 0 ).toUpperCase() + type.slice( 1 );
-			dependency = this[ fnName ]( index );
-			this.cache.add( cacheKey, dependency );
+		// 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 THREE.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<Object>}
+	 */
+	GLTFParser.prototype.getDependency = function ( type, index ) {
+
+		var cacheKey = type + ':' + index;
+		var dependency = this.cache.get( cacheKey );
+
+		if ( ! dependency ) {
+
+			var fnName = 'load' + type.charAt( 0 ).toUpperCase() + type.slice( 1 );
+			dependency = this[ fnName ]( index );
+			this.cache.add( cacheKey, dependency );
+
+		}
 
 		return dependency;
 
@@ -1412,14 +1501,57 @@ THREE.GLTFLoader = ( function () {
 	 */
 	GLTFParser.prototype.getDependencies = function ( type ) {
 
-		var parser = this;
-		var defs = this.json[ type + 's' ] || [];
+		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 Promise.all( defs.map( function ( def, index ) {
+				return parser.getDependency( type, index );
 
-			return parser.getDependency( type, index );
+			} ) );
 
-		} ) );
+			this.cache.add( type, dependencies );
+
+		}
+
+		return dependencies;
+
+	};
+
+	/**
+	 * Requests all multiple dependencies of the specified types asynchronously, with caching.
+	 * @param {Array<string>} types
+	 * @return {Promise<Object<Array<Object>>>}
+	 */
+	GLTFParser.prototype.getMultiDependencies = function ( types ) {
+
+		var results = {};
+		var pendings = [];
+
+		for ( var i = 0, il = types.length; i < il; i ++ ) {
+
+			var type = types[ i ];
+			var value = this.getDependencies( type );
+
+			value = value.then( function ( key, value ) {
+
+				results[ key ] = value;
+
+			}.bind( this, type + ( type === 'mesh' ? 'es' : 's' ) ) );
+
+			pendings.push( value );
+
+		}
+
+		return Promise.all( pendings ).then( function () {
+
+			return results;
+
+		} );
 
 	};
 
@@ -1479,45 +1611,131 @@ THREE.GLTFLoader = ( function () {
 
 	};
 
-	GLTFParser.prototype.loadAccessors = function () {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
+	 * @param {number} accessorIndex
+	 * @return {Promise<THREE.BufferAttribute|THREE.InterleavedBufferAttribute>}
+	 */
+	GLTFParser.prototype.loadAccessor = function ( accessorIndex ) {
 
 		var parser = this;
 		var json = this.json;
 
-		return _each( json.accessors, function ( accessor ) {
+		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 null;
+
+		}
 
-			return parser.getDependency( 'bufferView', accessor.bufferView ).then( function ( bufferView ) {
+		var pendingBufferViews = [];
 
-				var itemSize = WEBGL_TYPE_SIZES[ accessor.type ];
-				var TypedArray = WEBGL_COMPONENT_TYPES[ accessor.componentType ];
+		if ( accessorDef.bufferView !== undefined ) {
 
-				// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
-				var elementBytes = TypedArray.BYTES_PER_ELEMENT;
-				var itemBytes = elementBytes * itemSize;
-				var byteStride = json.bufferViews[ accessor.bufferView ].byteStride;
-				var normalized = accessor.normalized === true;
-				var array;
+			pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) );
+
+		} else {
+
+			pendingBufferViews.push( null );
+
+		}
 
-				// The buffer is not interleaved if the stride is the item size in bytes.
-				if ( byteStride && byteStride !== itemBytes ) {
+		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 = json.bufferViews[ accessorDef.bufferView ].byteStride;
+			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 ) {
+
+				var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType;
+				var ib = parser.cache.get( ibCacheKey );
+
+				if ( ! ib ) {
 
 					// Use the full buffer if it's interleaved.
 					array = new TypedArray( bufferView );
 
 					// Integer parameters to IB/IBA are in array elements, not bytes.
-					var ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes );
+					ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes );
+
+					parser.cache.add( ibCacheKey, ib );
 
-					return new THREE.InterleavedBufferAttribute( ib, itemSize, accessor.byteOffset / elementBytes, normalized );
+				}
+
+				bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, byteOffset / elementBytes, normalized );
+
+			} else {
+
+				if ( bufferView === null ) {
+
+					array = new TypedArray( accessorDef.count * itemSize );
 
 				} else {
 
-					array = new TypedArray( bufferView, accessor.byteOffset, accessor.count * itemSize );
+					array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize );
+
+				}
+
+				bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized );
+
+			}
+
+			// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
+			if ( accessorDef.sparse !== undefined ) {
 
-					return new THREE.BufferAttribute( array, itemSize, normalized );
+				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.setArray( bufferAttribute.array.slice() );
 
 				}
 
-			} );
+				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;
 
 		} );
 
@@ -1546,15 +1764,14 @@ THREE.GLTFLoader = ( function () {
 
 			// Load binary image data from bufferView, if provided.
 
-			sourceURI = parser.getDependency( 'bufferView', source.bufferView )
-				.then( function ( bufferView ) {
+			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;
+				isObjectURL = true;
+				var blob = new Blob( [ bufferView ], { type: source.mimeType } );
+				sourceURI = URL.createObjectURL( blob );
+				return sourceURI;
 
-				} );
+			} );
 
 		}
 
@@ -1628,275 +1845,283 @@ THREE.GLTFLoader = ( function () {
 
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
-	 * @return {Promise<Array<THREE.Material>>}
+	 * @param {number} materialIndex
+	 * @return {Promise<THREE.Material>}
 	 */
-	GLTFParser.prototype.loadMaterials = function () {
+	GLTFParser.prototype.loadMaterial = function ( materialIndex ) {
 
 		var parser = this;
 		var json = this.json;
 		var extensions = this.extensions;
+		var materialDef = this.json.materials[ materialIndex ];
 
-		return _each( json.materials, function ( material ) {
-
-			var materialType;
-			var materialParams = {};
-			var materialExtensions = material.extensions || {};
-
-			var pending = [];
+		var materialType;
+		var materialParams = {};
+		var materialExtensions = materialDef.extensions || {};
 
-			if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_COMMON ] ) {
-
-				var khcExtension = extensions[ EXTENSIONS.KHR_MATERIALS_COMMON ];
-				materialType = khcExtension.getMaterialType( material );
-				pending.push( khcExtension.extendParams( materialParams, material, parser ) );
+		var pending = [];
 
-			} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_CMN_CONSTANT ] ) {
+		if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {
 
-				materialType = THREE.MeshBasicMaterial;
+			var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
+			materialType = sgExtension.getMaterialType( materialDef );
+			pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) );
 
-			} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {
+		} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) {
 
-				var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
-				materialType = sgExtension.getMaterialType( material );
-				pending.push( sgExtension.extendParams( materialParams, material, parser ) );
+			var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ];
+			materialType = kmuExtension.getMaterialType( materialDef );
+			pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) );
 
-			} else if ( material.pbrMetallicRoughness !== undefined ) {
+		} else if ( materialDef.pbrMetallicRoughness !== undefined ) {
 
-				// Specification:
-				// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material
+			// Specification:
+			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material
 
-				materialType = THREE.MeshStandardMaterial;
+			materialType = THREE.MeshStandardMaterial;
 
-				var metallicRoughness = material.pbrMetallicRoughness;
+			var metallicRoughness = materialDef.pbrMetallicRoughness;
 
-				materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
-				materialParams.opacity = 1.0;
+			materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
+			materialParams.opacity = 1.0;
 
-				if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {
+			if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {
 
-					var array = metallicRoughness.baseColorFactor;
+				var array = metallicRoughness.baseColorFactor;
 
-					materialParams.color.fromArray( array );
-					materialParams.opacity = array[ 3 ];
+				materialParams.color.fromArray( array );
+				materialParams.opacity = array[ 3 ];
 
-				}
+			}
 
-				if ( metallicRoughness.baseColorTexture !== undefined ) {
+			if ( metallicRoughness.baseColorTexture !== undefined ) {
 
-					pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) );
+				pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture.index ) );
 
-				}
+			}
 
-				materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
-				materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;
+			materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
+			materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;
 
-				if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {
+			if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {
 
-					var textureIndex = metallicRoughness.metallicRoughnessTexture.index;
-					pending.push( parser.assignTexture( materialParams, 'metalnessMap', textureIndex ) );
-					pending.push( parser.assignTexture( materialParams, 'roughnessMap', textureIndex ) );
+				var textureIndex = metallicRoughness.metallicRoughnessTexture.index;
+				pending.push( parser.assignTexture( materialParams, 'metalnessMap', textureIndex ) );
+				pending.push( parser.assignTexture( materialParams, 'roughnessMap', textureIndex ) );
 
-				}
+			}
 
-			} else {
+		} else {
 
-				materialType = THREE.MeshPhongMaterial;
+			materialType = THREE.MeshPhongMaterial;
 
-			}
-
-			if ( material.doubleSided === true ) {
+		}
 
-				materialParams.side = THREE.DoubleSide;
+		if ( materialDef.doubleSided === true ) {
 
-			}
+			materialParams.side = THREE.DoubleSide;
 
-			var alphaMode = material.alphaMode || ALPHA_MODES.OPAQUE;
+		}
 
-			if ( alphaMode !== ALPHA_MODES.OPAQUE ) {
+		var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;
 
-				materialParams.transparent = true;
+		if ( alphaMode === ALPHA_MODES.BLEND ) {
 
-				if ( alphaMode === ALPHA_MODES.MASK ) {
+			materialParams.transparent = true;
 
-					materialParams.alphaTest = material.alphaCutoff !== undefined ? material.alphaCutoff : 0.5;
+		} else {
 
-				}
+			materialParams.transparent = false;
 
-			} else {
+			if ( alphaMode === ALPHA_MODES.MASK ) {
 
-				materialParams.transparent = false;
+				materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;
 
 			}
 
-			if ( material.normalTexture !== undefined ) {
+		}
 
-				pending.push( parser.assignTexture( materialParams, 'normalMap', material.normalTexture.index ) );
+		if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {
 
-				materialParams.normalScale = new THREE.Vector2( 1, 1 );
+			pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture.index ) );
 
-				if ( material.normalTexture.scale !== undefined ) {
+			materialParams.normalScale = new THREE.Vector2( 1, 1 );
 
-					materialParams.normalScale.set( material.normalTexture.scale, material.normalTexture.scale );
+			if ( materialDef.normalTexture.scale !== undefined ) {
 
-				}
+				materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale );
 
 			}
 
-			if ( material.occlusionTexture !== undefined ) {
+		}
 
-				pending.push( parser.assignTexture( materialParams, 'aoMap', material.occlusionTexture.index ) );
+		if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {
 
-				if ( material.occlusionTexture.strength !== undefined ) {
+			pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture.index ) );
 
-					materialParams.aoMapIntensity = material.occlusionTexture.strength;
+			if ( materialDef.occlusionTexture.strength !== undefined ) {
 
-				}
+				materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;
 
 			}
 
-			if ( material.emissiveFactor !== undefined ) {
+		}
 
-				if ( materialType === THREE.MeshBasicMaterial ) {
+		if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) {
 
-					materialParams.color = new THREE.Color().fromArray( material.emissiveFactor );
+			materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor );
 
-				} else {
+		}
 
-					materialParams.emissive = new THREE.Color().fromArray( material.emissiveFactor );
+		if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {
 
-				}
+			pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture.index ) );
 
-			}
+		}
 
-			if ( material.emissiveTexture !== undefined ) {
+		return Promise.all( pending ).then( function () {
 
-				if ( materialType === THREE.MeshBasicMaterial ) {
+			var material;
 
-					pending.push( parser.assignTexture( materialParams, 'map', material.emissiveTexture.index ) );
+			if ( materialType === THREE.ShaderMaterial ) {
 
-				} else {
+				material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );
 
-					pending.push( parser.assignTexture( materialParams, 'emissiveMap', material.emissiveTexture.index ) );
+			} else {
 
-				}
+				material = new materialType( materialParams );
 
 			}
 
-			return Promise.all( pending ).then( function () {
+			if ( materialDef.name !== undefined ) material.name = materialDef.name;
 
-				var _material;
+			// Normal map textures use OpenGL conventions:
+			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture
+			if ( material.normalScale ) {
 
-				if ( materialType === THREE.ShaderMaterial ) {
+				material.normalScale.x = - material.normalScale.x;
 
-					_material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );
+			}
 
-				} else {
+			// emissiveTexture and baseColorTexture use sRGB encoding.
+			if ( material.map ) material.map.encoding = THREE.sRGBEncoding;
+			if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding;
 
-					_material = new materialType( materialParams );
+			if ( materialDef.extras ) material.userData = materialDef.extras;
 
-				}
+			return material;
 
-				if ( material.name !== undefined ) _material.name = material.name;
+		} );
 
-				// Normal map textures use OpenGL conventions:
-				// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture
-				if ( _material.normalScale ) {
+	};
 
-					_material.normalScale.x = - _material.normalScale.x;
+	/**
+	 * @param  {THREE.BufferGeometry} geometry
+	 * @param  {GLTF.Primitive} primitiveDef
+	 * @param  {Array<THREE.BufferAttribute>} accessors
+	 */
+	function addPrimitiveAttributes ( geometry, primitiveDef, accessors ) {
 
-				}
+		var attributes = primitiveDef.attributes;
 
-				// emissiveTexture and baseColorTexture use sRGB encoding.
-				// TODO: Figure out why we need to comment this out. Textures that are exported as sRGB appear darker.
-				//if ( _material.map ) _material.map.encoding = THREE.sRGBEncoding;
-				//if ( _material.emissiveMap ) _material.emissiveMap.encoding = THREE.sRGBEncoding;
+		for ( var gltfAttributeName in attributes ) {
 
-				if ( material.extras ) _material.userData = material.extras;
+			var threeAttributeName = ATTRIBUTES[ gltfAttributeName ];
+			var bufferAttribute = accessors[ attributes[ gltfAttributeName ] ];
 
-				return _material;
+			// Skip attributes already provided by e.g. Draco extension.
+			if ( !threeAttributeName ) continue;
+			if ( threeAttributeName in geometry.attributes ) continue;
 
-			} );
+			geometry.addAttribute( threeAttributeName, bufferAttribute );
 
-		} );
+		}
 
-	};
+		if ( primitiveDef.indices !== undefined && !geometry.index ) {
 
-	GLTFParser.prototype.loadGeometries = function ( primitives ) {
+			geometry.setIndex( accessors[ primitiveDef.indices ] );
 
-		return this._withDependencies( [
+		}
 
-			'accessors',
+	}
 
-		] ).then( function ( dependencies ) {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
+	 * @param {Array<Object>} primitives
+	 * @return {Promise<Array<THREE.BufferGeometry>>}
+	 */
+	GLTFParser.prototype.loadGeometries = function ( primitives ) {
 
-			return _each( primitives, function ( primitive ) {
+		var parser = this;
+		var extensions = this.extensions;
+		var cache = this.primitiveCache;
 
-				var geometry = new THREE.BufferGeometry();
+		return this.getDependencies( 'accessor' ).then( function ( accessors ) {
 
-				var attributes = primitive.attributes;
+			var geometries = [];
+			var pending = [];
 
-				for ( var attributeId in attributes ) {
+			for ( var i = 0, il = primitives.length; i < il; i ++ ) {
 
-					var attributeEntry = attributes[ attributeId ];
+				var primitive = primitives[ i ];
 
-					if ( attributeEntry === undefined ) return;
+				// See if we've already created this geometry
+				var cached = getCachedGeometry( cache, primitive );
 
-					var bufferAttribute = dependencies.accessors[ attributeEntry ];
+				var geometry;
 
-					switch ( attributeId ) {
+				if ( cached ) {
 
-						case 'POSITION':
+					// Use the cached geometry if it exists
+					pending.push( cached.then( function ( geometry ) {
 
-							geometry.addAttribute( 'position', bufferAttribute );
-							break;
+						geometries.push( geometry );
 
-						case 'NORMAL':
+					} ) );
 
-							geometry.addAttribute( 'normal', bufferAttribute );
-							break;
+				} else if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) {
 
-						case 'TEXCOORD_0':
-						case 'TEXCOORD0':
-						case 'TEXCOORD':
+					// Use DRACO geometry if available
+					var geometryPromise = extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
+						.decodePrimitive( primitive, parser )
+						.then( function ( geometry ) {
 
-							geometry.addAttribute( 'uv', bufferAttribute );
-							break;
+							addPrimitiveAttributes( geometry, primitive, accessors );
 
-						case 'TEXCOORD_1':
+							geometries.push( geometry );
 
-							geometry.addAttribute( 'uv2', bufferAttribute );
-							break;
+							return geometry;
 
-						case 'COLOR_0':
-						case 'COLOR0':
-						case 'COLOR':
+						} );
 
-							geometry.addAttribute( 'color', bufferAttribute );
-							break;
+					cache.push( { primitive: primitive, promise: geometryPromise  } );
 
-						case 'WEIGHTS_0':
-						case 'WEIGHT': // WEIGHT semantic deprecated.
+					pending.push( geometryPromise );
 
-							geometry.addAttribute( 'skinWeight', bufferAttribute );
-							break;
+				} else  {
 
-						case 'JOINTS_0':
-						case 'JOINT': // JOINT semantic deprecated.
+					// Otherwise create a new geometry
+					geometry = new THREE.BufferGeometry();
 
-							geometry.addAttribute( 'skinIndex', bufferAttribute );
-							break;
+					addPrimitiveAttributes( geometry, primitive, accessors );
 
-					}
+					// Cache this geometry
+					cache.push( {
 
-				}
+						primitive: primitive,
+						promise: Promise.resolve( geometry )
 
-				if ( primitive.indices !== undefined ) {
+					} );
 
-					geometry.setIndex( dependencies.accessors[ primitive.indices ] );
+					geometries.push( geometry );
 
 				}
 
-				return geometry;
+			}
+
+			return Promise.all( pending ).then( function () {
+
+				return geometries;
 
 			} );
 
@@ -1906,94 +2131,132 @@ THREE.GLTFLoader = ( function () {
 
 	/**
 	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
+	 * @param {number} meshIndex
+	 * @return {Promise<THREE.Group|THREE.Mesh|THREE.SkinnedMesh>}
 	 */
-	GLTFParser.prototype.loadMeshes = function () {
+	GLTFParser.prototype.loadMesh = function ( meshIndex ) {
 
 		var scope = this;
 		var json = this.json;
 		var extensions = this.extensions;
 
-		return this._withDependencies( [
+		var meshDef = this.json.meshes[ meshIndex ];
+
+		return this.getMultiDependencies( [
 
-			'accessors',
-			'materials'
+			'accessor',
+			'material'
 
 		] ).then( function ( dependencies ) {
 
-			return _each( json.meshes, function ( meshDef, meshIndex ) {
+			var group = new THREE.Group();
 
-				var group = new THREE.Group();
+			var primitives = meshDef.primitives;
 
-				var primitives = meshDef.primitives || [];
+			return scope.loadGeometries( primitives ).then( function ( geometries ) {
 
-				return scope.loadGeometries( primitives ).then( function ( geometries ) {
+				for ( var i = 0, il = primitives.length; i < il; i ++ ) {
 
-					for ( var i = 0; i < primitives.length; i ++ ) {
+					var primitive = primitives[ i ];
+					var geometry = geometries[ i ];
 
-						var primitive = primitives[ i ];
-						var geometry = geometries[ i ];
+					var material = primitive.material === undefined
+						? createDefaultMaterial()
+						: dependencies.materials[ primitive.material ];
 
-						var material = primitive.material === undefined
-							? createDefaultMaterial()
-							: dependencies.materials[ primitive.material ];
+					if ( material.aoMap
+							&& geometry.attributes.uv2 === undefined
+							&& geometry.attributes.uv !== undefined ) {
 
-						if ( material.aoMap
-								&& geometry.attributes.uv2 === undefined
-								&& geometry.attributes.uv !== undefined ) {
+						console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' );
+						geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
 
-							console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' );
-							geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
+					}
 
-						}
+					// If the material will be modified later on, clone it now.
+					var useVertexColors = geometry.attributes.color !== undefined;
+					var useFlatShading = geometry.attributes.normal === undefined;
+					var useSkinning = meshDef.isSkinnedMesh === true;
+					var useMorphTargets = primitive.targets !== undefined;
 
-						var useVertexColors = geometry.attributes.color !== undefined;
-						var useFlatShading = geometry.attributes.normal === undefined;
+					if ( useVertexColors || useFlatShading || useSkinning || useMorphTargets ) {
 
-						if ( useVertexColors || useFlatShading ) {
+						if ( material.isGLTFSpecularGlossinessMaterial ) {
 
-							if ( material.isGLTFSpecularGlossinessMaterial ) {
+							var specGlossExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
+							material = specGlossExtension.cloneMaterial( material );
 
-								var specGlossExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
-								material = specGlossExtension.cloneMaterial( material );
+						} else {
 
-							} else {
+							material = material.clone();
 
-								material = material.clone();
+						}
 
-							}
+					}
 
-						}
+					if ( useVertexColors ) {
 
-						if ( useVertexColors ) {
+						material.vertexColors = THREE.VertexColors;
+						material.needsUpdate = true;
 
-							material.vertexColors = THREE.VertexColors;
-							material.needsUpdate = true;
+					}
 
-						}
+					if ( useFlatShading ) {
 
-						if ( useFlatShading ) {
+						material.flatShading = true;
 
-							material.flatShading = true;
+					}
 
-						}
+					var mesh;
 
-						var mesh;
+					if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
+						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
+						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
+						primitive.mode === undefined ) {
 
-						if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES || primitive.mode === undefined ) {
+						if ( useSkinning ) {
 
-							mesh = new THREE.Mesh( geometry, material );
+							mesh = new THREE.SkinnedMesh( geometry, material );
+							material.skinning = true;
 
-						} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {
+						} else {
 
 							mesh = new THREE.Mesh( geometry, material );
+
+						}
+
+						if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {
+
 							mesh.drawMode = THREE.TriangleStripDrawMode;
 
 						} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {
 
-							mesh = new THREE.Mesh( geometry, material );
 							mesh.drawMode = THREE.TriangleFanDrawMode;
 
-						} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {
+						}
+
+					} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ||
+						primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ||
+						primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {
+
+						var cacheKey = 'LineBasicMaterial:' + material.uuid;
+
+						var lineMaterial = scope.cache.get( cacheKey );
+
+						if ( ! lineMaterial ) {
+
+							lineMaterial = new THREE.LineBasicMaterial();
+							THREE.Material.prototype.copy.call( lineMaterial, material );
+							lineMaterial.color.copy( material.color );
+							lineMaterial.lights = false;  // LineBasicMaterial doesn't support lights yet
+
+							scope.cache.add( cacheKey, lineMaterial );
+
+						}
+
+						material = lineMaterial;
+
+						if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {
 
 							mesh = new THREE.LineSegments( geometry, material );
 
@@ -2001,47 +2264,73 @@ THREE.GLTFLoader = ( function () {
 
 							mesh = new THREE.Line( geometry, material );
 
-						} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {
+						} else {
 
 							mesh = new THREE.LineLoop( geometry, material );
 
-						} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {
+						}
 
-							mesh = new THREE.Points( geometry, material );
+					} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {
 
-						} else {
+						var cacheKey = 'PointsMaterial:' + material.uuid;
 
-							throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ', primitive.mode );
+						var pointsMaterial = scope.cache.get( cacheKey );
+
+						if ( ! pointsMaterial ) {
+
+							pointsMaterial = new THREE.PointsMaterial();
+							THREE.Material.prototype.copy.call( pointsMaterial, material );
+							pointsMaterial.color.copy( material.color );
+							pointsMaterial.map = material.map;
+							pointsMaterial.lights = false;  // PointsMaterial doesn't support lights yet
+
+							scope.cache.add( cacheKey, pointsMaterial );
 
 						}
 
-						mesh.name = meshDef.name || ( 'mesh_' + meshIndex );
+						material = pointsMaterial;
 
-						if ( primitive.targets !== undefined ) {
+						mesh = new THREE.Points( geometry, material );
 
-							addMorphTargets( mesh, meshDef, primitive, dependencies );
+					} else {
 
-						}
+						throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );
 
-						if ( primitive.extras ) mesh.userData = primitive.extras;
+					}
 
-						if ( primitives.length > 1 ) {
+					mesh.name = meshDef.name || ( 'mesh_' + meshIndex );
 
-							mesh.name += '_' + i;
+					if ( useMorphTargets ) {
 
-							group.add( mesh );
+						addMorphTargets( mesh, meshDef, primitive, dependencies.accessors );
 
-						} else {
+					}
 
-							return mesh;
+					if ( meshDef.extras !== undefined ) mesh.userData = meshDef.extras;
+					if ( primitive.extras !== undefined ) mesh.geometry.userData = primitive.extras;
 
-						}
+					// for Specular-Glossiness.
+					if ( material.isGLTFSpecularGlossinessMaterial === true ) {
+
+						mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms;
 
 					}
 
-					return group;
+					if ( primitives.length > 1 ) {
 
-				} );
+						mesh.name += '_' + i;
+
+						group.add( mesh );
+
+					} else {
+
+						return mesh;
+
+					}
+
+				}
+
+				return group;
 
 			} );
 
@@ -2087,386 +2376,375 @@ THREE.GLTFLoader = ( function () {
 
 	};
 
-	GLTFParser.prototype.loadSkins = function () {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
+	 * @param {number} skinIndex
+	 * @return {Promise<Object>}
+	 */
+	GLTFParser.prototype.loadSkin = function ( skinIndex ) {
 
-		var json = this.json;
+		var skinDef = this.json.skins[ skinIndex ];
 
-		return this._withDependencies( [
+		var skinEntry = { joints: skinDef.joints };
 
-			'accessors'
+		if ( skinDef.inverseBindMatrices === undefined ) {
 
-		] ).then( function ( dependencies ) {
+			return Promise.resolve( skinEntry );
 
-			return _each( json.skins, function ( skin ) {
+		}
 
-				var _skin = {
-					joints: skin.joints,
-					inverseBindMatrices: dependencies.accessors[ skin.inverseBindMatrices ]
-				};
+		return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) {
 
-				return _skin;
+			skinEntry.inverseBindMatrices = accessor;
 
-			} );
+			return skinEntry;
 
 		} );
 
 	};
 
-	GLTFParser.prototype.loadAnimations = function () {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
+	 * @param {number} animationIndex
+	 * @return {Promise<THREE.AnimationClip>}
+	 */
+	GLTFParser.prototype.loadAnimation = function ( animationIndex ) {
 
 		var json = this.json;
 
-		return this._withDependencies( [
+		var animationDef = this.json.animations[ animationIndex ];
 
-			'accessors',
-			'nodes'
+		return this.getMultiDependencies( [
+
+			'accessor',
+			'node'
 
 		] ).then( function ( dependencies ) {
 
-			return _each( json.animations, function ( animation, animationId ) {
+			var tracks = [];
+
+			for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) {
 
-				var tracks = [];
+				var channel = animationDef.channels[ i ];
+				var sampler = animationDef.samplers[ channel.sampler ];
 
-				for ( var i = 0; i < animation.channels.length; i ++ ) {
+				if ( sampler ) {
 
-					var channel = animation.channels[ i ];
-					var sampler = animation.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;
 
-					if ( sampler ) {
+					var inputAccessor = dependencies.accessors[ input ];
+					var outputAccessor = dependencies.accessors[ output ];
 
-						var target = channel.target;
-						var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
-						var input = animation.parameters !== undefined ? animation.parameters[ sampler.input ] : sampler.input;
-						var output = animation.parameters !== undefined ? animation.parameters[ sampler.output ] : sampler.output;
+					var node = dependencies.nodes[ name ];
 
-						var inputAccessor = dependencies.accessors[ input ];
-						var outputAccessor = dependencies.accessors[ output ];
+					if ( node ) {
 
-						var node = dependencies.nodes[ name ];
+						node.updateMatrix();
+						node.matrixAutoUpdate = true;
 
-						if ( node ) {
+						var TypedKeyframeTrack;
 
-							node.updateMatrix();
-							node.matrixAutoUpdate = true;
+						switch ( PATH_PROPERTIES[ target.path ] ) {
 
-							var TypedKeyframeTrack;
+							case PATH_PROPERTIES.weights:
 
-							switch ( PATH_PROPERTIES[ target.path ] ) {
+								TypedKeyframeTrack = THREE.NumberKeyframeTrack;
+								break;
 
-								case PATH_PROPERTIES.weights:
+							case PATH_PROPERTIES.rotation:
 
-									TypedKeyframeTrack = THREE.NumberKeyframeTrack;
-									break;
+								TypedKeyframeTrack = THREE.QuaternionKeyframeTrack;
+								break;
 
-								case PATH_PROPERTIES.rotation:
+							case PATH_PROPERTIES.position:
+							case PATH_PROPERTIES.scale:
+							default:
 
-									TypedKeyframeTrack = THREE.QuaternionKeyframeTrack;
-									break;
+								TypedKeyframeTrack = THREE.VectorKeyframeTrack;
+								break;
 
-								case PATH_PROPERTIES.position:
-								case PATH_PROPERTIES.scale:
-								default:
+						}
 
-									TypedKeyframeTrack = THREE.VectorKeyframeTrack;
-									break;
+						var targetName = node.name ? node.name : node.uuid;
 
-							}
+						var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear;
 
-							var targetName = node.name ? node.name : node.uuid;
+						var targetNames = [];
 
-							if ( sampler.interpolation === 'CATMULLROMSPLINE' ) {
+						if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {
 
-								console.warn( 'THREE.GLTFLoader: CATMULLROMSPLINE interpolation is not supported. Using CUBICSPLINE instead.' );
+							// node should be THREE.Group here but
+							// PATH_PROPERTIES.weights(morphTargetInfluences) should be
+							// the property of a mesh object under node.
+							// So finding targets here.
 
-							}
+							node.traverse( function ( object ) {
 
-							var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear;
+								if ( object.isMesh === true && object.material.morphTargets === true ) {
 
-							var targetNames = [];
+									targetNames.push( object.name ? object.name : object.uuid );
 
-							if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {
+								}
 
-								// node should be THREE.Group here but
-								// PATH_PROPERTIES.weights(morphTargetInfluences) should be
-								// the property of a mesh object under node.
-								// So finding targets here.
+							} );
 
-								node.traverse( function ( object ) {
+						} else {
 
-									if ( object.isMesh === true && object.material.morphTargets === true ) {
+							targetNames.push( targetName );
 
-										targetNames.push( object.name ? object.name : object.uuid );
+						}
 
-									}
+						// KeyframeTrack.optimize() will modify given 'times' and 'values'
+						// buffers before creating a truncated copy to keep. Because buffers may
+						// be reused by other tracks, make copies here.
+						for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {
 
-								} );
+							var track = new TypedKeyframeTrack(
+								targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
+								THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ),
+								THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ),
+								interpolation
+							);
 
-							} else {
+							// Here is the trick to enable custom interpolation.
+							// Overrides .createInterpolant in a factory method which creates custom interpolation.
+							if ( sampler.interpolation === 'CUBICSPLINE' ) {
 
-								targetNames.push( targetName );
+								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 );
 
-							// KeyframeTrack.optimize() will modify given 'times' and 'values'
-							// buffers before creating a truncated copy to keep. Because buffers may
-							// be reused by other tracks, make copies here.
-							for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {
+								};
 
-								tracks.push( new TypedKeyframeTrack(
-									targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
-									THREE.AnimationUtils.arraySlice( inputAccessor.array, 0 ),
-									THREE.AnimationUtils.arraySlice( outputAccessor.array, 0 ),
-									interpolation
-								) );
+								// Workaround, provide an alternate way to know if the interpolant type is cubis spline to track.
+								// track.getInterpolation() doesn't return valid value for custom interpolant.
+								track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;
 
 							}
 
+							tracks.push( track );
+
 						}
 
 					}
 
 				}
 
-				var name = animation.name !== undefined ? animation.name : 'animation_' + animationId;
+			}
 
-				return new THREE.AnimationClip( name, undefined, tracks );
+			var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex;
 
-			} );
+			return new THREE.AnimationClip( name, undefined, tracks );
 
 		} );
 
 	};
 
-	GLTFParser.prototype.loadNodes = function () {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
+	 * @param {number} nodeIndex
+	 * @return {Promise<THREE.Object3D>}
+	 */
+	GLTFParser.prototype.loadNode = function ( nodeIndex ) {
 
 		var json = this.json;
 		var extensions = this.extensions;
-		var scope = this;
 
-		var nodes = json.nodes || [];
-		var skins = json.skins || [];
+		var meshReferences = this.json.meshReferences;
+		var meshUses = this.json.meshUses;
 
-		var meshReferences = {};
-		var meshUses = {};
+		var nodeDef = this.json.nodes[ nodeIndex ];
 
-		// 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; skinIndex < skins.length; skinIndex ++ ) {
+		return this.getMultiDependencies( [
 
-			var joints = skins[ skinIndex ].joints;
+			'mesh',
+			'skin',
+			'camera'
 
-			for ( var i = 0; i < joints.length; ++ i ) {
+		] ).then( function ( dependencies ) {
 
-				nodes[ joints[ i ] ].isBone = true;
+			var node;
 
-			}
+			if ( nodeDef.isBone === true ) {
 
-		}
+				node = new THREE.Bone();
 
-		// Meshes can (and should) be reused by multiple nodes in a glTF asset. To
-		// avoid having more than one THREE.Mesh with the same name, count
-		// references and rename instances below.
-		//
-		// Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
-		for ( var nodeIndex = 0; nodeIndex < nodes.length; nodeIndex ++ ) {
+			} else if ( nodeDef.mesh !== undefined ) {
 
-			var nodeDef = nodes[ nodeIndex ];
+				var mesh = dependencies.meshes[ nodeDef.mesh ];
 
-			if ( nodeDef.mesh !== undefined ) {
+				node = mesh.clone();
 
-				if ( meshReferences[ nodeDef.mesh ] === undefined ) {
+				// for Specular-Glossiness
+				if ( mesh.isGroup === true ) {
 
-					meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0;
+					for ( var i = 0, il = mesh.children.length; i < il; i ++ ) {
 
-				}
+						var child = mesh.children[ i ];
 
-				meshReferences[ nodeDef.mesh ] ++;
+						if ( child.material && child.material.isGLTFSpecularGlossinessMaterial === true ) {
 
-			}
-
-		}
+							node.children[ i ].onBeforeRender = child.onBeforeRender;
 
-		return scope._withDependencies( [
-
-			'meshes',
-			'skins',
-			'cameras'
-
-		] ).then( function ( dependencies ) {
-
-			return _each( json.nodes, function ( nodeDef ) {
-
-				if ( nodeDef.isBone === true ) {
-
-					return new THREE.Bone();
+						}
 
-				} else if ( nodeDef.mesh !== undefined ) {
+					}
 
-					var mesh = dependencies.meshes[ nodeDef.mesh ].clone();
+				} else {
 
-					if ( meshReferences[ nodeDef.mesh ] > 1 ) {
+					if ( mesh.material && mesh.material.isGLTFSpecularGlossinessMaterial === true ) {
 
-						mesh.name += '_instance_' + meshUses[ nodeDef.mesh ] ++;
+						node.onBeforeRender = mesh.onBeforeRender;
 
 					}
 
-					return mesh;
+				}
 
-				} else if ( nodeDef.camera !== undefined ) {
+				if ( meshReferences[ nodeDef.mesh ] > 1 ) {
 
-					return scope.getDependency( 'camera', nodeDef.camera );
+					node.name += '_instance_' + meshUses[ nodeDef.mesh ] ++;
 
-				} else if ( nodeDef.extensions
-								 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ]
-								 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {
+				}
 
-					var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights;
-					return lights[ nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ];
+			} else if ( nodeDef.camera !== undefined ) {
 
-				} else {
+				node = dependencies.cameras[ nodeDef.camera ];
 
-					return new THREE.Object3D();
+			} else if ( nodeDef.extensions
+					 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ]
+					 && nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {
 
-				}
+				var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights;
+				node = lights[ nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ];
 
-			} ).then( function ( __nodes ) {
+			} else {
 
-				return _each( __nodes, function ( node, nodeIndex ) {
+				node = new THREE.Object3D();
 
-					var nodeDef = json.nodes[ nodeIndex ];
+			}
 
-					if ( nodeDef.name !== undefined ) {
+			if ( nodeDef.name !== undefined ) {
 
-						node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name );
+				node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name );
 
-					}
+			}
 
-					if ( nodeDef.extras ) node.userData = nodeDef.extras;
+			if ( nodeDef.extras ) node.userData = nodeDef.extras;
 
-					if ( nodeDef.matrix !== undefined ) {
+			if ( nodeDef.matrix !== undefined ) {
 
-						var matrix = new THREE.Matrix4();
-						matrix.fromArray( nodeDef.matrix );
-						node.applyMatrix( matrix );
+				var matrix = new THREE.Matrix4();
+				matrix.fromArray( nodeDef.matrix );
+				node.applyMatrix( matrix );
 
-					} else {
+			} else {
 
-						if ( nodeDef.translation !== undefined ) {
+				if ( nodeDef.translation !== undefined ) {
 
-							node.position.fromArray( nodeDef.translation );
+					node.position.fromArray( nodeDef.translation );
 
-						}
+				}
 
-						if ( nodeDef.rotation !== undefined ) {
+				if ( nodeDef.rotation !== undefined ) {
 
-							node.quaternion.fromArray( nodeDef.rotation );
+					node.quaternion.fromArray( nodeDef.rotation );
 
-						}
+				}
 
-						if ( nodeDef.scale !== undefined ) {
+				if ( nodeDef.scale !== undefined ) {
 
-							node.scale.fromArray( nodeDef.scale );
+					node.scale.fromArray( nodeDef.scale );
 
-						}
+				}
 
-					}
+			}
 
-					if ( nodeDef.skin !== undefined ) {
+			return node;
 
-						var skinnedMeshes = [];
+		} );
 
-						var meshes = node.children.length > 0 ? node.children : [ node ];
+	};
 
-						for ( var i = 0; i < meshes.length; i ++ ) {
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
+	 * @param {number} sceneIndex
+	 * @return {Promise<THREE.Scene>}
+	 */
+	GLTFParser.prototype.loadScene = function () {
 
-							var mesh = meshes[ i ];
-							var skinEntry = dependencies.skins[ nodeDef.skin ];
+		// scene node hierachy builder
 
-							// Replace Mesh with SkinnedMesh.
-							var geometry = mesh.geometry;
-							var material = mesh.material;
-							material.skinning = true;
+		function buildNodeHierachy( nodeId, parentObject, json, allNodes, skins ) {
 
-							var skinnedMesh = new THREE.SkinnedMesh( geometry, material );
-							skinnedMesh.morphTargetInfluences = mesh.morphTargetInfluences;
-							skinnedMesh.userData = mesh.userData;
-							skinnedMesh.name = mesh.name;
+			var node = allNodes[ nodeId ];
+			var nodeDef = json.nodes[ nodeId ];
 
-							var bones = [];
-							var boneInverses = [];
+			// build skeleton here as well
 
-							for ( var j = 0, l = skinEntry.joints.length; j < l; j ++ ) {
+			if ( nodeDef.skin !== undefined ) {
 
-								var jointId = skinEntry.joints[ j ];
-								var jointNode = __nodes[ jointId ];
+				var meshes = node.isGroup === true ? node.children : [ node ];
 
-								if ( jointNode ) {
+				for ( var i = 0, il = meshes.length; i < il; i ++ ) {
 
-									bones.push( jointNode );
+					var mesh = meshes[ i ];
+					var skinEntry = skins[ nodeDef.skin ];
 
-									var m = skinEntry.inverseBindMatrices.array;
-									var mat = new THREE.Matrix4().fromArray( m, j * 16 );
-									boneInverses.push( mat );
+					var bones = [];
+					var boneInverses = [];
 
-								} else {
+					for ( var j = 0, jl = skinEntry.joints.length; j < jl; j ++ ) {
 
-									console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', jointId );
+						var jointId = skinEntry.joints[ j ];
+						var jointNode = allNodes[ jointId ];
 
-								}
+						if ( jointNode ) {
 
-							}
+							bones.push( jointNode );
 
-							skinnedMesh.bind( new THREE.Skeleton( bones, boneInverses ), skinnedMesh.matrixWorld );
+							var mat = new THREE.Matrix4();
 
-							skinnedMeshes.push( skinnedMesh );
+							if ( skinEntry.inverseBindMatrices !== undefined ) {
 
-						}
+								mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 );
 
-						if ( node.children.length > 0 ) {
+							}
 
-							node.remove.apply( node, node.children );
-							node.add.apply( node, skinnedMeshes );
+							boneInverses.push( mat );
 
 						} else {
 
-							node = skinnedMeshes[ 0 ];
+							console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', jointId );
 
 						}
 
 					}
 
-					return node;
-
-				} );
-
-			} );
-
-		} );
-
-	};
-
-	GLTFParser.prototype.loadScenes = function () {
-
-		var json = this.json;
-		var extensions = this.extensions;
+					mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld );
 
-		// scene node hierachy builder
+				}
 
-		function buildNodeHierachy( nodeId, parentObject, allNodes ) {
+			}
 
-			var _node = allNodes[ nodeId ];
-			parentObject.add( _node );
+			// build node hierachy
 
-			var node = json.nodes[ nodeId ];
+			parentObject.add( node );
 
-			if ( node.children ) {
+			if ( nodeDef.children ) {
 
-				var children = node.children;
+				var children = nodeDef.children;
 
-				for ( var i = 0, l = children.length; i < l; i ++ ) {
+				for ( var i = 0, il = children.length; i < il; i ++ ) {
 
 					var child = children[ i ];
-					buildNodeHierachy( child, _node, allNodes );
+					buildNodeHierachy( child, node, json, allNodes, skins );
 
 				}
 
@@ -2474,56 +2752,49 @@ THREE.GLTFLoader = ( function () {
 
 		}
 
-		return this._withDependencies( [
-
-			'nodes'
-
-		] ).then( function ( dependencies ) {
-
-			return _each( json.scenes, function ( scene ) {
-
-				var _scene = new THREE.Scene();
-				if ( scene.name !== undefined ) _scene.name = scene.name;
+		return function loadScene( sceneIndex ) {
 
-				if ( scene.extras ) _scene.userData = scene.extras;
+			var json = this.json;
+			var extensions = this.extensions;
+			var sceneDef = this.json.scenes[ sceneIndex ];
 
-				var nodes = scene.nodes || [];
+			return this.getMultiDependencies( [
 
-				for ( var i = 0, l = nodes.length; i < l; i ++ ) {
+				'node',
+				'skin'
 
-					var nodeId = nodes[ i ];
-					buildNodeHierachy( nodeId, _scene, dependencies.nodes );
+			] ).then( function ( dependencies ) {
 
-				}
+				var scene = new THREE.Scene();
+				if ( sceneDef.name !== undefined ) scene.name = sceneDef.name;
 
-				_scene.traverse( function ( child ) {
+				if ( sceneDef.extras ) scene.userData = sceneDef.extras;
 
-					// for Specular-Glossiness.
-					if ( child.material && child.material.isGLTFSpecularGlossinessMaterial ) {
+				var nodeIds = sceneDef.nodes || [];
 
-						child.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms;
+				for ( var i = 0, il = nodeIds.length; i < il; i ++ ) {
 
-					}
+					buildNodeHierachy( nodeIds[ i ], scene, json, dependencies.nodes, dependencies.skins );
 
-				} );
+				}
 
 				// Ambient lighting, if present, is always attached to the scene root.
-				if ( scene.extensions
-							 && scene.extensions[ EXTENSIONS.KHR_LIGHTS ]
-							 && scene.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {
+				if ( sceneDef.extensions
+						 && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ]
+						 && sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light !== undefined ) {
 
 					var lights = extensions[ EXTENSIONS.KHR_LIGHTS ].lights;
-					_scene.add( lights[ scene.extensions[ EXTENSIONS.KHR_LIGHTS ].light ] );
+					scene.add( lights[ sceneDef.extensions[ EXTENSIONS.KHR_LIGHTS ].light ] );
 
 				}
 
-				return _scene;
+				return scene;
 
 			} );
 
-		} );
+		};
 
-	};
+	}();
 
 	return GLTFLoader;
 
diff --git a/webpack.config.js b/webpack.config.js
index 8448e44fdd15f209315286bda8a57bcf2b7d9556..47691454412d6ed538d46f2c2091126190e3db38 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -114,7 +114,13 @@ module.exports = {
         loader: "html-loader",
         options: {
           // <a-asset-item>'s src property is overwritten with the correct transformed asset url.
-          attrs: ["img:src", "a-asset-item:src"],
+          attrs: [
+            "img:src",
+            "a-asset-item:src",
+            "a-progressive-asset:src",
+            "a-progressive-asset:high-src",
+            "a-progressive-asset:low-src"
+          ],
           // You can get transformed asset urls in an html template using ${require("pathToFile.ext")}
           interpolate: "require"
         }