// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using UltimateXR.Avatar;
using UltimateXR.Core;
using UltimateXR.Core.Components.Composite;
using UltimateXR.Devices;
using UltimateXR.Devices.Visualization;
using UltimateXR.Extensions.Unity.Render;
using UltimateXR.UI.UnityInputModule;
using UnityEngine;
using UnityEngine.Rendering;
namespace UltimateXR.UI
{
///
/// Component that, added to an object in an , allows it to interact with user interfaces
/// using a laser pointer. It is normally added to the hand, so that it points in a forward direction from the hand,
/// but can also be added to inanimate objects.
///
public class UxrLaserPointer : UxrAvatarComponent
{
#region Inspector Properties/Serialized Fields
// General
[SerializeField] protected UxrHandSide _handSide = UxrHandSide.Left;
[SerializeField] protected bool _useControllerForward = true;
// Interaction
[SerializeField] protected UxrLaserPointerTargetTypes _targetTypes = UxrLaserPointerTargetTypes.UI | UxrLaserPointerTargetTypes.Colliders2D | UxrLaserPointerTargetTypes.Colliders3D;
[SerializeField] private QueryTriggerInteraction _triggerCollidersInteraction = QueryTriggerInteraction.Ignore;
[SerializeField] private LayerMask _blockingMask = ~0;
// Input
[SerializeField] protected UxrInputButtons _clickInput = UxrInputButtons.Trigger;
[SerializeField] protected UxrInputButtons _showLaserInput = UxrInputButtons.Joystick;
[SerializeField] protected UxrButtonEventType _showLaserButtonEvent = UxrButtonEventType.Touching;
// Appearance
[SerializeField] protected bool _invisible = false;
[SerializeField] protected float _rayLength = 100.0f;
[SerializeField] protected float _rayWidth = 0.003f;
[SerializeField] protected Color _rayColorInteractive = new Color(0.0f, 1.0f, 0.0f, 0.5f);
[SerializeField] protected Color _rayColorNonInteractive = new Color(1.0f, 0.0f, 0.0f, 0.5f);
[SerializeField] protected Material _rayHitMaterial = null;
[SerializeField] protected float _rayHitSize = 0.004f;
[SerializeField] protected GameObject _optionalEnableWhenLaserOn = null;
#endregion
#region Public Types & Data
///
/// Gets whether the laser is currently enabled.
///
public bool IsLaserEnabled => gameObject.activeInHierarchy && enabled &&
(ForceLaserEnabled ||
IsAutoEnabled ||
(Avatar.ControllerInput.IsControllerEnabled(_handSide) && Avatar.ControllerInput.GetButtonsEvent(_handSide, ShowLaserInput, ShowLaserButtonEvent)));
///
/// Gets the that is used to compute the direction in which the laser points. The laser will
/// point in the direction.
///
public Transform LaserTransform
{
get
{
if (UseControllerForward && !Avatar.HasDummyControllerInput)
{
UxrController3DModel model = Avatar.ControllerInput.GetController3DModel(_handSide);
if (model && model.gameObject.activeInHierarchy)
{
return model.Forward != null ? model.Forward : transform;
}
}
return transform;
}
}
///
/// Gets the laser origin position.
///
public Vector3 LaserPos => LaserTransform.position;
///
/// Gets the laser direction.
///
public Vector3 LaserDir => LaserTransform.forward;
///
/// Gets the hand the laser pointer belongs to.
///
public UxrHandSide HandSide => _handSide;
///
/// Gets or sets whether the laser should be forcefully enabled. This is useful when
/// is used or a controller input is required to enable the laser
/// pointer.
///
public bool ForceLaserEnabled { get; set; }
///
/// Gets or sets whether the laser should ignore the
/// property in canvases.
///
public bool IgnoreAutoEnable { get; set; }
///
/// Gets or sets whether to use the real controller forward instead of the component's forward.
///
public bool UseControllerForward
{
get => _useControllerForward;
set => _useControllerForward = value;
}
///
/// Gets or sets the elements the laser can interact with.
///
public UxrLaserPointerTargetTypes TargetTypes
{
get => _targetTypes;
set => _targetTypes = value;
}
///
/// Gets or sets how to treat collisions against trigger volumes.
/// By default the laser doesn't collide against trigger volumes.
///
public QueryTriggerInteraction TriggerCollidersInteraction
{
get => _triggerCollidersInteraction;
set => _triggerCollidersInteraction = value;
}
///
/// Gets or sets the which layers will block the laser for 3D objects.
///
public LayerMask BlockingMask
{
get => _blockingMask;
set => _blockingMask = value;
}
///
/// Gets or sets the input button(s) required for a click.
///
public UxrInputButtons ClickInput
{
get => _clickInput;
set => _clickInput = value;
}
///
/// Gets or sets the input button(s) required to show the laser. Use to have the
/// laser always enabled or to have it always disabled and let
/// handle the enabling/disabling.
///
public UxrInputButtons ShowLaserInput
{
get => _showLaserInput;
set => _showLaserInput = value;
}
///
/// Gets or sets the button event type required for .
///
public UxrButtonEventType ShowLaserButtonEvent
{
get => _showLaserButtonEvent;
set => _showLaserButtonEvent = value;
}
///
/// Gets or sets whether to use an invisible laser ray.
///
public bool IsInvisible
{
get => _invisible;
set => _invisible = value;
}
///
/// Gets or sets the maximum laser length. This is the distance that the ray will travel if not occluded.
///
public float MaxRayLength
{
get => _rayLength;
set => _rayLength = value;
}
///
/// Gets the current laser ray length.
///
public float CurrentRayLength { get; private set; }
///
/// Gets or sets the laser ray width.
///
public float RayWidth
{
get => _rayWidth;
set => _rayWidth = value;
}
///
/// Gets or sets the ray color when it's pointing to an interactive element.
///
public Color RayColorInteractive
{
get => _rayColorInteractive;
set => _rayColorInteractive = value;
}
///
/// Gets or sets the ray color when it's not pointing to an interactive element.
///
public Color RayColorNonInteractive
{
get => _rayColorNonInteractive;
set => _rayColorNonInteractive = value;
}
///
/// Gets or sets the size of the ray hit quad..
///
public float RayHitSize
{
get => _rayHitSize;
set => _rayHitSize = value;
}
///
/// Gets or sets an optional GameObject that will be enabled or disabled along with the laser.
///
public GameObject OptionalEnableWhenLaserOn
{
get => _optionalEnableWhenLaserOn;
set => _optionalEnableWhenLaserOn = value;
}
#endregion
#region Internal Types & Data
///
/// Gets or sets whether the laser is enabled automatically due to pointing at a UI.
///
internal bool IsAutoEnabled { get; set; }
#endregion
#region Public Methods
///
/// Checks whether the user performed a click this frame (released the input button after pressing).
///
/// Whether the user performed a click action
public bool IsClickedThisFrame()
{
return Avatar.ControllerInput.GetButtonsEvent(_handSide, ClickInput, UxrButtonEventType.PressDown);
}
///
/// Checks whether the user performed a press this frame (pressed the input button).
///
/// Whether the user performed a press action
public bool IsReleasedThisFrame()
{
return Avatar.ControllerInput.GetButtonsEvent(_handSide, ClickInput, UxrButtonEventType.PressUp);
}
#endregion
#region Unity
///
/// Initializes the component.
///
protected override void Awake()
{
base.Awake();
if (Avatar == null)
{
UxrManager.LogMissingAvatarInHierarchyError(this);
}
// Set up line renderer
_lineRenderer = gameObject.AddComponent();
_lineRenderer.useWorldSpace = false;
SetLineRendererMesh(MaxRayLength);
_lineRenderer.material = new Material(ShaderExt.UnlitTransparentColor);
_lineRenderer.material.renderQueue = (int)RenderQueue.Overlay + 1;
// Set up raycast hit quad
_hitQuad = new GameObject("Laser Hit");
_hitQuad.transform.parent = transform;
MeshFilter laserHitMeshFilter = _hitQuad.AddComponent();
laserHitMeshFilter.sharedMesh = MeshExt.CreateQuad(1.0f);
_laserHitRenderer = _hitQuad.AddComponent();
_laserHitRenderer.receiveShadows = false;
_laserHitRenderer.shadowCastingMode = ShadowCastingMode.Off;
_laserHitRenderer.sharedMaterial = _rayHitMaterial;
_hitQuad.SetActive(false);
}
///
/// Updates the laser pointer.
///
private void LateUpdate()
{
if (OptionalEnableWhenLaserOn != null)
{
OptionalEnableWhenLaserOn.SetActive(IsLaserEnabled);
}
// TODO: In order to use UxrLaserPointer for other than Unity UI, the following part should be extracted.
UxrPointerEventData laserPointerEventData = UxrPointerInputModule.Instance != null ? UxrPointerInputModule.Instance.GetPointerEventData(this) : null;
if (_lineRenderer)
{
_lineRenderer.enabled = IsLaserEnabled && !IsInvisible;
_lineRenderer.material.color = laserPointerEventData != null && laserPointerEventData.IsInteractive ? RayColorInteractive : RayColorNonInteractive;
if (_laserHitRenderer)
{
_laserHitRenderer.enabled = !IsInvisible;
_laserHitRenderer.material.color = _lineRenderer.material.color;
}
}
CurrentRayLength = MaxRayLength;
if (laserPointerEventData != null && laserPointerEventData.HasData && IsLaserEnabled)
{
CurrentRayLength = laserPointerEventData.pointerCurrentRaycast.distance;
if (Avatar.CameraComponent && _hitQuad)
{
_hitQuad.SetActive(true);
_hitQuad.transform.position = LaserTransform.TransformPoint(Vector3.forward * CurrentRayLength);
_hitQuad.transform.LookAt(Avatar.CameraPosition);
Plane plane = new Plane(Avatar.CameraForward, Avatar.CameraPosition);
float dist = plane.GetDistanceToPoint(_hitQuad.transform.position);
_hitQuad.transform.localScale = RayHitSize * Mathf.Max(2.0f, dist) * Vector3.one;
}
}
else
{
_hitQuad.SetActive(false);
}
if (_lineRenderer && _lineRenderer.enabled)
{
SetLineRendererMesh(CurrentRayLength);
}
}
#endregion
#region Private Methods
///
/// Updates the line renderer mesh.
///
/// New ray length
private void SetLineRendererMesh(float rayLength)
{
_lineRenderer.startWidth = RayWidth;
_lineRenderer.endWidth = RayWidth;
float t1 = Mathf.Min(rayLength * 0.33f, GradientLength);
float t2 = Mathf.Max(rayLength * 0.66f, rayLength - GradientLength);
Vector3[] positions =
{
new Vector3(0.0f, 0.0f, 0.0f),
new Vector3(0.0f, 0.0f, t1),
new Vector3(0.0f, 0.0f, t2),
new Vector3(0.0f, 0.0f, rayLength)
};
for (int i = 0; i < positions.Length; ++i)
{
positions[i] = _lineRenderer.transform.InverseTransformPoint(LaserTransform.TransformPoint(positions[i]));
}
_lineRenderer.SetPositions(positions);
Gradient colorGradient = new Gradient();
colorGradient.colorKeys = new[]
{
new GradientColorKey(Color.white, 0.0f),
new GradientColorKey(Color.white, t1 / rayLength),
new GradientColorKey(Color.white, t2 / rayLength),
new GradientColorKey(Color.white, 1.0f)
};
colorGradient.alphaKeys = new[]
{
new GradientAlphaKey(0.0f, 0.0f),
new GradientAlphaKey(1.0f, t1 / rayLength),
new GradientAlphaKey(1.0f, t2 / rayLength),
new GradientAlphaKey(0.0f, 1.0f)
};
_lineRenderer.colorGradient = colorGradient;
_lineRenderer.positionCount = 4;
}
#endregion
#region Private Types & Data
private const float GradientLength = 0.4f;
private LineRenderer _lineRenderer;
private Renderer _laserHitRenderer;
private bool _isAutoEnabled;
private GameObject _hitQuad;
#endregion
}
}