// "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 UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
///
/// This class is designed to implement IHandGrabber, allowing objects to grab grabbable objects.
///
public class HandGrabInteractor : MonoBehaviour, IHandGrabber
{
private enum GrabState
{
None,
Hover,
Grabbing,
};
#region Public States
private HandGrabInteractable m_Grabbable = null;
public IGrabbable grabbable => m_Grabbable;
public bool isGrabbing => m_Grabbable != null;
[SerializeField]
private Handedness m_Handedness = Handedness.Left;
public Handedness handedness => m_Handedness;
private HandGrabState m_HandGrabState = null;
public HandGrabState handGrabState => m_HandGrabState;
[SerializeField]
private float m_GrabDistance = 0.03f;
public float grabDistance { get { return m_GrabDistance; } set { m_GrabDistance = value; } }
[SerializeField]
private bool m_EnableCollider = true;
public bool enableCollider
{
get { return m_EnableCollider; }
set
{
m_EnableCollider = value;
if (colliderManager != null)
{
colliderManager.EnableCollider(m_EnableCollider);
}
}
}
public bool isLeft => handedness == Handedness.Left;
#endregion
[SerializeField]
private GrabColliderManager colliderManager;
private readonly float MinGrabScore = 0.25f;
private readonly float MinDistanceScore = 0.25f;
private HandGrabInteractable currentCaidate = null;
private GrabPose grabPose = GrabPose.Identity;
private GrabState m_State = GrabState.None;
private Pose[] fingerTipPoses => new Pose[]
{
m_HandGrabState.GetJointPose(JointType.Thumb_Tip),
m_HandGrabState.GetJointPose(JointType.Index_Tip),
m_HandGrabState.GetJointPose(JointType.Middle_Tip),
m_HandGrabState.GetJointPose(JointType.Ring_Tip),
m_HandGrabState.GetJointPose(JointType.Pinky_Tip),
};
private Pose palmPose => m_HandGrabState.GetJointPose(JointType.Palm);
private Pose[] frozenPoses = new Pose[(int)JointType.Count];
private bool isFrozen = false;
private OnBeginGrab beginGrabHandler;
private OnEndGrab endGrabHandler;
#region MonoBehaviour
private void Awake()
{
m_HandGrabState = new HandGrabState(isLeft);
}
private void Start()
{
if (colliderManager != null)
{
colliderManager.EnableCollider(m_EnableCollider);
}
}
private void OnEnable()
{
GrabManager.RegisterGrabber(this);
if (colliderManager != null)
{
colliderManager.AddImmovableCollisionListener(FreezeHandPose);
}
}
private void OnDisable()
{
GrabManager.UnregisterGrabber(this);
if (colliderManager != null)
{
colliderManager.RemoveImmovableCollisionListener(FreezeHandPose);
}
}
private void Update()
{
m_HandGrabState.UpdateState();
if (isFrozen)
{
return;
}
if (m_State != GrabState.Grabbing)
{
FindCandidate();
}
switch (m_State)
{
case GrabState.None:
NoneUpdate();
break;
case GrabState.Hover:
HoverUpdate();
break;
case GrabState.Grabbing:
GrabbingUpdate();
break;
}
}
#endregion
#region Public Interface
///
/// Get the current joint pose of the grabber.
///
/// The id of the joint for which to get the pose.
/// The current pose of the specified joint.
public Pose GetCurrentJointPose(int jointId)
{
if (isFrozen)
{
return frozenPoses[jointId];
}
return m_HandGrabState.GetJointPose(jointId);
}
///
/// Get the rotation of the joint in the grab pose.
///
/// The id of the joint for which to get the rotation.
/// The rotation of the joint in the grab pose.
public bool GetGrabPoseJointRotation(int jointId, out Quaternion rotation)
{
rotation = Quaternion.identity;
if (m_Grabbable == null) { return false; }
if (jointId >= 0 && grabPose.recordedGrabRotations.Length > jointId)
{
rotation = grabPose.recordedGrabRotations[jointId];
return true;
}
else if (grabPose.handGrabGesture != HandGrabGesture.Identity)
{
rotation = m_HandGrabState.GetDefaultJointRotationInGesture(grabPose.handGrabGesture, jointId);
return true;
}
return false;
}
///
/// Check if the specific joint is necessary for grabbing.
///
/// JointType of the specified joint.
/// Return true if this joint is needed for grabbing, otherwise false.
public bool IsRequiredJoint(JointType joint)
{
if (m_Grabbable != null)
{
GetJointIndex(joint, out int group, out _);
switch (group)
{
case 2: return m_Grabbable.fingerRequirement.thumb == GrabRequirement.Required;
case 3: return m_Grabbable.fingerRequirement.index == GrabRequirement.Required;
case 4: return m_Grabbable.fingerRequirement.middle == GrabRequirement.Required;
case 5: return m_Grabbable.fingerRequirement.ring == GrabRequirement.Required;
case 6: return m_Grabbable.fingerRequirement.pinky == GrabRequirement.Required;
}
}
return false;
}
///
/// Add a listener for the event triggered when the grabber begins grabbing.
///
/// The method to be called when the grabber begins grabbing.
public void AddBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler += handler;
}
///
/// Remove a listener for the event triggered when the grabber begins grabbing.
///
/// The method to be removed from the event listeners.
public void RemoveBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler -= handler;
}
///
/// Add a listener for the event triggered when the grabber ends grabbing.
///
/// The method to be called when the grabber ends grabbing.
public void AddEndGrabListener(OnEndGrab handler)
{
endGrabHandler += handler;
}
///
/// Remove a listener for the event triggered when the grabber ends grabbing.
///
/// The method to be removed from the event listeners.
public void RemoveEndGrabListener(OnEndGrab handler)
{
endGrabHandler -= handler;
}
#endregion
///
/// Find the candidate grabbable object for grabber.
///
private void FindCandidate()
{
float distanceScore = float.MinValue;
if (GetClosestGrabbable(m_GrabDistance, out HandGrabInteractable grabbable, out float score) && score > distanceScore)
{
distanceScore = score;
currentCaidate = grabbable;
}
if (currentCaidate != null)
{
float grabScore = Grab.CalculateHandGrabScore(this, currentCaidate);
if (distanceScore < MinDistanceScore || grabScore < MinGrabScore)
{
currentCaidate = null;
}
}
}
///
/// Get the closest grabbable object for grabber.
///
/// The maximum grab distance between the grabber and the grabbable object.
/// The closest grabbable object.
/// The maximum score indicating the closeness of the grabbable object.
/// True if a grabbable object is found within the grab distance; otherwise, false.
private bool GetClosestGrabbable(float grabDistance, out HandGrabInteractable grabbable, out float maxScore)
{
grabbable = null;
maxScore = 0f;
foreach (HandGrabInteractable interactable in GrabManager.handGrabbables)
{
interactable.ShowIndicator(false, this);
foreach (Pose fingerTipPose in fingerTipPoses)
{
float distanceScore = interactable.CalculateDistanceScore(fingerTipPose.position, grabDistance);
if (distanceScore > maxScore)
{
maxScore = distanceScore;
grabbable = interactable;
}
}
}
if (grabbable != null)
{
grabbable.ShowIndicator(true, this);
}
return grabbable != null;
}
///
/// Set the state to GrabState.Hover if a candidate is found.
///
private void NoneUpdate()
{
if (currentCaidate != null)
{
m_State = GrabState.Hover;
}
}
///
/// Update the state and related information when the grabber begins grabbing the grabbable.
///
private void HoverUpdate()
{
if (currentCaidate == null)
{
m_State = GrabState.None;
return;
}
if (Grab.HandBeginGrab(this, currentCaidate))
{
m_State = GrabState.Grabbing;
m_Grabbable = currentCaidate;
m_Grabbable.SetGrabber(this);
m_Grabbable.ShowIndicator(false, this);
grabPose = m_Grabbable.bestGrabPose;
if (grabPose == GrabPose.Identity)
{
Vector3 posOffset = m_Grabbable.transform.position - palmPose.position;
Quaternion rotOffset = palmPose.rotation;
grabPose.grabOffset = new GrabOffset(m_Grabbable.transform.position, m_Grabbable.transform.rotation, posOffset, rotOffset);
}
beginGrabHandler?.Invoke(this);
}
}
///
/// Update the position of grabbable object according to the movement of the grabber.
///
private void GrabbingUpdate()
{
if (Grab.HandDoneGrab(this, m_Grabbable) || !Grab.HandIsGrabbing(this, m_Grabbable))
{
m_Grabbable.SetGrabber(null);
m_Grabbable = null;
m_State = GrabState.Hover;
endGrabHandler?.Invoke(this);
return;
}
Quaternion handRotOffset = palmPose.rotation * Quaternion.Inverse(grabPose.grabOffset.rotation);
Vector3 currentPos = palmPose.position + handRotOffset * grabPose.grabOffset.position;
Quaternion currentRot = handRotOffset * grabPose.grabOffset.targetRotation;
m_Grabbable.transform.SetPositionAndRotation(currentPos, currentRot);
}
///
/// Freezes or unfreezes the hand pose.
///
/// True to freeze the hand pose; False to unfreeze.
private void FreezeHandPose(bool enable)
{
isFrozen = enable;
if (isFrozen)
{
for (int i = 0; i < frozenPoses.Length; i++)
{
frozenPoses[i] = m_HandGrabState.GetJointPose(i);
}
}
}
///
/// Get the position of a specific joint.
///
/// The type of joint to get.
/// The reference to store the position of the joint.
/// True if the joint position is successfully retrieved; otherwise, false.
private void GetJointIndex(JointType joint, out int group, out int index)
{
int jointId = (int)joint + 1;
group = 0;
index = jointId;
// palm, wrist, thumb, index, middle, ring, pinky
int[] fingerGroup = { 1, 1, 4, 5, 5, 5, 5 };
while (index > fingerGroup[group])
{
index -= fingerGroup[group];
group += 1;
}
index -= 1;
}
}
}