// "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; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction { public class HandColliderController : MonoBehaviour { #region Log const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.HandColliderController"; 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 private HandMeshManager m_HandMesh; public HandMeshManager handMesh { get { return m_HandMesh; } set { m_HandMesh = value; } } private bool m_EnableCollider = true; public bool enableCollider { get { return m_EnableCollider; } set { m_EnableCollider = value; rootJoint.gameObject.SetActive(m_EnableCollider); } } #region Name Definition // The order of joint name MUST align with runtime's definition private readonly string[] JointsName = new string[] { "WaveBone_0", // WVR_HandJoint_Palm = 0 "WaveBone_1", // WVR_HandJoint_Wrist = 1 "WaveBone_2", // WVR_HandJoint_Thumb_Joint0 = 2 "WaveBone_3", // WVR_HandJoint_Thumb_Joint1 = 3 "WaveBone_4", // WVR_HandJoint_Thumb_Joint2 = 4 "WaveBone_5", // WVR_HandJoint_Thumb_Tip = 5 "WaveBone_6", // WVR_HandJoint_Index_Joint0 = 6 "WaveBone_7", // WVR_HandJoint_Index_Joint1 = 7 "WaveBone_8", // WVR_HandJoint_Index_Joint2 = 8 "WaveBone_9", // WVR_HandJoint_Index_Joint3 = 9 "WaveBone_10", // WVR_HandJoint_Index_Tip = 10 "WaveBone_11", // WVR_HandJoint_Middle_Joint0 = 11 "WaveBone_12", // WVR_HandJoint_Middle_Joint1 = 12 "WaveBone_13", // WVR_HandJoint_Middle_Joint2 = 13 "WaveBone_14", // WVR_HandJoint_Middle_Joint3 = 14 "WaveBone_15", // WVR_HandJoint_Middle_Tip = 15 "WaveBone_16", // WVR_HandJoint_Ring_Joint0 = 16 "WaveBone_17", // WVR_HandJoint_Ring_Joint1 = 17 "WaveBone_18", // WVR_HandJoint_Ring_Joint2 = 18 "WaveBone_19", // WVR_HandJoint_Ring_Joint3 = 19 "WaveBone_20", // WVR_HandJoint_Ring_Tip = 20 "WaveBone_21", // WVR_HandJoint_Pinky_Joint0 = 21 "WaveBone_22", // WVR_HandJoint_Pinky_Joint0 = 22 "WaveBone_23", // WVR_HandJoint_Pinky_Joint0 = 23 "WaveBone_24", // WVR_HandJoint_Pinky_Joint0 = 24 "WaveBone_25" // WVR_HandJoint_Pinky_Tip = 25 }; #endregion private float handMass = 1.0f; private JointCollider[] jointsCollider = new JointCollider[(int)JointType.Count]; private Transform rootJoint = null; private Transform rootJointParent = null; private Rigidbody rootJointRigidbody = null; private Vector3 lastRootPos; private Quaternion lastRotation; private bool isInit = false; private bool isTracked = true; private const int k_MaxCollisionCount = 100; private readonly ContactPoint[] contactPointsBuffer = new ContactPoint[k_MaxCollisionCount]; private readonly Vector3[] collisionsDirection = new Vector3[k_MaxCollisionCount]; private readonly object collisionLock = new object(); private int currentCollisionCount = 0; private bool isGrabbing = false; #region MonoBehaviour private void OnEnable() { StartCoroutine(WaitForInit()); } private void OnDisable() { if (rootJoint != null) { JointCollider jointCollider = rootJoint.GetComponent(); if (jointCollider != null) { jointCollider.RemoveJointCollisionListener(OnJointCollision); } Destroy(rootJoint.gameObject); } isInit = false; } private void Start() { JointCollider[] fullJointsCollider = FindObjectsOfType(true); for (int i = 0; i < fullJointsCollider.Length - 1; i++) { for (int j = i + 1; j < fullJointsCollider.Length; j++) { Physics.IgnoreCollision(fullJointsCollider[i].Collider, fullJointsCollider[j].Collider); } } } private void Update() { if (!isInit) { return; } HandPose handPose = HandPoseProvider.GetHandPose(m_HandMesh.isLeft ? HandPoseType.HAND_LEFT : HandPoseType.HAND_RIGHT); if (isTracked != handPose.IsTracked()) { isTracked = handPose.IsTracked(); ChangeTrackingState(isTracked); } if (!isTracked) { return; } for (int i = 0; i < jointsCollider.Length; i++) { if (jointsCollider[i] == null) { continue; } handPose.GetPosition((JointType)i, out Vector3 jointPosition); handPose.GetRotation((JointType)i, out Quaternion jointRotation); if (i == (int)JointType.Wrist) { if (lastRootPos == Vector3.zero || lastRotation == Quaternion.identity) { rootJoint.localPosition = jointPosition; rootJoint.localRotation = jointRotation; } lastRootPos = jointPosition; lastRotation = jointRotation; } jointsCollider[i].transform.rotation = rootJointParent.rotation * jointRotation; } if (!isGrabbing) { m_HandMesh.SetJointPositionAndRotation(JointType.Wrist, rootJoint.position, rootJoint.rotation); } } private void FixedUpdate() { if (lastRootPos == Vector3.zero || lastRotation == Quaternion.identity) { return; } if (isGrabbing) { #if UNITY_6000_0_OR_NEWER rootJointRigidbody.linearVelocity = Vector3.zero; #else rootJointRigidbody.velocity = Vector3.zero; #endif rootJointRigidbody.angularVelocity = Vector3.zero; rootJoint.localPosition = lastRootPos; rootJoint.localRotation = lastRotation; } else { UpdateVelocity(); UpdateAngularVelocity(); } } #endregion private IEnumerator WaitForInit() { yield return new WaitUntil(() => m_HandMesh != null); if (rootJoint != null) { JointCollider jointCollider = rootJoint.GetComponent(); jointCollider.AddJointCollisionListener(OnJointCollision); rootJointRigidbody = jointCollider.InitRigidbody(handMass); isInit = true; DEBUG("Hand Collider init successfully."); } } /// /// Initializes by copying the structure of the hand mesh and sequentially adding joint collider. /// public void InitJointColliders(Transform handRootJoint) { rootJointParent = handRootJoint.parent; rootJoint = Instantiate(handRootJoint, rootJointParent); rootJoint.name = handRootJoint.name; List totalTransforms = new List() { rootJoint }; GetAllChildrenTransforms(rootJoint, ref totalTransforms); for (int i = 0; i < (int)JointType.Count; i++) { Transform target = totalTransforms.FirstOrDefault(x => x.name == JointsName[i]); if (target != null) { JointCollider jointCollider = target.gameObject.AddComponent(); jointCollider.SetJointId(i); jointsCollider[i] = jointCollider; } } } private void GetAllChildrenTransforms(Transform parent, ref List childrenTransforms) { foreach (Transform child in parent) { childrenTransforms.Add(child); GetAllChildrenTransforms(child, ref childrenTransforms); } } /// /// Updates the velocity of a Rigidbody. /// private void UpdateVelocity() { Vector3 vel = (lastRootPos - rootJoint.position) / Time.deltaTime; if (IsValidVelocity(vel)) { lock (collisionLock) { if (currentCollisionCount > 0) { float minAngle = float.MaxValue; Vector3 closestDirection = Vector3.zero; for (int i = 0; i < currentCollisionCount; i++) { Vector3 direction = collisionsDirection[i]; float angle = Mathf.Abs(Vector3.Angle(direction, vel)); if (angle < minAngle) { minAngle = angle; closestDirection = direction; } } Vector3 adjustedDirection = closestDirection; if (Vector3.Dot(vel, closestDirection) > 0) { adjustedDirection *= -1f; } vel = Vector3.ProjectOnPlane(vel, adjustedDirection); if (vel.magnitude > 1) { vel.Normalize(); } currentCollisionCount = 0; } } #if UNITY_6000_0_OR_NEWER rootJointRigidbody.linearVelocity = vel; #else rootJointRigidbody.velocity = vel; #endif } } /// /// Updates the angularVelocity of a Rigidbody. /// private void UpdateAngularVelocity() { Quaternion diffRotation = Quaternion.Inverse(rootJoint.rotation) * lastRotation; diffRotation.ToAngleAxis(out float angleInDegree, out Vector3 rotationAxis); Vector3 angVel = (angleInDegree * rotationAxis * Mathf.Deg2Rad) / Time.deltaTime; if (IsValidVelocity(angVel)) { rootJointRigidbody.angularVelocity = angVel; } } /// /// Checks if the vector is valid. /// /// The vector to be checked. /// rue if the vector is valid; otherwise, false. private bool IsValidVelocity(Vector3 vector) { return !float.IsNaN(vector.x) && !float.IsNaN(vector.y) && !float.IsNaN(vector.z) && !float.IsInfinity(vector.x) && !float.IsInfinity(vector.y) && !float.IsInfinity(vector.z); } #region Event CallBack /// /// When tracking state changing, reset the pose and enable/disable collider. /// /// The bool that tracking state. private void ChangeTrackingState(bool isTracked) { if (!isTracked) { lastRootPos = Vector3.zero; lastRotation = Quaternion.identity; #if UNITY_6000_0_OR_NEWER rootJointRigidbody.linearVelocity = Vector3.zero; #else rootJointRigidbody.velocity = Vector3.zero; #endif rootJointRigidbody.angularVelocity = Vector3.zero; } foreach (JointCollider jointCollider in jointsCollider) { jointCollider.Collider.enabled = m_EnableCollider && isTracked; } } /// /// Called when the hand begins grabbing an object. /// /// The grabber that grabbing something. public void OnHandBeginGrab(IGrabber grabber) { EnableKinematic(true); isGrabbing = true; } /// /// Called when the hand ends grabbing an object. /// /// The grabber that releasing something. public void OnHandEndGrab(IGrabber grabber) { EnableKinematic(false); isGrabbing = false; } /// /// Enable/Disable the rigidbody of hand collider. /// /// The bool that enable or disable the rigidbody. private void EnableKinematic(bool enable) { if (rootJointRigidbody != null) { rootJointRigidbody.isKinematic = enable; } } /// /// Called when a joint collider collides with another object. /// /// The collision data. /// The state of the collision. private void OnJointCollision(Collision collision, JointCollider.CollisionState state) { if (isGrabbing) { return; } switch (state) { case JointCollider.CollisionState.Enter: case JointCollider.CollisionState.Stay: if (collision.contactCount > 0 && (collision.rigidbody == null || collision.rigidbody.isKinematic)) { lock (collisionLock) { currentCollisionCount = Mathf.Min(contactPointsBuffer.Length, collision.contactCount); collision.GetContacts(contactPointsBuffer); for (int i = 0; i < currentCollisionCount; i++) { collisionsDirection[i] = contactPointsBuffer[i].normal * -1f; } } } break; } } #endregion } }