c# - 使用光线转换代替凝视指针

标签 c# unity-game-engine virtual-reality oculusgo

我是 VR 开发新手,我正在使用 oculus 集成,因此我使用线渲染器制作了一条光线线,但现在我需要使这条线与凝视指针(对象、UI、滚动)等所有内容进行交互。 .),,我正在 Oculus Go 上测试我的项目

screenshot of my project  here

我尝试了很多教程,但没有人关注这一点。

这是我对代码的尝试:

RaycastHit hit;
if(Physics.Raycast(transform.position,transform.forward ,out hit,0.8f))
{
    if(hit.collider != null)
    {
        if (OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger)) 
        { 
            go = hit.transform.gameObject;
            go.SetActive(false);
        }
    }
}

这适用于对象,但不适用于像 GazePointer 那样的 UI。

最佳答案

我也对这样的事情感兴趣,所以我更深入地了解了提到的 SteamVR Laser Pointer Menus .

总体思路非常简单:

  • 在所有可交互的 UI 上使用 BoxCollider
  • 然后您只需坚持使用Physics.Raycast
  • 如果指向 UI 元素,还会触发相应的 EventSystems.ExecuteEvents

实际上,这看起来有点复杂。我基于 steamvr_laserpointer.cs 创建了自己的脚本,但必须对其进行大量更改才能涵盖大部分交互。

它还没有完全工作 - 虽然它基本上已经非常适合 3D 对象和简单的按钮交互,但我在拖动正常工作方面遇到了一些麻烦。但我希望这对于每个尝试类似事情的人来说都是一个很好的起点:

LaserPointerController.cs

(最初基于steamvr_laserpointer.cs)

它附加到您的 Controller 游戏对象。它将创建一个子对象“LaserHolder”,它也有一个子对象“LaserPointer”。 “LaserPointer”基本上只是一个按比例缩放的立方体,因此它看起来像激光束。-

#define USE_CUSTOM_EDITOR

using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
#if UNITY_EDITOR && USE_CUSTOM_EDITOR
using UnityEditor;
#endif

public class LaserPointerController : MonoBehaviour
{
    #region Inspector

    [Header("Pointer Settings")]
    [Tooltip("Should the LaserPointer origin have an offset to the controller position (in local coordinates)?")]
    [SerializeField] private bool useOffset;
    [SerializeField] private Vector3 offset = Vector3.zero;

    [Tooltip("The maximal distance of the laser pointer (in Unity Units)")]
    [SerializeField] private float maxPointerDistance = 5f;

    [Tooltip("Should the laser have a Collider attached?\nUseful for e.g. directly using OnTriggerEnter\n\nNote: This will use Physics.RaycastAll which is less efficient.")]
    [SerializeField] private bool useCollider;
    [Tooltip("Should the laser Collider be a trigger?\nHas to be disabled to e.g. use OnCollisionEnter")]
    [SerializeField] private bool colliderIsTrigger;
    [Tooltip("Should the laser additionally have a RigidBody?\nRequired for some types of collisions.")]
    [SerializeField] private bool useRigidBody;

    [Header("Style Settings")]
    [Tooltip("Should the laser change its Color when pressed")]
    [SerializeField] private bool changeColor;
    [Tooltip("Should the laser change its thickness when pressed")]
    [SerializeField] private bool changeThickness;
    [Tooltip("Color of the Laser in idle")]
    [SerializeField] private Color idleColor = Color.red;
    [Tooltip("Color of the beam when pressed")]
    [SerializeField] private Color pressedColor = Color.green;
    [Tooltip("Thickness of the laser beam")]
    [SerializeField] private float idleThickness = 0.005f;
    [Tooltip("Thickness of the laser beam when pressed")]
    [SerializeField] private float pressedThickness = 0.007f;

    [Header("Debug")]
    [SerializeField] private bool isPressed;
    [SerializeField] private Vector3 pointerPosition3D;
    [SerializeField] private Vector2 pointerPosition2D;

#if UNITY_EDITOR && USE_CUSTOM_EDITOR
    [CustomEditor(typeof(LaserPointerController))]
    private class LaserPointerControllerEditor : Editor
    {
        private SerializedProperty useOffset;
        private SerializedProperty offset;

        private SerializedProperty maxPointerDistance;

        private SerializedProperty useCollider;
        private SerializedProperty colliderIsTrigger;
        private SerializedProperty useRigidBody;

        private SerializedProperty changeColor;
        private SerializedProperty changeThickness;
        private SerializedProperty idleColor;
        private SerializedProperty pressedColor;
        private SerializedProperty idleThickness;
        private SerializedProperty pressedThickness;

        private SerializedProperty _isPressed;
        private SerializedProperty pointerPosition3D;
        private SerializedProperty pointerPosition2D;

        private void OnEnable()
        {
            useOffset = serializedObject.FindProperty("useOffset");
            offset = serializedObject.FindProperty("offset");

            maxPointerDistance = serializedObject.FindProperty("maxPointerDistance");
            useCollider = serializedObject.FindProperty("useCollider");
            colliderIsTrigger = serializedObject.FindProperty("colliderIsTrigger");
            useRigidBody = serializedObject.FindProperty("useRigidBody");

            changeColor = serializedObject.FindProperty("changeColor");
            changeThickness = serializedObject.FindProperty("changeThickness");
            idleColor = serializedObject.FindProperty("idleColor");
            pressedColor = serializedObject.FindProperty("pressedColor");
            idleThickness = serializedObject.FindProperty("idleThickness");
            pressedThickness = serializedObject.FindProperty("pressedThickness");

            _isPressed = serializedObject.FindProperty("isPressed");
            pointerPosition3D = serializedObject.FindProperty("pointerPosition3D");
            pointerPosition2D = serializedObject.FindProperty("pointerPosition2D");
        }

        public override void OnInspectorGUI()
        {
            serializedObject.Update();

            DrawScriptField();

            EditorGUILayout.PropertyField(useOffset);
            if (useOffset.boolValue)
            {
                EditorGUILayout.PropertyField(offset);
                EditorGUILayout.Space();
            }

            EditorGUILayout.PropertyField(maxPointerDistance);

            EditorGUILayout.PropertyField(useCollider);
            if (useCollider.boolValue)
            {
                EditorGUILayout.PropertyField(colliderIsTrigger);
                EditorGUILayout.PropertyField(useRigidBody);
            }
            else
            {
                colliderIsTrigger.boolValue = true;
                useRigidBody.boolValue = false;
            }

            EditorGUILayout.PropertyField(changeColor);
            EditorGUILayout.PropertyField(changeThickness);

            EditorGUILayout.PropertyField(idleColor);
            if (changeColor.boolValue) EditorGUILayout.PropertyField(pressedColor);

            EditorGUILayout.PropertyField(idleThickness);
            if (changeThickness.boolValue) EditorGUILayout.PropertyField(pressedThickness);

            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                EditorGUILayout.PropertyField(_isPressed);

                EditorGUI.BeginDisabledGroup(true);
                {
                    EditorGUILayout.PropertyField(pointerPosition3D);
                    EditorGUILayout.PropertyField(pointerPosition2D);
                }
                EditorGUI.EndDisabledGroup();
            }

            serializedObject.ApplyModifiedProperties();
        }

        private void DrawScriptField()
        {
            EditorGUI.BeginDisabledGroup(true);
            {
                EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((LaserPointerController)target), typeof(LaserPointerController), false);
            }
            EditorGUI.EndDisabledGroup();

            EditorGUILayout.Space();
        }
    }
#endif

    #endregion Inspector


    #region Public Members

    public Vector3 PointerPosition3D
    {
        get { return pointerPosition3D; }
    }

    public Vector2 PointerPosition2D
    {
        get { return pointerPosition2D; }
    }

    public bool IsPressed
    {
        get { return isPressed; }
    }

    public Transform CurrentlyHoveredTransform
    {
        get { return _currentlyHovered; }
    }

    #endregion Public Members


    #region Private Members

    private Transform _holder;
    private Transform _laser;
    private Transform _currentlyHovered;
    private Material _laserMaterial;

    private PointerEventData _pointerEventData;
    private Vector2 _lastPointerPosition;
    private bool _isDragging;
    private bool _lastPressed;
    private Camera _camera;

    #endregion Private Members


    #region MonoBehaviour Messages
    private void Awake()
    {
        _holder = new GameObject("LaserHolder").transform;
        _holder.parent = transform;
        _holder.localScale = Vector3.one;
        _holder.localPosition = useOffset ? offset : Vector3.zero;
        _holder.localRotation = Quaternion.identity;

        _laser = GameObject.CreatePrimitive(PrimitiveType.Cube).transform;
        var collider = _laser.GetComponent<Collider>();

        if (!useCollider)
        {
            Destroy(_laser.GetComponent<Collider>());
        }
        else
        {
            collider.isTrigger = colliderIsTrigger;
        }

        if (useRigidBody)
        {
            var rigidBody = _laser.gameObject.AddComponent<Rigidbody>();
            rigidBody.useGravity = false;
            rigidBody.isKinematic = true;
        }

        _laser.name = "LaserPointer";
        _laser.transform.parent = _holder;
        _laser.transform.localScale = new Vector3(idleThickness, idleThickness, maxPointerDistance);
        _laser.transform.localPosition = new Vector3(0f, 0f, maxPointerDistance / 2f);
        _laser.transform.localRotation = Quaternion.identity;

        _laserMaterial = new Material(Shader.Find("Unlit/Color")) { color = idleColor };
        _laser.GetComponent<MeshRenderer>().material = _laserMaterial;

        _camera = Camera.main;

        _pointerEventData = new PointerEventData(EventSystem.current)
        {
            // most Unity comonents e.g. the ScrollRect expect this to be Left
            // otherwise they ignore pointer input
            button = PointerEventData.InputButton.Left
        };
    }

    private void OnEnable()
    {
        _holder.gameObject.SetActive(true);
    }

    private void OnDisable()
    {
        _holder.gameObject.SetActive(false);
    }

    // Update is called once per frame
    private void Update()
    {
        //TODO uncomment this in order to get the isPressed from the according controller
        // curently I only set it via the Inspector
        //isPressed = OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger);

        // Initially the dist is the maximum pointer distance
        var dist = maxPointerDistance;

        // create the ray from the laserpointer origin
        var ray = new Ray(_holder.position, _holder.forward);

        var hit = new RaycastHit();

        // If using a Collider we have to ignore the Collider of the LaserPointer itself
        if (useCollider)
        {
            var hits = Physics.RaycastAll(ray, maxPointerDistance);
            hit = hits.FirstOrDefault(h => h.transform != _laser.transform);
        }
        else
        {
            Physics.Raycast(ray, out hit, maxPointerDistance);
        }

        // Are we hitting something?
        if (hit.transform)
        {
            // ignore if still pressing the trigger
            if (!isPressed)
            {
                // Are we hitting something different to what we hit before?
                if (_currentlyHovered && _currentlyHovered != hit.transform)
                {
                    ExecuteEvents.ExecuteHierarchy(_currentlyHovered.gameObject, _pointerEventData, ExecuteEvents.pointerExitHandler);

                    _currentlyHovered = null;
                }

                // Are we hitting something new?
                if (_currentlyHovered != hit.transform)
                {
                    ExecuteEvents.ExecuteHierarchy(hit.transform.gameObject, _pointerEventData, ExecuteEvents.pointerEnterHandler);

                    _currentlyHovered = hit.transform;
                }
            }

            if (_currentlyHovered == hit.transform)
            {
                // If we are hitting something correct the dist value
                if (hit.distance < maxPointerDistance)
                {
                    dist = hit.distance;
                }

                if (isPressed)
                {
                    HandlePointerDown(hit.transform);
                }
                else
                {
                    HandlePointerUp(hit.transform);
                }
            }
        }
        else
        {
            if (!isPressed && _currentlyHovered)
            {
                HandlePointerUp(_currentlyHovered, true);

                ExecuteEvents.ExecuteHierarchy(_currentlyHovered.gameObject, _pointerEventData, ExecuteEvents.pointerExitHandler);

                _currentlyHovered = null;
            }
        }

        // Apply changes in the thickness and set the laser dimensions using the dist
        var thickness = !changeThickness || !isPressed ? idleThickness : pressedThickness;
        _laser.transform.localScale = new Vector3(thickness, thickness, dist);
        _laser.transform.localPosition = new Vector3(0f, 0f, dist / 2f);

        // Apply color changes
        if (changeColor) _laserMaterial.color = isPressed ? pressedColor : idleColor;

        // Update the pointerEventData
        pointerPosition3D = _laser.transform.position + _laser.transform.forward * dist / 2f;
        pointerPosition2D = _camera.WorldToScreenPoint(pointerPosition3D);
        _pointerEventData.position = pointerPosition2D;
        _pointerEventData.delta = _pointerEventData.position - _lastPointerPosition;
        _lastPointerPosition = _pointerEventData.position;

        _lastPressed = isPressed;
    }

    #endregion MonoBehaviour Messages


    #region Private Methods

    private void HandlePointerDown(Component target)
    {
        if (!target) return;

        // only call once if IsPressed changed its value this frame
        if (_lastPressed != isPressed)
        {
            // Try pointerClickHandler, only use submitHandler as fallback
            // e.g. UI.Button implements both ... would lead to duplicate clicks
            var done = ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.pointerClickHandler);
            if (!done) ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.submitHandler);

            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.pointerDownHandler);
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.selectHandler);
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.initializePotentialDrag);
        }
        else
        {
            if (!_isDragging)
            {
                // in the first frame use beginDragHandler
                ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.beginDragHandler);
                _isDragging = true;
                _pointerEventData.dragging = true;
            }
            else
            {
                // later use dragHandler
                ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.dragHandler);
            }
        }
    }

    private void HandlePointerUp(Component target, bool forceInvoke = false)
    {
        if (!target) return;

        // only call once if IsPressed changed its value this frame
        // or forceInvoke is set
        if (_lastPressed != isPressed || forceInvoke)
        {
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.pointerUpHandler);
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.deselectHandler);
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.endDragHandler);
            ExecuteEvents.ExecuteHierarchy(target.gameObject, _pointerEventData, ExecuteEvents.dropHandler);

            _isDragging = false;
            _pointerEventData.dragging = false;
        }
    }

    #endregion Private Methods
}

VRUIItem.cs

(基于 VRUIItem.cs 但添加了 Update 以便动态 UI,例如 ScrollBarHandles > 在 ScrollRect 中更新其 Collider)

这必须放置在基本上每个可与激光交互的 UI 元素上

using UnityEngine;

[RequireComponent(typeof(RectTransform))]
public class VRUIItem : MonoBehaviour
{
    private BoxCollider _boxCollider;
    private RectTransform _rectTransform;

    private void OnEnable()
    {
        ValidateCollider();
    }

    private void Update()
    {
        ValidateCollider();
    }

    private void OnValidate()
    {
        ValidateCollider();
    }

    private void ValidateCollider()
    {
        if (!_rectTransform) _rectTransform = GetComponent<RectTransform>();

        if (!_boxCollider) _boxCollider = GetComponent<BoxCollider>();
        if (!_boxCollider) _boxCollider = gameObject.AddComponent<BoxCollider>();

        _boxCollider.size = _rectTransform.rect.size;
    }
}

Interactable3D.cs

如何使用 IPointerXYHandler 接口(interface)创建交互式 3D 游戏对象的示例

using System;
using UnityEngine;
using UnityEngine.EventSystems;

public class Interactable3D : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{
    public Color normalColor;
    public Color hoverColor;

    private Renderer renderer;

    private void Awake()
    {
        renderer = GetComponent<Renderer>();
        renderer.material.color = normalColor;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        Debug.Log(" ENTEREd!");
        transform.localScale *= 1.2f;
        renderer.material.color = hoverColor;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        Debug.Log("EXIT!");
        transform.localScale /= 1.2f;
        renderer.material.color = normalColor;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        Debug.Log("CLICKED!");
    }
}

演示

正如所说,滚动还不能完全正确地工作,但是您可以看到它基本上是可能的 - 也许只是我通过 _pointerEventData.position 传入的指针坐标有问题 - <强>但所有其他交互都已经起作用:

enter image description here

关于c# - 使用光线转换代替凝视指针,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56609334/

相关文章:

C# 文本文件读取停止工作 - 使用 Unity

unity-game-engine - Unity3D VR应用程序: fade to another scene after a certain time

ffmpeg 通过复制单个输入输出立体

c++ - 如何创建和分发 SteamVR 仪表板覆盖

c# - 在 C# 中将指针用作外部函数的缓冲区

c# - DateTime 可以代表基督之前/我们这个时代之前的日期吗?或者这是如何管理的?

c# - 如何使用 C# 在 Google Admin SDK 目录中创建具有别名的用户?

c# - 加密配置文件段(Settings.settings)

android - Unity - Google Play 游戏服务不工作?

Android:如何创建类似 YouTube 的 VR 控件?