SteamVR Overlay with Unity: Dashboard Overlay
Tags: unity3d, steamvr, openvr, vr
Published: 2024-06-16 19:06:40

Let’s create a setting screen on the SteamVR dashboard. It switches which hand to display the watch overlay.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/namc1joyp1kyuqg0ulv4.gif)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnuterx5qpltp3h0if05.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/977ayctboi1x1gnt32lv.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17vxwrmbmws71c2uh3d4.png)
*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`.

```diff
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()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_CreateDashboardOverlay_System_String_System_String_System_UInt64__System_UInt64__). (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::CreateDashboardOverlay) 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.

```cs
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`
```diff

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`
```diff
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`
```diff
// ...

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

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d3np8qh5nugd6q6c5ujo.png)




Destroy the dashboard overlay at the end of the application.

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

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x9h1plm0hnbcfo8df25e.png)

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.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gdbsdtx4yalu4u4br0z6.png)

### Dashboard group

Create the below objects under the `Dashboard` object.

- Camera
- UI > Canvas

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tbz2qki1cbdyoxnypzmc.png)

### 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)**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ubfchjb56u2vbqc45ws.png)

Also, `remove` the **AudioListener** component.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ar4wj8e7bocy0pnqhz2e.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ul9eq9vpvgaqahp4p4j0.png)

Set the **Size** to **1024 x 768** in the inspector.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnk14m3z91uxe7bix5ou.png)

Click `Camera` under `Dashboard` in the hierarchy, drag `DashboardRenderTexture` asset to `Target Texture` property.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0qrhx77hnwhojkci8b93.png)

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h1ty920x7i6ulc9jof48.png)

### Move group

Open `Dashboard` the object’s inspector.
**Set Position X to 20** to avoid overlapping groups.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/byahp0tu3sstuex4vdwn.png)

### Create button

Right click `Canvas` object under `Dashboard` then create **UI > Button — TextMeshPro** to create a new button named `LeftHandButton`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9b5eiiq0a0jclw0xtcxx.png)

Click `Text (TMP)` under `LeftHandButton`, change the text to **“Left Hand”**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ea71sip8bsumx3cl39is.png)

Select `LeftHandButton` and set the **Width to 700 and Height to 200**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7dauydwrw65w5v31k7ze.png)

Select the `Text (TMP)` and set the **Font Size to 100**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/albd1tk5knyq9jkv4j4b.png)

Duplicate `LeftHandButton` by **right click > Duplicate**.
Change the duplicated object name to `RightHandButton`.
Similarly, change the button text to **“Right Hand”**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7tszxur5miv0m9m4pkoc.png)

Set **LeftHandButton Pos Y to 150** and **RightHandButton Pos Y to -150**.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pn1f4r8tq4ru5puda32e.png)

Here, the appearance of the setting screen is done.

## Draw to dashboard

Display the setting screen as a dashboard overlay.

### Add camera variable

Add `camera` member variable to `DashboardOverlay.cs`.

```diff
public class DashboardOverlay : MonoBehaviour
{
+   public Camera camera;
    private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
    private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;

    // ...
```

Open `DashboardOverlay` object inspector, drag `Camera` object under `Dashboard` to the `Camera` variable.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3q7cica97prze31e9z94.png)

### Add render texture

Add `renderTexture` variable in `DashboardOverlay.cs`.

```diff
public class DashboardOverlay : MonoBehaviour
{
    public Camera camera;
+   public RenderTexture renderTexture; 
    private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
    private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;

    // ...
```

Select `DashboardOverlay` in the hierarchy, drag `Assets/RenderTextures/DashboardRenderTexture` asset to the `Render Texture` variable.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1r69omlr1qrsfkyo0gn.png)

### Flip vertical and set size

In advance, flip the texture vertically with `FlipOverlayVertical()` and set overlay size to **2.5 m** with `SetOverlaySize()`.

### Draw render texture to dashboard overlay

Create `Update()` and draw the render texture to the dashboard overlay.

`DashboardOverlay.cs`

```diff
public class DashboardOverlay : MonoBehaviour
{
    public Camera camera;
    public RenderTexture renderTexture; 
    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);
        }

        var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
        Overlay.SetOverlayFromFile(thumbnailHandle, filePath);
        
        renderTexture = new RenderTexture(1024, 768, 16, RenderTextureFormat.ARGBFloat);
        camera.targetTexture = renderTexture;

        Overlay.SetOverlaySize(dashboardHandle, 2.5f);
        Overlay.FlipOverlayVertical(dashboardHandle);
    }

+   private void Update()
+   {
+       Overlay.SetOverlayRenderTexture(dashboardHandle, renderTexture);
+   }

    // ...
```

Run the program, open the dashboard, and click the thumbnail. The setting screen should be shown.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qq6z0zzonb54lf8oxfxy.png)

## Organize code

Move the creating dashboard overlay code to `CreateDashboardOverlay()`.
It has to return two values `dashboardhandle` and `thumbnailHandle`, so we use a tuple to combine them.

Add the method to the utility’s `Overlay` class.

`OpenVRUtil.cs`
```diff
...

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 (ulong, ulong) CreateDashboardOverlay(string key, string name)
+ {
+     ulong dashboardHandle = 0;
+     ulong thumbnailHandle = 0;
+     var error = OpenVR.Overlay.CreateDashboardOverlay(key, name, ref dashboardHandle, ref thumbnailHandle);
+     if (error != EVROverlayError.None)
+     {
+         throw new Exception("Failed to create dashboard overlay: " + error);
+     }
+ 
+     return (dashboardHandle, thumbnailHandle);
+ }

...
```

Accordingly, change the function call.

`DashboardOverlay.cs`
```diff
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);
-   }
+   (dashboardHandle, thumbnailHandle) = Overlay.CreateDashboardOverlay("WatchDashboardKey", "Watch Setting");

    var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
    Overlay.SetOverlayFromFile(thumbnailHandle, filePath);

    Overlay.SetOverlaySize(dashboardHandle, 2.5f);
    Overlay.FlipOverlayVertical(dashboardHandle);
}
```

## Final code

`DashboardOverlay.cs`
```cs
using UnityEngine;
using Valve.VR;
using OpenVRUtil;

public class DashboardOverlay : MonoBehaviour
{
    public Camera camera;
    public RenderTexture renderTexture;
    private ulong dashboardHandle = OpenVR.k_ulOverlayHandleInvalid;
    private ulong thumbnailHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        OpenVRUtil.System.InitOpenVR();

        (dashboardHandle, thumbnailHandle) = Overlay.CreateDashboardOverlay("WatchDashboardKey", "Watch Setting");

        var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
        Overlay.SetOverlayFromFile(thumbnailHandle, filePath);

        Overlay.FlipOverlayVertical(dashboardHandle);
        Overlay.SetOverlaySize(dashboardHandle, 2.5f);
    }

    private void Update()
    {
        Overlay.SetOverlayRenderTexture(dashboardHandle, renderTexture);
    }

    private void OnApplicationQuit()
    {
        Overlay.DestroyOverlay(dashboardHandle);
    }

    private void OnDestroy()
    {
        OpenVRUtil.System.ShutdownOpenVR();
    }
}
```

`OpenVRUtil.cs`
```cs
using UnityEngine;
using Valve.VR;
using System;

namespace OpenVRUtil
{
    public static class System
    {
        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);
            }
        }

        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 (ulong, ulong) CreateDashboardOverlay(string key, string name)
        {
            ulong dashboardHandle = 0;
            ulong thumbnailHandle = 0;
            var error = OpenVR.Overlay.CreateDashboardOverlay(key, name, ref dashboardHandle, ref thumbnailHandle);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to create dashboard overlay: " + error);
            }
 
            return (dashboardHandle, thumbnailHandle);
        }
        
        public static void DestroyOverlay(ulong handle)
        {
            if (handle != OpenVR.k_ulOverlayHandleInvalid)
            {
                var error = OpenVR.Overlay.DestroyOverlay(handle);
                if (error != EVROverlayError.None)
                {
                    throw new Exception("Failed to dispose overlay: " + error);
                }
            }
        }

        public static void SetOverlayFromFile(ulong handle, string path)
        {
            var error = OpenVR.Overlay.SetOverlayFromFile(handle, path);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to draw image file: " + error);
            }
        }

        public static void ShowOverlay(ulong handle)
        {
            var error = OpenVR.Overlay.ShowOverlay(handle);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed show overlay: " + error);
            }
        }

        public static void SetOverlaySize(ulong handle, float size)
        {
            var error = OpenVR.Overlay.SetOverlayWidthInMeters(handle, size);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to set overlay size: " + error);
            }
        }

        public static void SetOverlayTransformAbsolute(ulong handle, Vector3 position, Quaternion rotation)
        {
            var rigidTransform = new SteamVR_Utils.RigidTransform(position, rotation);
            var matrix = rigidTransform.ToHmdMatrix34();
            var error = OpenVR.Overlay.SetOverlayTransformAbsolute(handle, ETrackingUniverseOrigin.TrackingUniverseStanding, ref matrix);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to set overlay position: " + error);
            }
        }

        public static void SetOverlayTransformRelative(ulong handle, uint deviceIndex, Vector3 position, Quaternion rotation)
        {
            var rigidTransform = new SteamVR_Utils.RigidTransform(position, rotation);
            var matrix = rigidTransform.ToHmdMatrix34();
            var error = OpenVR.Overlay.SetOverlayTransformTrackedDeviceRelative(handle, deviceIndex, ref matrix);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to set overlay position: " + error);
            }
        }

        public static void FlipOverlayVertical(ulong handle)
        {
            var bounds = new VRTextureBounds_t
            {
                uMin = 0,
                uMax = 1,
                vMin = 1,
                vMax = 0
            };

            var error = OpenVR.Overlay.SetOverlayTextureBounds(handle, ref bounds);
            if (error != EVROverlayError.None)
            {
                throw new Exception("Failed to flip texture: " + error);
            }
        }

        public static void SetOverlayRenderTexture(ulong handle, RenderTexture renderTexture)
        {
            if (!renderTexture.IsCreated()) return;

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

Now, we have created the setting screen as a dashboard overlay. We will make the button events in the next part.