// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Utilities;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Input
{
///
/// Cursor used to aide in near finger interactions.
///
[AddComponentMenu("Scripts/MRTK/SDK/FingerCursor")]
public class FingerCursor : BaseCursor
{
[Header("Ring Motion")]
[SerializeField]
[Tooltip("Should the cursor react to near grabbables.")]
private bool checkForGrabbables = false;
[SerializeField]
[Tooltip("Positional offset from the finger's skin surface.")]
private float skinSurfaceOffset = 0.01f;
[SerializeField]
[Tooltip("At what distance should the cursor align with the surface. (Should be < alignWithFingerDistance)")]
private float alignWithSurfaceDistance = 0.1f;
[Header("Ring Visualization")]
[SerializeField]
[Tooltip("Renderer representing the ring attached to the index finger using an MRTK/Standard material with the round corner feature enabled.")]
protected Renderer indexFingerRingRenderer;
private MaterialPropertyBlock materialPropertyBlock;
private int proximityDistanceID;
private readonly Quaternion fingerPadRotation = Quaternion.Euler(90.0f, 0.0f, 0.0f);
private const float MinVisibleRingDistance = 0.1f;
protected virtual void Awake()
{
materialPropertyBlock = new MaterialPropertyBlock();
proximityDistanceID = Shader.PropertyToID("_Proximity_Distance_");
}
///
/// Override base behavior to align the cursor with the finger, else perform normal cursor transformations.
///
protected override void UpdateCursorTransform()
{
IMixedRealityNearPointer nearPointer = (IMixedRealityNearPointer)Pointer;
// When the pointer has a IMixedRealityNearPointer interface we don't call base.UpdateCursorTransform because we handle
// cursor transformation a bit differently.
if (nearPointer.IsNotNull())
{
float deltaTime = UseUnscaledTime
? Time.unscaledDeltaTime
: Time.deltaTime;
// If we are unable to get the hand joint default to the Near Pointer's position and rotation
if (!TryGetJoint(TrackedHandJoint.IndexTip, out Vector3 indexFingerPosition, out Quaternion indexFingerRotation))
{
indexFingerPosition = Pointer.Position;
indexFingerRotation = Pointer.Rotation;
}
// If we are unable to get the hand joint default to the Near Pointer's position
if (!TryGetJoint(TrackedHandJoint.IndexKnuckle, out Vector3 indexKnucklePosition, out _)) // knuckle rotation not used
{
indexKnucklePosition = Pointer.Position;
}
float distance = float.MaxValue;
Vector3 surfaceNormal = Vector3.zero;
bool surfaceNormalFound = false;
bool showVisual = true;
bool nearPokeable = nearPointer.IsNearObject;
// Show the cursor if we are deemed to be near an object or if it is near a grabbable object
if (nearPokeable)
{
// If the pointer is near an object translate the primary ring to the index finger tip and rotate to surface normal if close.
// The secondary ring should be hidden.
if (!nearPointer.TryGetDistanceToNearestSurface(out distance))
{
distance = float.MaxValue;
}
surfaceNormalFound = nearPointer.TryGetNormalToNearestSurface(out surfaceNormal);
}
else
{
// If the pointer is near a grabbable object position and rotate the ring to the default,
// else hide it.
bool nearGrabbable = checkForGrabbables && IsNearGrabbableObject();
// There is no good way to get the distance of the nearest grabbable object at the moment, so we either return the MinVisibleRingDistance or 1 (invisible).
distance = nearGrabbable ? MinVisibleRingDistance : 1.0f;
// Only show the visual if we are near a grabbable
showVisual = nearGrabbable;
surfaceNormalFound = false;
}
if (indexFingerRingRenderer != null)
{
TranslateToFinger(indexFingerRingRenderer.transform, deltaTime, indexFingerPosition, indexKnucklePosition);
if ((distance < alignWithSurfaceDistance) && surfaceNormalFound)
{
RotateToSurfaceNormal(indexFingerRingRenderer.transform, surfaceNormal, indexFingerRotation, distance);
TranslateFromTipToPad(indexFingerRingRenderer.transform, indexFingerPosition, indexKnucklePosition, surfaceNormal, distance);
}
else
{
RotateToFinger(indexFingerRingRenderer.transform, deltaTime, indexFingerRotation);
}
UpdateVisuals(indexFingerRingRenderer, distance, showVisual);
}
}
else
{
base.UpdateCursorTransform();
}
}
///
/// Applies material overrides to a ring renderer.
///
/// Renderer using an MRTK/Standard material with the round corner feature enabled.
/// Distance between the ring and surface.
/// Should the ring be visible?
protected virtual void UpdateVisuals(Renderer ringRenderer, float distance, bool visible)
{
base.SetVisibility(visible);
ringRenderer.GetPropertyBlock(materialPropertyBlock);
materialPropertyBlock.SetFloat(proximityDistanceID, visible ? distance : 1.0f);
ringRenderer.SetPropertyBlock(materialPropertyBlock);
}
private SpherePointer cachedSpherePointer = null;
///
/// Gets if the associated sphere pointer on this controller is near any grabbable objects.
///
/// True if associated sphere pointer is near any grabbable objects, else false.
/// Out parameter gets the distance to the grabbable.
protected virtual bool IsNearGrabbableObject()
{
if (cachedSpherePointer == null || cachedSpherePointer.Controller != Pointer.Controller)
{
IMixedRealityFocusProvider focusProvider = CoreServices.InputSystem?.FocusProvider;
if (focusProvider.IsNotNull())
{
var spherePointers = focusProvider.GetPointers();
foreach (var spherePointer in spherePointers)
{
if (spherePointer.Controller == Pointer.Controller)
{
cachedSpherePointer = spherePointer;
break;
}
}
}
}
return cachedSpherePointer != null && cachedSpherePointer.IsNearObject;
}
///
/// Tries and get's hand joints based on the current pointer.
///
/// The joint type to get.
/// Out parameter filled with joint position, otherwise
/// Vector3.zero
/// Out parameter filled with joint rotation, otherwise
/// Quaternion.identity
protected bool TryGetJoint(TrackedHandJoint joint, out Vector3 position, out Quaternion rotation)
{
if (Pointer != null)
{
if (Pointer.Controller is IMixedRealityHand hand)
{
if (hand.TryGetJoint(joint, out MixedRealityPose handJoint))
{
position = handJoint.Position;
rotation = handJoint.Rotation;
return true;
}
}
}
position = Vector3.zero;
rotation = Quaternion.identity;
return false;
}
private void TranslateToFinger(Transform target, float deltaTime, Vector3 fingerPosition, Vector3 knucklePosition)
{
var targetPosition = fingerPosition + (fingerPosition - knucklePosition).normalized * skinSurfaceOffset;
target.position = Vector3.Lerp(target.position, targetPosition, deltaTime / PositionLerpTime);
}
private void RotateToFinger(Transform target, float deltaTime, Quaternion pointerRotation)
{
target.rotation = Quaternion.Lerp(target.rotation, pointerRotation, deltaTime / RotationLerpTime);
}
private void RotateToSurfaceNormal(Transform target, Vector3 surfaceNormal, Quaternion pointerRotation, float distance)
{
var t = distance / alignWithSurfaceDistance;
var targetRotation = Quaternion.LookRotation(-surfaceNormal);
target.rotation = Quaternion.Slerp(targetRotation, pointerRotation, t);
}
private void TranslateFromTipToPad(Transform target, Vector3 fingerPosition, Vector3 knucklePosition, Vector3 surfaceNormal, float distance)
{
var t = distance / alignWithSurfaceDistance;
Vector3 tipNormal = (fingerPosition - knucklePosition).normalized;
Vector3 tipPosition = fingerPosition + tipNormal * skinSurfaceOffset;
Vector3 tipOffset = tipPosition - fingerPosition;
// Check how perpendicular the finger normal is to the surface, so that the cursor will
// not translate to the finger pad if the user is poking with a horizontal finger
float fingerSurfaceDot = Vector3.Dot(tipNormal, -surfaceNormal);
// Lerping an angular measurement from 0 degrees (default cursor position at tip of finger) to
// 90 degrees (a new position on the fingertip pad) around the fingertip's X axis.
Quaternion degreesRelative = Quaternion.AngleAxis((1f - t) * 90f * (1f - fingerSurfaceDot), target.right);
Vector3 tipToPadPosition = fingerPosition + degreesRelative * tipOffset;
target.position = tipToPadPosition;
}
}
}