Part 8 — Dashboard Overlay

Part 8 — Dashboard Overlay

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 UnityEngine;
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 UnityEngine;
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 UnityEngine;
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 UnityEngine;
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 UnityEngine;
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

private void OnDestroy()
{
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

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();
}
}
}

+ 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 System;
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 UnityEngine;
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.

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 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.

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(“ダッシュボードオーバーレイの作成に失敗しました: ” + 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.

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 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.

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(“ダッシュボードオーバーレイの作成に失敗しました: ” + 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