SteamVR Overlay with Unity: Draw Camera Output
Tags: unity3d, steamvr, openvr, vr
Published: 2024-06-16 19:06:13

## Prepare camera output

Before displaying the current time to the watch overlay, try displaying the camera output of a simple 3d scene.

### Prepare scene

In the hierarchy window, create the following game objects.

- Camera
- 3D Object > Cube
- Light > Directional Light

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

Arrange the objects as the camera captures the cube. It’s OK to arrange roughly.

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

### Rotate the cube

We want to make animated camera output so create a script to rotate the cube. Create **Rotate.cs** inside the Scripts folder, then copy the following code.

```cs
using UnityEngine;

public class Rotate : MonoBehaviour
{
    void Update()
    {
        transform.Rotate(0, 0.5f, 0);
    }
}
```

Attach the `Rotate.cs` to the Cube object in the scene.

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

Run the program and check the cube is rotating.

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

Here, the test scene setup is done.

### Add camera reference

Add a camera member variable to `WatchOverlay.cs`.

```diff
public class WatchOverlay : MonoBehaviour
{
+   public Camera camera;
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    [Range(0, 0.5f)] public float size;
    [Range(-0.2f, 0.2f)] public float x;
    [Range(-0.2f, 0.2f)] public float y;
    [Range(-0.2f, 0.2f)] public float z;
    [Range(0, 360)] public int rotationX;
    [Range(0, 360)] public int rotationY;
    [Range(0, 360)] public int rotationZ;

    ...
```

Set the scene `camera` to the camera variable of the `WatchOverlay` object in the Unity inspector.

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

### Remove image file code

To use the camera output, remove the image file code.

```diff
private void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");
    
-   var filePath = Application.streamingAssetsPath + "/sns-icon.jpg";
-   SetOverlayFromFile(overlayHandle, filePath);

    SetOverlaySize(overlayHandle, size);
    ShowOverlay(overlayHandle);
}
```

### Create Render Texture

Create a new `RenderTextures` folder inside the `Assets` in the project window. Right click the `RenderTextures` **folder > Create > RenderTexture** to create a new render texture asset, and named `WatchRenderTexture`. We will set the scene camera output to this render texture asset.

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


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

### Write camera output to the RenderTexture asset

On the hierarchy, click `Camera` object and open the inspector. Drag `WatchRenderTexture` from the project window to the camera **Target Texture** property in the inspector.

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

Then the camera output is written into `WatchRenderTexture` asset.

### RenderTexture setting

On the project window, click `WatchRenderTexture` asset and open the inspector. Change **Size** to **512 x 512**.

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

---

### Optional: When you want to create RenderTexture by script

If you want to create RenderTexture in the code without creating a RenderTexture asset, you can write as below.

```diff
public class WatchOverlay : MonoBehaviour
{
    public Camera camera;
+   private RenderTexture renderTexture;
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    ...

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

+       // Set camera.targetTexture to write the camera output to the render texture.
+       renderTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.ARGBFloat);
+       camera.targetTexture = renderTexture;
        
        SetOverlaySize(overlayHandle, size);
        ShowOverlay(overlayHandle);
    }

    ...
```

---

### Add Render Texture variable

Add a member variable to store RenderTexture.

```diff
public class WatchOverlay : MonoBehaviour
{
    public Camera camera;
+   public RenderTexture renderTexture;
    private ulong overlayHandle = OpenVR.k_ulOverlayHandleInvalid;

    ...
```

Open `WatchOverlay` game object inspector, and set `WatchRenderTexture` asset to `renderTexture` variable.

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

### Wait for texture creation

Wait for the render texture to be ready to use.

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

+   if (!renderTexture.IsCreated())
+   {
+       return;
+   }
+
+   // add render code here
}
```

### Get a native texture pointer

The texture data type to pass to the OpenVR is a graphics API, such as DirectX or OpenGL, which works under the Unity API layer.
We can get a native texture pointer to access the low level API texture data with Unity’s [GetNativeTexturePtr()](https://docs.unity3d.com/ScriptReference/Texture.GetNativeTexturePtr.html).

Call `GetNativeTexturePtr()` in `Update()` to get the texture data reference which will be passed to the OpenVR API.

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

    if (!renderTexture.IsCreated())
    {
        return;
    }
+   var nativeTexturePtr = renderTexture.GetNativeTexturePtr();
}
```

---

### Optional: Sync rendering thread

The [GetNativeTexturePtr() documentation](https://docs.unity3d.com/ScriptReference/Texture.GetNativeTexturePtr.html) does not recommend calling it every frame for performance. However, we call it in `Update()` in this tutorial, because rendering texture to the overlay without syncing to the rendering thread sometimes crashes the  program.

---

### Create an OpenVR texture

Create a texture variable of OpenVR texture data type `Texture_t`.

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

    if (!renderTexture.IsCreated())
    {
        return;
    }
    var nativeTexturePtr = renderTexture.GetNativeTexturePtr();
+   var texture = new Texture_t
+   {
+       eColorSpace = EColorSpace.Auto,
+       eType = ETextureType.DirectX,
+       handle = nativeTexturePtr
+   };
}
```

Set graphics API type to `eType`. Since DirectX is the default graphics API in [this tutorial environment](https://medium.com/@kurohuku/bb99a334d897), we are going to assume that the graphics API is DirectX from now on.

Set the native texture pointer to `handle`.
The camera output is written into this texture.

---

### Optional: If you want to support APIs other than DirectX

You can detect current graphics API with `SystemInfo.graphicsDeviceType`.

```cs
switch (SystemInfo.graphicsDeviceType)
{
    case GraphicsDeviceType.Direct3D11:
        texture.eType = ETextureType.DirectX;
        break;
    case GraphicsDeviceType.Direct3D12:
        texture.eType = ETextureType.DirectX12;
        break;
    case GraphicsDeviceType.OpenGLES2:
    case GraphicsDeviceType.OpenGLES3:
    case GraphicsDeviceType.OpenGLCore:
        texture.eType = ETextureType.OpenGL;
        break;
    case GraphicsDeviceType.Vulkan:
        texture.eType = ETextureType.Vulkan;
        break;
}
```

You can check the program behavior with APIs other than Direct3D11 by unchecking **Project Settings > Player > Other Settings > Auto Graphics API for Windows**, and adding the desired graphics API to **Graphics APIs for Windows**.

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

https://docs.unity3d.com/Manual/GraphicsAPIs.html

---

### Draw texture to the overlay

Draw texture to the overlay with [SetOverlayTexture()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_SetOverlayTexture_System_UInt64_Valve_VR_Texture_t__). (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::SetOverlayTexture) for details)
Pass the `Texture_t` texture we created before.

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


    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(overlayHandle, ref texture);
+   if (error != EVROverlayError.None)
+   {
+       throw new Exception("Failed to draw texture: " + error);
+   }
}
```

Run the program. Make sure the camera output is shown on the overlay.

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

### Flip vertical

The camera output is vertically flipped, so we will have to flip it back.

This happens because the V-axis direction of the texture UV system is flipped between Unity and DirectX. Unity’s V-axis faces upwards while DirectX’s faces downwards.

https://docs.unity3d.com/Manual/SL-PlatformDifferences.html

There are different ways to fix this issue. However, we use the OpenVR [SetOverlayTextureBounds()](https://valvesoftware.github.io/steamvr_unity_plugin/api/Valve.VR.CVROverlay.html#Valve_VR_CVROverlay_SetOverlayTextureBounds_System_UInt64_Valve_VR_VRTextureBounds_t__) to vertically flip the V-axis. (read the [wiki](https://github.com/ValveSoftware/openvr/wiki/IVROverlay::SetOverlayTextureBounds) for details)

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

+   var bounds = new VRTextureBounds_t
+   {
+       uMin = 0,
+       uMax = 1,
+       vMin = 1,
+       vMax = 0
+   };
+   var error = OpenVR.Overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
+   if (error != EVROverlayError.None)
+   {
+       throw new Exception("Failed to flip texture: " + error);
+   }
    
    SetOverlaySize(overlayHandle, size);
    ShowOverlay(overlayHandle);
}
```

This flips texture vertically. Run the program and check the texture is flipped.

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

---

### Optional: Support for APIs other than DirectX

In this tutorial, we assume that the graphics API is DirectX so always flip the texture vertically.
If you want to support other APIs, check `graphicsDeviceType` and deal as “If the `graphicsDeviceType` is the OpenGL, don’t flip the texture” similar to the above section **“Optional: If you want to support APIs other than DirectX”**.

---

## Create a canvas to display the current time

We have displayed the camera output to the overlay. Let’s create the watch next.
Remove the Cube and Directional Light, we never use them later.

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

Open the Camera inspector, and click Reset on the Transform component to **reset the position to (0, 0, 0)**.

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

Create the below objects into the scene.

- **UI > Canvas**
- **UI > Text — TextMeshPro** inside the Canvas object

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

When we create TextMeshPro for the first time, it shows the dialog below, click **Import TMP Essentials** button. Close the dialog after importing.

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

Open the Canvas inspector, and change **Render mode** to **Screen Space -Camera**. Then drag the camera object from the hierarchy to the **Render Camera** property.

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

Open the Text (TMP) object inspector, and center the text vertically and horizontally with **Alignment**.
Input “00:00:00” to the text.

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

Select the Camera object, and set **Clear Flags** to **Solid Color**.
Click the **Background** color, and make sure **A (Alpha) is 0**.
This makes the camera background transparent and displays the current time only.

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

Select the Canvas object, and set the **Plane Distance to 10**. This makes editing easy on the Editor.

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

Select Text (TMP), click the anchor setting (the rectangle on the left top of the Rect Transform component), and select the right bottom blue crossed arrow (stretch to both directions).

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

Set Left, Top, Right, and Bottom to 0.

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

Scroll the inspector, and set the **TextMeshPro — Text (UI)** component **Font Size to 70**.

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

Run the program. Check if the current time is displayed on the left wrist.
Adjust the position and font size if it is off.

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

## Activate the watch

Create a new script `Watch.cs` inside the `Scripts` folder. Copy the following code.

```cs
using UnityEngine;
using System;
using TMPro;

public class Watch : MonoBehaviour
{
    private TextMeshProUGUI label;

    void Start()
    {
        label = GetComponent<TextMeshProUGUI>();
    }
    
    void Update()
    {
        var hour = DateTime.Now.Hour;
        var minute = DateTime.Now.Minute;
        var second = DateTime.Now.Second;
        label.text = $"{hour:00}:{minute:00}:{second:00}";
    }
}
```

Add `Watch.cs` to the **Text (TMP)** object into the scene.

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

Run the program. It should display the current time.

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

## Organize code

Organize `WatchOverlay.cs`.

### Flip overlay vertically

Move flipping code into `FlipOverlayVertical() `.

```diff
private void Start()
{
    InitOpenVR();
    overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");
    
-   var bounds = new VRTextureBounds_t
-   {
-       uMin = 0,
-       uMax = 1,
-       vMin = 1,
-       vMax = 0
-   };
-   var error = OpenVR.Overlay.SetOverlayTextureBounds(overlayHandle, ref bounds);
-   if (error != EVROverlayError.None)
-   {
-       throw new Exception("Failed to flip texture: " + error);
-   }
+   FlipOverlayVertical(overlayHandle);    
    SetOverlaySize(overlayHandle, size);
    ShowOverlay(overlayHandle);
}

...

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

### Draw RenderTexture

Move drawing code into `SetOverlayRenderTexture()`.

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

-   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);
-   }
+   SetOverlayRenderTexture(overlayHandle, renderTexture);
}

...

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

## Final code

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

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.2f, 0.2f)] public float x;
    [Range(-0.2f, 0.2f)] public float y;
    [Range(-0.2f, 0.2f)] 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();
        overlayHandle = CreateOverlay("WatchOverlayKey", "WatchOverlay");

        FlipOverlayVertical(overlayHandle);
        SetOverlaySize(overlayHandle, size);
        ShowOverlay(overlayHandle);
    }

    private void Update()
    {
        var position = new Vector3(x, y, z);
        var rotation = Quaternion.Euler(rotationX, rotationY, rotationZ);
        var leftControllerIndex = OpenVR.System.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
        if (leftControllerIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
        {
            SetOverlayTransformRelative(overlayHandle, leftControllerIndex, position, rotation);
        }

        SetOverlayRenderTexture(overlayHandle, renderTexture);
    }
    
    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);
        }
    }

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

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

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

Finally, we have displayed the current time on the left wrist. In the next part, we will make the dashboard setting screen to switch which hand to display the overlay.