diff --git a/.eslintrc.js b/.eslintrc.js
index 1cadfd103fd1910f79fb0e7928b0663b9e6a248a..fcd01630f75612e9262f216b79101005e14e7d5d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -14,6 +14,7 @@ module.exports = {
   rules: {
     "prettier/prettier": "error",
     "prefer-const": "error",
+    "no-use-before-define": "error",
     "no-var": "error",
     "no-throw-literal": "error",
     // Light console usage is useful but remove debug logs before merging to master.
diff --git a/package.json b/package.json
index 7ee4cd06bf14b02983d687d954184761a1d60bf2..70702a53f8890c2952a2d3d9f4de22eabb61e092 100644
--- a/package.json
+++ b/package.json
@@ -19,11 +19,11 @@
     "@fortawesome/fontawesome-free-solid": "^5.0.9",
     "@fortawesome/react-fontawesome": "^0.0.18",
     "aframe-billboard-component": "^1.0.0",
-    "aframe-extras": "^3.12.4",
+    "aframe-extras": "^4.0.0",
     "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array",
     "aframe-physics-extras": "https://github.com/infinitelee/aframe-physics-extras#fix/physics-collider-crash",
     "aframe-physics-system": "https://github.com/infinitelee/aframe-physics-system#feature/shape-component",
-    "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/pauseable",
+    "aframe-teleport-controls": "^0.3.1",
     "aframe-xr": "github:brianpeiris/aframe-xr#3162aed",
     "classnames": "^2.2.5",
     "detect-browser": "^2.1.0",
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index 1249dff3b0fbb3231bb52bd373e6cd91b7f3a544..8da41b1a2e4bf4f3928a8f74b6a521679b11759a 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -1,5 +1,6 @@
 const CLAMP_VELOCITY = 0.01;
 const MAX_DELTA = 0.2;
+const EPS = 10e-6;
 
 // Does not have any type of collisions yet.
 AFRAME.registerComponent("character-controller", {
@@ -79,6 +80,8 @@ AFRAME.registerComponent("character-controller", {
     const rotationInvMatrix = new THREE.Matrix4();
     const pivotRotationMatrix = new THREE.Matrix4();
     const pivotRotationInvMatrix = new THREE.Matrix4();
+    const start = new THREE.Vector3();
+    let navGroup, navNode;
 
     return function(t, dt) {
       const deltaSeconds = dt / 1000;
@@ -87,6 +90,8 @@ AFRAME.registerComponent("character-controller", {
       const distance = this.data.groundAcc * deltaSeconds;
       const rotationDelta = this.data.rotationSpeed * this.angularVelocity * deltaSeconds;
 
+      start.copy(root.position);
+
       // Other aframe components like teleport-controls set position/rotation/scale, not the matrix, so we need to make sure to compose them back into the matrix
       root.updateMatrix();
 
@@ -128,9 +133,19 @@ AFRAME.registerComponent("character-controller", {
         z: root.rotation.z * THREE.Math.RAD2DEG
       });
 
-      this.el.setAttribute("position", root.position);
-
       this.pendingSnapRotationMatrix.identity(); // Revert to identity
+
+      //copied from aframe-extras movement-controls
+      const nav = this.el.sceneEl.systems.nav;
+      if (nav.navMesh && this.velocity.lengthSq() > EPS) {
+        if (!navGroup) {
+          navGroup = nav.getGroup(start);
+        }
+        navNode = navNode || nav.getNode(start, navGroup);
+        navNode = nav.clampStep(start, root.position, navGroup, navNode, root.position);
+      } else {
+        this.el.setAttribute("position", root.position);
+      }
     };
   })(),
 
diff --git a/src/components/gltf-bundle.js b/src/components/gltf-bundle.js
index 38120d8f369389995839c5eb9836e6682fc4b98f..ba6f7b3dc6d78aa08646352e49b6e3f91031056a 100644
--- a/src/components/gltf-bundle.js
+++ b/src/components/gltf-bundle.js
@@ -23,7 +23,7 @@ AFRAME.registerComponent("gltf-bundle", {
 
       const src = new URL(asset.src, this.baseURL).href;
       const gltfEl = document.createElement("a-entity");
-      gltfEl.setAttribute("gltf-model-plus", { src });
+      gltfEl.setAttribute("gltf-model-plus", { src, inflate: true });
       loaded.push(new Promise(resolve => gltfEl.addEventListener("model-loaded", resolve)));
       this.el.appendChild(gltfEl);
     }
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 3864f9327c92ce63deddf8f08766a9316cf4a3da..d9ff8d8ff722388d3b0a63f4731d5541e91bde5e 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -127,7 +127,7 @@ const inflateEntities = function(parentEl, node) {
   const entityComponents = node.userData.components;
   if (entityComponents) {
     for (const prop in entityComponents) {
-      if (entityComponents.hasOwnProperty(prop)) {
+      if (entityComponents.hasOwnProperty(prop) && AFRAME.GLTFModelPlus.components.hasOwnProperty(prop)) {
         const { inflator, componentName } = AFRAME.GLTFModelPlus.components[prop];
 
         if (inflator) {
diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js
index 691b76ae73813b7c52ca5b4e10730aa051878bea..c93bf8da15234ed38ab1eba7b55712f59adea20f 100644
--- a/src/components/super-cursor.js
+++ b/src/components/super-cursor.js
@@ -118,7 +118,7 @@ AFRAME.registerComponent("super-cursor", {
   _handleMouseDown: function() {
     if (this.isInteractable) {
       const lookControls = this.data.camera.components["look-controls"];
-      lookControls.pause();
+      if (lookControls) lookControls.pause();
     }
     this.data.cursor.emit("action_grab", {});
   },
@@ -129,7 +129,7 @@ AFRAME.registerComponent("super-cursor", {
 
   _handleMouseUp: function() {
     const lookControls = this.data.camera.components["look-controls"];
-    lookControls.play();
+    if (lookControls) lookControls.play();
     this.data.cursor.emit("action_release", {});
   },
 
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index d1f9d87b0adc70e470af1ce3218008b4929a53a9..2166e7a62a186c9caa1ce3da8f39b5cafa4fff31 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -3,6 +3,5 @@ import "./components/gltf-model-plus";
 AFRAME.GLTFModelPlus.registerComponent("scale-audio-feedback", "scale-audio-feedback");
 AFRAME.GLTFModelPlus.registerComponent("loop-animation", "loop-animation");
 AFRAME.GLTFModelPlus.registerComponent("shape", "shape");
-AFRAME.GLTFModelPlus.registerComponent("box-shape", "box-shape");
-AFRAME.GLTFModelPlus.registerComponent("sphere-shape", "sphere-shape");
-AFRAME.GLTFModelPlus.registerComponent("cylinder-shape", "cylinder-shape");
+AFRAME.GLTFModelPlus.registerComponent("visible", "visible");
+AFRAME.GLTFModelPlus.registerComponent("nav-mesh", "nav-mesh");
diff --git a/src/hub.html b/src/hub.html
index d6ab08e28441a2081f12c092f53085bad23544b3..d032444c9b1c41d29726a8982c9331dab3bbffb4 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -19,7 +19,7 @@
 
     <a-scene
         networked-scene="adapter: janus; audio: true; debug: true; connectOnLoad: false;"
-        physics
+        physics="debug: true"
         mute-mic="eventSrc: a-scene; toggleEvents: action_mute"
         personal-space-bubble="debug: false;"
 
@@ -135,10 +135,10 @@
             scale="2 2 2"
             class="interactable" 
             super-spawner="template: #interactable-template;" 
-            position="2.5 1.2 0" 
+            position="2.9 1.2 0" 
             body="mass: 0; type: static; shape: box;"
         ></a-entity>
-
+        
         <a-entity
             id="super-cursor"
             super-cursor="cursor: #3d-cursor; camera: #player-camera;"
@@ -187,7 +187,6 @@
                 camera
                 position="0 1.6 0"
                 personal-space-bubble="radius: 0.4"
-                look-controls
             ></a-entity>
 
             <a-entity
@@ -195,7 +194,11 @@
                 class="left-controller"
                 hand-controls2="left"
                 tracked-controls
-                teleport-controls="cameraRig: #player-rig; teleportOrigin: #player-camera; button: action_teleport_"
+                teleport-controls="
+                    cameraRig: #player-rig; 
+                    teleportOrigin: #player-camera; 
+                    button: action_teleport_; 
+                    collisionEntities: [nav-mesh]"
                 app-mode-toggle-playing__teleport-controls="mode: hud; invert: true;"
                 haptic-feedback
             ></a-entity>
@@ -205,7 +208,11 @@
                 class="right-controller"
                 hand-controls2="right"
                 tracked-controls
-                teleport-controls="cameraRig: #player-rig; teleportOrigin: #player-camera; button: action_teleport_"
+                teleport-controls="
+                    cameraRig: #player-rig; 
+                    teleportOrigin: #player-camera; 
+                    button: action_teleport_; 
+                    collisionEntities: [nav-mesh]"
                 haptic-feedback
                 raycaster="objects:.hud; showLine: true; far: 2;"
                 cursor="fuse: false; downEvents: action_ui_select_down; upEvents: action_ui_select_up;"
@@ -237,14 +244,6 @@
 
                 <template data-selector=".LeftHand">
                     <a-entity bone-visibility>
-                        <a-entity
-                            id="watch"
-                            gltf-model-plus="src: #watch-model"
-                            bone-mute-state-indicator
-                            scale="1.5 1.5 1.5"
-                            rotation="0 -90 90"
-                            position="0 -0.04 0"
-                        ></a--entity>
                         <a-entity
                             event-repeater="events: action_grab, action_release; eventSource: #player-left-controller"
                             static-body="shape: sphere; sphereRadius: 0.02"
diff --git a/src/hub.js b/src/hub.js
index 0b34551cea7e804f2e0ab48d729dbcc1972d58f6..fc843649946ff6630563dfa1c1efd78e1d2b00d1 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -69,6 +69,7 @@ if (qs.quality) {
 
 import "aframe-physics-system";
 import "aframe-physics-extras";
+import "aframe-extras/src/pathfinding";
 import "super-hands";
 import "./components/super-networked-interactable";
 import "./components/networked-counter";
@@ -126,6 +127,7 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
   const scene = document.querySelector("a-scene");
   const playerRig = document.querySelector("#player-rig");
   document.querySelector("a-scene canvas").classList.remove("blurred");
+  scene.render();
 
   if (enterInVR) {
     scene.enterVR();
@@ -133,7 +135,7 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
 
   AFRAME.registerInputActions(inGameActions, "default");
 
-  document.querySelector("#player-camera").setAttribute("look-controls");
+  document.querySelector("#player-camera").setAttribute("look-controls", "");
 
   scene.setAttribute("networked-scene", {
     room: janusRoomId,
@@ -179,11 +181,7 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
     screenEntity.setAttribute("visible", sharingScreen);
   });
 
-  if (qsTruthy("offline")) {
-    onConnect();
-  } else {
-    document.body.addEventListener("connected", onConnect);
-
+  if (!qsTruthy("offline")) {
     scene.components["networked-scene"].connect();
 
     if (mediaStream) {
@@ -207,8 +205,6 @@ async function enterScene(mediaStream, enterInVR, janusRoomId) {
   }
 }
 
-function onConnect() {}
-
 function mountUI(scene) {
   const disableAutoExitOnConcurrentLoad = qsTruthy("allow_multi");
   const forcedVREntryType = qs.vr_entry_type || null;
@@ -255,7 +251,11 @@ const onReady = async () => {
   const environmentRoot = document.querySelector("#environment-root");
 
   const initialEnvironmentEl = document.createElement("a-entity");
-  initialEnvironmentEl.addEventListener("bundleloaded", () => uiRoot.setState({ initialEnvironmentLoaded: true }));
+  initialEnvironmentEl.addEventListener("bundleloaded", () => {
+    uiRoot.setState({ initialEnvironmentLoaded: true });
+    // Wait a tick so that the environments actually render.
+    setTimeout(() => scene.renderer.animate(null));
+  });
   environmentRoot.appendChild(initialEnvironmentEl);
 
   if (qs.room) {
diff --git a/src/vendor/GLTFLoader.js b/src/vendor/GLTFLoader.js
index 25e397ce98c7ebc2be929195c6863338154c01bc..7610044b41b6dd9956b2d84984e46671de46e494 100644
--- a/src/vendor/GLTFLoader.js
+++ b/src/vendor/GLTFLoader.js
@@ -29,6 +29,8 @@ THREE.GLTFLoader = ( function () {
 
 			var scope = this;
 
+			scope.url = url;
+
 			var path = this.path !== undefined ? this.path : THREE.LoaderUtils.extractUrlBase( url );
 
 			var loader = new THREE.FileLoader( scope.manager );
@@ -82,6 +84,7 @@ THREE.GLTFLoader = ( function () {
 
 		parse: function ( data, path, onLoad, onError ) {
 
+			var scope = this;
 			var content;
 			var extensions = {};
 
@@ -159,7 +162,7 @@ THREE.GLTFLoader = ( function () {
 
 			}
 
-			console.time( 'GLTFLoader' );
+			console.time( `GLTFLoader - ${scope.url}` );
 
 			var parser = new GLTFParser( json, extensions, {
 
@@ -171,7 +174,7 @@ THREE.GLTFLoader = ( function () {
 
 			parser.parse( function ( scene, scenes, cameras, animations, asset ) {
 
-				console.timeEnd( 'GLTFLoader' );
+				console.timeEnd( `GLTFLoader - ${scope.url}` );
 
 				var glTF = {
 					scene: scene,
diff --git a/yarn.lock b/yarn.lock
index c89c01a87f233fd07aeabbd529dc7e7729636de2..e5a3f201925ccf10aff81f04c80d3bdc520e34bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -160,12 +160,11 @@ aframe-billboard-component@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/aframe-billboard-component/-/aframe-billboard-component-1.0.0.tgz#10ce2482729eef7386c5844d65917581a62d3adc"
 
-aframe-extras@^3.12.4:
-  version "3.13.1"
-  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-3.13.1.tgz#f8b6ef18c29e92538d05d94913640942a307c46c"
+aframe-extras@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-4.0.0.tgz#fc851e2a1312c30a4d4addc3e0fa2dbf3e723ead"
   dependencies:
-    aframe-physics-system "^1.4.3"
-    three-pathfinding "^0.2.2"
+    three-pathfinding "^0.5.5"
 
 "aframe-input-mapping-component@https://github.com/johnshaughnessy/aframe-input-mapping-component#feature/map-to-array":
   version "0.1.2"
@@ -181,13 +180,6 @@ aframe-lerp-component@^1.1.0:
   version "0.1.2"
   resolved "https://github.com/infinitelee/aframe-physics-extras#49b2d5d3c0caac905783aee51d9e89dbdf7199b8"
 
-aframe-physics-system@^1.4.3:
-  version "1.4.3"
-  resolved "https://registry.yarnpkg.com/aframe-physics-system/-/aframe-physics-system-1.4.3.tgz#c6927e847081bfe546658314aa4c04958ef27934"
-  dependencies:
-    cannon "github:donmccurdy/cannon.js#v0.6.2-dev1"
-    three-to-cannon "^1.1.1"
-
 "aframe-physics-system@https://github.com/infinitelee/aframe-physics-system#feature/shape-component":
   version "3.0.2"
   resolved "https://github.com/infinitelee/aframe-physics-system#c8add507adcb6d67ecb7613924662e413a811454"
@@ -198,9 +190,9 @@ aframe-physics-system@^1.4.3:
     three-to-cannon "^1.2.0"
     webworkify "^1.4.0"
 
-"aframe-teleport-controls@https://github.com/netpro2k/aframe-teleport-controls#feature/pauseable":
-  version "0.3.2"
-  resolved "https://github.com/netpro2k/aframe-teleport-controls#7f67003dd3bd1348357fbf89aaeed916ef2d4016"
+aframe-teleport-controls@^0.3.1:
+  version "0.3.1"
+  resolved "https://registry.yarnpkg.com/aframe-teleport-controls/-/aframe-teleport-controls-0.3.1.tgz#7d7ef206f483ea92425a6333b0f8fb26c9596d1c"
 
 "aframe-xr@github:brianpeiris/aframe-xr#3162aed":
   version "0.0.9"
@@ -7622,11 +7614,11 @@ textextensions@2:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286"
 
-three-pathfinding@^0.2.2:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/three-pathfinding/-/three-pathfinding-0.2.3.tgz#469bb26fb6b331f536c9ec88fde78e9c9219f637"
+three-pathfinding@^0.5.5:
+  version "0.5.5"
+  resolved "https://registry.yarnpkg.com/three-pathfinding/-/three-pathfinding-0.5.5.tgz#ff05a8f8cfba343b3b07c05c25d11b0d161911f8"
 
-three-to-cannon@^1.1.1, three-to-cannon@^1.2.0:
+three-to-cannon@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/three-to-cannon/-/three-to-cannon-1.2.0.tgz#92b9a756a270851aa98c3058c51ef15891507c01"