// "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
}
}