Let’s create a setting screen on the SteamVR dashboard. It switches which hand to display the watch overlay.
Create dashboard overlay
Dashboard overlay is an overlay that is displayed on the SteamVR dashboard. We use this overlay as the setting screen.
Create new script
Create DashboardOverlay.cs inside Scripts folder and copy the following code.
using Valve.VR;
using System;
public class DashboardOverlay : MonoBehaviour
{
private void Start()
{
}
}
Put the script into the scene
On hierarchy, right click > Create Empty to create a new game object named DashboardOverlay. Drag DashboardOverlay.cs to the object.
Prepare overlay handles
Dashboard overlay consists of the main overlay and thumbnail overlay. Both overlays have their handle.
The thumbnail overlay is a small overlay at the bottom of the dashboard used to switch between overlays.
The red rectangle is the thumbnail overlay. The large “Right Hand” button is on the main overlay.
Create two variables for the two overlay handles in the DashboardOverlay.cs.
using Valve.VR;
using System;
public class DashboardOverlay : MonoBehaviour
{
+ private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
+ private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;
}
Create dashboard overlay
Create dashboard overlay with CreateDashboardOverlay(). (read the wiki for details)
Here, we set key as “WatchDashboardKey” and name as “Watch Setting”.
CreateDashboardOverlay() creates two overlays and sets their handles to the variables of the 3rd and 4th arguments.
Create utility class
We want to run the program but we must initialize the OpenVR before using the API. The initialization code is in WatchOverlay.cs. If DashboardOverlay.cs runs earlier than that initialization, it will result in an error.
There are various ways but this time, we will create a utility to share common code like initializing OpenVR that is called from other classes.
Create new script
Create OpenVRUtil.cs inside Scripts folder. Copy the following code.
using Valve.VR;
using System;
namespace OpenVRUtil
{
public static class System
{
}
}
Move OpenVR initialization
Move the InitOpenVR() from WatchOverlay.cs to OpenVRUtil.cs. Add static to allow access from other external classes.
WatchOverlay.cs
private void OnDestroy()
{
DestroyOverlay(overlayHandle);
ShutdownOpenVR();
}
– private void InitOpenVR()
– {
– if (OpenVR.System != null) return;
–
– var error = EVRInitError.None;
– OpenVR.Init(ref error, EVRApplicationType.VRApplication_Overlay);
– if (error != EVRInitError.None)
– {
– throw new Exception(“Failed to initialize OpenVR: ” + error);
– }
– }
private void ShutdownOpenVR()
{
if (OpenVR.System != null)
{
OpenVR.Shutdown();
}
}
…
OpenVRUtil.cs
using Valve.VR;
using System;
namespace OpenVRUtil
{
public static class System
{
+ // Add as public static method
+ public static void InitOpenVR()
+ {
+ if (OpenVR.System != null) return;
+
+ var error = EVRInitError.None;
+ OpenVR.Init(ref error, EVRApplicationType.VRApplication_Overlay);
+ if (error != EVRInitError.None)
+ {
+ throw new Exception(“Failed to initialize OpenVR: ” + error);
+ }
+ }
}
}
Move OpenVR cleanup
Similarly, move the ShutdownOpenVR() as a static method.
WatchOverlay.cs
private void OnDestroy()
{
DestroyOverlay(overlayHandle);
ShutdownOpenVR();
}
– private void ShutdownOpenVR()
– {
– if (OpenVR.System != null)
– {
– OpenVR.Shutdown();
– }
– }
private ulong CreateOverlay(string key, string name)
{
var handle = OpenVR.k_ulOverlayHandleInvalid;
var error = OpenVR.Overlay.CreateOverlay(key, name, ref handle);
if (error != EVROverlayError.None)
{
throw new Exception(“Failed to dispose OpenVR: ” + error);
}
return handle;
}
// …
OpenVRUtil.cs
using Valve.VR;
using System;
namespace OpenVRUtil
{
public static class System
{
public static void InitOpenVR()
{
if (OpenVR.System != null) return;
var initError = EVRInitError.None;
OpenVR.Init(ref initError, EVRApplicationType.VRApplication_Overlay);
if (initError != EVRInitError.None)
{
throw new Exception(“Failed to initialize OpenVR: ” + initError);
}
}
+ public static void ShutdownOpenVR()
+ {
+ if (OpenVR.System != null)
+ {
+ OpenVR.Shutdown();
+ }
+ }
}
}
Move overlay methods
Move overlay methods from WatchOverlay.cs to OpenVRUtil.cs because they will be used by other classes later.
Move all overlay methods from CreateOverlay() to SetOverlayRenderTexture() in WatchOverlay.cs.
CreateOverlay()
DestroyOverlya()
SetOverlayFromFile()
ShowOverlay()
SetoverlaySize()
SetOverlayTransformAbsolute()
SetOverlayTransformRelative()
FlipOverlayVertical()
SetOverlayRenderTexture()
WatchOverlay.cs
{
DestroyOverlay(overlayHandle);
OpenVRUtil.System.ShutdownOpenVR();
}
– private ulong CreateOverlay(string key, string name)
– {
– var handle = OpenVR.k_ulOverlayHandleInvalid;
– var error = OpenVR.Overlay.CreateOverlay(key, name, ref handle);
– if (error != EVROverlayError.None)
– {
– throw new Exception(“Failed to create overlay: ” + error);
– }
–
– return handle;
– }
–
– …
–
– private void SetOverlayRenderTexture(RenderTexture renderTexture)
– {
– var nativeTexturePtr = renderTexture.GetNativeTexturePtr();
– var texture = new Texture_t
– {
– eColorSpace = EColorSpace.Auto,
– eType = ETextureType.DirectX,
– handle = nativeTexturePtr
– };
– var error = OpenVR.Overlay.SetOverlayTexture(overlayHandle, ref texture);
– if (error != EVROverlayError.None)
– {
– throw new Exception(“Failed to draw texture: ” + error);
– }
– }
Create new static class Overlay to OpenVRUtil.cs and add all the methods as public static method.
OpenVRUtil.cs
{
public static class System
{
public static void InitOpenVR()
{
if (OpenVR.System != null) return;
var initError = EVRInitError.None;
OpenVR.Init(ref initError, EVRApplicationType.VRApplication_Overlay);
if (initError != EVRInitError.None)
{
throw new Exception(“Failed to initialize OpenVR: ” + initError);
}
}
public static void ShutdownOpenVR()
{
if (OpenVR.System != null)
{
OpenVR.Shutdown();
}
}
}
+ public static class Overlay
+ {
+ public static ulong CreateOverlay(string key, string name)
+ {
+ var handle = OpenVR.k_ulOverlayHandleInvalid;
+ var error = OpenVR.Overlay.CreateOverlay(key, name, ref handle);
+ if (error != EVROverlayError.None)
+ {
+ throw new Exception(“Failed to create overlay: ” + error);
+ }
+
+ return handle;
+ }
+
+ …
+
+ public static void SetOverlayRenderTexture(ulong handle, RenderTexture renderTexture)
+ {
+ var nativeTexturePtr = renderTexture.GetNativeTexturePtr();
+ var texture = new Texture_t
+ {
+ eColorSpace = EColorSpace.Auto,
+ eType = ETextureType.DirectX,
+ handle = nativeTexturePtr
+ };
+ var error = OpenVR.Overlay.SetOverlayTexture(handle, ref texture);
+ if (error != EVROverlayError.None)
+ {
+ throw new Exception(“Failed to draw texture: ” + error);
+ }
+ }
+ }
}
Update method calls in existing code
Change WatchOverlay.cs code to call the overlay methods from the OpenVRUtil instead of WatchOverlay.cs itself.
WatchOverlay.cs
using UnityEngine;
using Valve.VR;
+ using OpenVRUtil;
public class WatchOverlay : MonoBehaviour
{
public Camera camera;
public RenderTexture renderTexture;
private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;
[Range(0, 0.5f)] public float size;
[Range(-0.5f, 0.5f)] public float x;
[Range(-0.5f, 0.5f)] public float y;
[Range(-0.5f, 0.5f)] public float z;
[Range(0, 360)] public int rotationX;
[Range(0, 360)] public int rotationY;
[Range(0, 360)] public int rotationZ;
private void Start()
{
– InitOpenVR();
+ OpenVRUtil.System.InitOpenVR();
– overlayHandle = CreateOverlay(“WatchOverlayKey”, “WatchOverlay”);
+ overlayHandle = Overlay.CreateOverlay(“WatchOverlayKey”, “WatchOverlay”);
– FlipOverlayVertical(overlayHandle);
– SetOverlaySize(overlayHandle, size);
– ShowOverlay(overlayHandle);
+ Overlay.FlipOverlayVertical(overlayHandle);
+ Overlay.SetOverlaySize(overlayHandle, size);
+ Overlay.ShowOverlay(overlayHandle);
}
private void Update()
{
var leftControllerIndex = OpenVR.System.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
if (leftControllerIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
var position = new Vector3(x, y, z);
var rotation = Quaternion.Euler(rotationX, rotationY, rotationZ);
– SetOverlayTransformRelative(overlayHandle, leftControllerIndex, position, rotation);
+ Overlay.SetOverlayTransformRelative(overlayHandle, leftControllerIndex, position, rotation);
}
– SetOverlayRenderTexture(overlayHandle, renderTexture);
+ Overlay.SetOverlayRenderTexture(overlayHandle, renderTexture);
}
private void OnApplicationQuit()
{
– DestroyOverlay(overlayHandle);
+ Overlay.DestroyOverlay(overlayHandle);
}
private void OnDestroy()
{
– ShutdownOpenVR();
+ OpenVRUtil.System.ShutdownOpenVR();
}
}
Add OpenVR initialize and cleanup
We made the utility class to share the common code. Let’s go back to the DashboardOverlay.cs to create a dashboard overlay.
Add OpenVR initialization and cleanup code. We already call the initialize and cleanup functions in the WatchOverlay.cs but there is no problem because if the OpenVR is already initialized or cleaned up, the methods do nothing.
DashboardOverlay.cs
using Valve.VR;
using System;
+ using OpenVRUtil;
public class DashboardOverlay : MonoBehaviour
{
private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;
private void Start()
{
+ OpenVRUtil.System.InitOpenVR();
var error = OpenVR.Overlay.CreateDashboardOverlay(“WatchDashboardKey”, “Watch Setting”, ref dashboardHandle, ref thumbnailHandle);
if (error != EVROverlayError.None)
{
throw new Exception(“Failed to create dashboard overlay: ” + error);
}
}
+ private void OnDestroy()
+ {
+ OpenVRUtil.System.ShutdownOpenVR();
+ }
}
Destroy dashboard overlay
Destroy the dashboard overlay at the end of the application.
{
private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;
private void Start()
{
OpenVRUtil.System.InitOpenVR();
var error = OpenVR.Overlay.CreateDashboardOverlay(“WatchDashboardKey”, “Watch Setting”, ref dashboardHandle, ref thumbnailHandle);
if (error != EVROverlayError.None)
{
throw new Exception(“Failed to create dashboard overlay: ” + error);
}
}
+ private void OnApplicationQuit()
+ {
+ Overlay.DestroyOverlay(dashboardHandle);
+ }
private void OnDestroy()
{
OpenVRUtil.System.ShutdownOpenVR();
}
}
As a note, we can destroy the main overlay only. If we pass the thumbnail overlay handle to DestroyOverlay(), ThumbnailCantBeDestroyed error will occur.
Show thumbnail
Let’s show image to the thumbnail.
We made a function SetOverlayFromFile() and a image in part.2 so we will use it.
{
OpenVRUtil.System.InitOpenVR();
var error = OpenVR.Overlay.CreateDashboardOverlay(“WatchDashboardKey”, “Watch Setting”, ref dashboardHandle, ref thumbnailHandle);
if (error != EVROverlayError.None)
{
throw new Exception(“ダッシュボードオーバーレイの作成に失敗しました: ” + error);
}
+ var filePath = Application.streamingAssetsPath + “/sns-icon.jpg”;
+ Overlay.SetOverlayFromFile(thumbnailHandle, filePath);
}
Run the program, and check if the thumbnail is shown at the bottom of the dashboard.
Destroy the dashboard overlay at the end of the application.
{
private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;
private void Start()
{
OpenVRUtil.System.InitOpenVR();
var error = OpenVR.Overlay.CreateDashboardOverlay(“WatchDashboardKey”, “Watch Setting”, ref dashboardHandle, ref thumbnailHandle);
if (error != EVROverlayError.None)
{
throw new Exception(“Failed to create dashboard overlay: ” + error);
}
}
+ private void OnApplicationQuit()
+ {
+ Overlay.DestroyOverlay(dashboardHandle);
+ }
private void OnDestroy()
{
OpenVRUtil.System.ShutdownOpenVR();
}
}
As a note, we can destroy the main overlay only. If we pass the thumbnail overlay handle to DestroyOverlay(), ThumbnailCantBeDestroyed error will occur.
Show thumbnail
Let’s show image to the thumbnail.
We made a function SetOverlayFromFile() and a image in part.2 so we will use it.
{
OpenVRUtil.System.InitOpenVR();
var error = OpenVR.Overlay.CreateDashboardOverlay(“WatchDashboardKey”, “Watch Setting”, ref dashboardHandle, ref thumbnailHandle);
if (error != EVROverlayError.None)
{
throw new Exception(“ダッシュボードオーバーレイの作成に失敗しました: ” + error);
}
+ var filePath = Application.streamingAssetsPath + “/sns-icon.jpg”;
+ Overlay.SetOverlayFromFile(thumbnailHandle, filePath);
}
Run the program, and check if the thumbnail is shown at the bottom of the dashboard.
We don’t draw any image yet to the main overlay so it shows nothing when the thumbnail is clicked.
The name passed to CreateDashboardOverlay() is shown when the laser pointer hovers over the thumbnail. This time, it’s “Watch Settings”.
Create setting screen
Let’s create the setting screen.
First, divide the game objects into two groups: the watch group and the dashboard group.
Watch Group
Right click in hierarchy > Create Empty to create an empty game object named Watch.
Move existing WatchOverlay, Camera, Canvas objects under the Watch object.
Dashboard group
Create the below objects under the Dashboard object.
Camera
UI > Canvas
Camera setting
Select Camera under the Dashboard, set Clear Flags to Solid Color in the inspector.
Click Background color, then change to opaque gray (RGBA = 64, 64, 64, 255).
Also, remove the AudioListener component.
Create render texture
Create render texture for dashboard overlay.
In the project window, right click Assets/RenderTexture folder > Render Texture to create a new render texture asset.
Change the asset name to DashboardRenderTexture.
Set the Size to 1024 x 768 in the inspector.
Click Camera under Dashboard in the hierarchy, drag DashboardRenderTexture asset to Target Texture property.
Here, the dashboard camera output goes to the render texture asset.
Canvas setting
Select Canvas object under Dashboard.
Set Render Mode to Screen Space — Camera in the Canvas inspector.
Drag Camera object under Dashboard to Render Camera.
Set Plane Distance to 10.
Move group
Open Dashboard the object’s inspector.
Set Position X to 20 to avoid overlapping groups.
Create button