SteamVR Unity Tutorial #01

Introduction / Getting Started


Regan Russell - July 20th, 2016
Contents:
  1. Introduction
  2. Setup
  3. Understanding SteamVR
  4. Writing a Controller Wrapper
  5. Simple Teleport

Introduction

This tutorial series is intended to teach you how to use the Unity3D engine to make VR applications via SteamVR. By the end of this tutorial, you will have installed Unity3D and the associated SteamVR plugin, created a simple VR project, written a controller wrapper class and implemented a basic teleportation system via tracked controllers. This tutorial is focused primarily on VR-specific concepts, so it is assumed you either understand the basics of Unity/C# or are willing to figure them out.

Setup

To get started, let's make sure you have everything that you need: Unity3D, and the SteamVR Plugin for Unity. The SteamVR Plugin is what provides an interface for Unity to work with VR devices. Download and install Unity (if you already have Unity installed, make sure you have Unity 4.7.1 or greater installed, or SteamVR will not work!), then open it and create a new 3D project. With your new project open in Unity, go to the SteamVR Plugin page and click "Open in Unity", then import the plugin to your project. In Unity, a window should pop up with recommended settings for SteamVR, click "Accept All". With SteamVR loaded into your project, find the SteamVR folder in your Asset Browser, then go to Prefabs and drag a [CameraRig] prefab into your scene, then remove the Main Camera from your scene. Voila! Start SteamVR, press Play and you should be in the game! The [CameraRig] prefab defines your play area (outlined in blue), and sets up your camera and two tracked controllers by default. If you select the [CameraRig] and look at the Inspector panel, you'll notice a property called Size under the script SteamVR_PlayArea, set this to "Calibrated" to tell SteamVR to use whatever play bounds you established when doing your room-scale set up, otherwise it'll just display a play area of preset size by default. By default, these play area bounds aren't rendered in-game. If you want to see them in-game, enable the checkbox labeled Draw In Game.

Understanding SteamVR

Disclaimer: You really should read this, but if you're in a hurry, you can just skip ahead and start developing using the provided controller wrapper in the next section. In the next section we'll write a wrapper for more conveniently working with tracked controllers, but it's important to first understand some basics of SteamVR first. The [CameraRig] prefab is basically an all-in-one package for a SteamVR project, it initializes SteamVR and defines your play area, headset and tracked controllers if available. If you expand the [CameraRig] prefab, you'll see the left and right controllers (Controller (left/right)), as well as the headset (Camera (head)). Both of the controllers have a child GameObject called Model, which will be instantiated and filled with a model of your tracked controllers when the game starts. All three of these GameObjects have a script attached to them, SteamVR_TrackedObject, which defines a barebones class for any objects tracked by SteamVR. Each of these SteamVR_TrackedObjects has a public enumerator, index, representing which tracked object within SteamVR the GameObject represents. On the HTC Vive, the headset has the index "Hmd", while the two wand controllers are indexed "Device1" and "Device2". The controllers' indexes are assigned by which one is powered on first, and do not represent left and right. You can access the left and right controllers' GameObjects via the SteamVR_ControllerManager's public left and right properties at any time, or you can use GameObject.Find() to locate them by searching for "[CameraRig]/Controller (left/right)". These GameObjects represent the orientation of the controllers in the world automatically via SteamVR, so when you want to know the rotation or position of a controller, just access it via the GameObject's transform like you would any other Unity object. If you look at the source of SteamVR_TrackedObject in the SteamVR/Scripts folder, you can see it doesn't do much. The bulk of the tracked controllers' function is implemented in SteamVR_Controller, specifically the Device inner-class. The SteamVR_Controller.Device inner-class contains methods like GetPress(), GetTouch(), and GetAxis() which allow you to check the status of buttons, touchpads and triggers on the controllers. This class also contains the public properties velocity and angularVelocity, which provide the real-world velocity and angular velocity of the controller.

Writing a Controller Wrapper

Disclaimer: This tutorial was written while Oculus Touch was unreleased to the public. Once Oculus Touch is out, this tutorial will most likely be updated to include a wrapper for Touch. Until then, this tutorial exclusively covers the HTC Vive and the tracked controllers that ship with it. I find that it helps to write a wrapper class for tracked controllers in order to simplify working with them. The alternative is just using SteamVR functions to manually poll for changes in controller state like button presses and touchpad/trigger axis, which can be a bit tedious, so we'll go ahead and write a wrapper to make everything a lot simpler. SteamVR includes a class called SteamVR_TrackedController, found in the Extras directory of the SteamVR plugin, which already does most of the work for you by updating the state of the controller automatically via simple public properties. Let's take a quick look:

public class SteamVR_TrackedController : MonoBehaviour
{
    public uint controllerIndex;
    public VRControllerState_t controllerState;
    public bool triggerPressed = false;
    public bool steamPressed = false;
    public bool menuPressed = false;
    public bool padPressed = false;
    public bool padTouched = false;
    public bool gripped = false;

    public event ClickedEventHandler MenuButtonClicked;
    public event ClickedEventHandler MenuButtonUnclicked;
    public event ClickedEventHandler TriggerClicked;
    public event ClickedEventHandler TriggerUnclicked;
    public event ClickedEventHandler SteamClicked;
    public event ClickedEventHandler PadClicked;
    public event ClickedEventHandler PadUnclicked;
    public event ClickedEventHandler PadTouched;
    public event ClickedEventHandler PadUntouched;
    public event ClickedEventHandler Gripped;
    public event ClickedEventHandler Ungripped;

    ...

SteamVR_TrackedController defines a few bools which let you know whether certain buttons are pressed or not, as well as some Events to let you easily respond to button presses. SteamVR_TrackedController already does a great job of wrapping up controllers into a neat and easy to access bundle, but let's make it even easier with our own wrapper. Start by creating a new C# script in your project, and naming it WandController.cs. Once your script is created, add it to both the left and right controllers under [CameraRig] by dragging and dropping the script on to the GameObjects in either the Inspector or the Hierarchy. We want our WandController class to be based on the SteamVR_TrackedController, so replace MonoBehaviour in the class declaration with SteamVR_TrackedController to make the WandController class inherit from it:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    void Start()
    {

    }

    void Update()
    {
    
    }
}

Next, we need to address an "issue" with SteamVR_TrackedController in order to be able to base our WandController off of it. We need to modify SteamVR_TrackedController's Start() and Update() methods to be protected and virtual. The methods must be protected in order for us to access them from our child-class, WandController, and they need to be virtual in order for us to override them. So find the Start() and Update() functions in SteamVR_TrackedController.cs and add "protected virtual" to both functions, so they look like this:

public class SteamVR_TrackedController : MonoBehaviour
{
    ...

    protected virtual void Start()
    {
        ...
    }

    protected virtual void Update()
    {
        ...
    }

    ...

Now we can override the Start() and Update() functions in our own WandController class to add our own functionality:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    protected override void Start()
    {
        base.Start();

        // Your code here
    }

    protected override void Update()
    {
        base.Update();

        // Your code here
    }
}

Adding "protected override" to each function will (obviously) make them protected access, as well as overriding the parent's implementation of this function. However, we need to be sure to call base.Start() and base.Update() in order to let the parent-class' implementations of these methods run, otherwise both the parent and child class become useless. With that out of the way, now we can get to the good stuff! SteamVR_TrackedController defines and calls a bunch of virtual methods (which in turn fire the Events we talked about earlier) to let you know when a button is pressed on the controller. If we override all of these methods, we can add our own code that will run whenever certain buttons are pressed:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    protected override void Start()
    {
        base.Start();
    }

    protected override void Update()
    {
        base.Update();
    }

    public override void OnTriggerClicked(ClickedEventArgs e)
    {
        base.OnTriggerClicked(e);
    }

    public override void OnTriggerUnclicked(ClickedEventArgs e)
    {
        base.OnTriggerUnclicked(e);
    }

    public override void OnMenuClicked(ClickedEventArgs e)
    {
        base.OnMenuClicked(e);
    }

    public override void OnMenuUnclicked(ClickedEventArgs e)
    {
        base.OnMenuUnclicked(e);
    }

    public override void OnSteamClicked(ClickedEventArgs e)
    {
        base.OnSteamClicked(e);
    }

    public override void OnPadClicked(ClickedEventArgs e)
    {
        base.OnPadClicked(e);
    }

    public override void OnPadUnclicked(ClickedEventArgs e)
    {
        base.OnPadUnclicked(e);
    }

    public override void OnPadTouched(ClickedEventArgs e)
    {
        base.OnPadTouched(e);
    }

    public override void OnPadUntouched(ClickedEventArgs e)
    {
        base.OnPadUntouched(e);
    }

    public override void OnGripped(ClickedEventArgs e)
    {
        base.OnGripped(e);
    }

    public override void OnUngripped(ClickedEventArgs e)
    {
        base.OnUngripped(e);
    }
}

It should be immediately clear when each of these methods are called, based on the names. Note that you don't need to override all of these methods, only the ones which you intend on adding some code to. For instance, if you wanted to teleport when the trigger is pressed, you would only need to override OnTriggerClicked(). With these methods overridden, we can add our own code which will be triggered when the appropriate button is pressed on our controller. Next, we need to expose access to two important properties of the SteamVR_Controller.Device class we talked about earlier: velocity and angularVelocity, which provide the real-world velocities of the controllers in Unity. At the top of our WandController class, let's add the following code:

public class WandController : SteamVR_TrackedController {

    public SteamVR_Controller.Device controller { get { return SteamVR_Controller.Input((int) controllerIndex); } }
    public Vector3 velocity { get { return controller.velocity; } }
    public Vector3 angularVelocity { get { return controller.angularVelocity; } }

    ...

The first of these three lines defines a public SteamVR_Controller.Device property, and uses C# accessors to retrieve the controller property by passing controllerIndex (a property of the parent-class SteamVR_TrackedController) to SteamVR_Controller.Input(), which returns the SteamVR_Controller.Device associated with that device index. The next two lines just retrieve the velocity and angular velocity from the controller property. At this point, we know when each button on the controller is clicked and unclicked, and we have access to our real-world velocities for each controller, but we still don't have access to where the user's thumb is on the touchpad, nor do we know how much the trigger is pulled on the controller. We can fix that with two simple methods to finish off our wrapper class:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    ...

    public float GetTriggerAxis()
    {
        // If the controller isn't valid, return 0
        if (controller == null)
            return 0;

        // Use SteamVR_Controller.Device's GetAxis() method (mentioned earlier) to get the trigger's axis value
        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_Axis1).x;
    }

    public Vector2 GetTouchpadAxis()
    {
        // If the controller isn't valid, return (basically) 0
        if (controller == null)
            return new Vector2();

        // Use SteamVR_Controller.Device's GetAxis() method (mentioned earlier) to get the touchpad's axis value
        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Touchpad);
    }
    
    ...

GetAxis() returns a Vector2D representing the X/Y axis values of the specified control. Valve.VR.EVRButtonId.k_EButton_SteamVR_Touchpad represents the touchpad and Valve.VR.EVRButtonId.k_EButton_Axis1 represents the trigger's axis, although only the X axis is used for the trigger. This is why we return the float x-value of the axis for GetTriggerAxis(), rather than the entire Vector2D. The trigger's axis value ranges from 0 (unpressed) to 1 (fully pressed), and the Vector2D returned by GetTouchpadAxis() ranges from (-1,-1) in the bottom-left, to (1,1) in the top-right, as demonstrated below:

At this point, your WandController class should look like this:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    public SteamVR_Controller.Device controller { get { return SteamVR_Controller.Input((int) controllerIndex); } }
    public Vector3 velocity { get { return controller.velocity; } }
    public Vector3 angularVelocity { get { return controller.angularVelocity; } }

    protected override void Start()
    {
        base.Start();
    }

    protected override void Update()
    {
        base.Update();
    }

    public float GetTriggerAxis()
    {
        if (controller == null)
            return 0;

        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_Axis1).x;
    }

    public Vector2 GetTouchpadAxis()
    {
        if (controller == null)
            return new Vector2();

        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Touchpad);
    }

    // If you have a bunch of overridden OnClick/OnUnclick functions, that's okay!
    // They're not necessary unless you've added your own code though, so I've omitted them here.
}

Simple Teleport

Finally, let's put put everything we've learned together to make a simple teleportation feature! First off, make a little platform to teleport around on by adding a plane to your scene, then (optionally) sprinkle some cubes around the plane so when you teleport around, you actually have a reference point and can tell you've changed positions. Next, let's open up our WandController class and override the OnTriggerClicked() function to make it so that when the trigger is pulled, we teleport to wherever the controller is aiming:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    ...

    public override void OnTriggerClicked(ClickedEventArgs e)
    {
        base.OnTriggerClicked(e);

        // We want to move the whole [CameraRig] around when we teleport,
        // which should be the parent of this controller. If we can't find the
        // [CameraRig], we can't teleport, so return.
        if (transform.parent == null)
            return;

        RaycastHit hit;
        Vector3 startPos = transform.position;

        // Perform a raycast starting from the controller's position and going 1000 meters
        // out in the forward direction of the controller to see if we hit something to teleport to.
        if (Physics.Raycast(startPos, transform.forward, out hit, 1000.0f))
        {
            transform.parent.position = hit.point;
        }
    }

    ...

As previously mentioned, this tutorial isn't meant to be a comprehensive guide to Unity. If you have trouble understanding what's going on in the block above, take a look at the documentation for Physics.Raycast() and it should become clear to you. If you load the game and try out your new controllers, you should be able to teleport around the platform and even on top of cubes! However, you can't really tell exactly where you're going because there is no pointer or laser showing where your controller is aiming. Let's add a LineRenderer to our script to give us more control:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    ...

    protected LineRenderer lineRenderer;
    protected Vector3[] lineRendererVertices;

    protected override void Start()
    {
        base.Start();

        // Initialize our LineRenderer
        lineRenderer = gameObject.AddComponent<LineRenderer>();
        lineRenderer.material = new Material(Shader.Find("Particles/Additive"));
        lineRenderer.SetWidth(0.01f, 0.01f);
        lineRenderer.SetVertexCount(2);

        // Initialize our vertex array. This will just contain
        // two Vector3's which represent the start and end locations
        // of our LineRenderer
        lineRendererVertices = new Vector3[2];
    }

    protected override void Update()
    {
        base.Update();

        // Update our LineRenderer
        if (lineRenderer && lineRenderer.enabled)
        {
            RaycastHit hit;
            Vector3 startPos = transform.position;

            // If our raycast hits, end the line at that position. Otherwise,
            // just make our line point straight out for 1000 meters.
            // If the raycast hits, the line will be green, otherwise it'll be red.
            if (Physics.Raycast(startPos, transform.forward, out hit, 1000.0f))
            {
                lineRendererVertices[1] = hit.point;
                lineRenderer.SetColors(Color.green, Color.green);
            }
            else
            {
                lineRendererVertices[1] = startPos + transform.forward * 1000.0f;
                lineRenderer.SetColors(Color.red, Color.red);
            }

            lineRendererVertices[0] = transform.position;
            lineRenderer.SetPositions(lineRendererVertices);
        }
    }

    ...

    public override void OnTriggerClicked(ClickedEventArgs e)
    {
        base.OnTriggerClicked(e);

        // We want to move the whole [CameraRig] around when we teleport,
        // which should be the parent of this controller. If we can't find the
        // [CameraRig], we can't teleport, so return.
        if (transform.parent == null)
            return;

        RaycastHit hit;
        Vector3 startPos = transform.position;

        // Perform a raycast starting from the controller's position and going 1000 meters
        // out in the forward direction of the controller to see if we hit something to teleport to.
        if (Physics.Raycast(startPos, transform.forward, out hit, 1000.0f))
        {
            transform.parent.position = hit.point;
        }
    }
}

By overriding methods from our parent class, we were able to initialize a LineRenderer and its vertex array in Start(), update the start/end vertices and color of the line in Update() and teleport the player when the trigger is pulled in OnTriggerClicked(). If you load the game now, you should have a fully functioning teleporter wand with a laser to help you aim. In the next tutorial, we'll focus on improving our teleporter and explore other methods of locomotion and how they can be implemented. Here's what your finished WandController.cs should look like:

using UnityEngine;
using System.Collections;

public class WandController : SteamVR_TrackedController {

    public SteamVR_Controller.Device controller { get { return SteamVR_Controller.Input((int) controllerIndex); } }
    public Vector3 velocity { get { return controller.velocity; } }
    public Vector3 angularVelocity { get { return controller.angularVelocity; } }

    protected LineRenderer lineRenderer;
    protected Vector3[] lineRendererVertices;

    protected override void Start()
    {
        base.Start();

        // Initialize our LineRenderer
        lineRenderer = gameObject.AddComponent<LineRenderer>();
        lineRenderer.material = new Material(Shader.Find("Particles/Additive"));
        lineRenderer.SetWidth(0.01f, 0.01f);
        lineRenderer.SetVertexCount(2);

        // Initialize our vertex array. This will just contain
        // two Vector3's which represent the start and end locations
        // of our LineRenderer
        lineRendererVertices = new Vector3[2];
    }

    protected override void Update()
    {
        base.Update();

        // Update our LineRenderer
        if (lineRenderer && lineRenderer.enabled)
        {
            RaycastHit hit;
            Vector3 startPos = transform.position;

            // If our raycast hits, end the line at that position. Otherwise,
            // just make our line point straight out for 1000 meters.
            // If the raycast hits, the line will be green, otherwise it'll be red.
            if (Physics.Raycast(startPos, transform.forward, out hit, 1000.0f))
            {
                lineRendererVertices[1] = hit.point;
                lineRenderer.SetColors(Color.green, Color.green);
            }
            else
            {
                lineRendererVertices[1] = startPos + transform.forward * 1000.0f;
                lineRenderer.SetColors(Color.red, Color.red);
            }

            lineRendererVertices[0] = transform.position;
            lineRenderer.SetPositions(lineRendererVertices);
        }
    }

    public float GetTriggerAxis()
    {
        if (controller == null)
            return 0;

        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_Axis1).x;
    }

    public Vector2 GetTouchpadAxis()
    {
        if (controller == null)
            return new Vector2();

        return controller.GetAxis(Valve.VR.EVRButtonId.k_EButton_SteamVR_Touchpad);
    }

    public override void OnTriggerClicked(ClickedEventArgs e)
    {
        base.OnTriggerClicked(e);

        // We want to move the whole [CameraRig] around when we teleport,
        // which should be the parent of this controller. If we can't find the
        // [CameraRig], we can't teleport, so return.
        if (transform.parent == null)
            return;

        RaycastHit hit;
        Vector3 startPos = transform.position;

        // Perform a raycast starting from the controller's position and going 1000 meters
        // out in the forward direction of the controller to see if we hit something to teleport to.
        if (Physics.Raycast(startPos, transform.forward, out hit, 1000.0f))
        {
            transform.parent.position = hit.point;
        }
    }
}

You can download the project files used in this tutorial HERE.