// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Collections.Generic; using UltimateXR.Core; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace UltimateXR.UI.UnityInputModule { /// /// Raycaster compatible with Unity UI to use components on canvases, enabling /// interaction using laser pointers from a distance. /// [RequireComponent(typeof(Canvas))] public class UxrLaserPointerRaycaster : UxrGraphicRaycaster { #region Public Overrides GraphicRaycaster /// public override void Raycast(PointerEventData eventData, List resultAppendList) { // Check if it should be ray-casted UxrPointerEventData pointerEventData = eventData as UxrPointerEventData; if (pointerEventData == null || pointerEventData.LaserPointer == null) { return; } // Initialize if necessary if (_canvas == null) { _canvas = gameObject.GetComponent(); _canvasGroup = gameObject.GetComponent(); if (_canvas == null) { return; } } if (_canvasGroup != null && _canvasGroup.interactable == false) { return; } // Raycast against the canvas, gather all results and append to the list _raycastResults.Clear(); var ray = new Ray(pointerEventData.LaserPointer.LaserPos, pointerEventData.LaserPointer.LaserDir); Raycast(_canvas, pointerEventData.LaserPointer, eventCamera, ray, ref _raycastResults, ref resultAppendList, out float _); // Assign correct indices and get closest raycast RaycastResult? raycastNearest = null; for (int i = 0; i < _raycastResults.Count; ++i) { RaycastResult rayCastResult = _raycastResults[i]; rayCastResult.index = resultAppendList.Count + i; if (!raycastNearest.HasValue || rayCastResult.distance < raycastNearest.Value.distance) { raycastNearest = rayCastResult; } } // If a closest raycast was found, use it to compute the event delta if (raycastNearest.HasValue) { eventData.position = raycastNearest.Value.screenPosition; eventData.delta = eventData.position - _lastPosition; _lastPosition = eventData.position; eventData.pointerCurrentRaycast = raycastNearest.Value; } } #endregion #region Private Methods /// /// Performs a raycast to check which elements in a canvas were potentially interacted with. /// /// Canvas to check /// Source laser pointer or null if it could not be retrieved /// Camera from which to perform the checks /// Ray to use for intersection detection /// Returns the list of results sorted by increasing distance /// Returns a list where the results have been appended to the existing content /// Returns the occluder distance private void Raycast(Canvas canvas, UxrLaserPointer laserPointer, Camera cam, Ray ray, ref List results, ref List resultAppendList, out float occluderDistance) { occluderDistance = -1.0f; if (cam == null) { return; } float hitDistance = float.MaxValue; GameObject hitObject = null; Vector3 hitPosition = Vector3.zero; int hitDepth = 0; // Check for objects in the path. Set hitDistance to the ray length to the closest hit. if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None) { RectTransform rectTransformCanvas = canvas.GetComponent(); float maxDistance = Vector3.Distance(ray.origin, canvas.transform.position) + Mathf.Max(rectTransformCanvas.localScale.x, rectTransformCanvas.localScale.y, rectTransformCanvas.localScale.z) * Mathf.Max(rectTransformCanvas.rect.width, rectTransformCanvas.rect.height); bool blocking3D = laserPointer != null ? laserPointer.TargetTypes.HasFlag(UxrLaserPointerTargetTypes.Colliders3D) : blockingObjects.HasFlag(BlockingObjects.ThreeD); if (blocking3D) { if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, laserPointer != null ? laserPointer.BlockingMask : m_BlockingMask, laserPointer != null ? laserPointer.TriggerCollidersInteraction : QueryTriggerInteraction.Ignore)) { hitObject = hit.collider.gameObject; hitPosition = hit.point; hitDepth = UxrConstants.UI.Depth3DObject; hitDistance = hit.distance; occluderDistance = hitDistance; } } bool blocking2D = laserPointer != null ? laserPointer.TargetTypes.HasFlag(UxrLaserPointerTargetTypes.Colliders2D) : blockingObjects.HasFlag(BlockingObjects.TwoD); if (blocking2D) { RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction, maxDistance, laserPointer != null ? laserPointer.BlockingMask : m_BlockingMask); if (hit.collider != null && hit.distance < hitDistance) { hitObject = hit.collider.gameObject; hitPosition = hit.point; hitDepth = UxrConstants.UI.Depth2DObject; hitDistance = hit.fraction * maxDistance; occluderDistance = hitDistance; } } } if (hitObject != null) { var result = new RaycastResult { gameObject = hitObject, module = this, distance = hitDistance, screenPosition = cam.WorldToScreenPoint(hitPosition), worldPosition = hitPosition, depth = hitDepth, sortingLayer = 0, sortingOrder = 0, }; results.Add(result); } // Iterate over all canvas graphics bool processUI = laserPointer == null || laserPointer.TargetTypes.HasFlag(UxrLaserPointerTargetTypes.UI); if (processUI) { IList listGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas); for (int i = 0; i < listGraphics.Count; i++) { Graphic graphic = listGraphics[i]; if (graphic.depth == -1 || !graphic.raycastTarget) { continue; } float distance = Vector3.Dot(graphic.transform.forward, graphic.transform.position - ray.origin) / Vector3.Dot(graphic.transform.forward, ray.direction); if (distance < 0.0f) { continue; } if (distance - HitDistanceThreshold > hitDistance) { continue; } Vector3 position = ray.GetPoint(distance); Vector2 pointerPosition = cam.WorldToScreenPoint(position); if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, cam)) { continue; } if (graphic.Raycast(pointerPosition, cam)) { var result = new RaycastResult { gameObject = graphic.gameObject, module = this, distance = distance, screenPosition = pointerPosition, worldPosition = position, depth = graphic.depth, sortingLayer = canvas.sortingLayerID, sortingOrder = canvas.sortingOrder, }; results.Add(result); } } } results.Sort(CompareDepth); resultAppendList.AddRange(results); } #endregion #region Private Types & Data private const float HitDistanceThreshold = 0.001f; private Canvas _canvas; private CanvasGroup _canvasGroup; private List _raycastResults = new List(); private Vector2 _lastPosition; #endregion } }