Hello Unity

Hello Unity

Thanks to the convenience of modern libraries, getting started with WebXR is a matter of minutes and 57 lines of code.

The only applications you'll need to get up and running are a WebXR-capable web browser and a text editor.

By the end of this tutorial, you'll have a model rendered in a WebXR session. If you have a headset, you will be able to move around the model in 3D. However, displaying and using controllers will not be covered in this material.

Getting started

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hello WebXR</title>
  </head>
  <body>
    <!-- scripts will go here -->
  </body>
</html>

Dependencies

To get up and running as quickly as possible, we're relying on three.js, the industry standard 3D programming javascript library. We could certainly create a WebXR app without three.js, but it's much more complicated or requires other libraries. To skirt the question of whether to use javascript tooling on your machine to manage libraries, we'll be loading three.js directly from the web in our own file.

We'll need to add a little bit of code to tell our page how to find those files when our code asks for them. Inside of the body tag, we can replace our comment with:

<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.150.0/build/three.module.js",
      "three/examples/": "https://unpkg.com/three@0.150.0/examples/"
    }
  }
</script>
<script type="module">
  import * as THREE from 'three'
  import { VRButton } from 'three/examples/jsm/webxr/VRButton.js'
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 
  // Our application code will start here
</script>

You can see that we've added two script tags here. The first is the part that describes how to find the three.js files we need on the web. The second is where our code requests those files to be used in further code that we'll write.

In the first section, we have an object that will resolve imports in the second section that look either for “three” or for file paths starting with “three/examples”. In the second section, we first import from “three”, which resolves directly to the bundled javascript file for three.js. This is followed by two more imports. Since the path in each of those two imports begins with the “three/examples” path we have in the first section, it will replace that part of the path with the web url, then it will append the rest of the path to find the relevant files. (Don't worry if this isn't entirely clear; it's secondary to our goal of getting up and running. However, it will be useful to develop an understanding of the different methods of specifying and retrieving javascript libraries.)

A basic three.js scene

Now that we have our HTML and our dependencies in place, we can start to write the code for our application! In three.js, there are a few elements that are fundamental to any scene:

  • The Scene itself: this acts as a holder for all of the elements of your world
  • The Camera: we need to have a way to describe how to look at the things in our world
  • One or more Lights: without them, we can't actually see anything, generally speaking
  • The Renderer: this describes how everything will be drawn on your device

These can be created in our application by replacing our last comment with the following code:

const scene = new THREE.Scene()
 
const cameraRatio = window.innerWidth / window.innerHeight
const camera = new THREE.PerspectiveCamera(50, cameraRatio, 0.1, 10)
scene.add(camera)
 
const light = new THREE.DirectionalLight(0xffffff)
light.position.set(1, 1, 1)
scene.add(light)
 
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.xr.enabled = true

As you can see from the code above, once we create a scene, anything we want to exist in the world has to be added to the scene, including objects that aren't observed directly, like the camera and the light.

Loading a model

While we have a world, we have nothing to look at! To make sure that everything is working correctly, we'll want some kind of visible object in the scene. In our case, we'll be loading a simple model in the format.

To make sure that loading a model is as simple as possible, we've encoded our model as a base64 data url. This means that the data for the model will be embedded directly into our file, not loaded from anywhere else.

After our last two lines of renderer-related code, we can add the following to load a model into our scene:

const gltfLoader = new GLTFLoader();
let helloModel;
gltfLoader.load(
  “data_uri_here”,
  (gltf) => {
    helloModel = gltf.scene;
    helloModel.position.y = 1.6;
    helloModel.position.z = -0.25;
    scene.add(helloModel);
  }
);

This GLB File (opens in a new tab) creates an object that can load a model for us, then tells that object how to load the model. All of the data necessary is in that first, very large string passed to the load function. After that, we have a callback function that will receive the loaded model so that we can do something with it. As we know from our cameras and lights, if we want something in the scene, it has to be added, so we do that for our model here. Additionally, we do a little positioning to hopefully make it easier to see as soon as we start our WebXR session.

Rendering the scene

We're so close to having our first WebXR scene! We have all of the pieces in place for our XR world, but we need to tell the application how to render each frame. How will we do that? With the renderer, of course. At the end of our current javascript, we can add the following:

function render() {
  renderer.render(scene, camera)
}
 
renderer.setAnimationLoop(render)

This simply tells our renderer that, on each frame, it should call our application render method. We can add per-frame logic for our world to this application, but the one thing that it should always do is call the renderer.render method with our scene and camera, or what to look at and how to look at it.

Getting into the XR session

Finally! The last piece that we'll need is a way for the user to start the XR session. three.js makes this simple, providing an HTML button that we can render into the page. The user can click this button, and the XR session will start. Adding this to our web page is a one liner at the end of our script:

document.body.appendChild(VRButton.createButton(renderer))

That's it!

If you load your page into your browser now, you should see a button that says ENTER VR. Clicking this button should take you into a WebXR session in which you can move around a model in space. This is, of course, just the very first step in what can be a long journey into the world of XR. If you want to learn more, check out some of our other tutorials here, or look at the WebXR examples on the three.js website.

Full source

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
 
  <body>
    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/three@0.150.0/build/three.module.js",
          "three/examples/": "https://unpkg.com/three@0.150.0/examples/"
        }
      }
    </script>
    <script type="module">
      import * as THREE from "three";
      import { VRButton } from "three/examples/jsm/webxr/VRButton.js";
      import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
 
      const scene = new THREE.Scene();
 
      const cameraRatio = window.innerWidth / window.innerHeight;
      const camera = new THREE.PerspectiveCamera(50, cameraRatio, 0.1, 10);
      scene.add(camera);
 
      const light = new THREE.DirectionalLight(0xffffff);
      light.position.set(1, 1, 1);
      scene.add(light);
 
      const gltfLoader = new GLTFLoader();
      let helloModel;
      gltfLoader.load(
      “dataUri_here”,
          (gltf) => {
              helloModel = gltf.scene;
              helloModel.position.y = 1.6;
              helloModel.position.z = -0.25;
              scene.add(helloModel);
          }
      );
 
      const renderer = new THREE.WebGLRenderer({ antialias: true });
      renderer.xr.enabled = true;
 
      document.body.appendChild(VRButton.createButton(renderer));
 
      function render() {
          renderer.render(scene, camera);
      }
 
      renderer.setAnimationLoop(render);
    </script>
  </body>
</html>