diff --git a/package.json b/package.json
index 215cee49a9b31300f977579f29c697e42658e83f..b952d3a7ee378bc854b2e2adf13a28d9f3906727 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
     "jsonschema": "^1.2.2",
     "material-design-lite": "^1.3.0",
     "minijanus": "^0.4.0",
-    "naf-janus-adapter": "^0.3.0",
+    "naf-janus-adapter": "^0.4.0",
     "networked-aframe": "https://github.com/mozillareality/networked-aframe#mr-social-client/master",
     "nipplejs": "^0.6.7",
     "query-string": "^5.0.1",
diff --git a/src/App.js b/src/App.js
new file mode 100644
index 0000000000000000000000000000000000000000..c542f439f903e0a4426d087cb33d69852b25944d
--- /dev/null
+++ b/src/App.js
@@ -0,0 +1,21 @@
+export class App {
+  constructor() {
+    this.scene = null;
+    this.quality = "low";
+  }
+
+  setQuality(quality) {
+    if (this.quality === quality) {
+      return false;
+    }
+
+    this.quality = quality;
+
+    if (this.scene) {
+      console.log("quality-changed", quality);
+      this.scene.dispatchEvent(new CustomEvent("quality-changed", { detail: quality }));
+    }
+
+    return true;
+  }
+}
diff --git a/src/assets/avatars/BotDefault_Avatar.glb b/src/assets/avatars/BotDefault_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..ccb77bf21362f3a5b2058f59eafd56fa17a55ec4
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..ba33589d4c42fe6bf9756d5164f331e9830dab91
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/assets/hud/watch.bin b/src/assets/hud/watch.bin
deleted file mode 100644
index c90e4ecbe70f2623bfab3bcbf75dbee0e96a4144..0000000000000000000000000000000000000000
Binary files a/src/assets/hud/watch.bin and /dev/null differ
diff --git a/src/assets/hud/watch.glb b/src/assets/hud/watch.glb
index 4d6d9ed1673bc650558091c521edf13b086073a9..306fcfd7414d24584ecbf59aa3942318b2f33211 100644
Binary files a/src/assets/hud/watch.glb and b/src/assets/hud/watch.glb differ
diff --git a/src/assets/hud/watch.gltf b/src/assets/hud/watch.gltf
deleted file mode 100644
index d7fb78d83fa60dab9c23749699f814f344bbb232..0000000000000000000000000000000000000000
--- a/src/assets/hud/watch.gltf
+++ /dev/null
@@ -1,139 +0,0 @@
-{
-    "accessors" : [
-        {
-            "bufferView" : 0, 
-            "componentType" : 5121, 
-            "count" : 204, 
-            "max" : [
-                119
-            ], 
-            "min" : [
-                0
-            ], 
-            "type" : "SCALAR"
-        }, 
-        {
-            "bufferView" : 1, 
-            "componentType" : 5126, 
-            "count" : 120, 
-            "max" : [
-                0.10501318424940109, 
-                0.04724307730793953, 
-                0.10020249336957932
-            ], 
-            "min" : [
-                -0.10501328855752945, 
-                0.01861731894314289, 
-                -0.10663323104381561
-            ], 
-            "type" : "VEC3"
-        }, 
-        {
-            "bufferView" : 2, 
-            "componentType" : 5126, 
-            "count" : 120, 
-            "max" : [
-                0.9848077297210693, 
-                1.0, 
-                1.0
-            ], 
-            "min" : [
-                -0.9848077297210693, 
-                -1.0, 
-                -0.9396926164627075
-            ], 
-            "type" : "VEC3"
-        }
-    ], 
-    "asset" : {
-        "generator" : "Khronos Blender glTF 2.0 exporter", 
-        "version" : "2.0"
-    }, 
-    "bufferViews" : [
-        {
-            "buffer" : 0, 
-            "byteLength" : 204, 
-            "byteOffset" : 0, 
-            "target" : 34963
-        }, 
-        {
-            "buffer" : 0, 
-            "byteLength" : 1440, 
-            "byteOffset" : 204, 
-            "target" : 34962
-        }, 
-        {
-            "buffer" : 0, 
-            "byteLength" : 1440, 
-            "byteOffset" : 1644, 
-            "target" : 34962
-        }
-    ], 
-    "buffers" : [
-        {
-            "byteLength" : 3084, 
-            "uri" : "watch.bin"
-        }
-    ], 
-    "materials" : [
-        {
-            "name" : "Material.001",
-            "emissiveFactor": [
-                0.6400000190734865,
-                0.6400000190734865,
-                0.6400000190734865
-            ],
-            "pbrMetallicRoughness" : {
-                "baseColorFactor" : [
-                    0.6400000190734865, 
-                    0.6400000190734865, 
-                    0.6400000190734865, 
-                    1.0
-                ], 
-                "metallicFactor" : 0.0,
-                "roughnessFactor": 0
-            },
-            "extensions": {
-                "KHR_materials_cmnConstant": {}
-            }
-        }
-    ], 
-    "meshes" : [
-        {
-            "name" : "Cylinder.001", 
-            "primitives" : [
-                {
-                    "attributes" : {
-                        "NORMAL" : 2, 
-                        "POSITION" : 1
-                    }, 
-                    "indices" : 0, 
-                    "material" : 0
-                }
-            ]
-        }
-    ], 
-    "nodes" : [
-        {
-            "mesh" : 0, 
-            "name" : "Watch", 
-            "scale" : [
-                0.6932165622711182, 
-                0.6932165622711182, 
-                0.6932165622711182
-            ]
-        }
-    ], 
-    "scene" : 0, 
-    "scenes" : [
-        {
-            "name" : "Scene", 
-            "nodes" : [
-                0
-            ]
-        }
-    ],
-    "extensionsUsed": [
-        "KHR_materials_cmnConstant"
-    ]
-}
diff --git a/src/components/hide-when-quality.js b/src/components/hide-when-quality.js
new file mode 100644
index 0000000000000000000000000000000000000000..e09d3ae90db3ce48e7afb815b5e0fcf22aa79fc3
--- /dev/null
+++ b/src/components/hide-when-quality.js
@@ -0,0 +1,23 @@
+AFRAME.registerComponent("hide-when-quality", {
+  schema: { type: "string", default: "low" },
+
+  init() {
+    this.onQualityChanged = this.onQualityChanged.bind(this);
+    this.el.sceneEl.addEventListener("quality-changed", this.onQualityChanged);
+  },
+
+  onQualityChanged(event) {
+    this.updateComponentState(event.detail);
+  },
+
+  update(oldData) {
+    if (this.data !== oldData) {
+      this.updateComponentState(window.APP.quality);
+    }
+  },
+
+  updateComponentState(quality) {
+    console.log(quality);
+    this.el.setAttribute("visible", quality !== this.data);
+  }
+});
diff --git a/src/components/networked-video-player.css b/src/components/networked-video-player.css
index e6859395801b79bddc37c85c53a92e6a1780c8ec..00dd3a89730b18ba210d2b981c0929399c3f1902 100644
--- a/src/components/networked-video-player.css
+++ b/src/components/networked-video-player.css
@@ -1,12 +1,11 @@
 :local(.video) {
-  height: 100px;
+  /* 1x1px so that Safari on iOS allows us to autoplay the video */
+  width: 1px; height: 1px; /* toggle to show debug video elements */
   background: black;
-  margin: 5px;
 }
 
 :local(.container) {
     position: absolute;
     bottom: 0;
-    display: flex;
-    visibility: hidden; /* toggle to show debug video elements */
+    width: 10px; height: 10px; /* toggle to show debug video elements */
 }
diff --git a/src/components/networked-video-player.js b/src/components/networked-video-player.js
index 6e0218a9f0ace62a506be85d56036b17c00bfdb6..03cfba4f6d7a835ac825804724a4109d16690cdb 100644
--- a/src/components/networked-video-player.js
+++ b/src/components/networked-video-player.js
@@ -2,9 +2,7 @@ import styles from "./networked-video-player.css";
 
 const nafConnected = function() {
   return new Promise(resolve => {
-    NAF.clientId
-      ? resolve()
-      : document.body.addEventListener("connected", resolve);
+    NAF.clientId ? resolve() : document.body.addEventListener("connected", resolve);
   });
 };
 
@@ -21,21 +19,23 @@ AFRAME.registerComponent("networked-video-player", {
 
     await nafConnected();
 
-    const networkedEl = NAF.utils.getNetworkedEntity(this.el);
+    const networkedEl = await NAF.utils.getNetworkedEntity(this.el);
     if (!networkedEl) {
-      throw new Error(
-        "Video player must be added on a node, or a child of a node, with the `networked` component."
-      );
+      throw new Error("Video player must be added on a node, or a child of a node, with the `networked` component.");
     }
 
     const ownerId = networkedEl.components.networked.data.owner;
-    const stream = await NAF.connection.adapter.getMediaStream(ownerId);
+    const stream = await NAF.connection.adapter.getMediaStream(ownerId, "video");
     if (!stream) {
       return;
     }
 
     const v = document.createElement("video");
     v.id = `nvp-video-${ownerId}`;
+    // muted and autoplay so that more restrictive browsers (e.g. Safari on iOS) will actually play the video.
+    v.muted = true;
+    v.autoplay = true;
+    v.playsInline = true;
     v.classList.add(styles.video);
     v.srcObject = new MediaStream(stream.getVideoTracks()); // We only want the video track so make a new MediaStream
     container.appendChild(v);
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index ee59e3a24ead224a4095c69c70d2ea0da1c9cbd5..7caec327629a2d4059f0254a5a0438afe712b46b 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -1,5 +1,24 @@
 const GLTFCache = {};
 
+AFRAME.AGLTFEntity = {
+  defaultInflator(el, componentName, componentData) {
+    if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) {
+      for (let i = 0; i < componentData.length; i++) {
+        el.setAttribute(componentName + "__" + i, componentData[i]);
+      }
+    } else {
+      el.setAttribute(componentName, componentData);
+    }
+  },
+  registerComponent(componentKey, componentName, inflator) {
+    AFRAME.AGLTFEntity.components[componentKey] = {
+      inflator: inflator || AFRAME.AGLTFEntity.defaultInflator,
+      componentName
+    };
+  },
+  components: {}
+};
+
 // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
 function cloneGltf(gltf) {
@@ -83,6 +102,20 @@ const inflateEntities = function(classPrefix, parentEl, node) {
 
   el.setObject3D(node.type.toLowerCase(), node);
 
+  const entityComponents = node.userData.components;
+
+  if (entityComponents) {
+    for (const prop in entityComponents) {
+      if (entityComponents.hasOwnProperty(prop)) {
+        const { inflator, componentName } = AFRAME.AGLTFEntity.components[prop];
+
+        if (inflator) {
+          inflator(el, componentName, entityComponents[prop]);
+        }
+      }
+    }
+  }
+
   children.forEach(childNode => {
     inflateEntities(classPrefix, el, childNode);
   });
@@ -121,7 +154,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 && window.APP.quality === "high") {
+            src = highSrc;
+          } else if (lowSrc && window.APP.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..a367e374aece30163bc73802ab8cf11950f28c1a
--- /dev/null
+++ b/src/elements/a-progressive-asset.js
@@ -0,0 +1,67 @@
+/**
+ * Modified version of a-asset-item that adds high-src and low-src options
+ * Extracted from https://github.com/aframevr/aframe/blob/master/src/core/a-assets.js
+ */
+
+AFRAME.registerElement("a-progressive-asset", {
+  prototype: Object.create(AFRAME.ANode.prototype, {
+    createdCallback: {
+      value() {
+        this.data = null;
+        this.isAssetItem = true;
+
+        if (!this.parentNode.fileLoader) {
+          throw new Error("a-progressive-asset must be the child of an a-assets element.");
+        }
+
+        this.fileLoader = this.parentNode.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 (window.APP.quality === "high") {
+          src = highSrc;
+        } else if (window.APP.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/gltf-component-mappings.js b/src/gltf-component-mappings.js
new file mode 100644
index 0000000000000000000000000000000000000000..e73692a8ff9270985f3b533ff8bd1f8fdc0cb835
--- /dev/null
+++ b/src/gltf-component-mappings.js
@@ -0,0 +1,3 @@
+import "./elements/a-gltf-entity";
+
+AFRAME.AGLTFEntity.registerComponent("scale-audio-feedback", "scale-audio-feedback");
diff --git a/src/room.html b/src/room.html
index 85f370c9764e09124e9e7eab16e87799ea5ebed9..8ef44dfb116b36ee8de5c6a0f320774f5faab60c 100644
--- a/src/room.html
+++ b/src/room.html
@@ -7,9 +7,9 @@
 
     <meta http-equiv="origin-trial" data-feature="WebVR (For Chrome M62+)" data-expires="<%= ORIGIN_TRIAL_EXPIRES %>" content="<%= ORIGIN_TRIAL_TOKEN %>">
     <% if(NODE_ENV === "production") { %>
-        <script src="https://cdn.rawgit.com/brianpeiris/aframe/bba200440e3279753df85a1f52ba4c77a3b16e47/dist/aframe-master.min.js"></script>
+        <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script>
     <% } else { %>
-        <script src="https://cdn.rawgit.com/brianpeiris/aframe/bba200440e3279753df85a1f52ba4c77a3b16e47/dist/aframe-master.js"></script>
+        <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script>
     <% } %>
 </head>
 
@@ -28,7 +28,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>
@@ -44,10 +51,10 @@
                 <a-entity class="video" geometry="primitive: plane;" material="side: double" networked-video-player></a-entity>
             </template>
 
-            <template id="remote-avatar-template">                
+            <template id="remote-avatar-template">
                 <a-entity ik-root>
                     <a-entity class="camera"></a-entity>
-                    
+
                     <a-entity class="left-controller"></a-entity>
 
                     <a-entity class="right-controller"></a-entity>
@@ -64,14 +71,12 @@
                                 ></a-entity>
                              </a-entity>
                         </template>
-                        
+
                         <template data-selector=".Head">
                             <a-entity
                                 networked-audio-source
                                 networked-audio-analyser
-                                scale-audio-feedback
                                 personal-space-invader
-                                animation-mixer
                             >
                             </a-entity>
                         </template>
@@ -106,7 +111,7 @@
                 personal-space-bubble
                 look-controls
             ></a-entity>
-            
+
             <a-entity
                 id="player-left-controller"
                 class="left-controller"
@@ -151,6 +156,13 @@
             </a-gltf-entity>
         </a-entity>
 
+        <!-- Lights -->
+        <a-entity
+            hide-when-quality="low"
+            light="type: directional; color: #F9FFCE; intensity: 0.6"
+            position="0.002 5.231 -15.3"
+        ></a-entity>
+
         <!-- Environment -->
         <a-gltf-entity
             id="meeting-space"
diff --git a/src/room.js b/src/room.js
index 218783a060662b9b4f9c99b784f8fcdca6995e4e..afe163d273ff9825a2f596188cfa5dbf6b8f9008 100644
--- a/src/room.js
+++ b/src/room.js
@@ -38,6 +38,7 @@ import "./components/water";
 import "./components/skybox";
 import "./components/layers";
 import "./components/spawn-controller";
+import "./components/hide-when-quality";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -45,7 +46,22 @@ import UIRoot from "./react-components/ui-root";
 
 import "./systems/personal-space-bubble";
 
-import "./elements/a-gltf-entity";
+import "./gltf-component-mappings";
+
+import { App } from "./App";
+
+window.APP = new App();
+
+const qs = queryString.parse(location.search);
+const isMobile = AFRAME.utils.device.isMobile();
+
+if (qs.quality) {
+  window.APP.quality = qs.quality;
+} else {
+  window.APP.quality = isMobile ? "low" : "high";
+}
+
+import "./elements/a-progressive-asset";
 
 import registerNetworkSchemas from "./network-schemas";
 import { inGameActions, config } from "./input-mappings";
@@ -70,6 +86,33 @@ const store = new Store();
 // Always layer in any new default profile bits
 store.update({ profile:  { ...generateDefaultProfile(), ...(store.state.profile || {}) }})
 
+async function shareMedia(audio, video) {
+  const constraints = {
+    audio: !!audio,
+    video: video ? { mediaSource: "screen", height: 720, frameRate: 30 } : false
+  };
+  const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
+  NAF.connection.adapter.setLocalMediaStream(mediaStream);
+
+  const id = `${NAF.clientId}-screen`;
+  let entity = document.getElementById(id);
+  if (entity) {
+    entity.setAttribute("visible", !!video);
+  } else if (video) {
+    const sceneEl = document.querySelector("a-scene");
+    entity = document.createElement("a-entity");
+    entity.id = id;
+    entity.setAttribute("offset-relative-to", {
+      target: "#player-camera",
+      offset: "0 0 -2",
+      on: "action_share_screen"
+    });
+    entity.setAttribute("networked", { template: "#video-template" });
+    sceneEl.appendChild(entity);
+  }
+}
+
+
 async function enterScene(mediaStream) {
   const qs = queryString.parse(location.search);
   const scene = document.querySelector("a-scene");
@@ -83,7 +126,7 @@ async function enterScene(mediaStream) {
     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", {});
   }
@@ -148,4 +191,9 @@ function mountUI() {
   });
 }
 
-document.addEventListener("DOMContentLoaded", () => mountUI());
+document.addEventListener("DOMContentLoaded", () => {
+  const scene = document.querySelector("a-scene");
+  window.APP.scene = scene;
+
+  mountUI();
+});
diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js
index 90afbfd648c059f98aaa547c31fee06dbe100735..0992a18dba1a474bed8999035e6c5e1376b568b3 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 9b21aae0841e10351ec7bfcdc401b845a3cea869..a9b388b51809208c840e933970169a3eda4f241f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -52,7 +52,7 @@ function createHTTPSConfig() {
     fs.writeFileSync(path.join(__dirname, "certs", "key.pem"), pems.private);
 
     return {
-      key: pems.public,
+      key: pems.private,
       cert: pems.cert
     };
   }
@@ -73,7 +73,7 @@ class LodashTemplatePlugin {
   }
 }
 
-module.exports = {
+const config = {
   entry: {
     lobby: path.join(__dirname, "src", "lobby.js"),
     room: path.join(__dirname, "src", "room.js"),
@@ -81,7 +81,8 @@ module.exports = {
   },
   output: {
     path: path.join(__dirname, "public"),
-    filename: "[name]-[chunkhash].js"
+    filename: "[name]-[chunkhash].js",
+    publicPath: process.env.BASE_ASSETS_PATH || ""
   },
   mode: "development",
   devtool: process.env.NODE_ENV === "production" ? "source-map" : "inline-source-map",
@@ -89,6 +90,7 @@ module.exports = {
     open: true,
     https: createHTTPSConfig(),
     host: "0.0.0.0",
+    useLocalIp: true,
     port: 8080,
     before: function(app) {
       // networked-aframe makes HEAD requests to the server for time syncing. Respond with an empty body.
@@ -114,7 +116,14 @@ 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", "audio:src"],
+          attrs: [
+            "img:src",
+            "a-asset-item:src",
+            "a-progressive-asset:src",
+            "a-progressive-asset:high-src",
+            "a-progressive-asset:low-src",
+            "audio:src"
+          ],
           // You can get transformed asset urls in an html template using ${require("pathToFile.ext")}
           interpolate: "require"
         }
@@ -198,3 +207,30 @@ module.exports = {
     })
   ]
 };
+
+module.exports = () => {
+  if (process.env.GENERATE_SMOKE_TESTS && process.env.BASE_ASSETS_PATH) {
+    const smokeConfig = Object.assign({}, config, {
+      // Set the public path for to point to the correct assets on the smoke-test build.
+      output: Object.assign({}, config.output, {
+        publicPath: process.env.BASE_ASSETS_PATH.replace("://", "://smoke-")
+      }),
+      // For this config
+      plugins: config.plugins.map(plugin => {
+        if (plugin instanceof HTMLWebpackPlugin) {
+          return new HTMLWebpackPlugin(
+            Object.assign({}, plugin.options, {
+              filename: "smoke-" + plugin.options.filename
+            })
+          );
+        }
+
+        return plugin;
+      })
+    });
+
+    return [config, smokeConfig];
+  } else {
+    return config;
+  }
+};
diff --git a/yarn.lock b/yarn.lock
index 2503a504c466982b1187c7e9e220a57082bee5f7..ca49471c81719770f30b8ce7a5765d9bb0490685 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4505,9 +4505,9 @@ mute-stream@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
 
-naf-janus-adapter@^0.3.0:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.3.0.tgz#fee55fe0f4724238da5f87fbb0e7f75cd522905e"
+naf-janus-adapter@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.4.0.tgz#22f14212a14d9e3d30c8d9441978704ff58392f4"
   dependencies:
     debug "^3.1.0"
     minijanus "^0.4.0"