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
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.
A rough diagram of the simple VR objects and their organizational structures. All of this should be covered down below.
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:
- Valve's OpenVR wiki - I would recommend using the search bar within the wiki.
- The openvr.h header file
- My OpenVR tracking example, specifically Lighthousetracking.cpp
- The Omnifinity OpenVR tracking example (Windows only)
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.
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.
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.
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.
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.
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).
The button events are:
- VREvent_ButtonPress (200)
- VREvent_ButtonUnpress (201)
- VREvent_ButtonTouch (202)
- VREvent_ButtonUntouch (203)
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)
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
}
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.
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.
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:
- Getting the ControllerState struct for the controller in question
- Testing for the proper axes ids for the trigger and touchpad
- Using the ControllerState struct and axis id to get analog button data
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.
A tranformation matrix conatins the position and rotation information about the device when the pose was captured and is returned by pose.mDeviceToAbsoluteTracking
.
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.
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
.
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.
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.
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.
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
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.
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.
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.
In practice, usage should look similar to this:
TrackedDevicePose_t trackedDevicePose;
VRControllerState_t controllerState;
vr_pointer->GetControllerStateWithPose(
TrackingUniverseStanding, deviceId, &controllerState,
sizeof(controllerState), &trackedDevicePose);
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
}
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.
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
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.
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)
}
}
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 positon (translation) vector can be found in the first three rows of the last column of the 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.
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.
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.
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 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.
If there are problems working with SteamVR or OpenVR, see the troubleshooting guide. The problem might have been solved on that page already.
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.