// 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; } } }