using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction { /// /// This class is designed to manage the positions of various joint nodes in the hand model. /// public class HandMeshManager : MonoBehaviour { #region Log const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.HandMeshManager"; 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 [SerializeField] private Handedness m_Handedness; public bool isLeft { get { return m_Handedness == Handedness.Left; } } [SerializeField] private bool m_EnableCollider = false; public bool enableCollider { get { return m_EnableCollider; } set { m_EnableCollider = value; if (m_EnableCollider && m_HandCollider == null) { InitHandCollider(); } else if (m_HandCollider != null) { m_HandCollider.enableCollider = m_EnableCollider; } } } private HandColliderController m_HandCollider = null; public HandColliderController handCollider => m_HandCollider; [SerializeField] private Transform[] m_HandJoints = new Transform[k_JointCount]; private const int k_JointCount = (int)JointType.Count; private const int k_RootId = (int)JointType.Wrist; private bool updateRoot = false; private int updatedFrameCount = 0; private bool isGrabbing = false; private bool isConstraint = false; private HandGrabInteractor handGrabber; private Quaternion[] grabJointsRotation = new Quaternion[k_JointCount]; #region MonoBehaviours private void OnEnable() { bool empty = m_HandJoints.Any(x => x == null); if (empty) { ClearJoints(); FindJoints(); } if (m_EnableCollider && m_HandCollider == null) { InitHandCollider(); } MeshHandPose meshHandPose = transform.gameObject.AddComponent(); meshHandPose.SetHandMeshRenderer(this); } private void OnDisable() { if (m_HandCollider != null) { Destroy(m_HandCollider); } MeshHandPose meshHandPose = transform.GetComponent(); if (meshHandPose != null) { Destroy(meshHandPose); } } private void Update() { HandData handData = CachedHand.Get(isLeft); EnableHandModel(handData.isTracked); if (!handData.isTracked) { return; } //if (m_UseRuntimeModel || (!m_UseRuntimeModel && m_UseScale)) //{ // Vector3 scale = Vector3.one; // if (GetHandScale(ref scale, isLeft)) // { // m_HandJoints[rootId].localScale = scale; // } // else // { // m_HandJoints[rootId].localScale = Vector3.one; // } //} if (Time.frameCount - updatedFrameCount > 5) { updateRoot = false; } if (!updateRoot) { Vector3 rootPosition = Vector3.zero; Quaternion rootRotation = Quaternion.identity; handData.GetJointPosition((JointType)k_RootId, ref rootPosition); handData.GetJointRotation((JointType)k_RootId, ref rootRotation); m_HandJoints[k_RootId].position = m_HandJoints[k_RootId].parent.position + rootPosition; m_HandJoints[k_RootId].rotation = m_HandJoints[k_RootId].parent.rotation * rootRotation; } for (int i = 0; i < m_HandJoints.Length; i++) { if (m_HandJoints[i] == null || i == k_RootId) { continue; } Quaternion jointRotation = Quaternion.identity; handData.GetJointRotation((JointType)i, ref jointRotation); m_HandJoints[i].rotation = m_HandJoints[k_RootId].parent.rotation * jointRotation; } if (isGrabbing) { for (int i = 0; i < m_HandJoints.Length; i++) { if (i == k_RootId) { continue; } Quaternion currentRotation = m_HandJoints[i].rotation; Quaternion maxRotation = m_HandJoints[i].parent.rotation * grabJointsRotation[i]; if (isConstraint || handGrabber.IsRequiredJoint((JointType)i) || OverFlex(currentRotation, maxRotation) >= 0 || FlexAngle(currentRotation, maxRotation) >= 110) { m_HandJoints[i].rotation = maxRotation; } } } } #endregion #region Public Interface public void OnHandBeginGrab(IGrabber grabber) { if (grabber is HandGrabInteractor handGrabber) { this.handGrabber = handGrabber; if (grabber.grabbable is HandGrabInteractable handGrabbable) { if (handGrabbable.bestGrabPose != GrabPose.Identity) { if (handGrabbable.bestGrabPose.recordedGrabRotations.Length == (int)JointType.Count) { grabJointsRotation = handGrabbable.bestGrabPose.recordedGrabRotations; } else if (handGrabbable.bestGrabPose.handGrabGesture != HandGrabGesture.Identity) { for (int i = 0; i < grabJointsRotation.Length; i++) { HandData.GetDefaultJointRotationInGesture(isLeft, handGrabbable.bestGrabPose.handGrabGesture, (JointType)i, ref grabJointsRotation[i]); } } isGrabbing = true; isConstraint = handGrabbable.isContraint; } } } if (m_EnableCollider && m_HandCollider != null) { m_HandCollider.OnHandBeginGrab(grabber); } } public void OnHandEndGrab(IGrabber grabber) { isGrabbing = false; this.handGrabber = null; if (m_EnableCollider && handCollider != null) { handCollider.OnHandEndGrab(grabber); } } /// /// Gets the position and rotation of the specified joint. /// /// The joint type to get position and rotation from. /// The position of the joint. /// The rotation of the joint. /// Whether to get the local position and rotation. /// True if the joint position and rotation are successfully obtained; otherwise, false. public bool GetJointPositionAndRotation(JointType joint, out Vector3 position, out Quaternion rotation, bool local = false) { position = Vector3.zero; rotation = Quaternion.identity; int jointId = (int)joint; if (jointId >= 0 && jointId < k_JointCount && m_HandJoints[jointId] != null) { if (!local) { position = m_HandJoints[jointId].position; rotation = m_HandJoints[jointId].rotation; } else { position = m_HandJoints[jointId].localPosition; rotation = m_HandJoints[jointId].localRotation; } return true; } return false; } /// /// Sets the position and rotation of the specified joint. /// /// The joint type to set position and rotation for. /// The new position of the joint. /// The new rotation of the joint. /// Whether to set the local position and rotation. /// True if the joint position and rotation are successfully set; otherwise, false. public bool SetJointPositionAndRotation(JointType joint, Vector3 position, Quaternion rotation, bool local = false) { int jointId = (int)joint; if (jointId >= 0 && jointId < k_JointCount && m_HandJoints[jointId] != null) { if (!local) { m_HandJoints[jointId].position = position; m_HandJoints[jointId].rotation = rotation; } else { m_HandJoints[jointId].localPosition = position; m_HandJoints[jointId].localRotation = rotation; } if (joint == JointType.Wrist) { updatedFrameCount = Time.frameCount; updateRoot = true; } return true; } return false; } #endregion #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 void GetAllChildrenTransforms(Transform parent, ref List childrenTransforms) { foreach (Transform child in parent) { childrenTransforms.Add(child); GetAllChildrenTransforms(child, ref childrenTransforms); } } public void FindJoints() { List totalTransforms = new List() { transform }; GetAllChildrenTransforms(transform, ref totalTransforms); for (int i = 0; i < m_HandJoints.Length; i++) { Transform jointTransform = totalTransforms.FirstOrDefault(x => x.name == JointsName[i]); if (jointTransform != null) { m_HandJoints[i] = jointTransform; } } } public void ClearJoints() { Array.Clear(m_HandJoints, 0, m_HandJoints.Length); } private void InitHandCollider() { m_HandCollider = gameObject.AddComponent(); m_HandCollider.InitJointColliders(m_HandJoints[k_RootId]); m_HandCollider.handMesh = this; } private void EnableHandModel(bool enable) { if (m_HandJoints[k_RootId].gameObject.activeSelf != enable) { m_HandJoints[k_RootId].gameObject.SetActive(enable); } } /// /// Calculate whether the current rotation exceeds the maximum rotation. /// If the product is greater than 0, it exceeds. /// /// Current rotation /// Maximum rotation /// The return value represents the dot product between the cross product of two rotations and the -x axis direction of the current rotation. private float OverFlex(Quaternion currentRot, Quaternion maxRot) { Vector3 currFwd = currentRot * Vector3.forward; Vector3 maxFwd = maxRot * Vector3.forward; return Vector3.Dot(currentRot * Vector3.left, Vector3.Cross(currFwd, maxFwd)); } /// /// Calculate the angle between the y-axis directions of two rotations. /// /// Current rotation /// Maximum rotation /// The return value represents the angle between the up directions of the two rotation private float FlexAngle(Quaternion currentRot, Quaternion maxRot) { Vector3 currFwd = currentRot * Vector3.up; Vector3 maxFwd = maxRot * Vector3.up; return Mathf.Acos(Vector3.Dot(currFwd, maxFwd) / (currFwd.magnitude * maxFwd.magnitude)) * Mathf.Rad2Deg; } } }