diff --git a/src/assets/hud/spawn_camera-hover.png b/src/assets/hud/spawn_camera-hover.png
new file mode 100644
index 0000000000000000000000000000000000000000..2352beaab00ce7ffccae97ac68be17d93f2e05db
Binary files /dev/null and b/src/assets/hud/spawn_camera-hover.png differ
diff --git a/src/assets/hud/spawn_camera.png b/src/assets/hud/spawn_camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..09b21ac68f1050223aaa224d1dea9cb647229ef6
Binary files /dev/null and b/src/assets/hud/spawn_camera.png differ
diff --git a/src/assets/stylesheets/2d-hud.scss b/src/assets/stylesheets/2d-hud.scss
index f8b56e42545b757b425135a2e7fb1a21abf29a62..b1cf61cf12a28b35bb1720d933c9b50f59d01988 100644
--- a/src/assets/stylesheets/2d-hud.scss
+++ b/src/assets/stylesheets/2d-hud.scss
@@ -22,6 +22,7 @@
   &:local(.column) {
     flex-direction: column;
     bottom: 20px;
+    z-index: 1;
   }
 }
 
@@ -120,6 +121,13 @@
   background-image: url(../hud/spawn_pen-hover.png);
 }
 
+:local(.iconButton.spawn_camera) {
+  background-image: url(../hud/spawn_camera.png);
+}
+:local(.iconButton.spawn_camera:hover) {
+  background-image: url(../hud/spawn_camera-hover.png);
+}
+
 :local(.iconButton.freeze) {
   background-image: url(../hud/freeze_off.png);
 }
diff --git a/src/assets/stylesheets/ui-root.scss b/src/assets/stylesheets/ui-root.scss
index e7234072461334f7e9edb973eb315324312d98a9..90f6e245efa030784d4a4733fa0010960c0a3c67 100644
--- a/src/assets/stylesheets/ui-root.scss
+++ b/src/assets/stylesheets/ui-root.scss
@@ -70,6 +70,10 @@
 
 .ui-interactive {
   pointer-events: auto;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
 }
 
 :local(.nag-button) {
diff --git a/src/components/camera-tool.js b/src/components/camera-tool.js
new file mode 100644
index 0000000000000000000000000000000000000000..2501dddc04dfcbf0c81e2d1c3426a40f6e939bb6
--- /dev/null
+++ b/src/components/camera-tool.js
@@ -0,0 +1,141 @@
+import { addMedia } from "../utils/media-utils";
+import { ObjectTypes } from "../object-types";
+
+const snapCanvas = document.createElement("canvas");
+async function pixelsToPNG(pixels, width, height) {
+  snapCanvas.width = width;
+  snapCanvas.height = height;
+  const context = snapCanvas.getContext("2d");
+
+  const imageData = context.createImageData(width, height);
+  imageData.data.set(pixels);
+  const bitmap = await createImageBitmap(imageData);
+  context.scale(1, -1);
+  context.drawImage(bitmap, 0, -height);
+  const blob = await new Promise(resolve => snapCanvas.toBlob(resolve));
+  return new File([blob], "snap.png", { type: "image/png" });
+}
+
+AFRAME.registerComponent("camera-tool", {
+  schema: {
+    previewFPS: { default: 6 },
+    imageWidth: { default: 512 },
+    imageHeight: { default: 512 }
+  },
+
+  init() {
+    this.stateAdded = this.stateAdded.bind(this);
+
+    this.lastUpdate = performance.now();
+
+    this.renderTarget = new THREE.WebGLRenderTarget(this.data.imageWidth, this.data.imageHeight, {
+      format: THREE.RGBAFormat,
+      minFilter: THREE.LinearFilter,
+      magFilter: THREE.NearestFilter,
+      encoding: THREE.sRGBEncoding,
+      depth: false,
+      stencil: false
+    });
+
+    this.camera = new THREE.PerspectiveCamera();
+    this.camera.rotation.set(0, Math.PI, 0);
+    this.el.setObject3D("camera", this.camera);
+
+    const material = new THREE.MeshBasicMaterial({
+      map: this.renderTarget.texture
+    });
+    // Bit of a hack here to only update the renderTarget when the screens are in view and at a reduced FPS
+    material.map.isVideoTexture = true;
+    material.map.update = () => {
+      if (performance.now() - this.lastUpdate >= 1000 / this.data.previewFPS) {
+        this.updateRenderTargetNextTick = true;
+      }
+    };
+
+    this.el.addEventListener(
+      "model-loaded",
+      () => {
+        const geometry = new THREE.PlaneGeometry(0.25, 0.25);
+
+        const screen = new THREE.Mesh(geometry, material);
+        screen.rotation.set(0, Math.PI, 0);
+        screen.position.set(0, -0.015, -0.08);
+        this.el.setObject3D("screen", screen);
+
+        const selfieScreen = new THREE.Mesh(geometry, material);
+        selfieScreen.position.set(0, 0.3, 0);
+        selfieScreen.scale.set(-1, 1, 1);
+        this.el.setObject3D("selfieScreen", selfieScreen);
+
+        this.updateRenderTargetNextTick = true;
+      },
+      { once: true }
+    );
+  },
+
+  play() {
+    this.el.addEventListener("stateadded", this.stateAdded);
+  },
+
+  pause() {
+    this.el.removeEventListener("stateadded", this.stateAdded);
+  },
+
+  stateAdded(evt) {
+    if (evt.detail === "activated") {
+      this.takeSnapshotNextTick = true;
+    }
+  },
+
+  tock: (function() {
+    const tempScale = new THREE.Vector3();
+    return function tock() {
+      const sceneEl = this.el.sceneEl;
+      const renderer = this.renderer || sceneEl.renderer;
+      const now = performance.now();
+
+      if (!this.playerHead) {
+        const headEl = document.getElementById("player-head");
+        this.playerHead = headEl && headEl.object3D;
+      }
+
+      if (this.takeSnapshotNextTick || this.updateRenderTargetNextTick) {
+        if (this.playerHead) {
+          tempScale.copy(this.playerHead.scale);
+          this.playerHead.scale.set(1, 1, 1);
+        }
+        const tmpVRFlag = renderer.vr.enabled;
+        const tmpOnAfterRender = sceneEl.object3D.onAfterRender;
+        delete sceneEl.object3D.onAfterRender;
+        renderer.vr.enabled = false;
+        renderer.render(sceneEl.object3D, this.camera, this.renderTarget, true);
+        renderer.vr.enabled = tmpVRFlag;
+        sceneEl.object3D.onAfterRender = tmpOnAfterRender;
+        if (this.playerHead) {
+          this.playerHead.scale.copy(tempScale);
+        }
+        this.lastUpdate = now;
+        this.updateRenderTargetNextTick = false;
+      }
+
+      if (this.takeSnapshotNextTick) {
+        const width = this.renderTarget.width;
+        const height = this.renderTarget.height;
+        if (!this.snapPixels) {
+          this.snapPixels = new Uint8Array(width * height * 4);
+        }
+        renderer.readRenderTargetPixels(this.renderTarget, 0, 0, width, height, this.snapPixels);
+        pixelsToPNG(this.snapPixels, width, height).then(file => {
+          const { entity, orientation } = addMedia(file, "#interactable-media", undefined, true);
+          orientation.then(() => {
+            entity.object3D.position.copy(this.el.object3D.position);
+            entity.object3D.rotation.copy(this.el.object3D.rotation);
+            entity.components["sticky-object"].setLocked(false);
+            sceneEl.emit("object_spawned", { objectType: ObjectTypes.CAMERA });
+          });
+        });
+        this.takeSnapshotNextTick = false;
+      }
+    };
+  })()
+});
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index 6edfd332a2fdc707d3d5a484b468218ff3b55000..dba2bc4dd66a7595dbef20ca85a8ead162f35bcf 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -1,6 +1,7 @@
 const CLAMP_VELOCITY = 0.01;
 const MAX_DELTA = 0.2;
 const EPS = 10e-6;
+const MAX_WARNINGS = 10;
 
 /**
  * Avatar movement controller that listens to move, rotate and teleportation events and moves the avatar accordingly.
@@ -25,6 +26,8 @@ AFRAME.registerComponent("character-controller", {
     this.accelerationInput = new THREE.Vector3(0, 0, 0);
     this.pendingSnapRotationMatrix = new THREE.Matrix4();
     this.angularVelocity = 0; // Scalar value because we only allow rotation around Y
+    this._withinWarningLimit = true;
+    this._warningCount = 0;
     this.setAccelerationInput = this.setAccelerationInput.bind(this);
     this.snapRotateLeft = this.snapRotateLeft.bind(this);
     this.snapRotateRight = this.snapRotateRight.bind(this);
@@ -158,6 +161,16 @@ AFRAME.registerComponent("character-controller", {
     };
   })(),
 
+  _warnWithWarningLimit: function(msg) {
+    if (!this._withinWarningLimit) return;
+    this._warningCount++;
+    if (this._warningCount > MAX_WARNINGS) {
+      this._withinWarningLimit = false;
+      msg = "Warning count exceeded. Will not log further warnings";
+    }
+    console.warn("character-controller", msg);
+  },
+
   setPositionOnNavMesh: function(start, end, object3D) {
     const pathfinder = this.el.sceneEl.systems.nav.pathfinder;
     const zone = this.navZone;
@@ -166,7 +179,16 @@ AFRAME.registerComponent("character-controller", {
         this.navGroup = pathfinder.getGroup(zone, end);
       }
       this.navNode = this.navNode || pathfinder.getClosestNode(end, zone, this.navGroup, true);
-      this.navNode = pathfinder.clampStep(start, end, this.navNode, zone, this.navGroup, object3D.position);
+      if (this.navNode) {
+        try {
+          this.navNode = pathfinder.clampStep(start, end, this.navNode, zone, this.navGroup, object3D.position);
+        } catch (e) {
+          // clampStep failed for whatever reason. Don't stop the main loop.
+          if (this._withinWarningLimit) this._warnWithWarningLimit(`setPositionOnNavMesh: clampStep failed. ${e}`);
+        }
+      } else {
+        if (this._withinWarningLimit) this._warnWithWarningLimit("setPositionOnNavMesh: navNode is null.");
+      }
     }
   },
 
@@ -176,7 +198,16 @@ AFRAME.registerComponent("character-controller", {
     if (zone in pathfinder.zones) {
       this.navGroup = pathfinder.getGroup(zone, position);
       this.navNode = pathfinder.getClosestNode(navPosition, zone, this.navGroup, true) || this.navNode;
-      this.navNode = pathfinder.clampStep(position, position, this.navNode, zone, this.navGroup, object3D.position);
+      if (this.navNode) {
+        try {
+          this.navNode = pathfinder.clampStep(position, position, this.navNode, zone, this.navGroup, object3D.position);
+        } catch (e) {
+          // clampStep failed for whatever reason. Don't stop the main loop.
+          if (this._withinWarningLimit) this._warnWithWarningLimit(`resetPositionOnNavMesh: clampStep failed. ${e}`);
+        }
+      } else {
+        if (this._withinWarningLimit) this._warnWithWarningLimit("resetPositionOnNavMesh: navNode is null.");
+      }
     }
   },
 
diff --git a/src/components/heightfield.js b/src/components/heightfield.js
index b1ed8b46bee36c243544476a7f59fb33b575a5fb..57853a5ccb54ac0060212264e82e21b62a1ffbfe 100644
--- a/src/components/heightfield.js
+++ b/src/components/heightfield.js
@@ -9,59 +9,19 @@ AFRAME.registerComponent("heightfield", {
     this.el.setAttribute("static-body", { shape: "none", mass: 0 });
   },
   generateAndAddHeightfield(body) {
-    const mesh = this.el.object3D.getObjectByProperty("type", "Mesh");
-    mesh.geometry.computeBoundingBox();
-    const size = new THREE.Vector3();
-    mesh.geometry.boundingBox.getSize(size);
-
-    const minDistance = 0.25;
-    const resolution = (size.x + size.z) / 2 / minDistance;
-    const distance = Math.max(minDistance, (size.x + size.z) / 2 / resolution);
-
-    const data = [];
-    const down = new THREE.Vector3(0, -1, 0);
-    const position = new THREE.Vector3();
-    const raycaster = new THREE.Raycaster();
-    const intersections = [];
-    const meshPos = new THREE.Vector3();
-    mesh.getWorldPosition(meshPos);
-    const offsetX = -size.x / 2 + meshPos.x;
-    const offsetZ = -size.z / 2 + meshPos.z;
-    let min = Infinity;
-    for (let z = 0; z < resolution; z++) {
-      data[z] = [];
-      for (let x = 0; x < resolution; x++) {
-        position.set(offsetX + x * distance, size.y / 2, offsetZ + z * distance);
-        raycaster.set(position, down);
-        intersections.length = 0;
-        raycaster.intersectObject(mesh, false, intersections);
-        let val;
-        if (intersections.length) {
-          val = -intersections[0].distance + size.y / 2;
-        } else {
-          val = -size.y / 2;
-        }
-        data[z][x] = val;
-        if (val < min) {
-          min = data[z][x];
-        }
-      }
-    }
-    // Cannon doesn't like heightfields with negative heights.
-    for (let z = 0; z < resolution; z++) {
-      for (let x = 0; x < resolution; x++) {
-        data[z][x] -= min;
-      }
-    }
+    const { offset, distance, data } = this.data;
 
     const orientation = new CANNON.Quaternion();
     orientation.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
+
     const rotation = new CANNON.Quaternion();
     rotation.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), -Math.PI / 2);
     rotation.mult(orientation, orientation);
-    const offset = new CANNON.Vec3(-size.x / 2, min, -size.z / 2);
+
+    const cannonOffset = new CANNON.Vec3(offset.x, offset.y, offset.z);
 
     const shape = new CANNON.Heightfield(data, { elementSize: distance });
-    body.addShape(shape, offset, orientation);
+
+    body.addShape(shape, cannonOffset, orientation);
   }
 });
diff --git a/src/components/in-world-hud.js b/src/components/in-world-hud.js
index 1bee398de9521f9a12e7fd0eb12b2af035a89041..aa0d8f83f1bf2e57a6a884b16c154b41d22a0bb2 100644
--- a/src/components/in-world-hud.js
+++ b/src/components/in-world-hud.js
@@ -12,11 +12,13 @@ AFRAME.registerComponent("in-world-hud", {
     this.mic = this.el.querySelector(".mic");
     this.freeze = this.el.querySelector(".freeze");
     this.pen = this.el.querySelector(".pen");
+    this.cameraBtn = this.el.querySelector(".cameraBtn");
     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.pen.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS;
+    this.cameraBtn.object3DMap.mesh.renderOrder = renderOrder.HUD_ICONS;
     this.background.object3DMap.mesh.renderORder = renderOrder.HUD_BACKGROUND;
 
     this.updateButtonStates = () => {
@@ -42,6 +44,10 @@ AFRAME.registerComponent("in-world-hud", {
     this.onPenClick = () => {
       this.el.emit("spawn_pen");
     };
+
+    this.onCameraClick = () => {
+      this.el.emit("action_spawn_camera");
+    };
   },
 
   play() {
@@ -51,6 +57,7 @@ AFRAME.registerComponent("in-world-hud", {
     this.mic.addEventListener("click", this.onMicClick);
     this.freeze.addEventListener("click", this.onFreezeClick);
     this.pen.addEventListener("mousedown", this.onPenClick);
+    this.cameraBtn.addEventListener("click", this.onCameraClick);
   },
 
   pause() {
@@ -60,5 +67,6 @@ AFRAME.registerComponent("in-world-hud", {
     this.mic.removeEventListener("click", this.onMicClick);
     this.freeze.removeEventListener("click", this.onFreezeClick);
     this.pen.removeEventListener("mousedown", this.onPenClick);
+    this.cameraBtn.removeEventListener("click", this.onCameraClick);
   }
 });
diff --git a/src/hub.html b/src/hub.html
index 787984a1889fecaf5f2aaa53ad3efe53c539e8c4..edb18d4b95c430debb86f6e6ae11948b0db73b52 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -58,6 +58,8 @@
             <img id="freeze-on-hover" crossorigin="anonymous" src="./assets/hud/freeze_on-hover.png">
             <img id="spawn-pen" crossorigin="anonymous" src="./assets/hud/spawn_pen.png">
             <img id="spawn-pen-hover" crossorigin="anonymous" src="./assets/hud/spawn_pen-hover.png">
+            <img id="spawn-camera" crossorigin="anonymous" src="./assets/hud/spawn_camera.png">
+            <img id="spawn-camera-hover" crossorigin="anonymous" src="./assets/hud/spawn_camera-hover.png">
 
             <a-asset-item id="botdefault" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotDefault_Avatar-9f71f8ff22.gltf"></a-asset-item>
             <a-asset-item id="botbobo" response-type="arraybuffer" src="https://asset-bundles-prod.reticulum.io/bots/BotBobo_Avatar-f9740a010b.gltf"></a-asset-item>
@@ -201,6 +203,27 @@
                 </a-entity>
             </template>
 
+            <template id="interactable-camera">
+                <a-entity
+                    class="interactable toggle"
+                    grabbable-toggle="maxGrabbers: 1;"
+                    hoverable
+                    activatable__snap-hand="buttonStartEvents: secondary_hand_grab; buttonEndEvents: secondary_hand_release;"
+                    activatable__snap-cursor="buttonStartEvents: secondary-cursor-grab; buttonEndEvents: secondary-cursor-release;"
+                    camera-tool
+                    body="type: dynamic; shape: none; mass: 1;"
+                    sticky-object="autoLockOnRelease: true; autoLockOnLoad: true;"
+                    media-loader="src: https://sketchfab.com/models/2e8778db90e845888e38edd80be28283; resize: true"
+                    super-networked-interactable="counter: #camera-counter;"
+                    position-at-box-shape-border="target:.delete-button"
+                    rotation
+                >
+                    <a-entity class="delete-button" visible-while-frozen>
+                        <a-entity mixin="rounded-text-button" remove-networked-object-button position="0 0 0"> </a-entity>
+                        <a-entity text=" value:Delete; width:2.5; align:center;" text-raycast-hack position="0 0 0.01"></a-entity>
+                    </a-entity>
+                </a-entity>
+            </template>
 
             <template id="interactable-drawing">
                 <a-entity
@@ -264,6 +287,8 @@
 
         <a-entity id="pen-counter" networked-counter="max: 10;"></a-entity>
 
+        <a-entity id="camera-counter" networked-counter="max: 1;"></a-entity>
+
         <a-entity id="drawing-manager" drawing-manager></a-entity>
 
         <a-entity
@@ -315,10 +340,11 @@
               vr-mode-toggle-playing__hud-controller
           >
             <a-entity in-world-hud="haptic:#player-right-controller;raycaster:#player-right-controller;" rotation="30 0 0">
-              <a-rounded height="0.13" width="0.48" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded>
+              <a-rounded height="0.13" width="0.59" color="#000000" position="-0.24 -0.065 0" radius="0.065" opacity="0.35" class="hud bg"></a-rounded>
               <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Mute Mic; activeTooltipText: Unmute Mic; image: #mute-off; hoverImage: #mute-off-hover; activeImage: #mute-on; activeHoverImage: #mute-on-hover" scale="0.1 0.1 0.1" position="-0.17 0 0.001" class="ui hud mic" material="alphaTest:0.1;"></a-image>
               <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Pause; activeTooltipText: Resume; image: #freeze-off; hoverImage: #freeze-off-hover; activeImage: #freeze-on; activeHoverImage: #freeze-on-hover" scale="0.2 0.2 0.2" position="0 0 0.005" class="ui hud freeze"></a-image>
               <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Spawn Pen; activeTooltipText: Spawn Pen; image: #spawn-pen; hoverImage: #spawn-pen-hover; activeImage: #spawn-pen; activeHoverImage: #spawn-pen-hover" scale="0.1 0.1 0.1" position="0.17 0 0.001" class="ui hud pen" material="alphaTest:0.1;"></a-image>
+              <a-image icon-button="tooltip: #hud-tooltip; tooltipText: Spawn Camera; activeTooltipText: Spawn Camera; image: #spawn-camera; hoverImage: #spawn-camera-hover; activeImage: #spawn-camera; activeHoverImage: #spawn-camera-hover" scale="0.1 0.1 0.1" position="0.28 0 0.001" class="ui hud cameraBtn" material="alphaTest:0.1;"></a-image>
               <a-rounded visible="false" id="hud-tooltip" height="0.08" width="0.3" color="#000000" position="-0.15 -0.2 0" rotation="-20 0 0" radius="0.025" opacity="0.35" class="hud bg">
                 <a-entity text="value: Mute Mic; align:center;" position="0.15 0.04 0.001" ></a-entity>
               </a-rounded>
@@ -414,7 +440,7 @@
             </template>
 
             <template data-name="Head">
-              <a-entity visible="false" bone-visibility></a-entity>
+              <a-entity id="player-head" visible="false" bone-visibility></a-entity>
             </template>
 
             <template data-name="LeftHand">
@@ -443,6 +469,7 @@
             superHand: #player-right-controller;
             cursorSuperHand: #cursor;"
         ></a-entity>
+
     </a-scene>
 
     <div id="ui-root"></div>
diff --git a/src/hub.js b/src/hub.js
index 7e22912fe9bc1fc2c9a1d8f2ef44b7c992a3f581..ce0e5dca5e8486bdc172e72562fda1367d3f36ac 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -85,6 +85,7 @@ import "./components/hemisphere-light";
 import "./components/point-light";
 import "./components/spot-light";
 import "./components/visible-to-owner";
+import "./components/camera-tool";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -344,7 +345,6 @@ const onReady = async () => {
     });
 
     const offset = { x: 0, y: 0, z: -1.5 };
-
     const spawnMediaInfrontOfPlayer = (src, contentOrigin) => {
       const { entity, orientation } = addMedia(src, "#interactable-media", contentOrigin, true);
 
@@ -363,6 +363,16 @@ const onReady = async () => {
       spawnMediaInfrontOfPlayer(e.detail, contentOrigin);
     });
 
+    scene.addEventListener("action_spawn_camera", () => {
+      const entity = document.createElement("a-entity");
+      entity.setAttribute("networked", { template: "#interactable-camera" });
+      entity.setAttribute("offset-relative-to", {
+        target: "#player-camera",
+        offset: { x: 0, y: 0, z: -1.5 }
+      });
+      scene.appendChild(entity);
+    });
+
     scene.addEventListener("object_spawned", e => {
       if (hubChannel) {
         hubChannel.sendObjectSpawnedEvent(e.detail.objectType);
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 8cef8df1653e06a89e25b5ecf046748606a7eeb2..3670af7be79393c83ef0d3d76fdc775fa5bc3a73 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -127,6 +127,11 @@ function registerNetworkSchemas() {
     ]
   });
 
+  NAF.schemas.add({
+    template: "#interactable-camera",
+    components: ["position", "rotation"]
+  });
+
   NAF.schemas.add({
     template: "#pen-interactable",
     components: [
diff --git a/src/object-types.js b/src/object-types.js
index d8f959c2531a59fe713c85b2141a013eed785be5..8c9ecc581b7485f4ad24187986dfd885a5c5f8a3 100644
--- a/src/object-types.js
+++ b/src/object-types.js
@@ -12,7 +12,7 @@ export const ObjectContentOrigins = {
 // Enumeration of spawnable object types, used for telemetry, which encapsulates
 // both the origin of the content for the object and also the type of content
 // contained in the object.
-const ObjectTypes = {
+export const ObjectTypes = {
   URL_IMAGE: 0,
   URL_VIDEO: 1,
   URL_MODEL: 2,
@@ -38,7 +38,7 @@ const ObjectTypes = {
   SPAWNER_PDF: 27,
   SPAWNER_AUDIO: 28,
   //SPAWNER_TEXT: 29,
-  //DRAWING: 30,
+  CAMERA: 30,
   UNKNOWN: 31
 };
 
diff --git a/src/react-components/2d-hud.js b/src/react-components/2d-hud.js
index 052817a81d6aac00a1b2881df7eb73a5268fc79c..6c31677f58655bec2bb4ef364bd83a078f9e3fc2 100644
--- a/src/react-components/2d-hud.js
+++ b/src/react-components/2d-hud.js
@@ -4,7 +4,7 @@ import cx from "classnames";
 
 import styles from "../assets/stylesheets/2d-hud.scss";
 
-const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) => (
+const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen, onSpawnCamera }) => (
   <div className={cx(styles.container, styles.top, styles.unselectable)}>
     <div className={cx("ui-interactive", styles.panel, styles.left)}>
       <div
@@ -20,6 +20,7 @@ const TopHUD = ({ muted, frozen, onToggleMute, onToggleFreeze, onSpawnPen }) =>
     />
     <div className={cx("ui-interactive", styles.panel, styles.right)}>
       <div className={cx(styles.iconButton, styles.spawn_pen)} title={"Drawing Pen"} onClick={onSpawnPen} />
+      <div className={cx(styles.iconButton, styles.spawn_camera)} title={"Camera"} onClick={onSpawnCamera} />
     </div>
   </div>
 );
@@ -29,7 +30,8 @@ TopHUD.propTypes = {
   frozen: PropTypes.bool,
   onToggleMute: PropTypes.func,
   onToggleFreeze: PropTypes.func,
-  onSpawnPen: PropTypes.func
+  onSpawnPen: PropTypes.func,
+  onSpawnCamera: PropTypes.func
 };
 
 const BottomHUD = ({ onCreateObject, showPhotoPicker, onMediaPicked }) => (
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index 66afe441730a37f377a75c454b144f0d2001bde9..347ddd7b9fec924ec3bd5e5af9a6ec439d7d21cf 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -918,6 +918,7 @@ class UIRoot extends Component {
                 onToggleFreeze={this.toggleFreeze}
                 onToggleSpaceBubble={this.toggleSpaceBubble}
                 onSpawnPen={this.spawnPen}
+                onSpawnCamera={() => this.props.scene.emit("action_spawn_camera")}
               />
               {!this.props.availableVREntryTypes.isInHMD &&
                 this.props.occupantCount <= 1 && (
diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js
index a43568cf5f6bf11914b191cbbb1f391634df1f46..a51b40fc67725cef2837007e5f3dc3afca3e9cf4 100644
--- a/src/utils/media-utils.js
+++ b/src/utils/media-utils.js
@@ -118,10 +118,12 @@ export const addMedia = (src, template, contentOrigin, resize = false) => {
       });
   }
 
-  entity.addEventListener("media_resolved", ({ detail }) => {
-    const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType);
-    scene.emit("object_spawned", { objectType });
-  });
+  if (contentOrigin) {
+    entity.addEventListener("media_resolved", ({ detail }) => {
+      const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType);
+      scene.emit("object_spawned", { objectType });
+    });
+  }
 
   return { entity, orientation };
 };