mixedreality/com.microsoft.mixedreality..../SDK/Features/UX/Scripts/Cursors/BaseCursor.cs

750 lines
28 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// Object that represents a cursor in 3D space.
/// </summary>
[AddComponentMenu("Scripts/MRTK/SDK/BaseCursor")]
public class BaseCursor : MonoBehaviour, IMixedRealityCursor
{
public CursorStateEnum CursorState { get; private set; } = CursorStateEnum.None;
public CursorContextEnum CursorContext { get; private set; } = CursorContextEnum.None;
/// <summary>
/// Surface distance to place the cursor off of the surface at
/// </summary>
[SerializeField]
[Tooltip("The distance from the hit surface to place the cursor")]
private float surfaceCursorDistance = 0.02f;
public float SurfaceCursorDistance => surfaceCursorDistance;
/// <summary>
/// When lerping, use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.
/// </summary>
public bool UseUnscaledTime
{
get { return useUnscaledTime; }
set { useUnscaledTime = value; }
}
[Header("Motion")]
[SerializeField]
[Tooltip("When lerping, use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.")]
private bool useUnscaledTime = true;
/// <summary>
/// Blend value for surface normal to user facing lerp.
/// </summary>
public float PositionLerpTime
{
get { return positionLerpTime; }
set { positionLerpTime = value; }
}
[SerializeField]
[Tooltip("Blend value for surface normal to user facing lerp")]
private float positionLerpTime = 0.01f;
/// <summary>
/// Blend value for surface normal to user facing lerp.
/// </summary>
public float ScaleLerpTime
{
get { return scaleLerpTime; }
set { scaleLerpTime = value; }
}
[SerializeField]
[Tooltip("Blend value for surface normal to user facing lerp")]
private float scaleLerpTime = 0.01f;
/// <summary>
/// Blend value for surface normal to user facing lerp.
/// </summary>
public float RotationLerpTime
{
get { return rotationLerpTime; }
set { rotationLerpTime = value; }
}
[SerializeField]
[Tooltip("Blend value for surface normal to user facing lerp")]
private float rotationLerpTime = 0.01f;
/// <summary>
/// Blend value for surface normal to user facing lerp.
/// </summary>
public float LookRotationBlend
{
get { return lookRotationBlend; }
set { lookRotationBlend = value; }
}
[Range(0, 1)]
[SerializeField]
[Tooltip("Blend value for surface normal to user facing lerp")]
private float lookRotationBlend = 0.5f;
/// <summary>
/// Dictates whether the cursor should resize based on distance.
/// If true, cursor will appear to be the same size no matter what distance it is from Main Camera.
/// </summary>
public bool ResizeCursorWithDistance
{
get { return resizeCursorWithDistance; }
set { resizeCursorWithDistance = value; }
}
[Header("Scaling")]
[SerializeField]
[Tooltip("Dictates whether the cursor should resize based on distance. If true, cursor will appear to be the same size no matter what distance it is from Main Camera.")]
private bool resizeCursorWithDistance = false;
/// <summary>
/// The angular scale of cursor in relation to Main Camera, assuming a mesh with bounds of Vector3(1,1,1)
/// </summary>
[Obsolete("Property obsolete. Use CursorAngularSize instead")]
public float CursorAngularScale
{
get { return CursorAngularSize; }
set { CursorAngularSize = value; }
}
/// <summary>
/// The angular size of cursor in relation to Main Camera, assuming a mesh with bounds of Vector3(1,1,1)
/// </summary>
public float CursorAngularSize
{
get { return cursorAngularSize; }
set { cursorAngularSize = value; }
}
[SerializeField, FormerlySerializedAs("cursorAngularScale")]
[Tooltip("The angular scale of cursor in relation to Main Camera, assuming a mesh with bounds of Vector3(1,1,1)")]
private float cursorAngularSize = 50.0f;
[Header("Transform References")]
[SerializeField]
[Tooltip("Visual that is displayed when cursor is active normally")]
protected Transform PrimaryCursorVisual = null;
protected bool IsSourceDetected => VisibleSourcesCount > 0;
public List<uint> SourceDownIds = new List<uint>();
public bool IsPointerDown => SourceDownIds.Count > 0;
protected GameObject TargetedObject = null;
public uint VisibleSourcesCount { get; set; } = 0;
protected Vector3 targetPosition;
protected Vector3 targetScale;
protected Quaternion targetRotation;
#region IMixedRealityCursor Implementation
/// <inheritdoc />
public virtual IMixedRealityPointer Pointer
{
get { return pointer; }
set
{
if (IsPointerValid && ReferenceEquals(pointer.BaseCursor, this))
{
// if the previous pointer was attached to this cursor, null out the
// pointer's cursor reference - that way we don't have multiple pointers
// trying to use the same cursor
pointer.BaseCursor = null;
}
pointer = value;
if (IsPointerValid)
{
pointer.BaseCursor = this;
}
ResetInputSourceState();
}
}
private IMixedRealityPointer pointer;
/// <summary>
/// Checks whether the associated pointer is null, and if the pointer is a UnityEngine.Object it also checks whether it has been destroyed.
/// </summary>
protected bool IsPointerValid => (pointer is UnityEngine.Object) ? ((pointer as UnityEngine.Object) != null) : (pointer != null);
/// <inheritdoc />
public float DefaultCursorDistance
{
get { return defaultCursorDistance; }
set { defaultCursorDistance = value; }
}
[SerializeField]
[Tooltip("The maximum distance the cursor can be with nothing hit")]
private float defaultCursorDistance = 2.0f;
/// <inheritdoc />
public virtual Vector3 Position => transform.position;
/// <inheritdoc />
public virtual Quaternion Rotation => transform.rotation;
/// <inheritdoc />
public virtual Vector3 LocalScale => transform.localScale;
public virtual void SetVisibility(bool visible)
{
if (PrimaryCursorVisual != null &&
PrimaryCursorVisual.gameObject.activeInHierarchy != visible)
{
PrimaryCursorVisual.gameObject.SetActive(visible);
}
}
/// <inheritdoc />
public virtual void Destroy()
{
// Cursor needs to unregister its input handlers explicitly, while input system is still active.
// If this would be done from OnDestroy, it will happen in the end of Update loop,
// when the input system itself is already destroyed.
UnregisterManagers();
}
/// <inheritdoc />
public bool IsVisible => PrimaryCursorVisual != null ? PrimaryCursorVisual.gameObject.activeInHierarchy : gameObject.activeInHierarchy;
/// <inheritdoc />
public bool SetVisibilityOnSourceDetected { get; set; } = false;
/// <inheritdoc />
public GameObject GameObjectReference => gameObject;
private FocusDetails focusDetails;
#endregion IMixedRealityCursor Implementation
#region IMixedRealitySourceStateHandler Implementation
/// <inheritdoc />
public virtual void OnSourceDetected(SourceStateEventData eventData)
{
if (IsPointerValid && eventData.Controller != null)
{
for (int i = 0; i < eventData.InputSource.Pointers.Length; i++)
{
// If a source is detected that's using this cursor's pointer, we increment the count to set the cursor state properly.
if (eventData.InputSource.Pointers[i].PointerId == Pointer.PointerId)
{
VisibleSourcesCount++;
if (SetVisibilityOnSourceDetected && VisibleSourcesCount == 1)
{
SetVisibility(true);
}
return;
}
}
}
}
/// <inheritdoc />
public virtual void OnSourceLost(SourceStateEventData eventData)
{
if (IsPointerValid && eventData.Controller != null)
{
for (int i = 0; i < eventData.InputSource.Pointers.Length; i++)
{
// If a source is lost that's using this cursor's pointer, we decrement the count to set the cursor state properly.
if (eventData.InputSource.Pointers[i].PointerId == Pointer.PointerId)
{
VisibleSourcesCount--;
break;
}
}
}
SourceDownIds.Remove(eventData.SourceId);
if (!IsSourceDetected && SetVisibilityOnSourceDetected)
{
SetVisibility(false);
}
}
#endregion IMixedRealitySourceStateHandler Implementation
#region IMixedRealityFocusChangedHandler Implementation
/// <inheritdoc />
public virtual void OnBeforeFocusChange(FocusEventData eventData)
{
if (IsPointerValid && Pointer.PointerId == eventData.Pointer.PointerId)
{
TargetedObject = eventData.NewFocusedObject;
}
}
/// <inheritdoc />
public virtual void OnFocusChanged(FocusEventData eventData) { }
#endregion IMixedRealityFocusChangedHandler Implementation
#region IMixedRealityPointerHandler Implementation
/// <inheritdoc />
public virtual void OnPointerDown(MixedRealityPointerEventData eventData)
{
if (IsPointerValid)
{
foreach (var sourcePointer in eventData.InputSource.Pointers)
{
if (sourcePointer.PointerId == Pointer.PointerId)
{
SourceDownIds.Add(eventData.SourceId);
return;
}
}
}
}
/// <inheritdoc />
public virtual void OnPointerDragged(MixedRealityPointerEventData eventData) { }
/// <inheritdoc />
public virtual void OnPointerClicked(MixedRealityPointerEventData eventData) { }
/// <inheritdoc />
public virtual void OnPointerUp(MixedRealityPointerEventData eventData)
{
if (IsPointerValid && eventData.InputSource != null)
{
foreach (var sourcePointer in eventData.InputSource.Pointers)
{
if (sourcePointer.PointerId == Pointer.PointerId)
{
SourceDownIds.Remove(eventData.SourceId);
return;
}
}
}
}
#endregion IMixedRealityPointerHandler Implementation
#region MonoBehaviour Implementation
protected virtual void Start()
{
RegisterManagers();
}
private void Update()
{
// Skip Update if the input system is missing during a runtime profile switch
if (CoreServices.InputSystem == null ||
CoreServices.InputSystem.FocusProvider == null)
{
return;
}
if (!CoreServices.InputSystem.FocusProvider.TryGetFocusDetails(Pointer, out focusDetails)
&& CoreServices.InputSystem.FocusProvider.IsPointerRegistered(Pointer))
{
Debug.LogError($"{name}: Unable to get focus details for {pointer.GetType().Name}!");
return;
}
UpdateCursorState();
UpdateCursorTransform();
}
protected virtual void OnEnable()
{
OnCursorStateChange(CursorStateEnum.None);
ResetInputSourceState();
}
protected virtual void OnDisable()
{
TargetedObject = null;
VisibleSourcesCount = 0;
OnCursorStateChange(CursorStateEnum.Contextual);
}
#endregion MonoBehaviour Implementation
/// <summary>
/// Register to events from the managers the cursor needs.
/// </summary>
protected virtual void RegisterManagers()
{
IMixedRealityInputSystem inputSystem = CoreServices.InputSystem;
if (inputSystem == null)
{
return;
}
// Register the cursor as a listener, so that it can always get input events it cares about
inputSystem.RegisterHandler<IMixedRealityCursor>(this);
// Setup the cursor to be able to respond to input being globally enabled / disabled
if (inputSystem.IsInputEnabled)
{
OnInputEnabled();
}
else
{
OnInputDisabled();
}
inputSystem.InputEnabled += OnInputEnabled;
inputSystem.InputDisabled += OnInputDisabled;
}
/// <summary>
/// Unregister from events from the managers the cursor needs.
/// </summary>
protected virtual void UnregisterManagers()
{
IMixedRealityInputSystem inputSystem = CoreServices.InputSystem;
if (inputSystem != null)
{
inputSystem.InputEnabled -= OnInputEnabled;
inputSystem.InputDisabled -= OnInputDisabled;
inputSystem.UnregisterHandler<IMixedRealityCursor>(this);
}
}
/// <summary>
/// Update the cursor's transform
/// </summary>
protected virtual void UpdateCursorTransform()
{
if (Pointer == null)
{
Debug.LogError($"[BaseCursor.{name}] No Pointer has been assigned!");
return;
}
GameObject newTargetedObject = CoreServices.InputSystem?.FocusProvider.GetFocusedObject(Pointer);
Vector3 lookForward;
// If no game object is hit, put the cursor at the default distance
if (newTargetedObject == null)
{
TargetedObject = null;
targetPosition = RayStep.GetPointByDistance(Pointer.Rays, defaultCursorDistance);
lookForward = -RayStep.GetDirectionByDistance(Pointer.Rays, defaultCursorDistance);
targetRotation = lookForward.magnitude > 0 ? Quaternion.LookRotation(lookForward, Vector3.up) : transform.rotation;
// If constant cursor scale is desired, skip resizing functionality
targetScale = resizeCursorWithDistance ? ComputeScaleWithAngularScale(targetPosition) : Vector3.one;
}
else
{
// Update currently targeted object
TargetedObject = newTargetedObject;
if (Pointer.CursorModifier != null)
{
Pointer.CursorModifier.GetModifiedTransform(this, out targetPosition, out targetRotation, out targetScale);
}
else
{
// If no modifier is on the target, just use the hit result to set cursor position
// Get the look forward by using distance between pointer origin and target position
// (This may not be strictly accurate for extremely wobbly pointers, but it should produce usable results)
float distanceToTarget = Vector3.Distance(Pointer.Rays[0].Origin, focusDetails.Point);
lookForward = -RayStep.GetDirectionByDistance(Pointer.Rays, distanceToTarget);
targetPosition = focusDetails.Point + (lookForward * surfaceCursorDistance);
Vector3 lookRotation = Vector3.Slerp(focusDetails.Normal, lookForward, lookRotationBlend);
targetRotation = Quaternion.LookRotation(lookRotation == Vector3.zero ? lookForward : lookRotation, Vector3.up);
// If constant cursor scale is desired, skip resizing functionality
targetScale = resizeCursorWithDistance ? ComputeScaleWithAngularScale(targetPosition) : Vector3.one;
}
}
LerpToTargetTransform();
}
protected void LerpToTargetTransform()
{
float deltaTime = useUnscaledTime
? Time.unscaledDeltaTime
: Time.deltaTime;
// Use the lerp times to blend the position to the target position
transform.position = Vector3.Lerp(transform.position, targetPosition, deltaTime / positionLerpTime);
transform.localScale = Vector3.Lerp(transform.localScale, targetScale, deltaTime / scaleLerpTime);
transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, deltaTime / rotationLerpTime);
}
protected void SnapToTargetTransform()
{
transform.position = targetPosition;
transform.localScale = targetScale;
transform.rotation = targetRotation;
}
/// <summary>
/// Calculates constant visual size of cursor based on cursorAngularScale
/// </summary>
private Vector3 ComputeScaleWithAngularScale(Vector3 targetPosition)
{
float cursorDistance = Vector3.Distance(CameraCache.Main.transform.position, targetPosition);
float desiredScale = MathUtilities.ScaleFromAngularSizeAndDistance(cursorAngularSize, cursorDistance);
return Vector3.one * desiredScale;
}
/// <summary>
/// Disable input and set to contextual to override input
/// </summary>
public virtual void OnInputDisabled()
{
// Reset visible hands on disable
VisibleSourcesCount = 0;
OnCursorStateChange(CursorStateEnum.Contextual);
}
/// <summary>
/// Enable input and set to none to reset cursor
/// </summary>
public virtual void OnInputEnabled()
{
OnCursorStateChange(CursorStateEnum.None);
ResetInputSourceState();
}
/// <summary>
/// Update visibleSourcesCount (and correspondingly IsSourceDetected) by looking at all input sources
/// registered with the input system (DetectedInputSources). This is useful for cases where the cursor
/// has not been listening for SourceDetected events (or the events have been disabled) and so the
/// count may have gotten out of sync.
/// It will also clear SourceDownIds (which will make IsPointerDown false, regardless of the underlying
/// input source state) - so it should really *only* be used in cases where the source state hadn't been
/// updating (for whatever reason).
/// </summary>
private void ResetInputSourceState()
{
SourceDownIds.Clear();
VisibleSourcesCount = 0;
if (IsPointerValid && CoreServices.InputSystem != null)
{
uint cursorPointerId = Pointer.PointerId;
foreach (IMixedRealityInputSource inputSource in CoreServices.InputSystem.DetectedInputSources)
{
if (inputSource.SourceType != InputSourceType.Head && inputSource.SourceType != InputSourceType.Eyes)
{
foreach (IMixedRealityPointer inputSourcePointer in inputSource.Pointers)
{
if (inputSourcePointer.PointerId == cursorPointerId)
{
++VisibleSourcesCount;
break;
}
}
}
}
}
if (SetVisibilityOnSourceDetected)
{
SetVisibility(IsSourceDetected);
}
}
/// <summary>
/// Internal update to check for cursor state changes
/// </summary>
private void UpdateCursorState()
{
CursorStateEnum newState = CheckCursorState();
if (CursorState != newState)
{
OnCursorStateChange(newState);
}
CursorContextEnum newContext = CheckCursorContext();
if (CursorContext != newContext)
{
OnCursorContextChange(newContext);
}
}
/// <summary>
/// Virtual function for checking state changes.
/// </summary>
public virtual CursorStateEnum CheckCursorState()
{
if (CursorState != CursorStateEnum.Contextual)
{
if (IsPointerDown)
{
return CursorStateEnum.Select;
}
if (CursorState == CursorStateEnum.Select)
{
return CursorStateEnum.Release;
}
if (IsSourceDetected)
{
return TargetedObject != null ? CursorStateEnum.InteractHover : CursorStateEnum.Interact;
}
return TargetedObject != null ? CursorStateEnum.ObserveHover : CursorStateEnum.Observe;
}
return CursorStateEnum.Contextual;
}
/// <summary>
/// Gets three axes where the forward is as close to the provided normal as
/// possible but where the axes are aligned to the TargetObject's transform
/// </summary>
private bool GetCursorTargetAxes(Vector3 normal, ref Vector3 right, ref Vector3 up, ref Vector3 forward)
{
if (TargetedObject)
{
Vector3 objRight = TargetedObject.transform.TransformDirection(Vector3.right);
Vector3 objUp = TargetedObject.transform.TransformDirection(Vector3.up);
Vector3 objForward = TargetedObject.transform.TransformDirection(Vector3.forward);
float dotRight = Vector3.Dot(normal, objRight);
float dotUp = Vector3.Dot(normal, objUp);
float dotForward = Vector3.Dot(normal, objForward);
if (Math.Abs(dotRight) > Math.Abs(dotUp) &&
Math.Abs(dotRight) > Math.Abs(dotForward))
{
forward = (dotRight > 0 ? objRight : -objRight).normalized;
}
else if (Math.Abs(dotUp) > Math.Abs(dotForward))
{
forward = (dotUp > 0 ? objUp : -objUp).normalized;
}
else
{
forward = (dotForward > 0 ? objForward : -objForward).normalized;
}
right = Vector3.Cross(Vector3.up, forward).normalized;
if (right == Vector3.zero)
{
right = Vector3.Cross(objForward, forward).normalized;
}
up = Vector3.Cross(forward, right).normalized;
return true;
}
return false;
}
/// <summary>
/// Virtual function for checking cursor context changes.
/// </summary>
public virtual CursorContextEnum CheckCursorContext()
{
if (CursorContext != CursorContextEnum.Contextual)
{
var cursorAction = CursorContextInfo.CursorAction.None;
Transform contextCenter = null;
if (TargetedObject)
{
#if UNITY_2019_4_OR_NEWER
if (TargetedObject.TryGetComponent(out CursorContextInfo contextInfo) && contextInfo != null)
#else
var contextInfo = TargetedObject.GetComponent<CursorContextInfo>();
if (contextInfo != null)
#endif
{
cursorAction = contextInfo.CurrentCursorAction;
contextCenter = contextInfo.ObjectCenter;
}
}
Vector3 right = Vector3.zero;
Vector3 up = Vector3.zero;
Vector3 forward = Vector3.zero;
if (!GetCursorTargetAxes(focusDetails.Normal, ref right, ref up, ref forward))
{
return CursorContextEnum.None;
}
if (cursorAction == CursorContextInfo.CursorAction.Move)
{
return CursorContextEnum.MoveCross;
}
else if (cursorAction == CursorContextInfo.CursorAction.Scale)
{
if (contextCenter != null)
{
Vector3 adjustedCursorPos = Position - contextCenter.position;
if (Vector3.Dot(adjustedCursorPos, up) * Vector3.Dot(adjustedCursorPos, right) > 0) // quadrant 1 and 3
{
return CursorContextEnum.MoveNorthwestSoutheast;
}
else // quadrant 2 and 4
{
return CursorContextEnum.MoveNortheastSouthwest;
}
}
}
else if (cursorAction == CursorContextInfo.CursorAction.Rotate)
{
if (contextCenter != null)
{
Vector3 adjustedCursorPos = Position - contextCenter.position;
if (Math.Abs(Vector3.Dot(adjustedCursorPos, right)) >
Math.Abs(Vector3.Dot(adjustedCursorPos, up)))
{
return CursorContextEnum.RotateEastWest;
}
else
{
return CursorContextEnum.RotateNorthSouth;
}
}
}
return CursorContextEnum.None;
}
return CursorContextEnum.Contextual;
}
/// <summary>
/// Change the cursor state to the new state. Override in cursor implementations.
/// </summary>
public virtual void OnCursorStateChange(CursorStateEnum state)
{
CursorState = state;
}
/// <summary>
/// Change the cursor context state to the new context. Override in cursor implementations.
/// </summary>
public virtual void OnCursorContextChange(CursorContextEnum context)
{
CursorContext = context;
}
}
}