© Rakesh Baruah 2021
R. BaruahAR and VR Using the WebXR APIhttps://doi.org/10.1007/978-1-4842-6318-1_6

6. Entering VR Through WebXR

Rakesh Baruah1  
(1)
Brookfield, WI, USA
 

In the previous chapter we delved into the 3D JavaScript library built atop the WebGL API called Three.js. By building a scene with geometric primitives and textures, we saw the ease Three.js provides to WebXR developers; gone were the bare-bones data structures of attribute buffers and vertex arrays. However, while Three.js offers a convenient, high-level abstraction of WebGL functionality, it is still, at its core, no more than a rendering tool for the browser. While Three.js may help a developer paint a three-dimensional scene to a Web browser’s HTML canvas element, it cannot, alone, pass that scene to a peripheral device such as a virtual reality headset. In this chapter we will use the Three.js scene we built in Exercise 4 of the previous lesson to launch an immersive WebXR session on an Oculus Quest VR headset. By the end of this chapter and its exercise, you will have an understanding of how the capabilities provided by the WebXR API cooperate with the rendering functionality built into Three.js.

In this chapter you will:
  • Learn to use the USB debugging features of the Oculus Quest

  • Access the debugging tools of the Oculus Quest through the Android Debug Bridge application

  • Create an interface to connect the Quest and a local development server through the WebXR API

  • Learn the value of Promises in JavaScript to handle asynchronous calls to Web services

  • Learn the importance of scope and closure to creating XR sessions on the Web

  • Use the browser’s developer tools to forward the content from a host machine to a peripheral XR device

Setting Up the Debug Environment

Before we begin with the fun of creating a connection between our Three.js project and the WebXR API, we must first set up our tools for development. For this exercise, the tool we need is a peripheral device for which we have developer access and on which we can view immersive VR content. If you do not have access to a device capable of virtual reality, you will not be excluded. Searching the extensions offered by your browser provider, you will likely find a WebXR Emulator tool created by the team at Mozilla Mixed Reality.1 Download the extension and review its documentation to better learn how to use the emulator in place of a physical device.

Debugging WebXR on an Oculus Quest

The following steps apply directly to connecting an Oculus Quest headset as a developer device through USB to a PC running Windows 10. Where appropriate, I’ve added footnotes for URLs that may host information helpful for case-specific troubleshooting.

Android Debug Bridge (ADB) and the Oculus Mobile App

  1. 1.
    Download and install Android Studio
    1. a.

      Android Studio provides the software developer kit (SDK) required for debugging on the Oculus Quest.2

       
    2. b.

      Android Studio provides the ADB program, which facilitates communication between a computer and a connected Android device such as the Oculus Quest.3

       
     
  2. 2.
    Download the ADB software drivers required by the manufacturer of the Android device on which you want to test WebXR applications.
    1. a.

      ADB drivers for Windows 10 can be found at this link provided by Oculus.4

       
    2. b.

      Machines running Mac and Chrome OS do not require the download of additional drivers, according to official documentation.5

       
    3. c.

      Users operating on Linux machines should check the Android Studio documentation for the requirements of their systems.6

       
     
  3. 3.
    After downloading the ADB drivers for the Oculus Quest for Windows 10, navigate to the unzipped folder location and right-click install on the winusb.inf file.
    1. a.

      If you do not see the winusb.inf file in your unzipped folder, confirm that your file explorer has enabled the display of hidden files by clicking the View menu in the file explorer toolbar.

       
    2. b.

      For convenience, move these files to a folder you name ADB at the root location of the local hard drive (C:).

       
     
  4. 4.

    To enable debugging on the Quest through a computer, you must first download the Oculus app to your smartphone.

     
  5. 5.

    Once the app has downloaded, open it and sync it with your Oculus device.

     
  6. 6.

    In the settings beneath the synced device on the app, activate USB debugging.

     
  7. 7.

    Connect the Quest device to your computer using a USB-C to USB-3 cable.7

     
  8. 8.
    Navigate to the location of the SDK platform-tools folder installed by Android Studio.
    1. a.

      By default, on Windows 10, Android Studio saves the SDK/platform-tools folder in a directory located within Local Disk(C:) ➤ Users ➤ [your_username]➤ AppData ➤ Local ➤ Android ➤ SDK ➤ platform-tools.

       
     
  9. 9.

    Open a command prompt in the Windows Start menu by entering cmd into the Windows search bar (note: you may have to run as the administrator, depending on the setup of your account).

     
  10. 10.

    Navigate to the SDK/platform-tools folder by entering cd at the command prompt [copy/paste the location path to the platform-tools folder from File Explorer].

     
  11. 11.

    Once inside the platform-tools folder in your command prompt window, type adb devices.

     
  12. 12.

    If you have successfully installed Android Studio, the SDK, and the required ADB drivers for your device, such as the Quest, then ADB should begin running and display a list of Android devices connected to your machine.

     
  13. 13.
    If a device appears as unauthorized, then activate your device, like the Quest, and enable USB debugging when provided by the prompt.
    1. a.

      If you do not see the prompt inside your headset, then open and run the Oculus app on your computer.8

       
     
  14. 14.

    Reenter adb devices into the windows command prompt. If you have successfully enabled USB debugging on your Quest device through your computer, then the Android device previously listed will appear as authorized.

     
  15. 15.

    To confirm Windows 10 has installed the ADB driver required by Android Studio and the Oculus Quest, navigate to the Computer Manager application through the Windows Start menu.

     
  16. 16.

    In Computer Manager, locate Device Manager ➤ Portable Devices ➤ Quest [or the brand of your device]. Right-click the device and select Update driver.

     
  17. 17.
    Select browse my computer for driver software and search for drivers in the location where you saved the OEM ADB drivers from Oculus or another manufacturer.
    1. a.

      If you followed step 3.b, then this folder is C:ADB[device_driver_name]usb_driver.

       
     
  18. 18.
    Select NEXT and confirm the MTP USB Device driver has been installed.
    1. a.

      If a driver other than the MTP USB Device driver appears, and your operating system has determined it is the best driver for the device attached, then either confirm the selection or refer to the documentation provided by the device manufacturer’s website.

       
     

Upon completion of the aforementioned steps, you are now ready to begin testing a WebXR application on your device.

Running a Demo from the Immersive Web

To see the WebXR API in action through our newly connected XR device, let’s access a sample project kindly provided to us by those responsible for creating the WebXR API, the Immersive Web Working Group. The first sample we will access is an immersive VR session. If your device is VR-compatible, then follow along.
  1. 1.

    Make sure your device is not only connected to the Internet but also provides an application to browse the Web. For example, the Oculus Quest has a built-in browser accessible through its main menu toolbar.

     
  2. 2.

    After opening the browser in your XR device, navigate to the following URL, which is a page of WebXR samples accessible through the Immersive Web Working Group’s GitHub repository: https://immersive-web.github.io/webxr-samples/.

     
  3. 3.

    Select the first sample listed on the page, “Immersive VR Session.”

     
  4. 4.

    If your device and browser are capable of hosting an immersive VR session, you will see a large ENTER button near the top-left of the browsing window. In the browser inside your headset, select ENTER.

     
  5. 5.

    You should see a 360-degree model of the solar system, with a counter depicting your device’s frames per a second.

     

If you have been able to experience the immersive VR scene created by the WebXR sample project, then you know your device and its browser are capable of viewing XR content through the WebXR API. Congratulations! Of course, this doesn’t answer the question of whether or not our devices can access WebXR content we have developed on the local Web server inside our computers. That is what the next section of this chapter is about.

Preparing Our Scene for Immersive VR

Now that we’ve taken steps to enable debugging of XR applications on our machines through a connected XR device, we can finally turn our attention to transforming the Three.js scene we created in Exercise 4 into one we can experience in VR.

As the WebXR API is an implementation of a specification set forth by a group of XR industry leaders, it has norms that we, as responsible XR developers, should follow. The existence of these norms serves the interests of an end user’s experience and security. Presumably, as a WebXR developer you’d like to create applications that people enjoy. To that end, the norms set forth by the Immersive Web Working Group serve the goals of both developers and users alike.

Life Cycle of a WebXR Application

The first important protocol laid out by the WebXR API is the life cycle of a VR application. The following are seven stages of an online VR app’s life cycle, as laid out by the documentation on the Immersive Web Working Group’s GitHub repository.9
  1. 1.

    Query to see if the desired XR mode is supported.

     
  2. 2.

    If support is available, advertise XR functionality to the user.

     
  3. 3.

    A user-activation event indicates that the user wishes to use XR.

     
  4. 4.

    Request an immersive session from the device.

     
  5. 5.

    Use the session to run a render loop that produces graphical frames to be displayed on the XR device.

     
  6. 6.

    Continue producing frames until the user indicates that they wish to exit XR mode.

     
  7. 7.

    End the XR session.

     

Before we begin adding WebXR functionality to the Three.js exercise we created in the last chapter, let’s copy the index.html and index.js files and place them in a new folder that is a sibling to the old folder, a child of the same parent. Arranging the file structure of the exercise like this will allow us to access the Three.js module source files we imported during the exercise in Chapter 5.

As we’ve moved the location of our index.html file into a new folder, we must change the relative path of the Three.js import statement at the top of our index.js page. For both convenience and clarity, I will rename my index.js file for this exercise to index_xr.js and refer to it as such from now on.

There are four things we will address in Part 1 of this exercise:
  1. 1.

    We will reconfigure the declaration of some variables into the global scope.

     
  2. 2.

    We will define a VR button element that we will use to launch our XR application.

     
  3. 3.

    We will change the manner in which we created and defined the WebGL Rendering Context.

     
  4. 4.

    We will begin to divide the function we previously defined as main into two distinct functions, init() and animate().

     

Exercise 5, Part 1: Creating an XR Session Through the WebXR API

Stage 1 of the WebXR API instructs us to query if the user’s Web browser supports the XR mode required by our application.

Stage 1: Is WebXR Supported?

As we want to test our Three.js scene in VR, the XR mode for which we’d like to query is “immersive-vr.” The WebXR API includes language to query other modes, which we will address in later chapters. To query a browser’s ability to display immersive-VR content, we can perform the following steps:
  1. 1.

    Create a new JS file called VRButton.js

     
  2. 2.

    Access the XR property of the browser’s Navigator API

     
  3. 3.

    Asynchronously check if the browser supports WebXR

     
  4. 4.

    Accept the Promise returned from the XR object

     
  5. 5.

    Confirm the user’s browser is secure

     

Create a New JS File Called VRButton.js

In the same folder for the HTML and index_xr.js files you created for this exercise, create a new JS file called VRButton.js.

Access the XR Object Through the Navigator API

The WebXR API provides a function we can use to check whether a browser supports the XR mode we’d like to request. To access functions provided by the WebXR API from our JavaScript files, we only need to access the XR object built into the navigator API10 that a Web browser provides.
  1. 1.
    In the VRButton.js file, create the following conditional block:
            if (navigator.xr) {
                var button = document.createElement("button");
                navigator.xr.isSessionSupported('immersive-vr')
                            .then(function(supported) {
                               if (supported) { EnterVR() }
                               else { NotFound(); }
                            })...
     

The if statement checks if the browser’s navigator property contains an XR object. If it does, we instruct the Document object of the Web page to create an HTML button element, which we save into a target variable called button.

Send an Asynchronous Request

Next, we use dot notation to call the WebXR API’s isSessionSupported() function on the navigator’s XR object. Because the XR mode for which we’d like to test support is a VR session, we enter as an argument to the isSessionSupported() function the string “immersive-vr,” which is part of a built-in enum data type provided by the WebXR API.11

You may not recognize the .then() function we call after the isSessionSupported(‘immersive-vr’) function. If you don’t, then you’re in for quite a treat courtesy of the maintainers of the JavaScript language. The .then() function is JavaScript syntax to handle an object called a Promise. Like the keywords async and await, promises facilitate with the complexity of asynchronous programming.

Asynchronous Programming

Asynchronous programming is a programming paradigm designed to handle the simultaneous requests and responses created by complex applications. One example of an asynchronous programming domain is a website’s call to a database. As the response from a database located on a remote server may take some time, holding the processing of an app until the response arrives may impact performance. Tools such as JavaScript promises allow apps to send requests to databases or Web services, like the WebXR API, while continuing their execution while the request pends.

Receive the Returned Promise

In JavaScript, a promise is like a burrito; it’s a package of nourishing bits wrapped for easy consumption. The contents of a promise burrito depend upon the function returning it. Because the WebXR API function isSessionSupported(‘immersive-vr’) returns a Boolean value, one that is true if the browser supports VR and false if it does not, the innards of the promise burrito we receive in our .then() function are in the form of either a true or false Boolean value. If the burrito sent by the isSessionSupported(‘immersive-vr’) function and received by its attached .then() function contains a true value, the .then() function executes the function within its parentheses.

If the XR object of the browser’s navigator property does indeed support an immersive-VR mode, the .then() function executes the anonymous function we’ve defined inside its parentheses. The anonymous function takes the value wrapped inside the promise burrito, supported, as its argument, and it executes another conditional within its code block, specified by the opening and closing curly braces. The code we have written states that if the content of the promise burrito passed into the .then() function is a true value, then our program should call a function we have yet to define called EnterVR() .

Confirm the User’s Browser is Secure

On the other hand, if the content of the promise burrito received by the .then() function is a false value, our program executes the NotFound() function inside the else clause. If the browser’s navigator doesn’t have an XR object, then the browser does not support the WebXR API.
  1. 1.
    We place the logic to handle this scenario in the else clause following the closing bracket of the if (navigator.xr) expression.
            ...} else {
                if (window.isSecureContext === false) {
                    console.log('WebXR needs HTTPS');
                } else {
                    console.log('WebXR not available');
                }
                return;
            }

    Inside the else clause we’ve written to handle a scenario in which a user’s browser does not support the WebXR API, we define another conditional block. For security reasons, the WebXR specification requires that a WebXR session only launch in a secure browsing context, which browsers identify with the URL prefix “https.”12 Whether or not a browser supports a secure context is answered by the isSecureContext property on the browser’s global window object. As XR developers, we can avail ourselves of this information provided by the global window object to determine how to respond to a failed call to a browser’s nonexistent XR object. If the navigator.xr object cannot be found for reasons pertaining to security, then we write to the browser’s console that WebXR requires a secure browsing context. Else, we simply notify the user through the browser’s console that their browser does not support functionality for the WebXR API.

    If the user’s browser does not support the WebXR API, or if their browsing context is not secure, then we have done all we can do for them. Unfortunately, they will not be able to experience our Three.js scene in VR.13 However, if the user’s browser has an XR object and supports the immersive-VR mode, then we must write the logic for the EnterVR() function we’ve called. Alternatively, if the user’s browser has XR capabilities but has no connected device to support an immersive-vr session, then we must write the logic for the NotFound() function we’ve called. Let’s begin by defining the logic for the NotFound() function, as that will be the easier of the two tasks.

     
  2. 2.
    In the VRButton.js document, above the if (navigator.xr) statement, define a new function called NotFound(). Enter the following code into the body of the NotFound() function.
            function NotFound() {
                console.log('immersive-vr mode not found');
            }

    That’s it! I told you it’d be easy. Now, let’s move on to the more difficult task: writing the logic for the EnterVR() function.

     
  3. 3.
    Above the NotFound() function declaration, compose a new function with opening and closing brackets called function EnterVR().
            function EnterVR() {
            }
     

To determine what logic to place inside this function, let’s refer to the second phase of a WebXR’s life cycle as defined by the WebXR API.

Stage 2: Advertise XR Functionality to the User

The WebXR API defines phase 2 of a WebXR application’s life cycle as one in which we, the developer, advertise XR functionality to the user. Because phase 3 of the XR app’s life cycle requires a user-activation event to launch the XR application, we will present the user with a button to click if they’d like to enter an immersive-vr session.
  1. 1.
    Inside the EnterVR() function, write the following:
                button.innerHTML = 'Enter XR';
                var currentSession = null;
     

Recall that the button to which we refer, we created through a call to the Document object in step 1 of this exercise. With the button HTML element already created as a child of the Document object, we can manipulate the text it shows the user by setting its innerHTML property to ‘Enter XR’. The innerHTML property is a built-in property provided by the HTMLElement interface, which our button, as an HTML element, inherits by default.

Why, though, do we create a new variable called currentSession and set it to null? The answer to that question has everything to do with Stage 3 of a WebXR’s life cycle, as defined by the WebXR API.

Stage 3: Enable a User Activation Event

Stage 3 of the WebXR application’s life cycle requires that we provide the user with the option to knowingly activate a WebXR session. We’ve provided a button for them to click, but we have not yet addressed what action our program will perform in response to a user’s input. For that, we leverage yet another built-in feature of the Document Object Model API, the Event Handler.

Add an Event Handler to the Button

  1. 1.
    Below the code you wrote in the previous step, create an onclick event handler on the button object with an empty code block.
             button.onclick = () => {
             }
     
The arrow syntax following the declaration of the button’s onclick event handler is an abbreviated way of creating an anonymous function in JavaScript. An equally valid way of defining the button’s onclick event handler function would be:
            button.onclick = function () {
            }
Anonymous Functions

The preceding syntax is akin to the way we defined an anonymous function to handle the promise burrito received by the .then() function in step 2 of this exercise. However, in the service of brevity, JavaScript allows developers to define anonymous functions with an arrow (=>) in lieu of the function keyword. The empty parentheses preceding the arrow syntax simply illustrates that the anonymous function requires no arguments. Our .then() function from step 2 at least took a Boolean value as an argument to the anonymous function it called. With no argument provided to our button’s onclick event handler’s anonymous function, what then do we execute? As has been our practice in this exercise, let’s return to the WebXR API’s list of an XR application’s life cycle.

Stage 4: Request an XR Session

As the fourth stage in an XR application’s life cycle, the WebXR API instructs developers to request an immersive session from the user’s device once the user has performed an action that demonstrates intention of launching an XR session. How do we request an XR session from a user’s device?

Access WebXR Functions Through the XR Object

Fortunately, the designers of the WebXR API have provided us with another built-in function we can call on the browser’s XR object.
  1. 1.
    Inside the empty code block of the button’s onclick event handler, call the following built-in functions on the navigator’s XR object:
            navigator.xr
                 .requestSession('immersive-vr', sessionInit)
                 .then(onSessionStarted);
            }
     

Like the xr.isSessionSupported() query from Stage 1, the XR object’s built-in requestSession() function returns a Promise object. Because the requestSession() function returns a promise object, we can use the .then() function to catch the promise burrito returned by the call to requestSession(), as we did in step 2. However, unlike the promise burrito returned in step 2 of this exercise, the promise burrito returned by the xr.requestSession() function in this step wraps an XR session object, not a Boolean true,false value.

XR Session Object

The code we have written handles the XR session returned in the promise burrito by sending it directly to a function called onSessionStarted(), a function we have yet to define. Though we haven’t yet defined the onSessionStarted() function in our VRButton.js document, writing it here reminds us of what information the logic of the function will have to handle. Before we can create the onSessionStarted() function, however, we must address the two parameters we have passed to the XR object’s requestSession() function. The string value ‘immersive-vr’ we already know to be the mode of the XR session we are requesting. The second argument, sessionInit, however, is a variable we haven’t even defined yet. What’s going on?

Types of XR Modes

According to the WebXR API, the requestSession() function made available through the navigator’s XR object accepts as parameters the mode of the XR session requested and features to implement upon an XR session's creation. The features requested can be either required or optional. The WebXR API allows the following features to be requested:
  • Local

  • Local-floor

  • Bounded-floor

  • Unbounded

Local refers to a stationary XR experience; local-floor defines a stationary XR experience that requires reference to a floor; bounded-floor means an XR experience should enable a user to move about a finite, defined space while wearing the XR device; and unbounded refers to a mobile XR experience that has no limitations to a user’s movement.

Though the VR experience we’ve created in our Three.js scene does not require any movement from the user, let’s define a sessionInit variable that holds a couple different optional features.

Initializing XR Session Features

  1. 1.
    Above the code written to request a session, define a sessionInit target variable and set its source value to a JavaScript object with the following key-value pair:
            let sessionInit = {
                optionalFeatures: ["local-floor", "bounded-floor"]
            };
     

Again, the names of both the key and value terms are provided by the WebXR API. Passing the JavaScript object to the XR object’s requestSession() function as a parameter is a behavior predefined by the API’s documentation.

Starting the XR Session

Now that we’ve requested an immersive-vr session with parameters defining optional features from the user’s XR device, we have to determine how to handle the session returned within the Promise from the requestSession() function. In step 8 we used the .then() function to pass the contents of the promise burrito to a function called onSessionStarted. Let’s write the logic of that function next.
  1. 1.
    Inside the body of the EnterVR() function, immediately below the initialization of the currentSession variable to null, write the stub of a function called onSessionStarted, which accepts as its parameter an XR session object.
            function onSessionStarted(session) {
            }
     

Recall that the onSessionStarted function is called by the onclick event handler attached to the button HTML element we have labeled “Enter XR.” When the user visits the Web page for our application, our JavaScript will first query if their browser supports the functionality required by an immersive-vr XR session. If their browser supports the functionality, then our script creates an HTML button element to place on the Web page that instructs the user to press to launch our Three.js scene in VR. If the user clicks that button, our script fires an onclick event handler that, first, requests a session from the user’s device. If the promise returned from the function contains an activated XR session, then our script passes the content of that promise, the XR session itself, to the function we have defined: onSessionStarted() .

Stage 5: Run Render Loop

Stage 5 of the WebXR app’s life cycle, as defined by the WebXR API, states that our onSessionStarted() function should run a render loop on our user’s device. Fortunately, we’ve already created a render loop inside our Three.js scene through a call to setAnimationLoop() on our Three.js WebGLRenderer object instantiated in the main() function of our index.js script. Therefore, the logic of our onSessionStarted() function must, primarily, notify the Three.js WebGL Renderer defined in our main JavaScript file to ready itself for XR rendering. Hmm, but how can we access a JavaScript object we created in another file from our VRButton.js script? This, we will address in Part 2 of the exercise.

Part 1 Recap

  • Created a VR Button JavaScript module

  • Accessed the window’s navigator API to query for an XR session

  • Used a JavaScript promise to handle the response from the WebXR API Web service

  • Requested an XR session with a parameter of optional features

  • Created a button element to advertise XR content

  • Attached an onclick event handler to the button to start and end an XR session

Exercise 5, Part 2: Scope, Closure, a Module, and a Singleton

The question of how to access the WebGL Rendering object from our main JS script inside our VRButton.js script is one seemingly easy to answer on its surface. For example, all we have to do is call the necessary function on the WebGL Renderer, which we saved in the target variable renderer in the index.js file in the previous chapter’s exercise, right? Let’s find out if this is the case together.

In Part 2 of this exercise you will:
  • Learn about the WebXRManager object in Three.js

  • Learn the importance of scope to a JS program

  • Learn how to use closure in JS to sustain the state of an XR session

  • Use the built-in functions of the three.js library to connect the Three.js rendering context to the XR session created through the WebXR API

WebXRManager in Three.js

First, we ask ourselves what function on the Three.js WebGL Rendering object , renderer, we must call to activate an XR rendering loop. A quick reference to the Three.js online documentation shows that the WebGL Rendering Object in Three.js has a property called xr, which in turn implements a Three.js interface called WebXRManager. After visiting the WebXRManager source code through the Three.js documentation, we learn that the WebXRManager interface provides a function conveniently called setSession(), which takes an XR session as an argument. Therefore, to connect the XR session we have requested in our VRButton.js script to the Three.js renderer on which we call our scene’s animation loop in our main function, we need only use JavaScript’s dot notation to access the setSession() function on our renderer object from within the VRButton.js script.

There is just one catch, though. While we aim to import our VRButton as a module into our main index JavaScript page, we do not have the ability to reach the Three.js renderer object from the code inside our VRButton.js script. The obstacle comes courtesy of a feature in JavaScript called scope.

Scope

Scope, in JavaScript, essentially refers to the accessibility of a variable from within the program of an application. For example, variables that we create inside functions cannot exist outside of their functions, unless we save them in other variables we either pass into the function or return from the function. Inadvertently, we’ve seen this principle at play in the previous exercise, in which we defined every new function within the curly braces of the main() function. If we had defined any functions outside the scope of the main function, then we would have had to take measures to pass variables required by both the main and additional functions back and forth as parameters and return values. While an approach like that is sound and effective, it does not make use of JavaScript’s unique abilities.

Connecting the WebXRManager to an XR Session

To leverage the tools JavaScript inherently provides us as WebXR developers, we can place the Three.js WebGL Rendering object within the scope of our VRButton.js script by passing it as a parameter. To do so, we only need to perform two tasks: 1) we have to create a function in our VRButton script that accepts a Three.js rendering context as an argument, and 2) we have to make that function available to the scope in which the Three.js rendering object currently exists. Let’s begin to solve this problem by completing the second task first.

Setup

At the beginning of this chapter I suggested that you copy the index.js file from the end of the exercise in Chapter 5 and rename it index_xr.js. I suggested the measure because in this section of the exercise we will apply some changes to the script. It will be best to retain an unaltered version of the JavaScript file to better understand the reasons behind the changes we will make.

Global Variables
  1. 1.

    First, we will define a slew of global variables just below the import statements at the top of the file.

     
var gl, cube, sphere, light, camera, scene;

Once we create the variables at the top of the script’s global scope, we no longer need the keywords const, let, or var to define the variables in the bodies of the function, as long as they too are in the global scope of the script.

Refactor
  1. 1.
    Second, we will break our main() function into two distinct functions called init() and animate(), which we will call just below the global declarations of our variables.
    init();
    animate();
     
Remove and Replace
  1. 1.

    Then, we will replace our main() function with separate functions beginning with the function called init(). Remove the declaration of the main function near the top of index_xr.js and replace it with the following function declaration:

     
function init() { ...
Though the content of the script will remain mostly the same as it was in its original version, I will represent it here for a bit more clarity in the context of our current lesson.
  1. 2.
    To that end, create the following headings using comment syntax within the init() function we renamed from main() in the previous step:
    function init() {
        // create context
        // create camera
        // create the scene
        // GEOMETRY
        // create the cube
        // Create the Sphere
        // Create the upright plane
        // MATERIALS
        // MESHES
        //LIGHTS
    }
     

The first significant change we will make to the init() function in index_xr.js will be to the code we’ve used to define the WebGL Rendering context.

Enable the WebXRManager

Beneath the //create context comment inside the init() function, write the following code to create and define the Three.js WebGL Renderer, which we will pass as a parameter to a function inside VRButton.js.
    // create context
    gl = new THREE.WebGLRenderer({antialias: true});
    gl.setPixelRatio(window.devicePixelRatio);
    gl.setSize(window.innerWidth, window.innerHeight);
    gl.outputEncoding = THREE.sRGBEncoding;
    gl.xr.enabled = true;
    document.body.appendChild(gl.domElement);
    document.body.appendChild(VRButton.createButton(gl));

The setPixelRatio() and setSize() functions and the gl.outputEncoding property are not of great importance at this step of the exercise. Their roles pertain to the resolution of the scene. What is important in this step, however, is 1) the instantiation of the Three.js WebGL Renderer in the variable gl without a canvas object passed into the constructor; 2) the Boolean value true set to the property of the Three.js WebXRManager interface property called enabled; 3) the use of the DOM API function appendChild() to add the Three.js WebGL Renderer object’s domElement property, which points to the HTML canvas element automatically created by the Three.js WebGLRenderer constructor, to the Web page’s <body> section; and 4) the use of the DOM API to append the VR Button we created in VRButton.js to the Web page, while simultaneously calling a function that accepts the Three.js WebGL Renderer as an argument.

Though we have not yet written the logic within the createButton() function that the init() function calls in the script index_xr.js, we have at least defined the mechanism through which the XR session created by the WebXR API will connect to the render loop run on the Three.js renderer in our primary script called upon the launch of our Three.js scene.

While the remainder of the init() function is similar to the main() function it replaces in execution, you may prefer to copy the following code, which I’ve refactored for clarity.
import * as THREE from '../Threejs_Ex1/modules/three.module.js';
import {VRButton} from './VRButton.js';
var gl, cube, sphere, light, camera, scene;
init();
animate();
function init() {
    // create context
    gl = new THREE.WebGLRenderer({antialias: true});
    gl.setPixelRatio(window.devicePixelRatio);
    gl.setSize(window.innerWidth, window.innerHeight);
    gl.outputEncoding = THREE.sRGBEncoding;
    gl.xr.enabled = true;
    document.body.appendChild(gl.domElement);
    document.body.appendChild(VRButton.createButton(gl));
    // create camera
    const angleOfView = 55;
    const aspectRatio = window.innerWidth / window.innerHeight;
    const nearPlane = 0.1;
    const farPlane = 1000;
    camera = new THREE.PerspectiveCamera(
        angleOfView,
        aspectRatio,
        nearPlane,
        farPlane
    );
    camera.position.set(0, 8, 30);
    // create the scene
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0.3, 0.5, 0.8);
    const fog = new THREE.Fog("grey", 1,90);
    scene.fog = fog;
    // GEOMETRY
    // create the cube
    const cubeSize = 4;
    const cubeGeometry = new THREE.BoxGeometry(
        cubeSize,
        cubeSize,
        cubeSize
    );
    // Create the Sphere
    const sphereRadius = 3;
    const sphereWidthSegments = 32;
    const sphereHeightSegments = 16;
    const sphereGeometry = new THREE.SphereGeometry(
        sphereRadius,
        sphereWidthSegments,
        sphereHeightSegments
    );
    // Create the upright plane
    const planeWidth = 256;
    const planeHeight =  128;
    const planeGeometry = new THREE.PlaneGeometry(
        planeWidth,
        planeHeight
    );
    // MATERIALS
    const textureLoader = new THREE.TextureLoader();
    const cubeMaterial = new THREE.MeshPhongMaterial({
        color: 'pink'
    });
    const sphereNormalMap = textureLoader.load('textures/sphere_normal.png');
    sphereNormalMap.wrapS = THREE.RepeatWrapping;
    sphereNormalMap.wrapT = THREE.RepeatWrapping;
    const sphereMaterial = new THREE.MeshStandardMaterial({
        color: 'tan',
        normalMap: sphereNormalMap
    });
    const planeTextureMap = textureLoader.load('textures/pebbles.png');
    planeTextureMap.wrapS = THREE.RepeatWrapping;
    planeTextureMap.wrapT = THREE.RepeatWrapping;
    planeTextureMap.repeat.set(16, 16);
    planeTextureMap.minFilter = THREE.NearestFilter;
    planeTextureMap.anisotropy = gl.getMaxAnisotropy();
    const planeNorm = textureLoader.load('textures/pebbles_normal.png');
    planeNorm.wrapS = THREE.RepeatWrapping;
    planeNorm.wrapT = THREE.RepeatWrapping;
    planeNorm.minFilter = THREE.NearestFilter;
    planeNorm.repeat.set(16, 16);
    const planeMaterial = new THREE.MeshStandardMaterial({
        map: planeTextureMap,
        side: THREE.DoubleSide,
        normalMap: planeNorm
    });
    // MESHES
    cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.position.set(cubeSize + 1, cubeSize + 1, 0);
    scene.add(cube);
    sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
    sphere.position.set(-sphereRadius - 1, sphereRadius + 2, 0);
    scene.add(sphere);
    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    plane.rotation.x = Math.PI / 2;
    //LIGHTS
    const color = 0xffffff;
    const intensity = .7;
    light = new THREE.DirectionalLight(color, intensity);
    light.target = plane;
    light.position.set(0, 30, 30);
    scene.add(light);
    scene.add(light.target);
    const ambientColor = 0xffffff;
    const ambientIntensity = 0.2;
    const ambientLight = new THREE.AmbientLight(ambientColor, ambientIntensity);
    scene.add(ambientLight);
}

Now that we’ve addressed the problem of scope originally presented by the separation of our index_xr.js and VRButton.js scripts, we can turn our attention to that other pickle common in JavaScript: closure.

Closure

In JavaScript, the concepts of closure and scope go hand in hand. While scope refers to the life cycle of a variable in a JavaScript application, closure refers to the practice of leveraging the boundaries of scope to sustain the state of an object. The key fundamental to closure in JS is that functions in JS can exist as objects passed into other functions. Because functions manage the life cycles of variables within their curly braces, they maintain their scope no matter where they are called in an application. For example, if a variable dies upon the completion of its scope, and an XR session exists in our program as a variable, then how can we ensure the XR session runs without interruption?

Sharing the WebXRManager Between Scripts

One way we can reach this end is to invoke the life cycle of an XR session from within a function connected to the life cycle of the render loop in our Three.js scene. A very good example toward better understanding closure in JavaScript is to write the body of the createButton function we called in our revamp of the function recently renamed init(). By leveraging the closure provided by a module's function called as an argument passed into another function, we may run an XR session through the WebXR API and the render loop of our Three.js scene simultaneously.

The Singleton Design Pattern

Notice that in the second line of the index_xr.js script we added an import statement that imported {VRButton} from VRButton.js. The module paradigm in JavaScript allows us, developers, to move pieces of our code around an application for convenience and simplicity. The convenience arrives from not having to rewrite our code; the simplicity arrives from providing a single interface to all the functionality of another script. Closure emerges as a fortuitous byproduct of the module paradigm’s separation of concerns. Encapsulating the functionality of a script in a singular JavaScript object is an example of the Singleton design pattern. To illustrate this phenomenon, let’s reconfigure our VRButton.js script to better tailor it for the module we import into index_xr.js.

Storing Functionality in a Single Object
  1. 1.
    At the top of VRButton.js add a declaration for a variable called VRButton and set it equal to an empty JavaScript object.
    var VRButton = {
    }
     

The empty space between the opening and closing brackets of the VRButton target variable will become the entire script we export into index_xr.js. Most of the work we’ve already done, as what remains is primarily moving the logic we’ve written into the body of the JavaScript object VRButton defines. However, one important detail we have not yet addressed is the definition of the createButton() function we added to the body of our HTML document and called from init(). Because calling a function on an HTML element upon its addition to the Document object immediately invokes the function, it’s imperative that we define the function first .

Storing a Function in a JS Object Property
  1. 1.
    Immediately within the opening curly brace of the VRButton target variable declaration, define a property called createButton and set its value to an anonymous function with two parameters.
    var VRButton = {
        createButton: function(gl, options) {
            if (options && options.referenceSpaceType) {
                gl.xr.setReferenceSpaceType(options.referenceSpaceType);
            }

    Remember that a JavaScript object can hold properties as key/value pairs. A colon indicates the name of a JS object’s property to its left and the property’s value to its right. We use the concept of anonymous functions in JS to immediately invoke the function upon a reference to the VRButton’s createButton property. The parameters accepted by the anonymous function defined by the createButton property are gl and options, which we will connect with the Three.js renderer in our application and the optional settings of the XRSessionInit variable, which Three.js defaults to “local-floor”.

     
  2. 2.

    Beneath the closing bracket of the if clause created above copy and paste the functions we’ve already created and defined in the VRButton.js document. These functions should include EnterVR(), function NotFound(), and the if/else conditional blocks that queried whether the navigator.xr object existed.

     

Now that we have a reference to our application’s Three.js renderer available from within our VRButton.js script, we can easily connect the XR session created by the WebXR API’s requestSession() function and passed as a resolved promise burrito to the function inside EnterVR() we’ve defined as onSessionStarted(session).

Connect the WebXRManager with the XR Session Loop
  1. 1.
    In the body of the OnSessionStarted() function, add the following code:
            function onSessionStarted(session) {
                session.addEventListener('end', onSessionEnded);
                gl.xr.setSession(session);
                button.textContent = 'Exit XR';
                currentSession = session;
            }
     

The first line of the function’s body, session.addEventListener(), comes courtesy of the WebXR specification, which informs developers that in the interest of user experience, developers should instantiate an XR session already equipped with a mechanism to terminate itself upon a user’s request.

The second line is the one that has eluded us and is the secret ingredient to launching our Three.js scene in accordance with the life cycle phases laid out by the WebXR API. Because we are encouraged to withhold the launch of an XR session until explicitly notified by the user through the click of a button on the Web page, we must connect the render loop on our Three.js renderer defined and called in index_xr.js with the XR session returned by the WebXR’s requestSession() method fired by the VR button element’s onclick event handler. We can finally fulfill this requirement by accessing the WebXRManager interface provided by the gl object, which serves as a proxy to the Three.js renderer instantiated in our init() function . Referencing the Three.js renderer through the variable gl, which points to the object passed into the anonymous function stored in the createButton key, successfully overcomes the limitation created by JavaScript’s treatment of variables in and out of scope.

Closure Sustains State

Most importantly, however, setting the source of the Three.js renderer's session to the session created inside the VRButton.js script allows us to apply the power of closure to sustain the state of the XR session. As we invoke the creation of the button object (with its quivering, waiting onclick handler ready to leap into action) near the top of our init() function in index_xr.js, we have guaranteed that the scope of the XR session, upon request, will endure through the existence of our animation loop.

Adding/Removing Event Listeners

Finally, the onSessionStarted() function replaces the text of the button element from “Enter XR” to “Exit XR” and sets the value of the variable currentSession, previously null, to the session initiated by the call to the WebXR API’s requestSession() function.

Naturally, as we’ve created an onSessionStarted() function to handle the creation of an XR session, we should also create a function to handle the destruction of an XR session.
  1. 1.
    Beneath the closing bracket of the onSessionStarted() function, define a function called onSessionEnded.
            function onSessionEnded() {
                currentSession.removeEventListener('end', onSessionEnded);
                button.textContent = 'Enter XR';
                currentSession = null;
            }

    Appropriately, the logic within the OnSessionEnded() body reverses the logic of its sibling function, onSessionStarted(). It removes the event listener from the currentSession object, restores the text of the button element, and resets the value of the variable currentSession to null.

    To better understand the reason behind setting the value of currentSession to null upon a session’s end, refer to step 6 of Part 1 of this exercise. In that step we initialized the currentSession value to null. Yet, we set the value of currentSession to the XR session created by the WebXR API inside the onSessionStarted() function. If we reset its value to null upon the calling of the end event on the XR session, then what purpose have we served by instantiating the currentSession variable with a null value?

    In step 7 of Part 1 of this exercise, we created an onclick event handler on the button element. In step 9, inside the body of the anonymous function we set to fire upon the user’s click of the button on the Web page, we defined the optional features of the XR sessionInit variable and requested an immersive-vr session via the XR object and WebXR API. Yet there is one scenario the logic we implemented does not address. What happens if a user clicks the “Exit XR” button on our Web page?

    As our onclick button event handler is written now, the browser will attempt to request a second immersive-vr XR session. As a WebGL rendering context cannot host more than one XR session, our XR application will at best crash and at worst lock our user into a never-ending loop. To prevent both outcomes, we can use the value of the currentSession variable as a flag to indicate whether the application should request an XR session or not.

     
  2. 2.
    To add this feature to our application, we introduce an if/else conditional clause into the body of our onclick handler’s anonymous function.
            button.onclick = () => {
                if (currentSession === null) {
                    let sessionInit = {
                        optionalFeatures: ["local-floor", "bounded-floor"]
                    };
                navigator.xr
                         .requestSession('immersive-vr', sessionInit)
                         .then(onSessionStarted);
                }
                else {
                    currentSession.end();
                }
            }

    With the onclick handler redefined, our application now contains logic to request a new XR session only if one does not currently exist, and to otherwise call the end() function built into an XR session object provided by the WebXR API.

    With the completion of the onclick handler for our VR Button, all that remains for us to do is to export the VRButton.js script’s functionality as an object that the index_xr.js script can import.

     
  3. 3.

    To export the functionality of the VRButton object created in the VRButton.js script, simply add the following code to the end of the VRButton.js script, outside the final bracket that marks the closing of the VRButton object.

     
export {VRButton};

With the VRButton set to export and the index_xr.js script included with the ability to import the button and its functionality, we are almost ready to test our Three.js scene using the WebXR API.

Part 2 Recap

  • Refactored the index.js file we created in the previous chapter to better suit the demands of the WebXR API

  • Leveraged the idea of closure in JS to launch a function and its scope upon the creation of an HTML button

  • Used the singleton pattern with a JS module to pass a WebGL Rendering object into the function that launched the XR session

Exercise 5, Part 3: The Homestretch

What remains to complete our application is the second function we defined at the top of our newly revamped index_xr.js page. We have initialized our scene and connected it to the creation of an XR session to reach the screen of a connected VR device. Now, we must write the logic required to run the render loop in Three.js.

In Part 3 of this exercise you will:
  • Refactor the requestAnimationFrame() function from the previous exercise into a Three.js-specific call better suited for the WebXR API

  • Reapply the render and resize functions to fit into the flow of the reformatted index_xr.js file

  • Use browser developer tools to forward the port on which the local development server hosts the Three.js scene to a connected VR device

At the top of the index_xr.js file, beneath the declaration of the global variables, we called two new functions: init() and animate(). In Part 3 of this chapter’s exercise, we will reconfigure the render call from our original main() function into two different functions: animate() and draw().
  1. 1.
    Beneath the closing curly brace of the init() function in index_xr.js, declare a function called animate with the following body:
    function animate() {
        gl.setAnimationLoop(render);
    }

    Recall that the variable gl refers to the WebGL Renderer Three.js object we declared in the script’s global scope and initialized in the function init(). Declaring the variable in the global scope allows us to access it in a function from outside the scope of the init() function’s opening and closing curly braces. The method setAnimationLoop() on the Three.js WebGL Renderer object is a method provided by Three.js that replaces the call to requestAnimationFrame() in WebXR applications. However, as was the case with the function requestAnimationFrame(), setAnimationLoop() accepts a callback function as its parameter. The method setAnimationLoop() will execute the value of its callback parameter once every frame.

    For the callback function to be called by setAnimationLoop(), we can repurpose the render() function we wrote in Exercise 4.

     
  2. 2.
    Repurpose the render function from the previous exercise as the callback function to be called by setAnimationLoop.
    function render(time) {
        time *= 0.001;
        if (resizeDisplay) {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
    }
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        cube.rotation.z += 0.01;
        sphere.rotation.x += 0.01;
        sphere.rotation.y += 0.01;
        sphere.rotation.y += 0.01;
        light.position.x = 20*Math.cos(time);
        light.position.y = 20*Math.sin(time);
        gl.render(scene, camera);
    }
     
  3. 3.
    Finally, we can also repurpose the resizeDisplay() function from Exercise 4, and place it immediately beneath the closing brace of the render() function.
    // UPDATE RESIZE
    function resizeDisplay() {
        const canvas = gl.domElement;
        const width = canvas.clientWidth;
        const height = canvas.clientHeight;
        const needResize = canvas.width != width || canvas.height != height;
        if (needResize) {
            gl.setSize(width, height, false);
        }
        return needResize;
    }
     

Save the index.html, index_xr.js, and VRButton.js files in your IDE and launch your local development server. Navigate to the index.html page you created for this exercise. If your browser supports WebXR, you should see the Enter VR button we scripted. Pressing the button will launch the XR session in your browser, displaying stereoscopic images inside your canvas. In the final section of this chapter we will forward our site to a VR device connected to our machine by USB and launch the Three.js scene in VR.

Enable Port Forwarding from a Local Development Server to a VR Device

The answer to the question of how to access a Web page from a connected VR device running on a server on our local computer lies behind a flag of the Microsoft Edge browser. As Edge and Google Chrome both use the same JavaScript engine, the procedure to connect a USB-enabled device with the output of a localhost is the same. For the steps to follow for other Web browsers, refer to the developer’s documentation. However, for Edge and Chrome, the steps are as follows:
  1. 1.
    Navigate to [browser_name]://inspect/#devices.
    1. a.

      Replace browser_name with either Edge or Chrome.

       
     
  2. 2.

    Activate the checkbox next to Discover USB Devices.

     
  3. 3.

    Click the button labeled Port Forwarding.

     
  4. 4.
    In the menu that appears, add the port actively serving the Three.js scene on your localhost server.
    1. a.

      For example, my version of live-server through VS Code by default serves my files on port 5500.

       
     
  5. 5.
    In the field labeled “IP address and port” type: localhost:[your_port_number]
    1. a.

      Where [your_port_number] is the port on which your computer is serving the page containing the Three.js scene you’d like to load into a headset

       
     
  6. 6.

    Select Enable port forwarding and click done.

     
  7. 7.

    Do not close the page as it will sever port forwarding.

     
  8. 8.

    Open a command prompt in the folder where you saved the Android SDK/Platform-Tools folder.

     
  9. 9.

    Type adb devices.

     
  10. 10.

    Authorize the peripheral device to enable USB debugging.

     

To test whether port forwarding works between your browser and USB-connected device and, more importantly, if the steps we’ve taken to connect our Three.js scene to the WebXR API have achieved their aim, navigate to the localhost address of your Three.js application in the browser on your USB-attached VR headset. After the Web page loads, you should be presented with a 2D version of the Three.js scene on the homepage and a button near the page’s bottom if the isSessionSupported() promise returns true in the VRButton.js script. If the button appears with the text ‘Enter XR’, click the button to enter the Three.js scene through the VR headset.

If the experience works, then you may notice that the scene places you directly beneath the sphere and cube rotating in the scene. To change the spawning location of the headset in VR, amend the camera position settings in the init() function in index_xr.js. Congratulations, you just created a WebXR application!

Part 3 Recap

In Part 3 of this exercise you:
  • Separated the rendering logic of our application into the animate() function

  • Kicked off the animation loop with the Three.js function setAnimationLoop

  • Called the render function as a callback to run once every frame

  • Moved the resize function into the render loop

  • Used browser developer tools to forward the port serving the localhost

  • Launched an ADB server from the command prompt

  • Opened a Three.js VR scene through WebXR in a VR headset and its browser

Summary

Three.js is a library built atop the WebGL API that dramatically simplifies the steps required to create a simple, functioning XR scene. However, despite the strengths of Three.js, it cannot broadcast immersive scenes to peripheral XR devices alone. Fortunately, the Immersive Web Working Group has developed the functionality of the WebXR API to a point where we, as XR developers, can conveniently plug the rendering engine of our Three.js scenes into the event loop of an XR session in the browser. Together, Three.js and the WebXR API, by extending the already considerable power of the WebGL interface built into most modern browsers, provide a robust and accessible portal into the creation of mobile, immersive content.

In this chapter you:
  • Set up USB debugging between a PC and Oculus Quest through Android Studio

  • Downloaded and installed USB drivers to launch and run an ADB session in the command prompt

  • Used the JavaScript Module design pattern to both export and import an HTML button element that contained logic to access the WebXR API

  • Accessed the Navigator API of the browser to access the WebXR API

  • Followed the steps suggested for the life cycle of a VR app defined by the Immersive Web Working group to create a program that safely launched an XR session in a secure browsing context

  • Leveraged the principles of scope and closure in JS to launch an XR session from a script outside the application’s main JS file

  • Used the singleton design pattern to instantiate a single instance of a button class that accepted as a parameter the WebGL Rendering object of a Three.js scene

  • Learned the meaning of a Promise object in JavaScript and used it to handle the request and response cycle of an XR session

  • Used the DOM API’s event handlers to provide the user with control to start and end an XR session

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.19.251