Programming Patterns

Efficient WebVR Development Using the Adapter Pattern in Three.js


In this post we’ll use the Adapter Pattern to improve the development experience when working on WebVR applications. We’ll apply some of the SOLID principles of object-oriented design to create an application that is both easy to write and understand. To illustrate the ideas in this post, I’ve created a very simple example using the MVC approach from my previous post.

View demo Download source

Developing for VR

The most straightforward way to write a WebVR application in Three.js is by working directly with the interfaces provided by THREE.VRControls and THREE.VREffect.

Straightforward Approach using MVC

Straightforward Approach using MVC

However if you do this, you’ll only be able to test your application by putting your VR headset on, which can be very time consuming, specially if you are used to having short cycles of coding and testing.

In my case, I don’t always have access to a VR headset (an HTC Vive) but I would still like to be able to work on my WebVR application just using my laptop. Even in the cases when I do have access to a VR headset, I might still want to be able to just use my laptop. For example, when I’m implementing complex algorithms, such as mesh deformation, I find it much quicker to do all the development on my laptop and move to the testing things in the VR headset only once everything is working.

Bottom line is that being able to run our webVR applications also as desktop applications makes the development process more efficient.

There is also the possibility that you might be writing an application that is supposed to run on both a VR headset and on the desktop. If that is the case, then you’ll find the approach below extremely useful.

Adapter Pattern

The idea is to write our application in such a way that it will be easy to run it on both VR headsets and desktop. We want our application-specific code to work on both environments without having to modify it every time we switch from VR to our desktop and vice versa. This means that our code cannot depend directly on Three.js classes responsible for WebVR scene control and rendering.

This use case is perfect for the Adapter Pattern. The idea is very simple: we define a standard interface between our application-specific code and the Three.js classes responsible for the controls, camera, scene, etc. Then we provide two implementations of this interface: one that works with webVR and one that works on the desktop. For my example, I’ve called the interface RenderingContext and the corresponding implementations VRRenderingContext and StandardRenderingContext.

Using the Adapter Pattern

Using the Adapter Pattern

If our code only interacts with the corresponding Three.js classes using the RenderingContext interface, then we can run our application on either environment just by selecting the corresponding implementation of the interface, without having to make changes to our code.

Implementations

The implementation for the VRRenderingContext class is very straightforward. It uses the THREE.VRControls, THREE.VREffect and THREE.ViveController to implement the required functionality.

The implementation for the StandardRenderingContext class is a bit trickier, as it’s not so clear what it should do. For my example, I wanted the desktop version to simulate the webVR controls and experience as good as possible. I decided to use the THREE.FlyControls controls to simulate the user moving. For simulating the Vive Controllers, I decided to add two draggable spheres to scene, which will act as virtual controllers. When the spheres are moved, they trigger the same event that the VRRenderingContext triggers when the Vive controllers are moved.

Virtual Controllers created by the StandardRenderingContext class

Virtual Controllers created by the StandardRenderingContext class

You can see that I’m using the THREE.TransformControls for dragging the spheres.

Here is the code for the View component, which is the part of the application that would normally deal with Three.js classes like Scene, Control andRenderer. You’ll see that in this case, it only deals with the RenderingContext interface. It doesn’t know whether it running as a WebVR or desktop application.

export default class MainView {
    constructor(controller, renderingContextFactory) {
        this.controller = controller;
        this.renderingContext = this.createRenderingContext(renderingContextFactory);
    }

    createRenderingContext(renderingContextFactory) {
        const domContainer = document.createElement('div');

        document.body.appendChild(domContainer);

        const renderingContext = renderingContextFactory.createRenderingContext(domContainer);

        domContainer.appendChild(renderingContext.getDomElement());

        return renderingContext;
    }

    initialize() {
        window.addEventListener( 'resize', (e) => this.onWindowResize(), false );
        this.renderingContext.addObserver( 'onControllerPositionChange', (e) => {
            this.controller.onControllerMoved(e.controller);
        });

        this.render();
    }

    render() {
        requestAnimationFrame(() => this.render());

        this.renderingContext.onRender();
    }

    onWindowResize(){
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();

        this.renderingContext.renderer.setSize(window.innerWidth, window.innerHeight);
    }

    get scene() {
        return this.renderingContext.scene;
    }

    get camera() {
        return this.renderingContext.camera;
    }

    get renderer() {
        return this.renderingContext.renderer;
    }
}

We also have a class called RenderingContextFactory, which is responsible for creating the right RenderingContext class.

import StandardRenderingContext from './StandardRenderingContext';
import VRRenderingContext from './VRRenderingContext';

export default class RenderingContextFactory {
    constructor(type) {
        this.type = type;
    }

    createRenderingContext(domContainer) {
        if (this.type === 'vr') {
            return new VRRenderingContext(domContainer);
        } else {
            return new StandardRenderingContext(domContainer);
        }
    }

}

In the demo I created, switching between modes/environments is as easy as adding a query string parameter mode=vr to the url. This is allows me to easily switch between environments when developing and maintain an efficient development cycle. So, to open the application in desktop mode click here. To open it in VR mode we just add ?mode=vr to the url.

Conclusions

The Adapter Pattern is one of the most useful patterns out there. It’s the perfect solution for decoupling specific low-level implementation from higher-level code. In this post, we’ve used it to decouple the specifics of how our Three.js scene is rendered and controlled, from our application-specific code.

An extra benefit of using the Adapter Pattern is that we get cleaner code. Each adapter implementation satisfies the Single responsibility principle, which makes them very easy to understand and unit test. Our view component also becomes easier to test, as it doesn’t depend on specific on specific WebVR classes anymore. We can also support new running environments by just adding new adapters, without changing existing code, which is an example of the Open closed principle.

Software Engineering
Welcoming change: how decoupling can make your application more flexible
Programming Patterns
Building Real-Time Collaboration Applications in Three.js
Programming Patterns
MVC Pattern For Building Three.js Applications