diff --git a/src/components/cursor-controller.js b/src/components/cursor-controller.js
index 7f3b1bbc42fad4f891b1eef4aaaa7047f8156b60..40f90d82e5e2cb8c90bf407cb6c9155e5830db60 100644
--- a/src/components/cursor-controller.js
+++ b/src/components/cursor-controller.js
@@ -2,6 +2,7 @@ const TARGET_TYPE_NONE = 1;
 const TARGET_TYPE_INTERACTABLE = 2;
 const TARGET_TYPE_UI = 4;
 const TARGET_TYPE_INTERACTABLE_OR_UI = TARGET_TYPE_INTERACTABLE | TARGET_TYPE_UI;
+const virtualJoystickCutoff = 0.8;
 
 AFRAME.registerComponent("cursor-controller", {
   dependencies: ["raycaster", "line"],
@@ -38,9 +39,13 @@ AFRAME.registerComponent("cursor-controller", {
     this.origin = new THREE.Vector3();
     this.direction = new THREE.Vector3();
     this.controllerQuaternion = new THREE.Quaternion();
+    this.activeTouch = null;
 
     this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
 
+    this._handleTouchStart = this._handleTouchStart.bind(this);
+    this._handleTouchMove = this._handleTouchMove.bind(this);
+    this._handleTouchEnd = this._handleTouchEnd.bind(this);
     this._handleMouseDown = this._handleMouseDown.bind(this);
     this._handleMouseMove = this._handleMouseMove.bind(this);
     this._handleMouseUp = this._handleMouseUp.bind(this);
@@ -54,17 +59,11 @@ AFRAME.registerComponent("cursor-controller", {
     this._handleControllerConnected = this._handleControllerConnected.bind(this);
     this._handleControllerDisconnected = this._handleControllerDisconnected.bind(this);
 
-    this._handleTouchStart = this._handleTouchStart.bind(this);
-    this._updateRaycasterIntersections = this._updateRaycasterIntersections.bind(this);
-    this._handleTouchMove = this._handleTouchMove.bind(this);
-    this._handleTouchEnd = this._handleTouchEnd.bind(this);
-
-    this.el.sceneEl.renderer.sortObjects = true;
-    this.data.cursor.addEventListener("loaded", this.cursorLoadedListener);
+    this.data.cursor.addEventListener("loaded", this._handleCursorLoaded);
   },
 
   remove: function() {
-    this.data.cursor.removeEventListener("loaded", this._cursorLoadedListener);
+    this.data.cursor.removeEventListener("loaded", this._handleCursorLoaded);
   },
 
   update: function(oldData) {
@@ -81,6 +80,7 @@ AFRAME.registerComponent("cursor-controller", {
     document.addEventListener("touchstart", this._handleTouchStart);
     document.addEventListener("touchmove", this._handleTouchMove);
     document.addEventListener("touchend", this._handleTouchEnd);
+    document.addEventListener("touchcancel", this._handleTouchEnd);
     document.addEventListener("mousedown", this._handleMouseDown);
     document.addEventListener("mousemove", this._handleMouseMove);
     document.addEventListener("mouseup", this._handleMouseUp);
@@ -106,6 +106,7 @@ AFRAME.registerComponent("cursor-controller", {
     document.removeEventListener("touchstart", this._handleTouchStart);
     document.removeEventListener("touchmove", this._handleTouchMove);
     document.removeEventListener("touchend", this._handleTouchEnd);
+    document.removeEventListener("touchcancel", this._handleTouchEnd);
     document.removeEventListener("mousedown", this._handleMouseDown);
     document.removeEventListener("mousemove", this._handleMouseMove);
     document.removeEventListener("mouseup", this._handleMouseUp);
@@ -259,50 +260,38 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleTouchStart: function(e) {
-    if (!this.isMobile || this.hasPointingDevice) return;
+    if (!this.isMobile || this.hasPointingDevice || this.activeTouch) return;
 
-    const touch = e.touches[0];
-    if (touch.clientY / window.innerHeight >= 0.8) return true;
-    this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
-    this._updateRaycasterIntersections();
-
-    // update cursor position
-    if (!this.isGrabbing) {
-      const intersections = this.el.components.raycaster.intersections;
-      if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
-        const intersection = intersections[0];
-        this.data.cursor.object3D.position.copy(intersection.point);
-        this.currentDistance = intersections[0].distance;
-        this.currentDistanceMod = 0;
-      } else {
-        this.currentDistance = this.data.maxDistance;
+    for (let i = e.touches.length - 1; i >= 0; i--) {
+      const touch = e.touches[i];
+      if (touch.clientY / window.innerHeight < virtualJoystickCutoff) {
+        this.activeTouch = touch;
+        break;
       }
     }
+    if (!this.activeTouch) return;
 
-    this._setLookControlsEnabled(false);
-
-    // Set timeout because if I don't, the duck moves is picked up at the
-    // the wrong offset from the cursor: If the cursor started below and
-    // to the left, the duck lifts above and to the right by the same amount.
-    // I don't understand exactly why this is, since I am setting the
-    // cursor object's position manually in this function, but something else
-    // must happen before cursor-grab ends up doing the right thing.
-    // TODO : Figure this out.
-    window.setTimeout(() => {
-      this.data.cursor.emit("cursor-grab", {});
-    }, 40);
-
-    this.lastTouch = touch;
-  },
-
-  _updateRaycasterIntersections: function() {
-    const raycaster = this.el.components.raycaster.raycaster;
+    // Update the ray and cursor positions
+    const raycasterComp = this.el.components.raycaster;
+    const raycaster = raycasterComp.raycaster;
     const camera = this.data.camera.components.camera.camera;
+    const cursor = this.data.cursor;
+    this.mousePos.set(
+      this.activeTouch.clientX / window.innerWidth * 2 - 1,
+      -(this.activeTouch.clientY / window.innerHeight) * 2 + 1
+    );
     raycaster.setFromCamera(this.mousePos, camera);
-    this.origin = raycaster.ray.origin;
-    this.direction = raycaster.ray.direction;
-    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
-    this.el.components.raycaster.checkIntersections();
+    this.el.setAttribute("raycaster", { origin: raycaster.ray.origin, direction: raycaster.ray.direction });
+    raycasterComp.checkIntersections();
+    const intersections = raycasterComp.intersections;
+    if (intersections.length === 0 || intersections[0].distance >= this.data.maxDistance) {
+      this.activeTouch = null;
+      return;
+    }
+    cursor.object3D.position.copy(intersections[0].point);
+    // Cursor position must be synced to physics before constraint is created
+    cursor.components["static-body"].syncToPhysics();
+    cursor.emit("cursor-grab", {});
   },
 
   _handleTouchMove: function(e) {
@@ -310,28 +299,28 @@ AFRAME.registerComponent("cursor-controller", {
 
     for (let i = 0; i < e.touches.length; i++) {
       const touch = e.touches[i];
-      if (touch.clientY / window.innerHeight >= 0.8) return true;
-      this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
-      this.lastTouch = touch;
+      if (
+        (!this.activeTouch && touch.clientY / window.innerHeight < virtualJoystickCutoff) ||
+        (this.activeTouch && touch.identifier === this.activeTouch.identifier)
+      ) {
+        this.mousePos.set(touch.clientX / window.innerWidth * 2 - 1, -(touch.clientY / window.innerHeight) * 2 + 1);
+        return;
+      }
     }
   },
 
   _handleTouchEnd: function(e) {
-    if (!this.isMobile || this.hasPointingDevice) return;
-
-    for (let i = 0; i < e.changedTouches.length; i++) {
-      const touch = e.changedTouches[i];
-      if (this.lastTouch) {
-        const thisTouchDidNotDriveMousePos =
-          Math.abs(touch.clientX - this.lastTouch.clientX) > 0.1 &&
-          Math.abs(touch.clientY - this.lastTouch.clientY) > 0.1;
-        if (thisTouchDidNotDriveMousePos) {
-          return;
-        }
-      }
+    if (
+      !this.isMobile ||
+      this.hasPointingDevice ||
+      !this.activeTouch ||
+      Array.prototype.some.call(e.touches, touch => touch.identifier === this.activeTouch.identifier)
+    ) {
+      return;
     }
-    this._setLookControlsEnabled(true);
+
     this.data.cursor.emit("cursor-release", {});
+    this.activeTouch = null;
   },
 
   _handleMouseDown: function() {
@@ -400,7 +389,6 @@ AFRAME.registerComponent("cursor-controller", {
   _handlePrimaryUp: function(e) {
     if (e.target === this.controller || e.target === this.data.playerRig) {
       if (this._isGrabbing() || this._isTargetOfType(TARGET_TYPE_UI)) {
-        this.grabStarting = false;
         this.data.cursor.emit("cursor-release", e.detail);
       } else if (e.type !== this.data.releaseEvent) {
         this._endTeleport();
@@ -413,7 +401,7 @@ AFRAME.registerComponent("cursor-controller", {
   },
 
   _handleCursorLoaded: function() {
-    this.data.cursor.object3DMap.mesh.renderOrder = 1;
+    this.data.cursor.object3DMap.mesh.renderOrder = window.APP.RENDER_ORDER.CURSOR;
   },
 
   _handleControllerConnected: function(e) {
diff --git a/src/components/hud-controller.js b/src/components/hud-controller.js
index 67f9c4616285d38efe31e84e63475879463d84fb..2fac822d84d187d141217fa00327a63555a04332 100644
--- a/src/components/hud-controller.js
+++ b/src/components/hud-controller.js
@@ -81,10 +81,8 @@ AFRAME.registerComponent("hud-controller", {
     const AppModeSystem = sceneEl.systems["app-mode"];
     if (pitch > lookCutoff && AppModeSystem.mode !== AppModes.HUD) {
       AppModeSystem.setMode(AppModes.HUD);
-      sceneEl.renderer.sortObjects = true;
     } else if (pitch < lookCutoff && AppModeSystem.mode === AppModes.HUD) {
       AppModeSystem.setMode(AppModes.DEFAULT);
-      sceneEl.renderer.sortObjects = false;
     }
   }
 });
diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js
index df42d283b956bfa2b90da54977a716ee1f072849..2077b7ef28236d7257c0aa9b8e536ddd9891a499 100644
--- a/src/components/in-world-hud.js
+++ b/src/components/in-world-hud.js
@@ -7,6 +7,12 @@ AFRAME.registerComponent("in-world-hud", {
     this.mic = this.el.querySelector(".mic");
     this.freeze = this.el.querySelector(".freeze");
     this.bubble = this.el.querySelector(".bubble");
+    this.background = this.el.querySelector(".bg");
+    const renderOrder = window.APP.RENDER_ORDER;
+    this.mic.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS;
+    this.freeze.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS;
+    this.bubble.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS;
+    this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND;
 
     this.updateButtonStates = () => {
       this.mic.setAttribute("icon-button", "active", this.el.sceneEl.is("muted"));
diff --git a/src/hub.js b/src/hub.js
index 80741c2e11939461503bb57cd99a1cee97a0ee75..b8906fd0f3f5a9a660c251b9fc5c8e08af125772 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -79,6 +79,11 @@ import { DEFAULT_ENVIRONMENT_URL } from "./assets/environments/environments";
 import { App } from "./App";
 
 window.APP = new App();
+window.APP.RENDER_ORDER = {
+  HUD_BACKGROUND: 1,
+  HUD_ICONS: 2,
+  CURSOR: 3
+};
 const store = window.APP.store;
 
 const qs = queryString.parse(location.search);
@@ -172,7 +177,7 @@ const onReady = async () => {
   const scene = document.querySelector("a-scene");
   const hubChannel = new HubChannel(store);
 
-  document.querySelector("a-scene canvas").classList.add("blurred");
+  document.querySelector("canvas").classList.add("blurred");
   window.APP.scene = scene;
 
   registerNetworkSchemas();
@@ -215,8 +220,9 @@ const onReady = async () => {
 
   const enterScene = async (mediaStream, enterInVR, hubId) => {
     const scene = document.querySelector("a-scene");
+    scene.renderer.sortObjects = true;
     const playerRig = document.querySelector("#player-rig");
-    document.querySelector("a-scene canvas").classList.remove("blurred");
+    document.querySelector("canvas").classList.remove("blurred");
     scene.render();
 
     if (enterInVR) {