diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index 80260e0fcfbf258492e536fa1ab930811cfef1eb..910123ba8bb2f5e54e64037ac478a1ac2e57e577 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -105,6 +105,7 @@ AFRAME.registerComponent("character-controller", {
     const startScale = new THREE.Vector3();
 
     return function(t, dt) {
+      if (!this.el.sceneEl.is("entered")) return;
       const deltaSeconds = dt / 1000;
       const root = this.el.object3D;
       const pivot = this.data.pivot.object3D;
diff --git a/src/components/gltf-model-plus.js b/src/components/gltf-model-plus.js
index 912490b97e552d7c4aff1b6b69c519c29a66869d..b4d25bb3989aea9619a3ebc2d4f6b3bc8e5de9cf 100644
--- a/src/components/gltf-model-plus.js
+++ b/src/components/gltf-model-plus.js
@@ -1,5 +1,6 @@
 import nextTick from "../utils/next-tick";
 import SketchfabZipWorker from "../workers/sketchfab-zip.worker.js";
+import MobileStandardMaterial from "../materials/MobileStandardMaterial";
 import cubeMapPosX from "../assets/images/cubemap/posx.jpg";
 import cubeMapNegX from "../assets/images/cubemap/negx.jpg";
 import cubeMapPosY from "../assets/images/cubemap/posy.jpg";
@@ -255,8 +256,12 @@ async function loadGLTF(src, contentType, preferredTechnique, onProgress) {
 
   gltf.scene.traverse(object => {
     if (object.material && object.material.type === "MeshStandardMaterial") {
-      object.material.envMap = envMap;
-      object.material.needsUpdate = true;
+      if (preferredTechnique === "KHR_materials_unlit") {
+        object.material = MobileStandardMaterial.fromStandardMaterial(object.material);
+      } else {
+        object.material.envMap = envMap;
+        object.material.needsUpdate = true;
+      }
     }
   });
 
diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js
index 4e50b1a500034affe300603340c5af0b8efb925b..534f796c45c304097768d5e486cac12db4d76664 100644
--- a/src/components/hand-controls2.js
+++ b/src/components/hand-controls2.js
@@ -50,6 +50,13 @@ AFRAME.registerComponent("hand-controls2", {
   init() {
     this.pose = POSES.open;
     this.el.setAttribute("visible", false);
+
+    this.connectedController = null;
+
+    this.onControllerConnected = this.onControllerConnected.bind(this);
+    this.onControllerDisconnected = this.onControllerDisconnected.bind(this);
+    this.el.addEventListener("controllerconnected", this.onControllerConnected);
+    this.el.addEventListener("controllerdisconnected", this.onControllerDisconnected);
   },
 
   update(prevData) {
@@ -109,5 +116,17 @@ AFRAME.registerComponent("hand-controls2", {
       this.pose = pose;
     }
     this.el.setAttribute("visible", hasPose);
+  },
+
+  // Show controller when connected
+  onControllerConnected(e) {
+    this.connectedController = e.detail.name;
+    this.el.setAttribute("visible", true);
+  },
+
+  // Hide controller on disconnect
+  onControllerDisconnected() {
+    this.connectedController = null;
+    this.el.setAttribute("visible", false);
   }
 });
diff --git a/src/components/tools/pen.js b/src/components/tools/pen.js
index 7e4b097b0d36ea65f4a39b1040b16828bd566b6d..dd52ca4ab14030ddf064a9d5ae9f0ef9a7d4456a 100644
--- a/src/components/tools/pen.js
+++ b/src/components/tools/pen.js
@@ -140,6 +140,10 @@ AFRAME.registerComponent("pen", {
 
       this.timeSinceLastDraw = time % this.data.drawFrequency;
     }
+
+    if (this.currentDrawing && !grabber) {
+      this._endDraw();
+    }
   },
 
   //helper function to get normal of direction of drawing cross direction to camera
diff --git a/src/hub.html b/src/hub.html
index b32855a0cda3e9a31fdd2215251c004f62adc5b8..9627bdf36cf679a3207c569968299bf3a29fc7e0 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -31,6 +31,7 @@
         personal-space-bubble="debug: false;"
         vr-mode-ui="enabled: false"
         stats-plus="false"
+        action-to-event__mute="path: /actions/muteMic; event: action_mute;"
     >
 
         <a-assets>
diff --git a/src/materials/MobileStandardMaterial.js b/src/materials/MobileStandardMaterial.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa9e10de101be73807c0259e1eb75f222e4d974d
--- /dev/null
+++ b/src/materials/MobileStandardMaterial.js
@@ -0,0 +1,110 @@
+const VERTEX_SHADER = `
+#include <common>
+#include <uv_pars_vertex>
+#include <uv2_pars_vertex>
+#include <color_pars_vertex>
+#include <fog_pars_vertex>
+#include <morphtarget_pars_vertex>
+#include <skinning_pars_vertex>
+#include <logdepthbuf_pars_vertex>
+#include <clipping_planes_pars_vertex>
+
+void main() {
+  #include <uv_vertex>
+  #include <uv2_vertex>
+  #include <color_vertex>
+  #include <skinbase_vertex>
+
+  #include <begin_vertex>
+  #include <morphtarget_vertex>
+  #include <skinning_vertex>
+  #include <project_vertex>
+  #include <logdepthbuf_vertex>
+
+  #include <worldpos_vertex>
+  #include <clipping_planes_vertex>
+  #include <fog_vertex>
+}
+`;
+
+const FRAGMENT_SHADER = `
+uniform vec3 diffuse;
+uniform vec3 emissive;
+uniform float opacity;
+
+#include <common>
+#include <color_pars_fragment>
+#include <uv_pars_fragment>
+#include <uv2_pars_fragment>
+#include <map_pars_fragment>
+#include <aomap_pars_fragment>
+#include <emissivemap_pars_fragment>
+#include <fog_pars_fragment>
+#include <logdepthbuf_pars_fragment>
+#include <clipping_planes_pars_fragment>
+
+void main() {
+  #include <clipping_planes_fragment>
+
+  vec4 diffuseColor = vec4(diffuse, opacity);
+  ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
+  vec3 totalEmissiveRadiance = emissive;
+
+  #include <logdepthbuf_fragment>
+  #include <map_fragment>
+  #include <color_fragment>
+  #include <alphatest_fragment>
+  #include <emissivemap_fragment>
+
+  reflectedLight.indirectDiffuse += vec3(1.0);
+
+  #include <aomap_fragment>
+
+  reflectedLight.indirectDiffuse *= diffuseColor.rgb;
+
+  vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
+
+  gl_FragColor = vec4(outgoingLight, diffuseColor.a);
+
+  #include <premultiplied_alpha_fragment>
+  #include <tonemapping_fragment>
+  #include <encodings_fragment>
+  #include <fog_fragment>
+}
+`;
+
+export default class MobileStandardMaterial extends THREE.ShaderMaterial {
+  static fromStandardMaterial(material) {
+    const parameters = {
+      vertexShader: VERTEX_SHADER,
+      fragmentShader: FRAGMENT_SHADER,
+      uniforms: {
+        uvTransform: { value: new THREE.Matrix3() },
+        diffuse: { value: material.color },
+        opacity: { value: material.opacity },
+        map: { value: material.map },
+        aoMapIntensity: { value: material.aoMapIntensity },
+        aoMap: { value: material.aoMap },
+        emissive: { value: material.emissive },
+        emissiveMap: { value: material.emissiveMap }
+      },
+      fog: true,
+      lights: false,
+      opacity: material.opacity,
+      transparent: material.transparent,
+      skinning: material.skinning,
+      morphTargets: material.morphTargets
+    };
+
+    const mobileMaterial = new MobileStandardMaterial(parameters);
+
+    mobileMaterial.color = material.color;
+    mobileMaterial.map = material.map;
+    mobileMaterial.aoMap = material.aoMap;
+    mobileMaterial.aoMapIntensity = material.aoMapIntensity;
+    mobileMaterial.emissive = material.emissive;
+    mobileMaterial.emissiveMap = material.emissiveMap;
+
+    return mobileMaterial;
+  }
+}
diff --git a/src/scene-entry-manager.js b/src/scene-entry-manager.js
index ffa5021d8c8db5a326f05c6d2bb92d06ad57b6ce..789bde3192259a19ca9fc0877805addc527f0a45 100644
--- a/src/scene-entry-manager.js
+++ b/src/scene-entry-manager.js
@@ -94,6 +94,8 @@ export default class SceneEntryManager {
         this.store.update({ activity: { lastEnteredAt: new Date().toISOString() } });
       });
     })();
+
+    this.scene.addState("entered");
   };
 
   whenSceneLoaded = callback => {
diff --git a/src/systems/userinput/bindings/keyboard-mouse-user.js b/src/systems/userinput/bindings/keyboard-mouse-user.js
index cd610085e64a1ce33a96f91e5bfe58c41983cab9..d38fb69f82063ba778924edd82eac1aadd0a6730 100644
--- a/src/systems/userinput/bindings/keyboard-mouse-user.js
+++ b/src/systems/userinput/bindings/keyboard-mouse-user.js
@@ -109,6 +109,15 @@ export const keyboardMouseUserBindings = {
       dest: { value: paths.actions.cameraDelta },
       xform: xforms.compose_vec2
     },
+    {
+      src: {
+        value: paths.device.keyboard.key("m")
+      },
+      dest: {
+        value: paths.actions.muteMic
+      },
+      xform: xforms.rising
+    },
     {
       src: {
         value: paths.device.keyboard.key("l")
diff --git a/src/systems/userinput/paths.js b/src/systems/userinput/paths.js
index cca20e88d5bff76fcedfb61320725cd156800860..75f655055b4578c0ce38f7781a561955f3ba5100 100644
--- a/src/systems/userinput/paths.js
+++ b/src/systems/userinput/paths.js
@@ -14,6 +14,7 @@ paths.actions.stopGazeTeleport = "/actions/stopTeleport";
 paths.actions.spawnPen = "/actions/spawnPen";
 paths.actions.ensureFrozen = "/actions/ensureFrozen";
 paths.actions.thaw = "/actions/thaw";
+paths.actions.muteMic = "/actions/muteMic";
 paths.actions.cursor = {};
 paths.actions.cursor.pose = "/actions/cursorPose";
 paths.actions.cursor.grab = "/actions/cursorGrab";
diff --git a/src/systems/userinput/userinput.js b/src/systems/userinput/userinput.js
index eb5b16ef0bf666d70d9d400bb3e312c2810c8432..2ffbdca79db0e2d07d464f32ea320fc8cb31c329 100644
--- a/src/systems/userinput/userinput.js
+++ b/src/systems/userinput/userinput.js
@@ -75,8 +75,6 @@ AFRAME.registerSystem("userinput", {
     this.registeredMappings = new Set([keyboardDebuggingBindings]);
     this.xformStates = new Map();
 
-    this.gamepads = [];
-
     const appAwareTouchscreenDevice = new AppAwareTouchscreenDevice();
     const updateBindingsForVRMode = () => {
       const inVRMode = this.el.sceneEl.is("vr-mode");
@@ -103,17 +101,16 @@ AFRAME.registerSystem("userinput", {
     window.addEventListener(
       "gamepadconnected",
       e => {
-        console.log(e.gamepad);
         let gamepadDevice;
-        if (e.gamepad.id === "OpenVR Gamepad") {
-          for (let i = 0; i < this.activeDevices.length; i++) {
-            const activeDevice = this.activeDevices[i];
-            if (activeDevice.gamepad && activeDevice.gamepad === e.gamepad) {
-              console.warn("ignoring gamepad");
-              return; // multiple connect events without a disconnect event
-            }
+        for (let i = 0; i < this.activeDevices.length; i++) {
+          const activeDevice = this.activeDevices[i];
+          if (activeDevice.gamepad && activeDevice.gamepad === e.gamepad) {
+            console.warn("ignoring gamepad", e.gamepad);
+            return; // multiple connect events without a disconnect event
           }
-          if (this.activeDevices) gamepadDevice = new ViveControllerDevice(e.gamepad);
+        }
+        if (e.gamepad.id === "OpenVR Gamepad") {
+          gamepadDevice = new ViveControllerDevice(e.gamepad);
           this.registeredMappings.add(viveUserBindings);
         } else if (e.gamepad.id.startsWith("Oculus Touch")) {
           gamepadDevice = new OculusTouchControllerDevice(e.gamepad);
@@ -132,16 +129,17 @@ AFRAME.registerSystem("userinput", {
           this.registeredMappings.add(gamepadBindings);
         }
         this.activeDevices.add(gamepadDevice);
-        this.gamepads[e.gamepad.index] = gamepadDevice;
       },
       false
     );
     window.addEventListener(
       "gamepaddisconnected",
       e => {
-        if (this.gamepads[e.gamepad.index]) {
-          this.activeDevices.delete(this.gamepads[e.gamepad.index]);
-          delete this.gamepads[e.gamepad.index];
+        for (const device of this.activeDevices) {
+          if (device.gamepad === e.gamepad) {
+            this.activeDevices.delete(device);
+            return;
+          }
         }
       },
       false