diff --git a/scripts/default.env b/scripts/default.env
index f26d4c84db07c28644e7c9d479842e6c771b1596..3d6556c0fa826b118d94dc8bc88a9f2555c5a520 100644
--- a/scripts/default.env
+++ b/scripts/default.env
@@ -4,3 +4,4 @@ ORIGIN_TRIAL_TOKEN="AgN/JtqSF6qpD3OZk8KgM5/UYqUUrwc166cOQSRCqvU+TIpHWdiwBUWH5V1K
 ORIGIN_TRIAL_EXPIRES="2018-05-15"
 JANUS_SERVER="wss://prod-janus.reticulum.io"
 DEV_RETICULUM_SERVER="dev.reticulum.io"
+ASSET_BUNDLE_SERVER="https://asset-bundles-prod.reticulum.io"
diff --git a/src/assets/environments/environments.js b/src/assets/environments/environments.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c20ce61da763de912493c4e8d35385d09347b5a
--- /dev/null
+++ b/src/assets/environments/environments.js
@@ -0,0 +1,7 @@
+export const ENVIRONMENT_URLS = [
+  process.env.ASSET_BUNDLE_SERVER + "/rooms/meetingroom/MeetingRoom.bundle.json",
+  process.env.ASSET_BUNDLE_SERVER + "/rooms/atrium/Atrium.bundle.json",
+  process.env.ASSET_BUNDLE_SERVER + "/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json"
+];
+
+export const DEFAULT_ENVIRONMENT_URL = ENVIRONMENT_URLS[0];
diff --git a/src/components/css-class.js b/src/components/css-class.js
new file mode 100644
index 0000000000000000000000000000000000000000..1528ed4d450aea61f63b7426524229ed63609044
--- /dev/null
+++ b/src/components/css-class.js
@@ -0,0 +1,17 @@
+AFRAME.registerComponent("css-class", {
+  schema: {
+    type: "string"
+  },
+  init() {
+    this.el.classList.add(this.data);
+  },
+  update(oldData) {
+    if (this.data !== oldData) {
+      this.el.classList.remove(oldData);
+      this.el.classList.add(this.data);
+    }
+  },
+  remove() {
+    this.el.classList.remove(this.data);
+  }
+});
diff --git a/src/components/scene-shadow.js b/src/components/scene-shadow.js
new file mode 100644
index 0000000000000000000000000000000000000000..72d77cf36b36c80279b0683531f4682f1f5b7a47
--- /dev/null
+++ b/src/components/scene-shadow.js
@@ -0,0 +1,30 @@
+// For use in environment gltf bundles to set scene shadow properties.
+AFRAME.registerComponent("scene-shadow", {
+  schema: {
+    autoUpdate: {
+      type: "boolean",
+      default: true
+    },
+    type: {
+      type: "string",
+      default: "pcf"
+    },
+    renderReverseSided: {
+      type: "boolean",
+      default: true
+    },
+    renderSingleSided: {
+      type: "boolean",
+      default: true
+    }
+  },
+  init() {
+    this.originalShadowProperties = this.el.sceneEl.getAttribute("shadow");
+  },
+  update() {
+    this.el.sceneEl.setAttribute("shadow", this.data);
+  },
+  remove() {
+    this.el.sceneEl.setAttribute("shadow", this.originalShadowProperties);
+  }
+});
diff --git a/src/components/spawn-controller.js b/src/components/spawn-controller.js
index eab8314c8664965cbdd2b19acf99fa74609052c5..1daf01b756e4308f1f9d50ceffd362aeb4b8e399 100644
--- a/src/components/spawn-controller.js
+++ b/src/components/spawn-controller.js
@@ -1,31 +1,26 @@
 AFRAME.registerComponent("spawn-controller", {
   schema: {
-    radius: { type: "number", default: 1 }
+    target: { type: "selector" },
+    loadedEvent: { type: "string" }
   },
-
   init() {
-    const el = this.el;
-    const center = el.getAttribute("position");
+    this.onLoad = this.onLoad.bind(this);
+    this.data.target.addEventListener(this.data.loadedEvent, this.onLoad);
+  },
+  onLoad() {
+    const spawnPoints = document.querySelectorAll("[spawn-point]");
 
-    const angleRad = Math.random() * 2 * Math.PI;
-    const circlePoint = this.getPointOnCircle(this.data.radius, angleRad);
-    const worldPoint = {
-      x: circlePoint.x + center.x,
-      y: center.y,
-      z: circlePoint.z + center.z
-    };
-    el.setAttribute("position", worldPoint);
+    if (spawnPoints.length === 0) {
+      // Keep default position
+      return;
+    }
 
-    const angleDeg = angleRad * THREE.Math.RAD2DEG;
-    const angleToCenter = -1 * angleDeg + 90;
-    el.setAttribute("rotation", { x: 0, y: angleToCenter, z: 0 });
+    const spawnPointIndex = Math.round((spawnPoints.length - 1) * Math.random());
+    const spawnPoint = spawnPoints[spawnPointIndex];
 
-    el.object3D.updateMatrix();
-  },
-
-  getPointOnCircle(radius, angleRad) {
-    const x = Math.cos(angleRad) * radius;
-    const z = Math.sin(angleRad) * radius;
-    return { x: x, z: z };
+    spawnPoint.object3D.getWorldPosition(this.el.object3D.position);
+    this.el.object3D.rotation.copy(spawnPoint.object3D.rotation);
   }
 });
+
+AFRAME.registerComponent("spawn-point", {});
diff --git a/src/components/water.js b/src/components/water.js
index b7f176131ea97f6f47fbbb31b27c59e80350b78b..cfa33e4b166eea99f5af30d5c5fa36d0075c7378 100644
--- a/src/components/water.js
+++ b/src/components/water.js
@@ -148,10 +148,14 @@ AFRAME.registerComponent("water", {
     distance: { type: "number", default: 1 },
     speed: { type: "number", default: 0.1 },
     forceMobile: { type: "boolean", default: false },
-    normalMap: { type: "asset" }
+    normalMap: { type: "asset", default: "#water-normal-map" }
   },
   init() {
-    const waterGeometry = new THREE.PlaneBufferGeometry(800, 800);
+    const waterMesh = this.el.getObject3D("mesh");
+    const waterGeometry = waterMesh.geometry;
+
+    // Render THREE.Water shader instead of THREE.Mesh
+    waterMesh.visible = false;
 
     const waterNormals = new THREE.Texture(this.data.normalMap);
     waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping;
@@ -223,5 +227,7 @@ AFRAME.registerComponent("water", {
 
   remove() {
     this.el.removeObject3D("water");
+    const waterMesh = this.el.getObject3D("mesh");
+    waterMesh.visible = true;
   }
 });
diff --git a/src/gltf-component-mappings.js b/src/gltf-component-mappings.js
index e2177fb25a1025ad56e9b135626b3abca64b1d97..99b9b0bf6c2dae246cfd33bc654bd21260d98eef 100644
--- a/src/gltf-component-mappings.js
+++ b/src/gltf-component-mappings.js
@@ -1,10 +1,27 @@
 import "./components/gltf-model-plus";
 import { resolveURL } from "./utils/resolveURL";
 
+AFRAME.GLTFModelPlus.registerComponent("quack", "quack");
+AFRAME.GLTFModelPlus.registerComponent("sound", "sound");
+AFRAME.GLTFModelPlus.registerComponent("collision-filter", "collision-filter");
+AFRAME.GLTFModelPlus.registerComponent("css-class", "css-class");
+AFRAME.GLTFModelPlus.registerComponent("scene-shadow", "scene-shadow");
+AFRAME.GLTFModelPlus.registerComponent("super-spawner", "super-spawner");
+AFRAME.GLTFModelPlus.registerComponent("gltf-model-plus", "gltf-model-plus");
+AFRAME.GLTFModelPlus.registerComponent("body", "body");
+AFRAME.GLTFModelPlus.registerComponent("hide-when-quality", "hide-when-quality");
+AFRAME.GLTFModelPlus.registerComponent("light", "light");
+AFRAME.GLTFModelPlus.registerComponent("skybox", "skybox");
+AFRAME.GLTFModelPlus.registerComponent("layers", "layers");
+AFRAME.GLTFModelPlus.registerComponent("shadow", "shadow");
+AFRAME.GLTFModelPlus.registerComponent("xr", "xr");
+AFRAME.GLTFModelPlus.registerComponent("water", "water");
 AFRAME.GLTFModelPlus.registerComponent("scale-audio-feedback", "scale-audio-feedback");
+AFRAME.GLTFModelPlus.registerComponent("animation-mixer", "animation-mixer");
 AFRAME.GLTFModelPlus.registerComponent("loop-animation", "loop-animation");
 AFRAME.GLTFModelPlus.registerComponent("shape", "shape");
 AFRAME.GLTFModelPlus.registerComponent("visible", "visible");
+AFRAME.GLTFModelPlus.registerComponent("spawn-point", "spawn-point");
 AFRAME.GLTFModelPlus.registerComponent("nav-mesh", "nav-mesh", (el, componentName, componentData, gltfPath) => {
   if (componentData.src) {
     componentData.src = resolveURL(componentData.src, gltfPath);
diff --git a/src/hub.html b/src/hub.html
index aa7bab1cd25efafbacdb25e7ef00e6d91671d16d..af1bb3ef2bf5ff1ccd6839793ac70b7395af89f0 100644
--- a/src/hub.html
+++ b/src/hub.html
@@ -190,19 +190,6 @@
         <!-- Interactables -->
         <a-entity id="counter" networked-counter="max: 3; ttl: 120"></a-entity>
 
-        <a-entity 
-            gltf-model-plus="src: #interactable-duck"
-            scale="2 2 2"
-            class="interactable" 
-            super-spawner="template: #interactable-template;" 
-            position="2.9 1.2 0" 
-            body="mass: 0; type: static; shape: box;"
-            collision-filter="collisionForces: false;"
-            quack
-            sound__quack="src: #quack; on: quack; poolSize: 2;"
-            sound__specialquack="src: #specialquack; on: specialquack;"
-        ></a-entity>
-
         <a-entity
             id="cursor-controller"
             cursor-controller="
@@ -236,7 +223,7 @@
         <a-entity
             id="player-rig"
             networked="template: #remote-avatar-template; attachTemplateToLocal: false;"
-            spawn-controller="radius: 4;"
+            spawn-controller="loadedEvent: bundleloaded; target: #environment-root"
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
@@ -357,37 +344,12 @@
             </a-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-entity 
             id="environment-root" 
             nav-mesh-helper
             static-body="shape: none;"
         ></a-entity>
-
-        <a-entity
-            id="skybox"
-            scale="8000 8000 8000"
-            skybox="azimuth:0.280; inclination:0.440"
-            light="type: ambient; color: #FFF"
-            layers="reflection:true"
-            xr="ar: false"
-        ></a-entity>
-
-        <a-entity
-            id="water"
-            water="forceMobile: true; normalMap:#water-normal-map"
-            rotation="-90 0 0"
-            position="0 -88.358 -332.424"
-            xr="ar: false"
-        ></a-entity>
-
     </a-scene>
 
     <div id="ui-root"></div>
diff --git a/src/hub.js b/src/hub.js
index 0a9775d92cf159ea203e9bf12db646a08a3931fc..554d1f3a7a449f3fd22571b7a251c6690ee74b77 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -57,6 +57,8 @@ import "./components/block-button";
 import "./components/visible-while-frozen";
 import "./components/stats-plus";
 import "./components/networked-avatar";
+import "./components/css-class";
+import "./components/scene-shadow";
 
 import ReactDOM from "react-dom";
 import React from "react";
@@ -68,6 +70,7 @@ import "./systems/app-mode";
 import "./systems/exit-on-blur";
 
 import "./gltf-component-mappings";
+import { DEFAULT_ENVIRONMENT_URL } from "./assets/environments/environments";
 
 import { App } from "./App";
 
@@ -347,10 +350,7 @@ const onReady = async () => {
     // If ?room is set, this is `yarn start`, so just use a default environment and query string room.
     remountUI({ janusRoomId: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1 });
     initialEnvironmentEl.setAttribute("gltf-bundle", {
-      src: "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json"
-      // src: "https://asset-bundles-prod.reticulum.io/rooms/theater/TheaterMeshes.bundle.json"
-      // src: "https://asset-bundles-prod.reticulum.io/rooms/atrium/AtriumMeshes.bundle.json"
-      // src: "https://asset-bundles-prod.reticulum.io/rooms/courtyard/CourtyardMeshes.bundle.json"
+      src: DEFAULT_ENVIRONMENT_URL
     });
     return;
   }
diff --git a/src/react-components/home-root.js b/src/react-components/home-root.js
index 6e780474b5ecf98164f6e34cc47e6dcc9a0551ac..401270832f397dc03039f67e84487173a461dd80 100644
--- a/src/react-components/home-root.js
+++ b/src/react-components/home-root.js
@@ -4,6 +4,7 @@ import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl";
 import en from "react-intl/locale-data/en";
 import homeVideo from "../assets/video/home.webm";
 import classNames from "classnames";
+import { ENVIRONMENT_URLS } from "../assets/environments/environments";
 
 import HubCreatePanel from "./hub-create-panel.js";
 import InfoDialog from "./info-dialog.js";
@@ -17,14 +18,6 @@ addLocaleData([...en]);
 
 const messages = localeData[lang] || localeData.en;
 
-const ENVIRONMENT_URLS = [
-  "https://asset-bundles-prod.reticulum.io/rooms/meetingroom/MeetingRoom.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/theater/Theater.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/atrium/Atrium.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/courtyard/Courtyard.bundle.json",
-  "https://asset-bundles-prod.reticulum.io/rooms/MedievalFantasyBook/MedievalFantasyBook.bundle.json"
-];
-
 class HomeRoot extends Component {
   static propTypes = {
     intl: PropTypes.object,
diff --git a/src/react-components/hub-create-panel.js b/src/react-components/hub-create-panel.js
index 0b74d55b2698d51f6688e23e02e871177ecea436..f925d6ae55d82e7019f68658d3f7c03a986a038e 100644
--- a/src/react-components/hub-create-panel.js
+++ b/src/react-components/hub-create-panel.js
@@ -6,6 +6,7 @@ import classNames from "classnames";
 import faAngleLeft from "@fortawesome/fontawesome-free-solid/faAngleLeft";
 import faAngleRight from "@fortawesome/fontawesome-free-solid/faAngleRight";
 import FontAwesomeIcon from "@fortawesome/react-fontawesome";
+import { resolveURL, extractUrlBase } from "../utils/resolveURL";
 
 import default_scene_preview_thumbnail from "../assets/images/default_thumbnail.png";
 
@@ -42,11 +43,24 @@ class HubCreatePanel extends Component {
   _getEnvironmentThumbnail = environmentIndex => {
     const environment = this.props.environments[environmentIndex];
     const meta = environment.meta || {};
-    return (
-      (meta.images || []).find(i => i.type === "preview-thumbnail") || {
-        srcset: default_scene_preview_thumbnail
+
+    let environmentThumbnail = {
+      srcset: default_scene_preview_thumbnail
+    };
+
+    if (meta.images) {
+      const thumbnailImage = meta.images.find(i => i.type === "preview-thumbnail");
+
+      if (thumbnailImage) {
+        const baseURL = new URL(extractUrlBase(environment.bundle_url), window.location.href);
+
+        environmentThumbnail = {
+          srcset: resolveURL(thumbnailImage.srcset, baseURL)
+        };
       }
-    );
+    }
+
+    return environmentThumbnail;
   };
 
   createHub = async e => {
diff --git a/src/utils/resolveURL.js b/src/utils/resolveURL.js
index ddc6c86803b9adc7ea028b2b127ae0783270f521..35ccc3150278558e4026240164116785620ecfc3 100644
--- a/src/utils/resolveURL.js
+++ b/src/utils/resolveURL.js
@@ -15,3 +15,11 @@ export function resolveURL(url, path) {
   // Relative URL
   return path + url;
 }
+
+export function extractUrlBase(url) {
+  const index = url.lastIndexOf("/");
+
+  if (index === -1) return "./";
+
+  return url.substr(0, index + 1);
+}
diff --git a/webpack.config.js b/webpack.config.js
index bd561cbdaeab520fb6bd47dcf66f159c4a0b01bc..4fc219ef830791bd6dbc6d201143ba82af6b961c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -234,7 +234,8 @@ const config = {
       "process.env": JSON.stringify({
         NODE_ENV: process.env.NODE_ENV,
         JANUS_SERVER: process.env.JANUS_SERVER,
-        DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER
+        DEV_RETICULUM_SERVER: process.env.DEV_RETICULUM_SERVER,
+        ASSET_BUNDLE_SERVER: process.env.ASSET_BUNDLE_SERVER
       })
     })
   ]