SteamVR Overlay with Unity: Draw Image
Tags: unity3d, steamvr, openvr, vr
Published: 2024-06-16 19:02:18

## Prepare an image file
Prepare an image file that satisfies the following conditions.

- Size is lower than 1920 x 1080 px.
- File format is in PNG, JPG, or TGA (24 or 32 bit color)

From here on, I explain with my SNS icon named **“sns-icon.jpg”**.

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

Put the image file inside **Assets/StreamingAssets** folder.

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

## Draw image
Call [SetOverlayFromFile()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_SetOverlayFromFile_System_UInt64_System_String_) to draw an image file. (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::SetOverlayFromFile) for details)

```diff
void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

+   var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
+   var error = OpenVR.Overlay.SetOverlayFromFile(overlayHandle, filePath);
+   if (error != EVROverlayError.None)
+   {
+       throw new Exception("Failed to draw image file: " + error);
+   }
}
```

Pass the overlay handle and image file path to `SetOverlayFromFile()`.
We use the [StreamingAssets](https://docs.unity3d.com/Manual/StreamingAssets.html) folder to get the file path at runtime with [Application.streamingAssetsPath](https://docs.unity3d.com/ScriptReference/Application-streamingAssetsPath.html).
Error handling remains the same as before.

Run the program, then launch the Overlay Viewer.
Click `WatchOverlayKey` and check the image is drawing on the right preview area.

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

## Switch overlay visibility
The overlay is hidden by default. It is shown on the Overlay Viewer but not in the VR space.
Switch the overlay visibility with [ShowOverlay()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_ShowOverlay_System_UInt64_) and [HideOverlay()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_HideOverlay_System_UInt64_). (read the wiki for details)

Let’s show the overlay with `ShowOverlay()` at the start of the application.

```diff
private void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

    var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
    var error = OpenVR.Overlay.SetOverlayFromFile(overlayHandle, filePath);
    if (error != EVROverlayError.None)
    {
        throw new Exception("Failed to draw image file: " + error);
    }

+   error = OpenVR.Overlay.ShowOverlay(overlayHandle);
+   if (error != EVROverlayError.None)
+   {
+       throw new Exception("Failed to show overlay: " + error);
+   }
}
```

Run the program, put on your HMD, and look down at the floor. The overlay should appear at the center of the floor. Since overlays can’t be seen from the back, if you can’t view the overlay, move to the opposite side.

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s7tqdz19w5mksgd90pam.png)
*It shows during a VR game (the screenshot is [Legendary Tales](https://store.steampowered.com/app/1465070/Legendary_Tales/))*

## Organize code
### Draw image file
Move the drawing code into `SetOverlayFromFile()`.

```diff
void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

    var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
-   var error = OpenVR.Overlay.SetOverlayFromFile(overlayHandle, filePath);
-   if (error != EVROverlayError.None)
-   {
-       throw new Exception("Failed to draw image file: " + error);
-   }
+   SetOverlayFromFile(overlayHandle, filePath);

    error = OpenVR.Overlay.ShowOverlay(overlayHandle);
    if (error != EVROverlayError.None)
    {
        throw new Exception("Failed to show overlay: " + error);
    }
}

...

+ // Variable names changed
+ // overlayHandle -> handle
+ // filePath -> path
+ private 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);
+     }
+ }
```

### Show overlay
Move the switching overlay visibility into `ShowOverlay()`.

```diff
void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

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

-   error = OpenVR.Overlay.ShowOverlay(overlayHandle);
-   if (error != EVROverlayError.None)
-   {
-       throw new Exception("Failed to show overlay: " + error);
-   }
+   ShowOverlay(overlayHandle);
}

...

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

## Final code
```cs
using UnityEngine;
using Valve.VR;
using System;

public class WatchOverlay : MonoBehaviour
{
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    private void Start()
    {
        InitOpenVR();
        overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

        var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
        SetOverlayFromFile(overlayHandle, filePath);
        ShowOverlay(overlayHandle);
    }

    private void OnApplicationQuit()
    {
        DestroyOverlay(overlayHandle);
    }
    
    private void OnDestroy()
    {
        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();
        }
    }

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

    private 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);
        }
    }
    
    private void ShowOverlay(ulong handle)
    {
        var error = OpenVR.Overlay.ShowOverlay(handle);
        if (error != EVROverlayError.None)
        {
            throw new Exception("Failed to show overlay: " + error);
        }
    }
}
```

Here, we have displayed the image on the overlay. Next part, we will change the overlay size and position.