想法就是将UI唤出时,在任何时候都可见且可交互。
本人使用的是Pico VR集成开发环境,其它的应该也能做参考。
在VR环境中用的是世界空间UI,如果使用多摄像机的叠加方法,就需要使用overlay,不仅需要多一道渲染目标的转换,很有可能还需要改集成环境的代码(比如Pico就会自行寻找前三个摄像机作为保留)。因此使用shader来改变渲染队列和深度测试是最可行的方法。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[ExecuteInEditMode] //Disable if you don't care about previewing outside of play mode
public class WorldSpaceOverlayUI : MonoBehaviour
{
private const string shaderTestMode = "unity_GUIZTestMode"; //The magic property we need to set
[SerializeField] UnityEngine.Rendering.CompareFunction desiredUIComparison = UnityEngine.Rendering.CompareFunction.Always; //If you want to try out other effects
[Tooltip("Set to blank to automatically populate from the child UI elements")]
[SerializeField] Graphic[] uiGraphicsToApplyTo;
[Tooltip("Set to blank to automatically populate from the child UI elements")]
[SerializeField] TextMeshProUGUI[] uiTextsToApplyTo;
//Allows us to reuse materials
private Dictionary<Material, Material> materialMappings = new Dictionary<Material, Material>();
protected virtual void Start()
{
if (uiGraphicsToApplyTo.Length == 0)
{
uiGraphicsToApplyTo = gameObject.GetComponentsInChildren<Graphic>();
}
if (uiTextsToApplyTo.Length == 0)
{
uiTextsToApplyTo = gameObject.GetComponentsInChildren<TextMeshProUGUI>();
}
foreach (var graphic in uiGraphicsToApplyTo)
{
Material material = graphic.materialForRendering;
if (material == null)
{
Debug.LogError($"{nameof(WorldSpaceOverlayUI)}: skipping target without material {graphic.name}.{graphic.GetType().Name}");
continue;
}
if (!materialMappings.TryGetValue(material, out Material materialCopy))
{
materialCopy = new Material(material);
materialMappings.Add(material, materialCopy);
}
materialCopy.SetInt(shaderTestMode, (int)desiredUIComparison);
graphic.material = materialCopy;
}
foreach (var text in uiTextsToApplyTo)
{
Material material = text.fontMaterial;
if (material == null)
{
Debug.LogError($"{nameof(WorldSpaceOverlayUI)}: skipping target without material {text.name}.{text.GetType().Name}");
continue;
}
if (!materialMappings.TryGetValue(material, out Material materialCopy))
{
materialCopy = new Material(material);
materialMappings.Add(material, materialCopy);
}
materialCopy.SetInt(shaderTestMode, (int)desiredUIComparison);
text.fontMaterial = materialCopy;
}
}
}
将上面的脚本挂载UI的根物体上,运行一次就可以了,记得将要显示的所有UI元素都设为active之后再运行(即使可能在某些时候需要隐藏部分元素),才能自动将所有子物体找齐。desired UI Comparison默认选择Always(始终)即可。
在Pico VR中,控制器拉出来的指示线在碰撞到第一个物体之后就不会继续画了,这时候如果UI在空间上处于某个物体的背后(当然,使用之前的代码之后在视觉上是可见的),非常难以看清楚到底点到哪儿了。我捣鼓了半天,在XR Ray Interactor中的选项Hit Closest Only(只与最近的进行碰撞),XR Interactor Line Visual中的Stop Line At First Raycast Hit(画线至第一个碰撞点),包括Reticle(标记)等通通整了一遍都没有用。如果哪位有更简单的方法请告知。
最后只能自己来实现。
首先是要找到UI被Raycast碰撞的点的坐标。使用XRRayInteractor的TryGetCurrentUIRaycastResult方法即可拿到当前帧的碰撞结果。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.XR.Interaction.Toolkit;
public class ShowUIPointer : MonoBehaviour
{
public XRRayInteractor interactor;
public GameObject go;
void Start()
{
interactor= GetComponent<XRRayInteractor>();
}
void Update()
{
interactor.TryGetCurrentUIRaycastResult(out RaycastResult result);
go.transform.position = result.worldPosition;
}
}
将上述代码挂载到控制器上,在拿到的坐标处画一个物体即可(如果左右控制器都需要显示,要挂两道)。另外,我们需要将此物体也画在最前面,所以也要使用overlay类型的shader,这里有个最简单的版本:
Shader "Custom/TextureOverlay" {
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)
[MaterialToggle] PixelSnap("Pixel snap", Float) = 0
[HideInInspector] _RendererColor("RendererColor", Color) = (1,1,1,1)
[HideInInspector] _Flip("Flip", Vector) = (1,1,1,1)
[PerRendererData] _AlphaTex("External Alpha", 2D) = "white" {}
[PerRendererData] _EnableExternalAlpha("Enable External Alpha", Float) = 0
}
SubShader
{
ZWrite Off
ZTest Always
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Overlay"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Cull Off
Lighting Off
ZWrite Off
Blend One OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex SpriteVert
#pragma fragment SpriteFrag
#pragma target 2.0
#pragma multi_compile_instancing
#pragma multi_compile _ PIXELSNAP_ON
#pragma multi_compile _ ETC1_EXTERNAL_ALPHA
#include "UnitySprites.cginc"
ENDCG
}
}
}
但是,将其放在3D物体上,仍然会被UI有部分遮挡,因此建议使用UI元素,比如用Image显示的一个红点,因为UI在Canvas上会自行进行排序和遮挡。