Unity的VR环境中如何在最前面显示世界空间UI,且能在UI界面位置靠后时显示控制器的选择标记

文章讲述了如何在PicoVR的VR环境中使用世界空间UI,并提到使用overlay和shader调整渲染队列以实现实时可见且可交互。作者还提供了自定义Shader和XRInteractionToolkit的方法来解决UI遮挡问题,以及如何通过Raycast获取UI点击位置以增强用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

想法就是将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上会自行进行排序和遮挡。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值