This approach ensures UI elements remain visible and interactive when activated in VR environments. While developed for Pico VR, the techniques apply to other VR platforms.
World Space UI in VR typically requires overlay rendering solutions. Using multiple cameras with overlay settings involves render target conversions and may require platform-specific modifications. A more efficient solution moidfies shader rendering queue and depth testing parameters.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[ExecuteInEditMode]
public class VRUIRenderOverride : MonoBehaviour
{
private const string DepthTestProperty = "unity_GUIZTestMode";
[SerializeField] UnityEngine.Rendering.CompareFunction depthComparison = UnityEngine.Rendering.CompareFunction.Always;
[SerializeField] Graphic[] targetGraphics;
[SerializeField] TextMeshProUGUI[] targetTextElements;
private Dictionary<material material=""> materialCache = new Dictionary<material material="">();
void Initialize()
{
if (targetGraphics.Length == 0)
targetGraphics = GetComponentsInChildren<graphic>();
if (targetTextElements.Length == 0)
targetTextElements = GetComponentsInChildren<textmeshprougui>();
ProcessGraphicElements();
ProcessTextElements();
}
void ProcessGraphicElements()
{
foreach (Graphic element in targetGraphics)
{
Material sourceMaterial = element.materialForRendering;
if (sourceMaterial == null) continue;
Material overrideMaterial = GetOverrideMaterial(sourceMaterial);
overrideMaterial.SetInt(DepthTestProperty, (int)depthComparison);
element.material = overrideMaterial;
}
}
void ProcessTextElements()
{
foreach (TextMeshProUGUI textElement in targetTextElements)
{
Material sourceMaterial = textElement.fontMaterial;
if (sourceMaterial == null) continue;
Material overrideMaterial = GetOverrideMaterial(sourceMaterial);
overrideMaterial.SetInt(DepthTestProperty, (int)depthComparison);
textElement.fontMaterial = overrideMaterial;
}
}
Material GetOverrideMaterial(Material original)
{
if (!materialCache.ContainsKey(original))
{
Material newMaterial = new Material(original);
materialCache.Add(original, newMaterial);
return newMaterial;
}
return materialCache[original];
}
}
</textmeshprougui></graphic></material></material>
Attach this script to the root UI object and execute once with all UI elements active. The depthComparison parameter should remain set to Always for optimal visibility.
In Pico VR environments, controller ray indicators stop at the first collision point. When UI appears behind other objects (though visually visible), identifying precise inetraction points becomes challenging. Standard XR Ray Interactor settings like Hit Closest Only and Stop Line At First Raycast Hit don't resolve this issue.
A custom solution tracks UI raycast collision points:
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.XR.Interaction.Toolkit;
public class UIRaycastVisualizer : MonoBehaviour
{
public XRRayInteractor rayController;
public Transform markerObject;
void Start()
{
rayController = GetComponent<xrrayinteractor>();
}
void Update()
{
RaycastResult hitResult;
if (rayController.TryGetCurrentUIRaycastResult(out hitResult))
{
markerObject.position = hitResult.worldPosition;
}
}
}
</xrrayinteractor>
Attach this script to controllers and position a visual marker at the detected coordinates. For bilateral controller support, attach to both controllers.
The marker requires overlay shader properties to maintain front-most visibility:
Shader "VR/OverlayTexture" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
}
SubShader {
Tags {
"Queue" = "Overlay"
"RenderType" = "Transparent"
}
ZWrite Off
ZTest Always
Blend SrcAlpha OneMinusSrcAlpha
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _Color;
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 col = tex2D(_MainTex, i.uv) * _Color;
return col;
}
ENDCG
}
}
}
While 3D objects with this shader may experience partial UI occlusion, using UI Image elements (like colored dots) within Canvas systems provides automatic sorting and proper visibility management.