OpenVR Quick Start - osudrl/CassieVrControls GitHub Wiki

This guide will outline absolute essentials of interfacing with OpenVR to get tracking and other data from the HTC Vive. The guide will have sections on:

  • Hardware and software setup
  • Essential VR namespace calls
  • Compiling and linking with OpenVR
  • A quick note on troubleshooting

Hardware and Software setup

The setup process includes:

  • Mounting of the base stations
  • Connecting the HTC Vive to the link box
  • Connecting the link box to the machine
  • Installing proper drivers and dependencies
  • Installing Steam and SteamVR
  • Cloning openvr from Valve

The setup process is covered in detail in the Vive Setup Guide for both Windows and Linux (Ubuntu) operating systems.

Essential VR Namespace Calls

A rough diagram of the simple VR objects and their organizational structures. All of this should be covered down below.

rough diagram

For each vr method that is in this section, there will be

  • One or more code snippets showing how to make that call
  • An explaination of how that method/action performs
  • A link to the action in the context of my tracking example

Here are a few more general resources that can help in learning to use OpenVR:

OpenVR track example

Header setup: Namespace and Include statements

Namespace

When interfacing with OpenVR in an application like the following steps details, it is best to include using namespace vr towards the top of the module's header file.

Without the "namespace" line, all VR calls and references need to be preceded by vr::. VR_Init becomes vr::VR_Init and EVRInitError becomes vr::EVRInitError. The rest of this guide asssumes you are using the vr namespace.

Include Statement

The openvr header needs to be included for any VR functions to work. I have had succcess including "headers/openvr.h" instead of just "openvr.h" because then only the path to the root of the openvr repository clone needs to be provided to compile the final product. More on linking and referencing openvr in the "Compiling and Linking with OpenVR" section of this guide.

TLDR

If you are unsure what to include and what your header file should look like at this point in the guide, copy the following snippet and move on.

//myvr.h - Sample header file stub
#include <headers/openvr.h>
using namespace vr;

//You may want to put the global variables in a class, or just leave them here.
IVRSystem* vr_pointer = NULL;

The same thing is done in the first few lines of Lighthousetracking.h in the OpenVR Tracking Example that I have been working on.

Initialization - VR_Init()

Before the VRSystem can be used, the system first needs to be initialized. The following snippet will initialize the VR System and check/print any errors that occur.

EVRInitError eError = VRInitError_None;
vr_pointer = VR_Init(&eError, VRApplication_Background);
if (eError != VRInitError_None)
{
	vr_pointer = NULL;
	printf("Unable to init VR runtime: %s \n", 
		VR_GetVRInitErrorAsEnglishDescription(eError));
	exit(EXIT_FAILURE);
}

VR_Init takes two arguments, the first is an error pointer and the second is the Application Type. In this case, VRApplication_Background is the Application Type, but other EVRApplicationTypes include:

  • VRApplication_Scene - A 3D application that will be drawing an environment.
  • VRApplication_Overlay - An application that only interacts with overlays or the dashboard.
  • VRApplication_Utility - This application type is appropriate for things like installers.

In the VRInit line, vr_pointer is declared as a global variable in the header file for use throughout the application.

The VRInit call is can be found in Lighthousetracking.cpp's constructor.

Shutdown - VR_Shutdown()

Technically, the only thing thats needed to shutdown the VR system/application is calling VR_Shutdown(), but in a larger application, this small function may be more helpful.

void Shutdown() 
{
	if (vr_pointer != NULL)
	{
		VR_Shutdown(); 
		vr_pointer = NULL;
	}
}

Shutdown is performed by the Lighthousetracking destructor in the tracking example.

Processsing Generic VR Events

Polling for VR events allows the application to test/output important events such as

  • The user shutting down the SteamVR tool
  • Button presses/releases
  • The SteamVR menu being opened
  • The connection/disconnection of controllers/other devices.

Polling for a VR event is done with VR_System()->PollNextEvent(event_pointer, size of event) and will not block the thread even if there is no VR event to process (in that case it will just rethrn false).

If there is no VR event to process, PollNextEvent() returns false and the thread can continue to do other things.

An example method to poll VR events is shown below. Assume that RunProcedure() is called by a while loop somewhere else in the code so that the method is called and can check for VREvents as often as possible.

void RunProcedure() 
{
	VREvent_t event;
	if(vr_pointer->PollNextEvent(&event, sizeof(event)))
	{
		//Process the VREvent
	}
	//Output tracking data or do other things
}

Where //Process the VREvent could be a switch block

switch(event.eventType)
{
	case VREvent_TrackedDeviceActivated:
		printf("EVENT (OpenVR) Device : %d attached\n", 
			event.trackedDeviceIndex);
	break;

	//and so on, can test for any amount of vr events

	default:
		printf("EVENT--(OpenVR) Event: %d\n", event.eventType); 
}

The some common VR events to test for include:

  • VREvent_TrackedDeviceActivated
  • VREvent_TrackedDeviceDeactivated
  • VREvent_TrackedDeviceUpdated
  • VREvent_DashboardActivated
  • VREvent_DashboardDeactivated
  • VREvent_ChaperoneDataHasChanged
  • VREvent_ChaperoneSettingsHaveChanged
  • VREvent_ChaperoneUniverseHasChanged
  • VREvent_ApplicationTransitionStarted
  • VREvent_ApplicationTransitionNewAppStarted
  • VREvent_TrackedDeviceUserInteractionStarted
  • VREvent_TrackedDeviceRoleChanged

While these common shutdown VR events should trigger some sort of internal shutdown and release of memory in the application:

  • VREvent_Quit
  • VREvent_ProcessQuit
  • VREvent_QuitAborted_UserPrompt
  • VREvent_QuitAcknowledged

To see the complete list of VREvents see the VREvent section of the openvr header.

In the tracking example, the RunProcedure method polls for events and calls ProcessVREvent to print the event data and determine if the application needs to shutdown (returns false if needs to shutdown).

Button Events and States

The Button Event Types

The button events are:

  • VREvent_ButtonPress (200)
  • VREvent_ButtonUnpress (201)
  • VREvent_ButtonTouch (202)
  • VREvent_ButtonUntouch (203)

Important Button Event Properties

There are two important properties of VREvents when processesing button events:

  • event.trackedDeviceIndex provides the index of the device that the event happened with (if applicable). When processing button events, use the trackedDeviceIndex to confirm that the button event came from one of the controllers and also can be used to determine if it was the left or right hand controller.
  • event.data.controller.button can be used to test which button was pressed.

The explainations of the above properties reference "button presses" because it is easier to explain, but the above properties are used for all four types of button events (press, release, touch, untouch)

Left or Right?

Unless the deviceIds of the controllers are stored as variables somewhere in your program, it needs to be determined if it was on the left or right controller that the button was pressed.

ETrackedDeviceClass trackedDeviceClass = 
	vr_pointer->GetTrackedDeviceClass(event.trackedDeviceIndex);
if(trackedDeviceClass != ETrackedDeviceClass::TrackedDeviceClass_Controller) {
	return; //this is a placeholder, but there isn't a controller 
	   //involved so the rest of the snippet should be skipped
}
ETrackedControllerRole role = 
   vr_pointer->GetControllerRoleForTrackedDeviceIndex(event.trackedDeviceIndex);
if (role == TrackedControllerRole_Invalid)
	// The controller is probably not visible to a base station.
	//    Invalid role comes up more often than you might think.
else if (role == TrackedControllerRole_LeftHand)
	// Left hand
else if (role == TrackedControllerRole_RightHand)
	// Right hand
}

Define the action

To determine what action should be taken, there are normally three pieces of information that are tested:

  • The hand of the controller (unless both controllers do the same thing)
  • The button that the event is referencing
  • What event type it is (press, release, touch, untouch)

The "hand" test and the list of event types are above.

The list of common buttons to test for include:

  • k_EButton_ApplicationMenu
  • k_EButton_Grip
  • k_EButton_SteamVR_Touchpad
  • k_EButton_SteamVR_Trigger

A full list of buttons that the button event code can test for can be found in the EVRButtonId enum declaration in the openvr header.

buttons diagram

Organizing the action your code could be organized like this:

switch( event.data.controller.button ) {
	case k_EButton_Grip:
		switch(event.eventType)	{
			case VREvent_ButtonPress:			
			break;

			case VREvent_ButtonUnpress:			
			break;
		}
	break;

	case k_EButton_SteamVR_Trigger:  
		switch(event.eventType) {
			case VREvent_ButtonPress: 			
			break;

			case VREvent_ButtonUnpress:			
			break;
		}
	break;

	case k_EButton_SteamVR_Touchpad:
		switch(event.eventType)	{
			case VREvent_ButtonPress:			
			break;

			case VREvent_ButtonUnpress:			
			break;

			case VREvent_ButtonTouch:			
			break;

			case VREvent_ButtonUntouch:			
			break;
		}
	break;

	case k_EButton_ApplicationMenu:  
		switch(event.eventType)	{
			case VREvent_ButtonPress: 			
			break;

			case VREvent_ButtonUnpress:			
			break;
		}
	break;
}

The tracking example processes button events in the dealWithButtonEvent method.

Analog Data

The ControllerState struct holds analog and real time button data like:

  • What percent of the way is the trigger pressed down right now? (Analog data)
  • Where on the touchpad is the user's finger?

This guide does not explain in detail how to use Controller States to get analog data from buttons at this time, but the following resources should be sufficient:

Poses, Translation Matricies, and Coordinates

Vocablulary

Pose

A pose is the struct that is returned the VR system that contains physics information about the device in question (position, rotation, velocity, angular velocity) as well as the "tracking result", "validity", and whether the device was connected when the pose was captured.

Accesssing the pose for a given device (HMD, controller, tracker) is described later in this section.

Tranformation Matrix

A tranformation matrix conatins the position and rotation information about the device when the pose was captured and is returned by pose.mDeviceToAbsoluteTracking.

transformation matrix

It is not necessesary to know how to use transformation matricies to work with vive physics and tracking, especially if only working with position. Code to extract position from the transformation matrix can be found further down in this section.

Tracking Result

The tracking result of a pose describes any considerations or issues with that pose. It say if the device is out of the range of the base stations or is tracking properly. It can be accessed by pose.eTrackingResult.

track result

Pose Validity

Pose validity is a boolean that is accessed by pose.bPoseIsValid. It can be used in conjunction or instead of the Tracking Result to determine whether the pose's coordinates will be legitimate. If it is false, the device is out of range of the base stations or is not functioning properly and tracking data cannot be extracted for that device until the problem is resolved.

Accessing a Device's current pose

There are different ways to get the current pose of a device depending on what type of device it is. See further below for a table if you are looking to quickly copy/paste.

Device Pose: HMD

HMD stands for head mounted device, and in case it is unclear what exactly is being tracked when talking about the HMD, it is this device:

The easiest way to request the current pose of the HMD is with the IVRSystem::GetDeviceToAbsoluteTrackingPose() function. An example of its implementation can be found here in the tracking example.

On the function's wiki page, it is explained that although "device" is general and doesn't specify that it is the HMD's pose that is being retrived, this function returns:

The pose that the tracker **thinks that the HMD will be in at the specified number of seconds into the future.

Usage

In practice, requesting the HMD's pose should look like this:

TrackedDevicePose_t trackedDevicePose;
vr_pointer->GetDeviceToAbsoluteTrackingPose(
	TrackingUniverseStanding, 0, &trackedDevicePose, 1);
// VRSystem()->GetDeviceToAbsoluteTrackingPose should work the same
Arg0: Tracking Universe

The first argument can be any of the following:

  • TrackingUniverseSeated
  • TrackingUniverseStanding
  • TrackingUniverseRaw

If using the Vive for position tracking, then TrackingUniverseStanding should always be used.

Arg1: Timing

According to wiki page, the second argument is:

float fPredictedSecondsToPhotonsFromNow - Number of seconds from now to predict poses for. Positive numbers are in the future. Pass 0 to get the state at the instant the function is called. See below for details on how to compute a good value here.

In using the Vive for position tracking, 0 has always been used as the second argument.

Device Pose: Controllers

The way to request the current pose of a controller is with the GetControllerStateWithPose() function (link to its wiki page). Implementation of getting controller poses can be found here in the Lighthousetracking module.

Usage

In practice, usage should look similar to this:

TrackedDevicePose_t trackedDevicePose;
VRControllerState_t controllerState;
vr_pointer->GetControllerStateWithPose(
	TrackingUniverseStanding, deviceId, &controllerState, 
	sizeof(controllerState), &trackedDevicePose);
Arg1: DeviceID

Note that unlike requesting the HMD's pose, it is impossible to get the current pose of a controller without knowing its deviceId. Unless your code chooses to store the relevant deviceIds as variables in a different function, the controller-pose-getting code could look like this:

for (unsigned int id = 0; id < k_unMaxTrackedDeviceCount; id++)  {
	ETrackedDeviceClass trackedDeviceClass = 
		vr_pointer->GetTrackedDeviceClass(id);
	if (trackedDeviceClass != 
			ETrackedDeviceClass::TrackedDeviceClass_Controller ||
		!vr_pointer->IsTrackedDeviceConnected(id))
		continue;

	//Confirmed that the device in question is a connected controller

	//This is all copied from above:
	TrackedDevicePose_t trackedDevicePose;
	VRControllerState_t controllerState;
	vr_pointer->GetControllerStateWithPose(
		TrackingUniverseStanding, id, &controllerState, 
		sizeof(controllerState), &trackedDevicePose);

	//Do things with the TrackedDevicePose_t struct
} 
Controller States

ControllerState structs are used to retrieve analog button data for the controller in question. They are filled as part of the GetControllerStateWithPose() function above and the Analog Data section of this guide covers how to extract the analog data from the struct.

Device Pose: Tracker (aka GenericTracker)

This is a Vive Tracker:

As shown in its implementation in the outdated Lighthousetracking class, the GetControllerStateWithPose() function should be used when getting poses for generic trackers in addition to contollers.

Obviously, the ControllerState struct doesn't matter for the tracker, but if the tracker deviceId is known, the following snippet will return the tracker's pose.

TrackedDevicePose_t trackedDevicePose;
VRControllerState_t controllerState;
vr_pointer->GetControllerStateWithPose(
	TrackingUniverseStanding, deviceId, &controllerState, 
	sizeof(controllerState), &trackedDevicePose);

And if you are trying to modify the "for loop" snippet above to test for trackers instead of controllers, change

trackedDeviceClass != ETrackedDeviceClass::TrackedDeviceClass_Controller

to

trackedDeviceClass != ETrackedDeviceClass::TrackedDeviceClass_GenericTracker

Getting Poses: TLDR

If you skipped all the individual cases in this section (HMD, controller, tracker), this simple structure should allow you to get the pose for any of the devices. Use the table below to copy and paste the right pieces into the snippet.

Code Snippet
for (unsigned int deviceId=0;deviceId<k_unMaxTrackedDeviceCount;deviceId++) {

	TrackedDevicePose_t trackedDevicePose;
	VRControllerState_t controllerState;

	ETrackedDeviceClass class = 
		vr_pointer->GetTrackedDeviceClass(id);

	if (!vr_pointer->IsTrackedDeviceConnected(id))
		continue;

	if (class  == ETrackedDeviceClass::/*<VR Class>*/) {
		vr_pointer->/*<Getting Pose>*/(/*<Arguments>*/);

		//perform actions with pose struct (see next section)
	}
} 
Copy/Paste Table
Device <VR Class> <Getting Pose> <Arguments> Need Device Id?
HMD Tracked Device Class _HMD Get Device To Absolute Tracking Pose() TrackingUniverseStanding, 0, &trackedDevicePose, 1 No
Controller Tracked Device Class _Controller Get Controller State With Pose() TrackingUniverseStanding, deviceId, &controllerState, sizeof(controllerState), &trackedDevicePose Yes
Tracker Tracked Device Class _GenericTracker Get Controller State With Pose() TrackingUniverseStanding, deviceId, &controllerState, sizeof(controllerState), &trackedDevicePose Yes

The Position Vector

The positon (translation) vector can be found in the first three rows of the last column of the transformation matrix.

transformation matrix

The following code snippet extracts the position vector from the transformation matrix.

HmdVector3_t vector;
vector.v[0] = matrix.m[0][3];
vector.v[1] = matrix.m[1][3];
vector.v[2] = matrix.m[2][3];

See the GetPosition() implementation in the tracking example.

The Rotation Quaternion

See the GetRotation() implementation from the Omnifinity tracking example where petethor extracts the rotation quaternion from the translation matrix.

The Game Math Quaternion Basics page was a great crash course for me on quaternions and their use in games. For the source file I wrote while practicing the above guide, see quat.c.

Haptic Feedback (Controller Vibration)

To trigger vibration on a controller, this method needs to be called every frame while the vibration should continue.

//To be called every frame of the application
vr_system->TriggerHapticPulse(controllerDeviceId, 0, strength);

The "strength" argument is an integer in the range [0-3999]. If this function is not called every frame, the vibration won't work at all.

This snippet shows how vibrating conrollers might look:

int leftId =  functionToGetLeftHandControllerDeviceId();
int rightId = functionToGetRightHandControllerDeviceId();
while (true) {
	PollAndProcessVREvent();
	GetAndActOnDevicePoses();
	if(shouldvibrate) {
		vr_system->TriggerHapticPulse(leftId, 0, 400);
		vr_system->TriggerHapticPulse(rightId, 0, 400);
	}
	sleep(1);
}

See the block that controlls controller vibration in the tracking example for more usage of haptic feedback.

More on OpenVR Device Ids

The device ids in the OpenVR system aren't guaranteed to referenced to any particular device. Often times, 0 is the id for the HMD, 1 and 2 are the base stations, and 3 and 4 are the controllers.

However, those device/id parings aren't guaranteed so it is not robust to "hard code" ids in code that is trying to work with particular devices.

The iterateAssignIds() function in the tracking example loops through all sixteen possible device ids to store the given device id for the HMD, controllers and trackers.

Compiling and Linking with OpenVR

Compiling with OpenVR and linking to the libraries at runtime is the same as using other libraries in c++, but linking and compiling with external libraries was completely new to me when working with OpenVR. I have been running the following commands for compiling on Linux and Windows respectively.

Linux: g++ -L/home/kellark/Documents/openvr/bin/linux64 -I/home/kellark/Documents/openvr -Wl,-rpath,/home/kellark/Documents/openvr/bin/linux64 -Wall -Wextra -std=c++0x -o build/track *.cpp *.c -lopenvr_api

Windows: g++ -LC:\\Users\\kellark\\Documents\\openvr\\bin\\win64 -IC:\\Users\\kellark\\Documents\\openvr -Wl,-rpath,C:\\Users\\kellark\\Documents\\openvr\\bin\\win64 -Wall -Wextra -std=c++0x -o build/track *.cpp *.c -lopenvr_api

The commands look long and monstrous and that is why the tracking example uses a python script to generate the build command. Where at this line the generic compile script is defined.

comp = 'g++ -L%s -I%s -Wl,-rpath,%s -Wall -Wextra  -std=c++0x -o build/track *.cpp *.c -lopenvr_api' % (openvr_bin,openvr_path,openvr_bin)

Maybe the "compile command" isn't very complicated if compared to a proper make configuration, but there are still quite a few important pieces that need to be in the command for the project to compile and link properly.

  • -L\<path-to-openvr-bin\> is the option to add to the path of libraries
  • -I\<path-to-openvr-root\> is the option to add to the "include path" when compiling. When I wrote the tracking example, the include statement was #include <headers/openvr.h> so that the openvr.h file would be included given that the compiler had access the the root of the openvr repository. If instead the path to the "openvr/headers" folder was provided through the -I option, then the include statement in the c++ code would be #include <openvr.h>.
  • -Wl,-rpath,\<path-to-openvr-bin\> is the option to include in the executable where it should expect the openvr binaries to be at runtime.
  • -lopenvr_api essentially tells the compiler/linker what files it should be looking for in the directories provided by -L and -rpath.

A Quick Note on Troubleshooting

If there are problems working with SteamVR or OpenVR, see the troubleshooting guide. The problem might have been solved on that page already.

Feedback

This guide was written by Kevin Kellar for Oregon State University's Dynamic Robotics Laboratory. For issues, comments, or suggestions with this guide, contact the developer or open an issue.

⚠️ **GitHub.com Fallback** ⚠️