diff --git a/src/assets/waternormals.jpg b/src/assets/waternormals.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9dfe03ce483a02fff31c35421094c53a333097bc
Binary files /dev/null and b/src/assets/waternormals.jpg differ
diff --git a/src/components/layers.js b/src/components/layers.js
new file mode 100644
index 0000000000000000000000000000000000000000..2838cf754efd2c525cfa5508246f230449f5c895
--- /dev/null
+++ b/src/components/layers.js
@@ -0,0 +1,30 @@
+export const Layers = {
+  // Layers 0 - 2 reserverd by ThreeJS and AFrame.
+  reflection: 3
+};
+
+AFRAME.registerComponent("layers", {
+  schema: {
+    reflection: { type: "boolean", default: false }
+  },
+  init() {
+    this.update = this.update.bind(this);
+    this.el.addEventListener("model-loaded", this.update);
+  },
+  update(oldData) {
+    if (this.data.reflection !== oldData.reflection) {
+      if (this.data.reflection) {
+        this.el.object3D.traverse(obj => {
+          obj.layers.enable(Layers.reflection);
+        });
+      } else {
+        this.el.object3D.traverse(obj => {
+          obj.layers.disable(Layers.reflection);
+        });
+      }
+    }
+  },
+  remove() {
+    this.el.removeEventListener("model-loaded", this.update);
+  }
+});
diff --git a/src/components/skybox.js b/src/components/skybox.js
new file mode 100644
index 0000000000000000000000000000000000000000..e5f26b12fe93d006fa15920378223dc1d566243b
--- /dev/null
+++ b/src/components/skybox.js
@@ -0,0 +1,280 @@
+/**
+ * @author zz85 / https://github.com/zz85
+ *
+ * Based on "A Practical Analytic Model for Daylight"
+ * aka The Preetham Model, the de facto standard analytic skydome model
+ * http://www.cs.utah.edu/~shirley/papers/sunsky/sunsky.pdf
+ *
+ * First implemented by Simon Wallner
+ * http://www.simonwallner.at/projects/atmospheric-scattering
+ *
+ * Improved by Martin Upitis
+ * http://blenderartists.org/forum/showthread.php?245954-preethams-sky-impementation-HDR
+ *
+ * Three.js integration by zz85 http://twitter.com/blurspline
+ */
+
+THREE.Sky = function() {
+  const shader = THREE.Sky.SkyShader;
+
+  const material = new THREE.ShaderMaterial({
+    fragmentShader: shader.fragmentShader,
+    vertexShader: shader.vertexShader,
+    uniforms: THREE.UniformsUtils.clone(shader.uniforms),
+    side: THREE.BackSide
+  });
+
+  THREE.Mesh.call(this, new THREE.BoxBufferGeometry(1, 1, 1), material);
+};
+
+THREE.Sky.prototype = Object.create(THREE.Mesh.prototype);
+THREE.Sky.prototype.constructor = THREE.Sky;
+
+THREE.Sky.SkyShader = {
+  uniforms: {
+    luminance: { value: 1 },
+    turbidity: { value: 2 },
+    rayleigh: { value: 1 },
+    mieCoefficient: { value: 0.005 },
+    mieDirectionalG: { value: 0.8 },
+    sunPosition: { value: new THREE.Vector3() }
+  },
+
+  vertexShader: [
+    "uniform vec3 sunPosition;",
+    "uniform float rayleigh;",
+    "uniform float turbidity;",
+    "uniform float mieCoefficient;",
+
+    "varying vec3 vWorldPosition;",
+    "varying vec3 vSunDirection;",
+    "varying float vSunfade;",
+    "varying vec3 vBetaR;",
+    "varying vec3 vBetaM;",
+    "varying float vSunE;",
+
+    "const vec3 up = vec3( 0.0, 1.0, 0.0 );",
+
+    // constants for atmospheric scattering
+    "const float e = 2.71828182845904523536028747135266249775724709369995957;",
+    "const float pi = 3.141592653589793238462643383279502884197169;",
+
+    // wavelength of used primaries, according to preetham
+    "const vec3 lambda = vec3( 680E-9, 550E-9, 450E-9 );",
+    // this pre-calcuation replaces older TotalRayleigh(vec3 lambda) function:
+    // (8.0 * pow(pi, 3.0) * pow(pow(n, 2.0) - 1.0, 2.0) * (6.0 + 3.0 * pn)) / (3.0 * N * pow(lambda, vec3(4.0)) * (6.0 - 7.0 * pn))
+    "const vec3 totalRayleigh = vec3( 5.804542996261093E-6, 1.3562911419845635E-5, 3.0265902468824876E-5 );",
+
+    // mie stuff
+    // K coefficient for the primaries
+    "const float v = 4.0;",
+    "const vec3 K = vec3( 0.686, 0.678, 0.666 );",
+    // MieConst = pi * pow( ( 2.0 * pi ) / lambda, vec3( v - 2.0 ) ) * K
+    "const vec3 MieConst = vec3( 1.8399918514433978E14, 2.7798023919660528E14, 4.0790479543861094E14 );",
+
+    // earth shadow hack
+    // cutoffAngle = pi / 1.95;
+    "const float cutoffAngle = 1.6110731556870734;",
+    "const float steepness = 1.5;",
+    "const float EE = 1000.0;",
+
+    "float sunIntensity( float zenithAngleCos ) {",
+    "	zenithAngleCos = clamp( zenithAngleCos, -1.0, 1.0 );",
+    "	return EE * max( 0.0, 1.0 - pow( e, -( ( cutoffAngle - acos( zenithAngleCos ) ) / steepness ) ) );",
+    "}",
+
+    "vec3 totalMie( float T ) {",
+    "	float c = ( 0.2 * T ) * 10E-18;",
+    "	return 0.434 * c * MieConst;",
+    "}",
+
+    "void main() {",
+
+    "	vec4 worldPosition = modelMatrix * vec4( position, 1.0 );",
+    "	vWorldPosition = worldPosition.xyz;",
+
+    "	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+
+    "	vSunDirection = normalize( sunPosition );",
+
+    "	vSunE = sunIntensity( dot( vSunDirection, up ) );",
+
+    "	vSunfade = 1.0 - clamp( 1.0 - exp( ( sunPosition.y / 450000.0 ) ), 0.0, 1.0 );",
+
+    "	float rayleighCoefficient = rayleigh - ( 1.0 * ( 1.0 - vSunfade ) );",
+
+    // extinction (absorbtion + out scattering)
+    // rayleigh coefficients
+    "	vBetaR = totalRayleigh * rayleighCoefficient;",
+
+    // mie coefficients
+    "	vBetaM = totalMie( turbidity ) * mieCoefficient;",
+
+    "}"
+  ].join("\n"),
+
+  fragmentShader: [
+    "varying vec3 vWorldPosition;",
+    "varying vec3 vSunDirection;",
+    "varying float vSunfade;",
+    "varying vec3 vBetaR;",
+    "varying vec3 vBetaM;",
+    "varying float vSunE;",
+
+    "uniform float luminance;",
+    "uniform float mieDirectionalG;",
+
+    "const vec3 cameraPos = vec3( 0.0, 0.0, 0.0 );",
+
+    // constants for atmospheric scattering
+    "const float pi = 3.141592653589793238462643383279502884197169;",
+
+    "const float n = 1.0003;", // refractive index of air
+    "const float N = 2.545E25;", // number of molecules per unit volume for air at
+    // 288.15K and 1013mb (sea level -45 celsius)
+
+    // optical length at zenith for molecules
+    "const float rayleighZenithLength = 8.4E3;",
+    "const float mieZenithLength = 1.25E3;",
+    "const vec3 up = vec3( 0.0, 1.0, 0.0 );",
+    // 66 arc seconds -> degrees, and the cosine of that
+    "const float sunAngularDiameterCos = 0.999956676946448443553574619906976478926848692873900859324;",
+
+    // 3.0 / ( 16.0 * pi )
+    "const float THREE_OVER_SIXTEENPI = 0.05968310365946075;",
+    // 1.0 / ( 4.0 * pi )
+    "const float ONE_OVER_FOURPI = 0.07957747154594767;",
+
+    "float rayleighPhase( float cosTheta ) {",
+    "	return THREE_OVER_SIXTEENPI * ( 1.0 + pow( cosTheta, 2.0 ) );",
+    "}",
+
+    "float hgPhase( float cosTheta, float g ) {",
+    "	float g2 = pow( g, 2.0 );",
+    "	float inverse = 1.0 / pow( 1.0 - 2.0 * g * cosTheta + g2, 1.5 );",
+    "	return ONE_OVER_FOURPI * ( ( 1.0 - g2 ) * inverse );",
+    "}",
+
+    // Filmic ToneMapping http://filmicgames.com/archives/75
+    "const float A = 0.15;",
+    "const float B = 0.50;",
+    "const float C = 0.10;",
+    "const float D = 0.20;",
+    "const float E = 0.02;",
+    "const float F = 0.30;",
+
+    "const float whiteScale = 1.0748724675633854;", // 1.0 / Uncharted2Tonemap(1000.0)
+
+    "vec3 Uncharted2Tonemap( vec3 x ) {",
+    "	return ( ( x * ( A * x + C * B ) + D * E ) / ( x * ( A * x + B ) + D * F ) ) - E / F;",
+    "}",
+
+    "void main() {",
+    // optical length
+    // cutoff angle at 90 to avoid singularity in next formula.
+    "	float zenithAngle = acos( max( 0.0, dot( up, normalize( vWorldPosition - cameraPos ) ) ) );",
+    "	float inverse = 1.0 / ( cos( zenithAngle ) + 0.15 * pow( 93.885 - ( ( zenithAngle * 180.0 ) / pi ), -1.253 ) );",
+    "	float sR = rayleighZenithLength * inverse;",
+    "	float sM = mieZenithLength * inverse;",
+
+    // combined extinction factor
+    "	vec3 Fex = exp( -( vBetaR * sR + vBetaM * sM ) );",
+
+    // in scattering
+    "	float cosTheta = dot( normalize( vWorldPosition - cameraPos ), vSunDirection );",
+
+    "	float rPhase = rayleighPhase( cosTheta * 0.5 + 0.5 );",
+    "	vec3 betaRTheta = vBetaR * rPhase;",
+
+    "	float mPhase = hgPhase( cosTheta, mieDirectionalG );",
+    "	vec3 betaMTheta = vBetaM * mPhase;",
+
+    "	vec3 Lin = pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * ( 1.0 - Fex ), vec3( 1.5 ) );",
+    "	Lin *= mix( vec3( 1.0 ), pow( vSunE * ( ( betaRTheta + betaMTheta ) / ( vBetaR + vBetaM ) ) * Fex, vec3( 1.0 / 2.0 ) ), clamp( pow( 1.0 - dot( up, vSunDirection ), 5.0 ), 0.0, 1.0 ) );",
+
+    // nightsky
+    "	vec3 direction = normalize( vWorldPosition - cameraPos );",
+    "	float theta = acos( direction.y ); // elevation --> y-axis, [-pi/2, pi/2]",
+    "	float phi = atan( direction.z, direction.x ); // azimuth --> x-axis [-pi/2, pi/2]",
+    "	vec2 uv = vec2( phi, theta ) / vec2( 2.0 * pi, pi ) + vec2( 0.5, 0.0 );",
+    "	vec3 L0 = vec3( 0.1 ) * Fex;",
+
+    // composition + solar disc
+    "	float sundisk = smoothstep( sunAngularDiameterCos, sunAngularDiameterCos + 0.00002, cosTheta );",
+    "	L0 += ( vSunE * 19000.0 * Fex ) * sundisk;",
+
+    "	vec3 texColor = ( Lin + L0 ) * 0.04 + vec3( 0.0, 0.0003, 0.00075 );",
+
+    "	vec3 curr = Uncharted2Tonemap( ( log2( 2.0 / pow( luminance, 4.0 ) ) ) * texColor );",
+    "	vec3 color = curr * whiteScale;",
+
+    "	vec3 retColor = pow( color, vec3( 1.0 / ( 1.2 + ( 1.2 * vSunfade ) ) ) );",
+
+    "	gl_FragColor = vec4( retColor, 1.0 );",
+
+    "}"
+  ].join("\n")
+};
+
+AFRAME.registerComponent("skybox", {
+  schema: {
+    turbidity: { type: "number", default: 10 },
+    rayleigh: { type: "number", default: 2 },
+    luminance: { type: "number", default: 1 },
+    mieCoefficient: { type: "number", default: 0.005 },
+    mieDirectionalG: { type: "number", default: 0.8 },
+    inclination: { type: "number", default: 0 },
+    azimuth: { type: "number", default: 0 },
+    distance: { type: "number", default: 8000 }
+  },
+
+  init() {
+    this.sky = new THREE.Sky();
+    this.el.setObject3D("mesh", this.sky);
+  },
+
+  update(oldData) {
+    const uniforms = this.sky.material.uniforms;
+
+    if (this.data.turbidity !== oldData.turbidity) {
+      uniforms.turbidity.value = this.data.turbidity;
+    }
+
+    if (this.data.rayleigh !== oldData.rayleigh) {
+      uniforms.rayleigh.value = this.data.rayleigh;
+    }
+
+    if (this.data.luminance !== oldData.luminance) {
+      uniforms.luminance.value = this.data.luminance;
+    }
+
+    if (this.data.mieCoefficient !== oldData.mieCoefficient) {
+      uniforms.mieCoefficient.value = this.data.mieCoefficient;
+    }
+
+    if (this.data.mieDirectionalG !== oldData.mieDirectionalG) {
+      uniforms.mieDirectionalG.value = this.data.mieDirectionalG;
+    }
+
+    if (
+      this.data.inclination !== oldData.inclination ||
+      this.data.azimuth !== oldData.azimuth ||
+      this.data.distance !== oldData.distance
+    ) {
+      const theta = Math.PI * (this.data.inclination - 0.5);
+      const phi = 2 * Math.PI * (this.data.azimuth - 0.5);
+
+      const distance = this.data.distance;
+
+      const x = distance * Math.cos(phi);
+      const y = distance * Math.sin(phi) * Math.sin(theta);
+      const z = distance * Math.sin(phi) * Math.cos(theta);
+
+      uniforms.sunPosition.value.set(x, y, z).normalize();
+    }
+  },
+
+  remove() {
+    this.el.removeObject3D("mesh");
+  }
+});
diff --git a/src/components/water.js b/src/components/water.js
new file mode 100644
index 0000000000000000000000000000000000000000..9152e548435ea8b184e9d1eb0f8cd4c2371217cc
--- /dev/null
+++ b/src/components/water.js
@@ -0,0 +1,237 @@
+import { Layers } from "./layers";
+import "../vendor/Water";
+
+/**
+ * @author jbouny / https://github.com/jbouny
+ *
+ * Work based on :
+ * @author Slayvin / http://slayvin.net : Flat mirror for three.js
+ * @author Stemkoski / http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror
+ * @author Jonas Wagner / http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL
+ */
+
+function MobileWater(geometry, options) {
+  THREE.Mesh.call(this, geometry);
+
+  const scope = this;
+
+  options = options || {};
+
+  const clipBias = options.clipBias !== undefined ? options.clipBias : 0.0;
+  const time = options.time !== undefined ? options.time : 0.0;
+  const normalSampler =
+    options.waterNormals !== undefined ? options.waterNormals : null;
+  const sunDirection =
+    options.sunDirection !== undefined
+      ? options.sunDirection
+      : new THREE.Vector3(0.70707, 0.70707, 0.0);
+  const sunColor = new THREE.Color(
+    options.sunColor !== undefined ? options.sunColor : 0xffffff
+  );
+  const waterColor = new THREE.Color(
+    options.waterColor !== undefined ? options.waterColor : 0x7f7f7f
+  );
+  const eye =
+    options.eye !== undefined ? options.eye : new THREE.Vector3(0, 0, 0);
+  const distortionScale =
+    options.distortionScale !== undefined ? options.distortionScale : 20.0;
+  const side = options.side !== undefined ? options.side : THREE.FrontSide;
+  const fog = options.fog !== undefined ? options.fog : false;
+
+  const mirrorShader = {
+    uniforms: THREE.UniformsUtils.merge([
+      THREE.UniformsLib["lights"],
+      {
+        normalSampler: { value: null },
+        time: { value: 0.0 },
+        size: { value: 1.0 },
+        distortionScale: { value: 20.0 },
+        sunColor: { value: new THREE.Color(0x7f7f7f) },
+        sunDirection: { value: new THREE.Vector3(0.70707, 0.70707, 0) },
+        eye: { value: new THREE.Vector3() },
+        waterColor: { value: new THREE.Color(0x555555) }
+      }
+    ]),
+
+    vertexShader: `
+      uniform float time;
+      varying vec4 worldPosition;
+
+      void main() {
+      	worldPosition = modelMatrix * vec4( position, 1.0 );
+      	vec4 mvPosition =  modelViewMatrix * vec4( position, 1.0 );
+      	gl_Position = projectionMatrix * mvPosition;
+
+      }
+    `,
+
+    fragmentShader: `
+      uniform float time;
+      uniform float size;
+      uniform float distortionScale;
+      uniform sampler2D normalSampler;
+      uniform vec3 sunColor;
+      uniform vec3 sunDirection;
+      uniform vec3 eye;
+      uniform vec3 waterColor;
+
+      varying vec4 worldPosition;
+
+      vec4 getNoise( vec2 uv ) {
+      	vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);
+      	vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );
+      	vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );
+      	vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );
+      	vec4 noise = texture2D( normalSampler, uv0 ) +
+      		texture2D( normalSampler, uv1 ) +
+      		texture2D( normalSampler, uv2 ) +
+      		texture2D( normalSampler, uv3 );
+      	return noise * 0.5 - 1.0;
+      }
+
+      void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) {
+      	vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) );
+      	float direction = max( 0.0, dot( eyeDirection, reflection ) );
+      	specularColor += pow( direction, shiny ) * sunColor * spec;
+      	diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse;
+      }
+
+      ${THREE.ShaderChunk["common"]}
+      ${THREE.ShaderChunk["packing"]}
+      ${THREE.ShaderChunk["bsdfs"]}
+      ${THREE.ShaderChunk["lights_pars"]}
+
+      void main() {
+      	vec4 noise = getNoise( worldPosition.xz * size );
+      	vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );
+
+      	vec3 diffuseLight = vec3(0.0);
+      	vec3 specularLight = vec3(0.0);
+
+      	vec3 worldToEye = eye-worldPosition.xyz;
+      	vec3 eyeDirection = normalize( worldToEye );
+      	sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );
+
+      	float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );
+      	float rf0 = 0.3;
+      	float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );
+      	vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;
+      	vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ), ( 0.5 + specularLight ), reflectance);
+      	vec3 outgoingLight = albedo;
+      	gl_FragColor = vec4( outgoingLight, 1 );
+      }
+    `
+  };
+
+  const material = new THREE.ShaderMaterial({
+    fragmentShader: mirrorShader.fragmentShader,
+    vertexShader: mirrorShader.vertexShader,
+    uniforms: THREE.UniformsUtils.clone(mirrorShader.uniforms),
+    transparent: false,
+    lights: true,
+    side: side,
+    fog: fog
+  });
+
+  material.uniforms.time.value = time;
+  material.uniforms.normalSampler.value = normalSampler;
+  material.uniforms.sunColor.value = sunColor;
+  material.uniforms.waterColor.value = waterColor;
+  material.uniforms.sunDirection.value = sunDirection;
+  material.uniforms.distortionScale.value = distortionScale;
+
+  material.uniforms.eye.value = eye;
+
+  scope.material = material;
+}
+
+MobileWater.prototype = Object.create(THREE.Mesh.prototype);
+MobileWater.prototype.constructor = THREE.Water;
+
+AFRAME.registerComponent("water", {
+  schema: {
+    waterColor: { type: "color", default: "#001e0f" },
+    distortionScale: { type: "number", default: 3.7 },
+    sunColor: { type: "color", default: "#ffffff" },
+    inclination: { type: "number", default: 0 },
+    azimuth: { type: "number", default: 0 },
+    distance: { type: "number", default: 1 },
+    speed: { type: "number", default: 0.1 },
+    forceMobile: { type: "boolean", default: false },
+    normalMap: { type: "asset" }
+  },
+  init() {
+    const waterGeometry = new THREE.PlaneBufferGeometry(800, 800);
+
+    const waterNormals = new THREE.Texture(this.data.normalMap);
+    waterNormals.wrapS = waterNormals.wrapT = THREE.RepeatWrapping;
+    waterNormals.needsUpdate = true;
+
+    const waterConfig = {
+      textureWidth: 512,
+      textureHeight: 512,
+      waterNormals: waterNormals,
+      sunDirection: this.data.sunDirection,
+      sunColor: new THREE.Color(this.data.sunColor),
+      waterColor: new THREE.Color(this.data.waterColor),
+      distortionScale: this.data.distortionScale,
+      fog: false
+    };
+
+    if (AFRAME.utils.device.isMobile() || this.data.forceMobile) {
+      this.water = new MobileWater(waterGeometry, waterConfig);
+    } else {
+      this.water = new THREE.Water(waterGeometry, waterConfig);
+      this.water.mirrorCamera.layers.set(Layers.reflection);
+    }
+
+    this.el.setObject3D("water", this.water);
+  },
+
+  update(oldData) {
+    const uniforms = this.water.material.uniforms;
+
+    if (this.data.forceMobile !== oldData.forceMobile) {
+      this.el.removeObject3D("water");
+      this.init();
+      return;
+    }
+
+    if (this.data.waterColor !== oldData.waterColor) {
+      uniforms.waterColor.value.setStyle(this.data.waterColor);
+    }
+
+    if (this.data.distortionScale !== oldData.distortionScale) {
+      uniforms.distortionScale.value = this.data.distortionScale;
+    }
+
+    if (this.data.sunColor !== oldData.sunColor) {
+      uniforms.sunColor.value.setStyle(this.data.sunColor);
+    }
+
+    if (
+      this.data.inclination !== oldData.inclination ||
+      this.data.azimuth !== oldData.azimuth ||
+      this.data.distance !== oldData.distance
+    ) {
+      const theta = Math.PI * (this.data.inclination - 0.5);
+      const phi = 2 * Math.PI * (this.data.azimuth - 0.5);
+
+      const distance = this.data.distance;
+
+      const x = distance * Math.cos(phi);
+      const y = distance * Math.sin(phi) * Math.sin(theta);
+      const z = distance * Math.sin(phi) * Math.cos(theta);
+
+      uniforms.sunDirection.value.set(x, y, z);
+    }
+  },
+
+  tick(time) {
+    this.water.material.uniforms.time.value = time / 1000 * this.data.speed;
+  },
+
+  remove() {
+    this.el.removeObject3D("water");
+  }
+});
diff --git a/src/room.js b/src/room.js
index 2330ffd76d063980a44352fb51430ea34ec35845..b79902452663c26a2c8b16e611f697ee67269107 100644
--- a/src/room.js
+++ b/src/room.js
@@ -23,6 +23,9 @@ import "./components/split-axis-events";
 import "./components/networked-video-player";
 import "./components/offset-relative-to";
 import "./components/cached-gltf-model";
+import "./components/water";
+import "./components/skybox";
+import "./components/layers";
 import "./components/spawn-controller";
 import "./systems/personal-space-bubble";
 
diff --git a/src/vendor/Water.js b/src/vendor/Water.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3160a754e0b41ba59e9219350f988ae6e03901f
--- /dev/null
+++ b/src/vendor/Water.js
@@ -0,0 +1,341 @@
+/**
+ * @author jbouny / https://github.com/jbouny
+ *
+ * Work based on :
+ * @author Slayvin / http://slayvin.net : Flat mirror for three.js
+ * @author Stemkoski / http://www.adelphi.edu/~stemkoski : An implementation of water shader based on the flat mirror
+ * @author Jonas Wagner / http://29a.ch/ && http://29a.ch/slides/2012/webglwater/ : Water shader explanations in WebGL
+ */
+
+THREE.Water = function(geometry, options) {
+  THREE.Mesh.call(this, geometry);
+
+  const scope = this;
+
+  options = options || {};
+
+  const textureWidth =
+    options.textureWidth !== undefined ? options.textureWidth : 512;
+  const textureHeight =
+    options.textureHeight !== undefined ? options.textureHeight : 512;
+
+  const clipBias = options.clipBias !== undefined ? options.clipBias : 0.0;
+  const alpha = options.alpha !== undefined ? options.alpha : 1.0;
+  const time = options.time !== undefined ? options.time : 0.0;
+  const normalSampler =
+    options.waterNormals !== undefined ? options.waterNormals : null;
+  const sunDirection =
+    options.sunDirection !== undefined
+      ? options.sunDirection
+      : new THREE.Vector3(0.70707, 0.70707, 0.0);
+  const sunColor = new THREE.Color(
+    options.sunColor !== undefined ? options.sunColor : 0xffffff
+  );
+  const waterColor = new THREE.Color(
+    options.waterColor !== undefined ? options.waterColor : 0x7f7f7f
+  );
+  const eye =
+    options.eye !== undefined ? options.eye : new THREE.Vector3(0, 0, 0);
+  const distortionScale =
+    options.distortionScale !== undefined ? options.distortionScale : 20.0;
+  const side = options.side !== undefined ? options.side : THREE.FrontSide;
+  const fog = options.fog !== undefined ? options.fog : false;
+
+  //
+
+  const mirrorPlane = new THREE.Plane();
+  const normal = new THREE.Vector3();
+  const mirrorWorldPosition = new THREE.Vector3();
+  const cameraWorldPosition = new THREE.Vector3();
+  const rotationMatrix = new THREE.Matrix4();
+  const lookAtPosition = new THREE.Vector3(0, 0, -1);
+  const clipPlane = new THREE.Vector4();
+
+  const view = new THREE.Vector3();
+  const target = new THREE.Vector3();
+  const q = new THREE.Vector4();
+
+  const textureMatrix = new THREE.Matrix4();
+
+  const mirrorCamera = new THREE.PerspectiveCamera();
+
+  const parameters = {
+    minFilter: THREE.LinearFilter,
+    magFilter: THREE.LinearFilter,
+    format: THREE.RGBFormat,
+    stencilBuffer: false
+  };
+
+  const renderTarget = new THREE.WebGLRenderTarget(
+    textureWidth,
+    textureHeight,
+    parameters
+  );
+
+  if (
+    !THREE.Math.isPowerOfTwo(textureWidth) ||
+    !THREE.Math.isPowerOfTwo(textureHeight)
+  ) {
+    renderTarget.texture.generateMipmaps = false;
+  }
+
+  const mirrorShader = {
+    uniforms: THREE.UniformsUtils.merge([
+      THREE.UniformsLib["fog"],
+      THREE.UniformsLib["lights"],
+      {
+        normalSampler: { value: null },
+        mirrorSampler: { value: null },
+        alpha: { value: 1.0 },
+        time: { value: 0.0 },
+        size: { value: 1.0 },
+        distortionScale: { value: 20.0 },
+        textureMatrix: { value: new THREE.Matrix4() },
+        sunColor: { value: new THREE.Color(0x7f7f7f) },
+        sunDirection: { value: new THREE.Vector3(0.70707, 0.70707, 0) },
+        eye: { value: new THREE.Vector3() },
+        waterColor: { value: new THREE.Color(0x555555) }
+      }
+    ]),
+
+    vertexShader: [
+      "uniform mat4 textureMatrix;",
+      "uniform float time;",
+
+      "varying vec4 mirrorCoord;",
+      "varying vec4 worldPosition;",
+
+      THREE.ShaderChunk["fog_pars_vertex"],
+      THREE.ShaderChunk["shadowmap_pars_vertex"],
+
+      "void main() {",
+      "	mirrorCoord = modelMatrix * vec4( position, 1.0 );",
+      "	worldPosition = mirrorCoord.xyzw;",
+      "	mirrorCoord = textureMatrix * mirrorCoord;",
+      "	vec4 mvPosition =  modelViewMatrix * vec4( position, 1.0 );",
+      "	gl_Position = projectionMatrix * mvPosition;",
+
+      THREE.ShaderChunk["fog_vertex"],
+      THREE.ShaderChunk["shadowmap_vertex"],
+
+      "}"
+    ].join("\n"),
+
+    fragmentShader: [
+      "uniform sampler2D mirrorSampler;",
+      "uniform float alpha;",
+      "uniform float time;",
+      "uniform float size;",
+      "uniform float distortionScale;",
+      "uniform sampler2D normalSampler;",
+      "uniform vec3 sunColor;",
+      "uniform vec3 sunDirection;",
+      "uniform vec3 eye;",
+      "uniform vec3 waterColor;",
+
+      "varying vec4 mirrorCoord;",
+      "varying vec4 worldPosition;",
+
+      "vec4 getNoise( vec2 uv ) {",
+      "	vec2 uv0 = ( uv / 103.0 ) + vec2(time / 17.0, time / 29.0);",
+      "	vec2 uv1 = uv / 107.0-vec2( time / -19.0, time / 31.0 );",
+      "	vec2 uv2 = uv / vec2( 8907.0, 9803.0 ) + vec2( time / 101.0, time / 97.0 );",
+      "	vec2 uv3 = uv / vec2( 1091.0, 1027.0 ) - vec2( time / 109.0, time / -113.0 );",
+      "	vec4 noise = texture2D( normalSampler, uv0 ) +",
+      "		texture2D( normalSampler, uv1 ) +",
+      "		texture2D( normalSampler, uv2 ) +",
+      "		texture2D( normalSampler, uv3 );",
+      "	return noise * 0.5 - 1.0;",
+      "}",
+
+      "void sunLight( const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse, inout vec3 diffuseColor, inout vec3 specularColor ) {",
+      "	vec3 reflection = normalize( reflect( -sunDirection, surfaceNormal ) );",
+      "	float direction = max( 0.0, dot( eyeDirection, reflection ) );",
+      "	specularColor += pow( direction, shiny ) * sunColor * spec;",
+      "	diffuseColor += max( dot( sunDirection, surfaceNormal ), 0.0 ) * sunColor * diffuse;",
+      "}",
+
+      THREE.ShaderChunk["common"],
+      THREE.ShaderChunk["packing"],
+      THREE.ShaderChunk["bsdfs"],
+      THREE.ShaderChunk["fog_pars_fragment"],
+      THREE.ShaderChunk["lights_pars"],
+      THREE.ShaderChunk["shadowmap_pars_fragment"],
+      THREE.ShaderChunk["shadowmask_pars_fragment"],
+
+      "void main() {",
+      "	vec4 noise = getNoise( worldPosition.xz * size );",
+      "	vec3 surfaceNormal = normalize( noise.xzy * vec3( 1.5, 1.0, 1.5 ) );",
+
+      "	vec3 diffuseLight = vec3(0.0);",
+      "	vec3 specularLight = vec3(0.0);",
+
+      "	vec3 worldToEye = eye-worldPosition.xyz;",
+      "	vec3 eyeDirection = normalize( worldToEye );",
+      "	sunLight( surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuseLight, specularLight );",
+
+      "	float distance = length(worldToEye);",
+
+      "	vec2 distortion = surfaceNormal.xz * ( 0.001 + 1.0 / distance ) * distortionScale;",
+      "	vec3 reflectionSample = vec3( texture2D( mirrorSampler, mirrorCoord.xy / mirrorCoord.z + distortion ) );",
+
+      "	float theta = max( dot( eyeDirection, surfaceNormal ), 0.0 );",
+      "	float rf0 = 0.3;",
+      "	float reflectance = rf0 + ( 1.0 - rf0 ) * pow( ( 1.0 - theta ), 5.0 );",
+      "	vec3 scatter = max( 0.0, dot( surfaceNormal, eyeDirection ) ) * waterColor;",
+      "	vec3 albedo = mix( ( sunColor * diffuseLight * 0.3 + scatter ) * getShadowMask(), ( vec3( 0.1 ) + reflectionSample * 0.9 + reflectionSample * specularLight ), reflectance);",
+      "	vec3 outgoingLight = albedo;",
+      "	gl_FragColor = vec4( outgoingLight, alpha );",
+
+      THREE.ShaderChunk["tonemapping_fragment"],
+      THREE.ShaderChunk["fog_fragment"],
+
+      "}"
+    ].join("\n")
+  };
+
+  const material = new THREE.ShaderMaterial({
+    fragmentShader: mirrorShader.fragmentShader,
+    vertexShader: mirrorShader.vertexShader,
+    uniforms: THREE.UniformsUtils.clone(mirrorShader.uniforms),
+    transparent: true,
+    lights: true,
+    side: side,
+    fog: fog
+  });
+
+  material.uniforms.mirrorSampler.value = renderTarget.texture;
+  material.uniforms.textureMatrix.value = textureMatrix;
+  material.uniforms.alpha.value = alpha;
+  material.uniforms.time.value = time;
+  material.uniforms.normalSampler.value = normalSampler;
+  material.uniforms.sunColor.value = sunColor;
+  material.uniforms.waterColor.value = waterColor;
+  material.uniforms.sunDirection.value = sunDirection;
+  material.uniforms.distortionScale.value = distortionScale;
+
+  material.uniforms.eye.value = eye;
+
+  scope.material = material;
+  scope.mirrorCamera = mirrorCamera;
+
+  scope.onBeforeRender = function(renderer, scene, camera) {
+    mirrorWorldPosition.setFromMatrixPosition(scope.matrixWorld);
+    cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
+
+    rotationMatrix.extractRotation(scope.matrixWorld);
+
+    normal.set(0, 0, 1);
+    normal.applyMatrix4(rotationMatrix);
+
+    view.subVectors(mirrorWorldPosition, cameraWorldPosition);
+
+    // Avoid rendering when mirror is facing away
+
+    if (view.dot(normal) > 0) return;
+
+    view.reflect(normal).negate();
+    view.add(mirrorWorldPosition);
+
+    rotationMatrix.extractRotation(camera.matrixWorld);
+
+    lookAtPosition.set(0, 0, -1);
+    lookAtPosition.applyMatrix4(rotationMatrix);
+    lookAtPosition.add(cameraWorldPosition);
+
+    target.subVectors(mirrorWorldPosition, lookAtPosition);
+    target.reflect(normal).negate();
+    target.add(mirrorWorldPosition);
+
+    mirrorCamera.position.copy(view);
+    mirrorCamera.up.set(0, 1, 0);
+    mirrorCamera.up.applyMatrix4(rotationMatrix);
+    mirrorCamera.up.reflect(normal);
+    mirrorCamera.lookAt(target);
+
+    mirrorCamera.far = camera.far; // Used in WebGLBackground
+
+    mirrorCamera.updateMatrixWorld();
+    mirrorCamera.projectionMatrix.copy(camera.projectionMatrix);
+
+    // Update the texture matrix
+    textureMatrix.set(
+      0.5,
+      0.0,
+      0.0,
+      0.5,
+      0.0,
+      0.5,
+      0.0,
+      0.5,
+      0.0,
+      0.0,
+      0.5,
+      0.5,
+      0.0,
+      0.0,
+      0.0,
+      1.0
+    );
+    textureMatrix.multiply(mirrorCamera.projectionMatrix);
+    textureMatrix.multiply(mirrorCamera.matrixWorldInverse);
+
+    // Now update projection matrix with new clip plane, implementing code from: http://www.terathon.com/code/oblique.html
+    // Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
+    mirrorPlane.setFromNormalAndCoplanarPoint(normal, mirrorWorldPosition);
+    mirrorPlane.applyMatrix4(mirrorCamera.matrixWorldInverse);
+
+    clipPlane.set(
+      mirrorPlane.normal.x,
+      mirrorPlane.normal.y,
+      mirrorPlane.normal.z,
+      mirrorPlane.constant
+    );
+
+    const projectionMatrix = mirrorCamera.projectionMatrix;
+
+    q.x =
+      (Math.sign(clipPlane.x) + projectionMatrix.elements[8]) /
+      projectionMatrix.elements[0];
+    q.y =
+      (Math.sign(clipPlane.y) + projectionMatrix.elements[9]) /
+      projectionMatrix.elements[5];
+    q.z = -1.0;
+    q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14];
+
+    // Calculate the scaled plane vector
+    clipPlane.multiplyScalar(2.0 / clipPlane.dot(q));
+
+    // Replacing the third row of the projection matrix
+    projectionMatrix.elements[2] = clipPlane.x;
+    projectionMatrix.elements[6] = clipPlane.y;
+    projectionMatrix.elements[10] = clipPlane.z + 1.0 - clipBias;
+    projectionMatrix.elements[14] = clipPlane.w;
+
+    eye.setFromMatrixPosition(camera.matrixWorld);
+
+    //
+
+    const currentRenderTarget = renderer.getRenderTarget();
+
+    const currentVrEnabled = renderer.vr.enabled;
+    const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
+
+    scope.visible = false;
+
+    renderer.vr.enabled = false; // Avoid camera modification and recursion
+    renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
+
+    renderer.render(scene, mirrorCamera, renderTarget, true);
+
+    scope.visible = true;
+
+    renderer.vr.enabled = currentVrEnabled;
+    renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
+
+    renderer.setRenderTarget(currentRenderTarget);
+  };
+};
+
+THREE.Water.prototype = Object.create(THREE.Mesh.prototype);
+THREE.Water.prototype.constructor = THREE.Water;
diff --git a/templates/room.hbs b/templates/room.hbs
index add5dff049b1731e33645724d2d848777e26d36f..0e90903456c0dda97f644339b5cc3eea899c90c1 100644
--- a/templates/room.hbs
+++ b/templates/room.hbs
@@ -55,6 +55,8 @@
             <a-asset-item id="floor-nav-mesh" response-type="arraybuffer" src="{{asset "assets/environments/FloorNav_mesh.glb"}}"></a-asset-item>
             <a-asset-item id="cliff-vista-mesh" response-type="arraybuffer" src="{{asset "assets/environments/CliffVista_mesh.glb"}}"></a-asset-item>
 
+            <img id="water-normal-map" src="{{asset "assets/waternormals.jpg"}}"></a-asset-item>
+
             <!-- Templates -->
             <script id="head-template" type="text/html">
                 <a-entity
@@ -123,8 +125,6 @@
             spawn-controller="radius: 4;"
             character-controller="pivot: #head"
         >
-            <a-sphere scale="0.1 0.1 0.1"></a-sphere>
-
             <a-entity
                 id="head"
                 camera="userHeight: 1.6"
@@ -174,26 +174,47 @@
             ></a-entity>
         </a-entity>
 
-        <a-entity light="type: ambient; color: #FFF"></a-entity>
-
         <!-- Environment -->
         <a-entity
+            id="meeting-space"
             cached-gltf-model="#meeting-space1-mesh"
             position="0 0 0"
         ></a-entity>
 
         <a-entity
+            id="outdoor-facade"
             cached-gltf-model="#outdoor-facade-mesh"
             position="0 0 0"
         ></a-entity>
 
         <a-entity
+            id="floor-nav"
             cached-gltf-model="#floor-nav-mesh"
             visible="false"
             position="0 0 0"
         ></a-entity>
 
-        <a-sky color="#DDFFD9"></a-sky>
+        <a-entity
+            id="cliff-vista"
+            cached-gltf-model="#cliff-vista-mesh"
+            layers="reflection:true"
+            position="0 0 0"
+        ></a-entity>
+
+        <a-entity id="skybox"
+            id="skybox"
+            scale="8000 8000 8000"
+            skybox="azimuth:0.280; inclination:0.440"
+            light="type: ambient; color: #FFF"
+            layers="reflection:true"
+        ></a-entity>
+
+        <a-entity
+            id="water"
+            water="normalMap:#water-normal-map"
+            rotation="-90 0 0"
+            position="0 -88.358 -332.424"
+        ></a-entity>
     </a-scene>
 
     <script>