diff --git a/public/webXR.html b/public/webXR.html
new file mode 100644
index 0000000000000000000000000000000000000000..3a8c33548ff959afe00590070da83bfa35742673
--- /dev/null
+++ b/public/webXR.html
@@ -0,0 +1,289 @@
+<!doctype html>
+<!--
+Copyright 2021 The Immersive Web Community Group
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<html>
+  <head>
+    <meta charset='utf-8'>
+    <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
+    <meta name='mobile-web-app-capable' content='yes'>
+    <meta name='apple-mobile-web-app-capable' content='yes'>
+    <link rel='icon' type='image/png' sizes='32x32' href='favicon-32x32.png'>
+    <link rel='icon' type='image/png' sizes='96x96' href='favicon-96x96.png'>
+    <link rel='stylesheet' href='css/common.css'>
+
+    <title>Anchors</title>
+  </head>
+  <body>
+    <header>
+      <details open>
+        <summary>Anchors</summary>
+        <p>
+          This sample demonstrates the use of anchors to place virtual objects in stable, real-world locations.
+          <a class="back" href="./">Back</a>
+        </p>
+      </details>
+    </header>
+    <script type="module">
+      import {WebXRButton} from './js/util/webxr-button.js';
+      import {Scene} from './js/render/scenes/scene.js';
+      import {Renderer, createWebGLContext} from './js/render/core/renderer.js';
+      import {SkyboxNode} from './js/render/nodes/skybox.js';
+      import {InlineViewerHelper} from './js/util/inline-viewer-helper.js';
+      import {Gltf2Node} from './js/render/nodes/gltf2.js';
+      import {QueryArgs} from './js/util/query-args.js';
+
+      // If requested, use the polyfill to provide support for mobile devices
+      // and devices which only support WebVR.
+      import WebXRPolyfill from './js/third-party/webxr-polyfill/build/webxr-polyfill.module.js';
+      if (QueryArgs.getBool('usePolyfill', true)) {
+        let polyfill = new WebXRPolyfill();
+      }
+
+      // XR globals.
+      let xrButton = null;
+      let xrImmersiveRefSpace = null;
+      let inlineViewerHelper = null;
+
+      let isARAvailable = false;
+      let isVRAvailable = false;
+      let xrSessionString = 'immersive-vr';
+
+      // WebGL scene globals.
+      let gl = null;
+      let renderer = null;
+      let scene = new Scene();
+      let solarSystem = new Gltf2Node({url: 'media/gltf/space/space.gltf'});
+      // The solar system is big (citation needed). Scale it down so that users
+      // can move around the planets more easily.
+      solarSystem.scale = [0.1, 0.1, 0.1];
+      scene.addNode(solarSystem);
+      // Still adding a skybox, but only for the benefit of the inline view.
+      let skybox = new SkyboxNode({url: 'media/textures/milky-way-4k.png'});
+      scene.addNode(skybox);
+
+      const MAX_ANCHORED_OBJECTS = 30;
+      let anchoredObjects = [];
+
+      // Set with all anchors tracked in a previous frame.
+      let all_previous_anchors = new Set();
+
+      function initXR() {
+        xrButton = new WebXRButton({
+          onRequestSession: onRequestSession,
+          onEndSession: onEndSession,
+          textEnterXRTitle: isARAvailable ? "START AR" : "START VR",
+          textXRNotFoundTitle: isARAvailable ? "AR NOT FOUND" : "VR NOT FOUND",
+          textExitXRTitle: isARAvailable ? "EXIT  AR" : "EXIT  VR",
+        });
+        document.querySelector('header').appendChild(xrButton.domElement);
+
+        if (navigator.xr) {
+          // Checks to ensure that 'immersive-ar' or 'immersive-vr' mode is available,
+          // and only enables the button if so.
+          navigator.xr.isSessionSupported('immersive-ar').then((supported) => {
+            isARAvailable = supported;
+            xrButton.enabled = supported;
+            if (!supported) {
+              navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
+                isVRAvailable = supported;
+                xrButton.enabled = supported;
+              });
+            } else {
+              xrSessionString = 'immersive-ar';
+            }
+          });
+
+          navigator.xr.requestSession('inline').then(onSessionStarted);
+        }
+      }
+
+      function onRequestSession() {
+        // Requests an 'immersive-ar' or 'immersive-vr' session, depending on which is supported,
+        // and requests the 'anchors' module as a required feature.
+        return navigator.xr.requestSession(xrSessionString, {requiredFeatures: ['anchors']})
+            .then((session) => {
+              xrButton.setSession(session);
+              session.isImmersive = true;
+              onSessionStarted(session);
+            });
+      }
+
+      function initGL() {
+        if (gl)
+          return;
+
+        gl = createWebGLContext({
+          xrCompatible: true
+        });
+        document.body.appendChild(gl.canvas);
+
+        function onResize() {
+          gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio;
+          gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio;
+        }
+        window.addEventListener('resize', onResize);
+        onResize();
+
+        renderer = new Renderer(gl);
+
+        scene.setRenderer(renderer);
+      }
+
+      function onSessionStarted(session) {
+        session.addEventListener('end', onSessionEnded);
+        session.addEventListener('select', onSelect);
+
+        if (session.isImmersive && isARAvailable) {
+          // When in 'immersive-ar' mode don't draw an opaque background because
+          // we want the real world to show through.
+          skybox.visible = false;
+        }
+
+        initGL();
+
+        // This and all future samples that visualize controllers will use this
+        // convenience method to listen for changes to the active XRInputSources
+        // and load the right meshes based on the profiles array.
+        scene.inputRenderer.useProfileControllerMeshes(session);
+
+        session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
+
+        let refSpaceType = session.isImmersive ? 'local' : 'viewer';
+        session.requestReferenceSpace(refSpaceType).then((refSpace) => {
+          if (session.isImmersive) {
+            xrImmersiveRefSpace = refSpace;
+          } else {
+            inlineViewerHelper = new InlineViewerHelper(gl.canvas, refSpace);
+          }
+          session.requestAnimationFrame(onXRFrame);
+        });
+      }
+
+      function onEndSession(session) {
+        session.end();
+      }
+
+      function onSessionEnded(event) {
+        if (event.session.isImmersive) {
+          xrButton.setSession(null);
+          // Turn the background back on when we go back to the inlive view.
+          skybox.visible = true;
+        }
+      }
+
+      function addAnchoredObjectToScene(anchor) {
+        console.debug("Anchor created");
+
+        anchor.context = {};
+
+        let flower = new Gltf2Node({url: 'media/gltf/sunflower/sunflower.gltf'});
+        scene.addNode(flower);
+        anchor.context.sceneObject = flower;
+        flower.anchor = anchor;
+        anchoredObjects.push(flower);
+
+        // For performance reasons if we add too many objects start
+        // removing the oldest ones to keep the scene complexity
+        // from growing too much.
+        if (anchoredObjects.length > MAX_ANCHORED_OBJECTS) {
+          let objectToRemove = anchoredObjects.shift();
+          scene.removeNode(objectToRemove);
+          objectToRemove.anchor.delete();
+        }
+      }
+
+      function onSelect(event) {
+        let frame = event.frame;
+        let session = frame.session;
+        let anchorPose = new XRRigidTransform();
+        let inputSource = event.inputSource;
+
+        // If the user is on a screen based device, place the anchor 1 meter in front of them.
+        // Otherwise place the anchor at the location of the input device
+        if (inputSource.targetRayMode == 'screen') {
+          anchorPose = new XRRigidTransform(
+          {x: 0, y: 0, z: -1},
+          {x: 0, y: 0, z: 0, w: 1});
+        }
+
+        if (session.isImmersive) {
+          // Create a free-floating anchor.
+          frame.createAnchor(anchorPose, inputSource.targetRaySpace).then((anchor) => {
+            addAnchoredObjectToScene(anchor);
+          }, (error) => {
+            console.error("Could not create anchor: " + error);
+          });
+        }
+      }
+
+      // Called every time a XRSession requests that a new frame be drawn.
+      function onXRFrame(t, frame) {
+        let session = frame.session;
+        let xrRefSpace = session.isImmersive ?
+                         xrImmersiveRefSpace :
+                         inlineViewerHelper.referenceSpace;
+        let pose = frame.getViewerPose(xrRefSpace);
+
+        // Update the position of all the anchored objects based on the currently reported positions of their anchors
+        const tracked_anchors = frame.trackedAnchors;
+        if(tracked_anchors){
+          all_previous_anchors.forEach(anchor => {
+            if(!tracked_anchors.has(anchor)){
+              scene.removeNode(anchor.sceneObject);
+            }
+          });
+
+          tracked_anchors.forEach(anchor => {
+            const anchorPose = frame.getPose(anchor.anchorSpace, xrRefSpace);
+            if (anchorPose) {
+              anchor.context.sceneObject.matrix = anchorPose.transform.matrix;
+              anchor.context.sceneObject.visible = true;
+            } else {
+              anchor.context.sceneObject.visible = false;
+            }
+          });
+
+          all_previous_anchors = tracked_anchors;
+        } else {
+          all_previous_anchors.forEach(anchor => {
+            scene.removeNode(anchor.sceneObject);
+          });
+
+          all_previous_anchors = new Set();
+        }
+
+        // In this sample and most samples after it we'll use a helper function
+        // to automatically add the right meshes for the session's input sources
+        // each frame. This also does simple hit detection to position the
+        // cursors correctly on the surface of selectable nodes.
+        scene.updateInputSources(frame, xrRefSpace);
+
+        scene.startFrame();
+
+        session.requestAnimationFrame(onXRFrame);
+
+        scene.drawXRFrame(frame, pose);
+
+        scene.endFrame();
+      }
+
+      // Start the XR application.
+      initXR();
+    </script>
+  </body>
+</html>