// "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 System.Text;
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 Log
const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.HandGrabInteractable";
private StringBuilder m_sb = null;
internal StringBuilder sb
{
get
{
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
private void DEBUG(string msg) { Debug.Log($"{LOG_TAG}, {msg}"); }
private void WARNING(string msg) { Debug.LogWarning($"{LOG_TAG}, {msg}"); }
private void ERROR(string msg) { Debug.LogError($"{LOG_TAG}, {msg}"); }
int logFrame = 0;
bool printIntervalLog => logFrame == 0;
#endregion
#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 FingerRequirement m_FingerRequirement;
public FingerRequirement fingerRequirement => m_FingerRequirement;
[SerializeField]
HandGrabbableEvent m_OnBeginGrabbed = new HandGrabbableEvent();
public HandGrabbableEvent onBeginGrabbed => m_OnBeginGrabbed;
[SerializeField]
HandGrabbableEvent m_OnEndGrabbed = new HandGrabbableEvent();
public HandGrabbableEvent onEndGrabbed => m_OnEndGrabbed;
#endregion
#region Public State
[SerializeField]
private Rigidbody m_Rigidbody = null;
#pragma warning disable
public Rigidbody rigidbody => m_Rigidbody;
#pragma warning enable
[SerializeField]
private List m_GrabPoses = new List();
public List grabPoses => m_GrabPoses;
private GrabPose m_BestGrabPose = GrabPose.Identity;
public GrabPose bestGrabPose => m_BestGrabPose;
#endregion
[SerializeField]
private bool m_ShowAllIndicator = false;
private List allColliders = new List();
private HandGrabInteractor closestGrabber = null;
[SerializeField]
private IOneHandContraintMovement m_OneHandContraintMovement;
public IOneHandContraintMovement oneHandContraintMovement { get { return m_OneHandContraintMovement; } set { m_OneHandContraintMovement = value; } }
public bool isContraint => m_OneHandContraintMovement != null;
#pragma warning disable
[SerializeField]
private int m_PreviewIndex = -1;
#pragma warning enable
private RaycastHit[] hitResults = new RaycastHit[10];
#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 handGrabber)
{
m_Grabber = handGrabber;
HandPose handPose = HandPoseProvider.GetHandPose(handGrabber.isLeft ? HandPoseType.HAND_LEFT : HandPoseType.HAND_RIGHT);
handPose.GetPosition(JointType.Wrist, out Vector3 wristPos);
handPose.GetRotation(JointType.Wrist, out Quaternion wristRot);
UpdateBestGrabPose(handGrabber.isLeft, new Pose(wristPos, wristRot));
m_OnBeginGrabbed?.Invoke(this);
DEBUG($"{transform.name} is grabbed by {handGrabber.name}");
}
else
{
m_Grabber = null;
m_BestGrabPose = GrabPose.Identity;
m_OnEndGrabbed?.Invoke(this);
DEBUG($"{transform.name} is released.");
}
}
///
/// 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
{
HandPose handPose = HandPoseProvider.GetHandPose(grabber.isLeft ? HandPoseType.HAND_LEFT : HandPoseType.HAND_RIGHT);
handPose.GetPosition(JointType.Wrist, out Vector3 wristPos);
handPose.GetRotation(JointType.Wrist, out Quaternion wristRot);
int index = FindBestGrabPose(grabber.isLeft, new Pose(wristPos, wristRot));
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 distanceSqr = (grabberPos - closestPoint).sqrMagnitude;
float grabDistSqr = grabDistance * grabDistance;
return distanceSqr > grabDistSqr ? 0 : 1 - (distanceSqr / grabDistSqr);
}
///
/// Update the position and rotation of the self with the pose of the hand that is grabbing it.
///
/// The pose of the hand.
public void UpdatePositionAndRotation(Pose grabberPose)
{
if (m_OneHandContraintMovement == null)
{
Quaternion handRotDiff = grabberPose.rotation * Quaternion.Inverse(m_BestGrabPose.grabOffset.sourceRotation);
transform.position = grabberPose.position + handRotDiff * m_BestGrabPose.grabOffset.posOffset;
transform.rotation = grabberPose.rotation * m_BestGrabPose.grabOffset.rotOffset;
}
if (m_OneHandContraintMovement != null)
{
m_OneHandContraintMovement.UpdatePose(grabberPose);
UpdateMeshPoseByGrabbable();
}
}
#endregion
///
/// Generate all indicators and calculate grab offsets.
///
private void Initialize()
{
if (m_OneHandContraintMovement != null)
{
m_OneHandContraintMovement.Initialize(this);
onBeginGrabbed.AddListener(m_OneHandContraintMovement.OnBeginGrabbed);
onEndGrabbed.AddListener(m_OneHandContraintMovement.OnEndGrabbed);
}
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.position, transform.rotation);
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];
Pose handPose = CalculateActualHandPose(grabPose.grabOffset);
Vector3 closestPoint = GetClosestPoint(handPose.position);
autoGenIndicator.SetPose(closestPoint, closestPoint - transform.position);
grabPose.indicator.Update(true, true, autoGenIndicator.gameObject);
grabPose.indicator.CalculateGrabOffset(transform.position, transform.rotation);
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 shortDistanceSqr = float.MaxValue;
for (int i = 0; i < allColliders.Count; i++)
{
Collider collider = allColliders[i];
Vector3 closePoint = collider.ClosestPointOnBounds(sourcePos);
float distanceSqr = (sourcePos - closePoint).sqrMagnitude;
if (distanceSqr < 0.001f)
{
return closePoint;
}
if (collider.bounds.Contains(closePoint))
{
Vector3 direction = closePoint - sourcePos;
direction.Normalize();
int hitCount = Physics.RaycastNonAlloc(sourcePos, direction, hitResults, Mathf.Sqrt(distanceSqr));
for (int j = 0; j < hitCount; j++)
{
RaycastHit hit = hitResults[j];
if (hit.collider == collider)
{
float hitDistanceSqr = (sourcePos - hit.point).sqrMagnitude;
if (distanceSqr > hitDistanceSqr)
{
distanceSqr = hitDistanceSqr;
closePoint = hit.point;
}
}
}
}
if (shortDistanceSqr > distanceSqr)
{
shortDistanceSqr = distanceSqr;
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 void UpdateBestGrabPose(bool isLeft, Pose grabberPose)
{
int index = FindBestGrabPose(isLeft, grabberPose);
if (index != -1 && index < m_GrabPoses.Count)
{
m_BestGrabPose = m_GrabPoses[index];
}
else
{
m_BestGrabPose.grabOffset = new GrabOffset(grabberPose.position, grabberPose.rotation, transform.position, transform.rotation);
HandPose handPose = HandPoseProvider.GetHandPose(isLeft ? HandPoseType.MESH_LEFT : HandPoseType.MESH_RIGHT);
if (handPose is MeshHandPose meshHandPose)
{
Quaternion[] grabRotations = new Quaternion[(int)JointType.Count];
for (int i = 0; i < (int)JointType.Count; i++)
{
meshHandPose.GetRotation((JointType)i, out grabRotations[i], local: true);
}
m_BestGrabPose.recordedGrabRotations = grabRotations;
}
}
}
///
/// 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)
{
Pose handPose = CalculateActualHandPose(m_GrabPoses[i].grabOffset);
Vector3 grabDirection = handPose.position - transform.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)
{
for (int i = 0; i < m_GrabPoses.Count; i++)
{
if (index != i)
{
m_GrabPoses[i].indicator.SetActive(false);
}
}
if (index >= 0 && index < m_GrabPoses.Count &&
m_GrabPoses[index].indicator.enableIndicator)
{
m_GrabPoses[index].indicator.UpdatePositionAndRotation(transform.position, transform.rotation);
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)
{
for (int i = 0; i < m_GrabPoses.Count; i++)
{
m_GrabPoses[i].indicator.SetActive(false);
}
foreach (var grabPose in m_GrabPoses)
{
if (grabPose.isLeft == isLeft)
{
grabPose.indicator.UpdatePositionAndRotation(transform.position, transform.rotation);
grabPose.indicator.SetActive(true);
}
}
}
private void UpdateMeshPoseByGrabbable()
{
if (grabber is HandGrabInteractor handGrabber)
{
HandPose handPose = HandPoseProvider.GetHandPose(handGrabber.isLeft ? HandPoseType.MESH_LEFT : HandPoseType.MESH_RIGHT);
if (handPose != null && handPose is MeshHandPose meshHandPose)
{
Pose realHandPose = CalculateActualHandPose(m_BestGrabPose.grabOffset);
meshHandPose.SetJointPose(JointType.Wrist, realHandPose);
}
}
}
private Pose CalculateActualHandPose(GrabOffset grabOffset)
{
Quaternion handRot = transform.rotation * Quaternion.Inverse(grabOffset.rotOffset);
Quaternion handRotDiff = handRot * Quaternion.Inverse(grabOffset.sourceRotation);
Vector3 handPos = transform.position - handRotDiff * grabOffset.posOffset;
return new Pose(handPos, handRot);
}
}
}