// "Wave SDK // © 2020 HTC Corporation. All Rights Reserved. // // Unless otherwise required by copyright law and practice, // upon the execution of HTC SDK license agreement, // HTC grants you access to and use of the Wave SDK(s). // You shall fully comply with all of HTC’s SDK license agreement terms and // conditions signed by you and all SDK and API requirements, // specifications, and documentation provided by HTC to You." using System.Collections.Generic; using UnityEngine; namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction { /// /// This class is designed to implement IHandGrabbable, allowing objects to be grabbed. /// public class HandGrabInteractable : MonoBehaviour, IHandGrabbable { #region Interface Implement private HandGrabInteractor m_Grabber = null; public IGrabber grabber => m_Grabber; public bool isGrabbed => m_Grabber != null; [SerializeField] private bool m_IsGrabbable = true; public bool isGrabbable { get { return m_IsGrabbable; } set { m_IsGrabbable = value; } } [SerializeField] private bool m_ForceMovable = true; public bool forceMovable { get { return m_ForceMovable; } set { m_ForceMovable = value; } } [SerializeField] private FingerRequirement m_FingerRequirement; public FingerRequirement fingerRequirement => m_FingerRequirement; #endregion #region Public State [SerializeField] private List m_GrabPoses = new List(); public List grabPoses => m_GrabPoses; public GrabPose bestGrabPose => bestGrabPoseId != -1 ? m_GrabPoses[bestGrabPoseId] : GrabPose.Identity; #endregion [SerializeField] private bool m_ShowAllIndicator = false; private List allColliders = new List(); private HandGrabInteractor closestGrabber = null; private int bestGrabPoseId = -1; private OnBeginGrabbed onBeginGrabbed; private OnEndGrabbed onEndGrabbed; #region MonoBehaviour private void Awake() { allColliders.AddRange(transform.GetComponentsInChildren(true)); } private void OnEnable() { GrabManager.RegisterGrabbable(this); Initialize(); } private void OnDisable() { GrabManager.UnregisterGrabbable(this); } #endregion #region Public Interface /// /// Set the grabber for the hand grabbable object. /// /// The grabber to set. public void SetGrabber(IGrabber grabber) { if (grabber is HandGrabInteractor) { HandGrabInteractor handGrabber = grabber as HandGrabInteractor; m_Grabber = handGrabber; UpdateBestGrabPose(handGrabber.isLeft, handGrabber.handGrabState.GetJointPose(JointType.Palm)); onBeginGrabbed?.Invoke(this); } else { m_Grabber = null; bestGrabPoseId = -1; onEndGrabbed?.Invoke(this); } } /// /// Enable/Disable indicators. If enabled, display the closest indicator based on grabber position. /// /// True to show the indicator, false to hide it. /// The grabber for which to show or hide this indicator. public void ShowIndicator(bool enable, HandGrabInteractor grabber) { if (enable) { closestGrabber = grabber; if (m_ShowAllIndicator) { ShowAllIndicator(grabber.isLeft); } else { int index = FindBestGrabPose(grabber.isLeft, grabber.handGrabState.GetJointPose((int)JointType.Palm)); ShowIndicatorByIndex(index); } } else { if (closestGrabber == grabber) { closestGrabber = null; ShowIndicatorByIndex(-1); } } } /// /// Calculate the shortest distance between the grabber and the grabbable and convert it into a score based on grabDistance. /// /// The current pose of grabber. /// The maximum grab distance between the grabber and the grabbable object. /// The score represents the distance between the grabber and the grabbable. public float CalculateDistanceScore(Vector3 grabberPos, float grabDistance = 0.03f) { if (!isGrabbable || isGrabbed) { return 0; } Vector3 closestPoint = GetClosestPoint(grabberPos); float distacne = Vector3.Distance(grabberPos, closestPoint); return distacne > grabDistance ? 0 : 1 - (distacne / grabDistance); } /// /// Add a listener for the event triggered when the grabbable object is grabbed. /// /// The method to be called when the grabbable object is grabbed. public void AddBeginGrabbedListener(OnBeginGrabbed handler) { onBeginGrabbed += handler; } /// /// Remove a listener for the event triggered when the grabbable object is grabbed. /// /// The method to be removed from the event listeners. public void RemoveBeginGrabbedListener(OnBeginGrabbed handler) { onBeginGrabbed -= handler; } /// /// Add a listener for the event triggered when the grabbable object is released. /// /// The method to be called when the grabbable object is released. public void AddEndGrabbedListener(OnEndGrabbed handler) { onEndGrabbed += handler; } /// /// Remove a listener for the event triggered when the grabbable object is released. /// /// The method to be removed from the event listeners. public void RemoveEndGrabbedListener(OnEndGrabbed handler) { onEndGrabbed -= handler; } #endregion /// /// Generate all indicators and calculate grab offsets. /// private void Initialize() { for (int i = 0; i < m_GrabPoses.Count; i++) { if (m_GrabPoses[i].indicator.enableIndicator || m_ShowAllIndicator) { if (m_GrabPoses[i].indicator.NeedGenerateIndicator()) { AutoGenerateIndicator(i); } else { GrabPose grabPose = m_GrabPoses[i]; grabPose.indicator.CalculateGrabOffset(transform); m_GrabPoses[i] = grabPose; } } } ShowIndicatorByIndex(-1); } /// /// Automatically generate an indicator by the index of the grab pose. /// /// The index of the grab pose. private void AutoGenerateIndicator(int index) { AutoGenIndicator autoGenIndicator = new GameObject($"Indicator {index}", typeof(AutoGenIndicator)).GetComponent(); GrabPose grabPose = m_GrabPoses[index]; // The grabPose.grabOffset was calculated as the position of the object minus the position of the hand, // so inverse calculation is needed here to infer the position of the hand. Vector3 offset = transform.rotation * Quaternion.Inverse(grabPose.grabOffset.targetRotation) * -grabPose.grabOffset.position; Vector3 defaultPosition = transform.position + offset; Vector3 closestPoint = GetClosestPoint(defaultPosition); autoGenIndicator.SetPose(closestPoint, offset.normalized); grabPose.indicator.Update(true, true, autoGenIndicator.gameObject); grabPose.indicator.CalculateGrabOffset(transform); m_GrabPoses[index] = grabPose; } /// /// Calculate the point closest to the source position. /// /// The position of source. /// The position which closest to the source position. private Vector3 GetClosestPoint(Vector3 sourcePos) { Vector3 closestPoint = Vector3.zero; float shortDistance = float.MaxValue; foreach (var collider in allColliders) { Vector3 closePoint = collider.ClosestPointOnBounds(sourcePos); float distance = Vector3.Distance(sourcePos, closePoint); if (collider.bounds.Contains(closePoint)) { Vector3 direction = (closePoint - sourcePos).normalized; RaycastHit[] hits = Physics.RaycastAll(sourcePos, direction, distance); foreach (var hit in hits) { if (hit.collider == collider) { float hitDistnace = Vector3.Distance(sourcePos, hit.point); if (distance > hitDistnace) { distance = hitDistnace; closePoint = hit.point; } } } } if (shortDistance > distance) { shortDistance = distance; closestPoint = closePoint; } } return closestPoint; } /// /// Find the best grab pose for the grabber and updates the bestGrabPoseId. /// /// Whether the grabber is the left hand. /// The pose of the grabber. /// True if a best grab pose is found; otherwise, false. private bool UpdateBestGrabPose(bool isLeft, Pose grabberPose) { int index = FindBestGrabPose(isLeft, grabberPose); if (index != -1 && index < m_GrabPoses.Count) { bestGrabPoseId = index; return true; } index = -1; return false; } /// /// Find the best grab pose for the grabber. /// /// Whether the grabber is the left hand. /// The pose of the grabber. /// The index of the best grab pose among the grab poses. private int FindBestGrabPose(bool isLeft, Pose grabberPose) { int index = -1; float maxDot = float.MinValue; Vector3 currentDirection = grabberPose.position - transform.position; for (int i = 0; i < m_GrabPoses.Count; i++) { if (m_GrabPoses[i].isLeft == isLeft) { Vector3 grabDirection = transform.rotation * Quaternion.Inverse(m_GrabPoses[i].grabOffset.targetRotation) * -m_GrabPoses[i].grabOffset.position; float dot = Vector3.Dot(currentDirection.normalized, grabDirection.normalized); if (dot > maxDot) { maxDot = dot; index = i; } } } return index; } /// /// Show the indicator corresponding to the specified index and hides others. /// /// The index of the indicator to show. private void ShowIndicatorByIndex(int index) { foreach (var grabPose in m_GrabPoses) { grabPose.indicator.SetActive(false); } if (index >= 0 && index < m_GrabPoses.Count && m_GrabPoses[index].indicator.enableIndicator) { m_GrabPoses[index].indicator.UpdatePositionAndRotation(transform); m_GrabPoses[index].indicator.SetActive(true); } } /// /// Show all indicators corresponding to the specified hand side and hides others. /// /// Whether the hand side is left. private void ShowAllIndicator(bool isLeft) { foreach (var grabPose in m_GrabPoses) { grabPose.indicator.SetActive(false); } foreach (var grabPose in m_GrabPoses) { if (grabPose.isLeft == isLeft) { grabPose.indicator.UpdatePositionAndRotation(transform); grabPose.indicator.SetActive(true); } } } } }