Creating WebXR Environments

WebXR is a set of enabling technologies that allow developers to create virtual reality and augmented reality experiences hosted in a browser. In this sense, it supercedes the older WebVR API.

Technology Stack

There are a number of things that come together to build WebXR experiences.

Unless you're planning on building your own rendering engine, you're better off looking at an existing framework to get going.

Frameworks

There are a number of frameworks you can use to build WebXR experiences. Here are some that I've used or I've looked into in the past.

I've run into BabylonJS back in my Windows Mixed Reality development days, so that's what I'm going with today.

Additional Tools

Hello World

Let's put together a hello world! kind of page.

Starting a Page

Let's start with a simple skeleton HTML5 page.

<!DOCTYPE html>
<html lang='en-US'>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Hello World XR!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
  </body>
</html>

OK, a simple Hello World page, nothing much to see here.

Importing IWER

Now let's import IWER so we can test this even without a headset.

<body>
  <script type="importmap">
  {
    "imports": {
      "iwer": "https://unpkg.com/iwer@1.0.0/build/iwer.module.js"
    }
  }
  </script>
  <script type='text/javascript'>
  
  async function runAppAsync() {
    const iwer = await import("iwer");
    const xrDevice = new iwer.XRDevice(iwer.metaQuest2);
    xrDevice.installRuntime();
  }
  runAppAsync();

  </script>
</body>

At this point, you should be able to refresh your page and see it run without any errors, but there's not much going on yet.

Let's fix that.

Importing BabylonJS

To import BabylonJS, we'll add the core library and the loaders.

<!-- add these lines after the importmap script -->
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>

Again, refreshing now won't do much, but it's good to see we haven't broken anything yet.

Starting BabylonJS

Let's add a rendering canvas and revisit our script block.

<canvas id="renderCanvas"></canvas>
<script type='text/javascript'>
let gameEngine = null;
let gameScene = null;
let gameXrDevice = null;

async function createSphereSceneAsync(engine, canvas) {
  // Create a scene that can be rendered.
  const scene = new BABYLON.Scene(engine);

  // Create a free camera
  const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
  camera.setTarget(BABYLON.Vector3.Zero());
  camera.attachControl(canvas, true);
  
  // Create a light in the scene.
  const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
  light.intensity = 0.7;

  // Create a 2m sphere.
  const sphere = BABYLON.MeshBuilder.CreateSphere("sphere1", { segments: 16, diameter: 2 }, scene);
  sphere.position.y = 1;

  // Create the default environment.
  const env = scene.createDefaultEnvironment();

  // here we add XR support
  const xr = await scene.createDefaultXRExperienceAsync({
    floorMeshes: [env.ground],
  });

  return scene;
};

async function runAppAsync() {
  const iwer = await import("iwer");
  const xrDevice = new iwer.XRDevice(iwer.metaQuest2);
  xrDevice.installRuntime();

  const canvas = document.getElementById("renderCanvas");
  const engine = new BABYLON.Engine(canvas, true);
  const scene = await createSphereSceneAsync(engine, canvas);

  // These globals make it easy to tweak things from the browser console.
  gameEngine = engine;
  gameScene = scene;
  gameXrDevice = xrDevice;

  // Register a render loop to repeatedly render the scene
  engine.runRenderLoop(function () {
    scene.render();
  });
};
runAppAsync();
</script>

OK, a lot of things are going to be happening this time around.

First, when you refresh the page, you'll see a sphere in the top-left corner of the page, and a button with VR goggles on the bottom-right. That's because Babylon has detected VR support, courtesy of IWER.

When you click on the goggles, you'll see the viewport grow larger and you'll find a pair of controllers. However, the sphere is gone. It's a classic - we're inside the sphere, so half of it is behind us, and everything else is removed by a backface cull (the sphere triangels are facing towards the outside).

We can correct this in the browser by adjusting our camera position with the globals we've conveniently set up for ourselves. Evaluate gameXrDevice.position.z = 6 and the sphere will be back in view.

Running on headset

Once we're ready to test on headset, we'll want to make sure IWER doesn't get in the way.

Let's go back to our runAppAsync function and adjust the preamble there.

async function runAppAsync() {
  const nativeXr = navigator.xr &&
    await navigator.xr.isSessionSupported('immersive-vr');
  if (!nativeXr) {
    console.log('Bootstrapping IWER ...');
    const iwer = await import("iwer");
    const xrDevice = new iwer.XRDevice(iwer.metaQuest2);
    xrDevice.installRuntime();
    gameXrDevice = xrDevice; // let's move this up here
  }

And with this, you're ready to walk around your beautiful sphere.

You can also speed up your iteration cycle by setting up port forwarding via adb to connect to a server on your development host.

The end result is here.

Building up your environment

OK, what are some things you'll need to add to your scene at this point?

You're now ready to fall down the rabbit hole of decorating your environment, making it interactive, acquiring or building assets, and so much more!

Happy reality crafting!

Tags:  graphicstutorialvr

Home