// "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; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction { public class GrabColliderManager : MonoBehaviour { const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.GrabColliderManager"; 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}"); } /// /// The struct is designed to record movable colliders, /// including which grabbable they belong to, which joints collisioned with, and whether they have been grabbed. /// private struct MovableHitInfo { public struct JointHitInfo { public JointType joint; public Vector3 hitOffset; public int hitTime { get; private set; } public JointHitInfo(JointType in_JointType, Vector3 in_HitOffset) { joint = in_JointType; hitOffset = in_HitOffset; hitTime = Time.frameCount; } public void Update() { hitTime = Time.frameCount; } } public HandGrabInteractable grabbable; public List jointHitInfos; public bool grabbed; public bool stopMove; public MovableHitInfo(HandGrabInteractable in_Grabbable, JointType in_Joint, Vector3 in_Offset) { grabbable = in_Grabbable; jointHitInfos = new List() { new JointHitInfo(in_Joint, in_Offset) }; grabbed = false; stopMove = false; } public void Update(bool in_Grabbed, bool in_StopMove) { grabbed = in_Grabbed; stopMove = in_StopMove; } public void Reset() { grabbed = false; stopMove = false; } /// /// Add a JointType. If it's already in the dictionary, update the time. /// /// The joint which needs to be added. public void AddJoint(JointType joint, Vector3 offset) { int hitId = jointHitInfos.FindIndex(x => x.joint == joint); if (hitId == -1) { jointHitInfos.Add(new JointHitInfo(joint, offset)); } else { JointHitInfo jointHitInfo = jointHitInfos[hitId]; jointHitInfo.Update(); jointHitInfos[hitId] = jointHitInfo; } } /// /// Remove a JointType and check if it has been grabbed. /// /// The joint which needs to be removed. public void RemoveJoint(JointType joint) { int hitId = jointHitInfos.FindIndex(x => x.joint == joint); if (hitId != -1) { jointHitInfos.RemoveAt(hitId); } } } [SerializeField] private HandMeshManager jointManager; private GrabCollider[] jointsCollider = new GrabCollider[(int)JointType.Count]; private Pose[] jointsPrevFramePose = new Pose[(int)JointType.Count]; private bool isImmovableCollision = false; private bool isGrabbing = false; private List movableHits = new List(); private Dictionary immovableHits = new Dictionary(); public delegate void OnImmovableCollision(bool enable); private OnImmovableCollision immovableCollisionHandler; private void Awake() { if (jointManager == null) { jointManager = transform.GetComponent(); if (jointManager == null) { ERROR("Failed to find HandJointManager."); } } } private void OnEnable() { if (jointManager != null) { jointManager.HandGrabber.AddBeginGrabListener(OnGrabberBeginGrab); jointManager.HandGrabber.AddEndGrabListener(OnGrabberEndGrab); } CreateJointsCollider(); } private void OnDisable() { if (jointManager != null) { jointManager.HandGrabber.RemoveBeginGrabListener(OnGrabberBeginGrab); jointManager.HandGrabber.RemoveEndGrabListener(OnGrabberEndGrab); } foreach (var collider in jointsCollider) { collider.RemoveListener(CollisionEvent); Destroy(collider); } Array.Clear(jointsCollider, 0, jointsCollider.Length); } private void Update() { if (jointManager == null) { return; } UpdateColliderPose(); if (!isGrabbing) { UpdateImmovable(); UpdateMovable(); } for (int i = 0; i < jointsCollider.Length; i++) { jointsPrevFramePose[i] = jointsCollider[i] == null ? Pose.identity : new Pose(jointsCollider[i].transform.position, jointsCollider[i].transform.rotation); } } /// /// Create colliders for each joint and set them do not collide with each other. /// private void CreateJointsCollider() { if (jointManager != null) { var cloneRoot = Instantiate(jointManager.HandRootJoint, jointManager.HandRootJoint.parent); cloneRoot.name = jointManager.HandRootJoint.name; List children = new List() { cloneRoot.gameObject }; GetChildren(cloneRoot, children); foreach (var child in children) { Transform target = jointManager.HandJoints.FirstOrDefault(x => x.name == child.name); if (target != null) { int index = Array.IndexOf(jointManager.HandJoints, target); GrabCollider grabCollider = child.AddComponent(); grabCollider.AddListener(CollisionEvent); grabCollider.SetJointId(index); jointsCollider[index] = grabCollider; } } } for (int i = 0; i < jointsCollider.Length; i++) { if (jointsCollider[i] == null) { continue; } for (int j = i + 1; j < jointsCollider.Length; j++) { if (jointsCollider[j] != null) { Physics.IgnoreCollision(jointsCollider[i].Collider, jointsCollider[j].Collider, true); } } } } private void GetChildren(Transform parent, List children) { foreach (Transform child in parent) { children.Add(child.gameObject); GetChildren(child, children); } } /// /// Update the position of the collider using the position of the joint. /// private void UpdateColliderPose() { HandData hand = CachedHand.Get(jointManager.IsLeft); bool isTracked = hand.isTracked; if (!isTracked) { return; } var parentTransform = jointManager.HandRootJoint.parent; var parentRotation = Matrix4x4.Rotate(parentTransform.rotation); Vector3 jointPosition = Vector3.zero; Quaternion jointRotation = Quaternion.identity; for (int i = 0; i < jointsCollider.Length; i++) { if (jointsCollider[i] == null) { continue; } hand.GetJointPosition((JointType)i, ref jointPosition); hand.GetJointRotation((JointType)i, ref jointRotation); if ((JointType)i == JointType.Wrist) { jointsCollider[i].transform.localPosition = jointPosition; jointsCollider[i].transform.localRotation = jointRotation; } jointsCollider[i].transform.rotation = (parentRotation * Matrix4x4.Rotate(jointRotation)).rotation; } } /// /// Save the hand pose if a collision has already occurred with a joint. /// private void UpdateImmovable() { bool isCollision = jointsCollider.Any(x => x != null && x.IsCollision); foreach (var jointCollider in jointsCollider) { jointCollider.Collider.enabled = isCollision ? jointCollider.IsCollision : true; } if (isImmovableCollision != isCollision) { isImmovableCollision = isCollision; immovableCollisionHandler?.Invoke(isImmovableCollision); } } /// /// Check all movableHits and move the object relative to the movement of the collisioned joint. /// private void UpdateMovable() { if (isImmovableCollision) { return; } const int k_MinCollisionTimeDiff = 5; const int k_MaxCollisionTimeDiff = 50; for (int i = movableHits.Count - 1; i >= 0; i--) { MovableHitInfo hit = movableHits[i]; if (hit.stopMove) { continue; } Vector3 totalPosition = Vector3.zero; Vector3 totalOffset = Vector3.zero; int validCount = 0; for (int j = hit.jointHitInfos.Count - 1; j >= 0; j--) { MovableHitInfo.JointHitInfo jointHit = hit.jointHitInfos[j]; int frameCountDiff = Time.frameCount - jointHit.hitTime; if (frameCountDiff > k_MinCollisionTimeDiff) { if (frameCountDiff > k_MaxCollisionTimeDiff) { hit.RemoveJoint(jointHit.joint); } continue; } int jointId = (int)jointHit.joint; Vector3 currentPose = jointsCollider[jointId].transform.position; Vector3 prevPose = jointsPrevFramePose[jointId].position; // Condition 1: Calculate the displacement between consecutive frames of joints, it should greater than 1E-6f as significant. // Condition 2: Calculate distance score relative to grabbable; the score of current pose should be greater than the previous pose. // Condition 3: The dot product of the vector between the current pose and the grabbable object, // and the vector representing finger movement direction should be less than 0. if (Vector3.Distance(prevPose, currentPose) > 1E-6f && movableHits[i].grabbable.CalculateDistanceScore(currentPose) >= movableHits[i].grabbable.CalculateDistanceScore(prevPose) && Vector3.Dot((currentPose - prevPose).normalized, (movableHits[i].grabbable.transform.position - prevPose).normalized) > 0) { validCount++; totalPosition += currentPose; totalOffset += jointHit.hitOffset; } } if (validCount > 0) { movableHits[i].grabbable.transform.position = (totalPosition - totalOffset) / validCount; } if (hit.jointHitInfos.Count == 0) { movableHits.RemoveAt(i); } else { movableHits[i] = hit; } } } /// /// Enable or disable the collider of joints. /// /// Enable (true) or disable (false) the colliders. public void EnableCollider(bool enable) { for (int i = 0; i < jointsCollider.Length; i++) { if (jointsCollider[i] != null) { jointsCollider[i].gameObject.SetActive(enable); } } } #region Collision Event /// /// Adds a listener for immovable collision events. /// /// The method to be called when an immovable collision occurs. public void AddImmovableCollisionListener(OnImmovableCollision handler) { immovableCollisionHandler += handler; } /// /// Removes a listener for immovable collision events. /// /// The method to be removed from the immovable collision event listeners. public void RemoveImmovableCollisionListener(OnImmovableCollision handler) { immovableCollisionHandler -= handler; } /// /// Event handler for when the grabber begins grabbing. /// /// The grabber of IGrabber. private void OnGrabberBeginGrab(IGrabber grabber) { isGrabbing = true; for (int i = 0; i < movableHits.Count; i++) { if (grabber.grabbable is HandGrabInteractable && (HandGrabInteractable)grabber.grabbable == movableHits[i].grabbable) { MovableHitInfo movableHit = movableHits[i]; movableHit.Update(true, true); movableHits[i] = movableHit; } } } private void OnGrabberEndGrab(IGrabber grabber) { isGrabbing = false; } /// /// Filter all collision events, check for grabbables, and update collision data. /// /// The joint which has been collision. /// The data of Collision. /// True when the collision event is OnCollisionEnter or OnCollisionStay. private void CollisionEvent(JointType joint, Collision collision, GrabCollider.CollisionState state) { bool isCollision = state != GrabCollider.CollisionState.end; Rigidbody rigidbody = collision.rigidbody; GrabManager.GetFirstHandGrabbableFromParent(collision.collider.gameObject, out HandGrabInteractable grabbable); if (collision.rigidbody == null && (grabbable == null || grabbable != null && !grabbable.enabled)) { return; } if ((rigidbody == null || rigidbody.isKinematic) && grabbable != null && grabbable.forceMovable) { if (isCollision) { UpdateMovableHits(joint, grabbable); } else { RemoveMovableHits(joint, grabbable); } } else if ((rigidbody != null && rigidbody.isKinematic) || (grabbable != null && !grabbable.forceMovable)) { UpdateImmovableHIts(joint, collision.collider, isCollision); } } private void UpdateMovableHits(JointType joint, HandGrabInteractable grabbable) { int index = movableHits.FindIndex(x => x.grabbable == grabbable); if (index != -1) { MovableHitInfo moveable = movableHits[index]; moveable.AddJoint(joint, jointsCollider[(int)joint].transform.position - grabbable.transform.position); movableHits[index] = moveable; } else { MovableHitInfo moveable = new MovableHitInfo(grabbable, joint, jointsCollider[(int)joint].transform.position - grabbable.transform.position); movableHits.Add(moveable); } } private void RemoveMovableHits(JointType joint, HandGrabInteractable grabbable) { int index = movableHits.FindIndex(x => x.grabbable == grabbable); if (index != -1) { MovableHitInfo movable = movableHits[index]; movable.RemoveJoint(joint); if (movable.jointHitInfos.Count == 0) { movableHits.Remove(movable); } else { movableHits[index] = movable; } } } private void UpdateImmovableHIts(JointType joint, Collider collider, bool isCollision) { GrabCollider grabCollider = jointsCollider[(int)joint]; grabCollider.IsCollision = isCollision; if (isCollision && !immovableHits.ContainsKey(grabCollider)) { immovableHits.Add(grabCollider, collider); } else if (!isCollision && immovableHits.ContainsKey(grabCollider)) { immovableHits.Remove(grabCollider); } } #endregion } }