version 2.4.0

This commit is contained in:
Sean Lu(呂祥榮)
2024-07-03 14:58:53 +08:00
parent 7f2a459592
commit 3dd72f5f56
162 changed files with 23632 additions and 27154 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ff1e9b9f321387c4e86e6a5fa6cd65c9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,44 @@
// "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 HTCs SDK license agreement terms and
// conditions signed by you and all SDK and API requirements,
// specifications, and documentation provided by HTC to You."
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(CustomGrabPose))]
public class CustomGrabPoseEditor : UnityEditor.Editor
{
private CustomGrabPose m_GrabPoseDesigner;
private void Awake()
{
m_GrabPoseDesigner = target as CustomGrabPose;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
GUILayout.Space(10f);
ShowGrabPosesMenu();
}
private void ShowGrabPosesMenu()
{
if (GUILayout.Button("Save HandGrab Pose"))
{
m_GrabPoseDesigner.FindNearInteractable();
m_GrabPoseDesigner.SavePoseWithCandidate();
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c01f88bc88bb27849a723888721c96f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,382 @@
// "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 HTCs SDK license agreement terms and
// conditions signed by you and all SDK and API requirements,
// specifications, and documentation provided by HTC to You."
using UnityEngine;
using System;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandGrabInteractable))]
public class HandGrabInteractableEditor : UnityEditor.Editor
{
private static HandGrabInteractable activeGrabbable = null;
private HandGrabInteractable handGrabbable = null;
private SerializedProperty m_IsGrabbable, m_FingerRequirement, m_Rigidbody, m_GrabPoses, m_ShowAllIndicator, m_OnBeginGrabbed, m_OnEndGrabbed,
m_OneHandContraintMovement, m_PreviewIndex, grabPoseName, gestureThumbPose, gestureIndexPose, gestureMiddlePose, gestureRingPose, gesturePinkyPose,
recordedGrabRotations, isLeft, enableIndicator, autoIndicator, indicatorObject, grabOffset;
private ReorderableList grabPoseList;
private bool showGrabPoses = false;
private bool showConstraint = false;
private bool showEvent = false;
private void OnEnable()
{
handGrabbable = target as HandGrabInteractable;
m_IsGrabbable = serializedObject.FindProperty("m_IsGrabbable");
m_FingerRequirement = serializedObject.FindProperty("m_FingerRequirement");
m_Rigidbody = serializedObject.FindProperty("m_Rigidbody");
m_GrabPoses = serializedObject.FindProperty("m_GrabPoses");
m_ShowAllIndicator = serializedObject.FindProperty("m_ShowAllIndicator");
m_OnBeginGrabbed = serializedObject.FindProperty("m_OnBeginGrabbed");
m_OnEndGrabbed = serializedObject.FindProperty("m_OnEndGrabbed");
m_OneHandContraintMovement = serializedObject.FindProperty("m_OneHandContraintMovement");
m_PreviewIndex = serializedObject.FindProperty("m_PreviewIndex");
#region ReorderableList
grabPoseList = new ReorderableList(serializedObject, m_GrabPoses, true, true, true, true);
grabPoseList.drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "Grab Pose List");
};
grabPoseList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
{
if (!UpdateGrabPose(m_GrabPoses.GetArrayElementAtIndex(index))) { return; }
if (string.IsNullOrEmpty(grabPoseName.stringValue))
{
grabPoseName.stringValue = $"Grab Pose {index + 1}";
}
Rect elementRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
grabPoseName.stringValue = EditorGUI.TextField(elementRect, grabPoseName.stringValue);
DrawGrabGesture(ref elementRect);
DrawHandedness(ref elementRect);
DrawIndicator(ref elementRect);
DrawMirrorButton(ref elementRect);
DrawPoseOffset(ref elementRect);
DrawFineTune(ref elementRect, index);
};
grabPoseList.elementHeightCallback = (int index) =>
{
if (!UpdateGrabPose(m_GrabPoses.GetArrayElementAtIndex(index))) { return EditorGUIUtility.singleLineHeight; }
// Including Title, Handness, Show Indicator, Mirror Pose, Position, Rotation, Fine Tune
int minHeight = 7;
// To Show GrabGesture
if (recordedGrabRotations.arraySize == 0)
{
minHeight += 5;
}
if (enableIndicator.boolValue)
{
// To Show Auto Indicator
minHeight += 1;
// To Show Indicator Gameobject
if (!autoIndicator.boolValue)
{
minHeight += 1;
}
}
return EditorGUIUtility.singleLineHeight * minHeight + EditorGUIUtility.standardVerticalSpacing * minHeight;
};
grabPoseList.onAddCallback = (ReorderableList list) =>
{
m_GrabPoses.arraySize++;
if (UpdateGrabPose(m_GrabPoses.GetArrayElementAtIndex(list.count - 1)))
{
grabPoseName.stringValue = $"Grab Pose {list.count}";
}
};
#endregion
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_Rigidbody);
EditorGUILayout.PropertyField(m_IsGrabbable);
EditorGUILayout.PropertyField(m_FingerRequirement);
showGrabPoses = EditorGUILayout.Foldout(showGrabPoses, "Grab Pose Settings");
if (showGrabPoses)
{
if (m_GrabPoses.arraySize == 0)
{
grabPoseList.elementHeight = EditorGUIUtility.singleLineHeight;
}
grabPoseList.DoLayoutList();
bool isToggle = EditorGUILayout.Toggle("Show All Indicator", m_ShowAllIndicator.boolValue);
if (isToggle != m_ShowAllIndicator.boolValue)
{
m_ShowAllIndicator.boolValue = isToggle;
for (int i = 0; i < m_GrabPoses.arraySize; i++)
{
if (UpdateGrabPose(m_GrabPoses.GetArrayElementAtIndex(i)))
{
enableIndicator.boolValue = m_ShowAllIndicator.boolValue;
}
}
}
}
showEvent = EditorGUILayout.Foldout(showEvent, "Grabbed Event");
if (showEvent)
{
EditorGUILayout.PropertyField(m_OnBeginGrabbed);
EditorGUILayout.PropertyField(m_OnEndGrabbed);
}
showConstraint = EditorGUILayout.Foldout(showConstraint, "Constraint Movement (Optional)");
if (showConstraint)
{
EditorGUILayout.PropertyField(m_OneHandContraintMovement);
}
serializedObject.ApplyModifiedProperties();
}
private bool UpdateGrabPose(SerializedProperty grabPose)
{
SerializedProperty indicator = grabPose.FindPropertyRelative("indicator");
if (grabPose == null || indicator == null) { return false; }
grabPoseName = grabPose.FindPropertyRelative("grabPoseName");
gestureThumbPose = grabPose.FindPropertyRelative("handGrabGesture.thumbPose");
gestureIndexPose = grabPose.FindPropertyRelative("handGrabGesture.indexPose");
gestureMiddlePose = grabPose.FindPropertyRelative("handGrabGesture.middlePose");
gestureRingPose = grabPose.FindPropertyRelative("handGrabGesture.ringPose");
gesturePinkyPose = grabPose.FindPropertyRelative("handGrabGesture.pinkyPose");
recordedGrabRotations = grabPose.FindPropertyRelative("recordedGrabRotations");
isLeft = grabPose.FindPropertyRelative("isLeft");
enableIndicator = indicator.FindPropertyRelative("enableIndicator");
autoIndicator = indicator.FindPropertyRelative("autoIndicator");
indicatorObject = indicator.FindPropertyRelative("target");
grabOffset = grabPose.FindPropertyRelative("grabOffset");
return true;
}
private void AddElementHeight(ref Rect rect)
{
rect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
}
private void DrawGrabGesture(ref Rect rect)
{
if (recordedGrabRotations.arraySize == 0)
{
AddElementHeight(ref rect);
EditorGUI.PropertyField(rect, gestureThumbPose);
AddElementHeight(ref rect);
EditorGUI.PropertyField(rect, gestureIndexPose);
AddElementHeight(ref rect);
EditorGUI.PropertyField(rect, gestureMiddlePose);
AddElementHeight(ref rect);
EditorGUI.PropertyField(rect, gestureRingPose);
AddElementHeight(ref rect);
EditorGUI.PropertyField(rect, gesturePinkyPose);
}
}
private void DrawHandedness(ref Rect rect)
{
AddElementHeight(ref rect);
bool isToggle = EditorGUI.Toggle(rect, "Is Left", isLeft.boolValue);
if (isToggle != isLeft.boolValue)
{
isLeft.boolValue = isToggle;
SwitchRotations(ref recordedGrabRotations);
}
}
private void DrawIndicator(ref Rect rect)
{
AddElementHeight(ref rect);
enableIndicator.boolValue = EditorGUI.Toggle(rect, "Show Indicator", enableIndicator.boolValue);
if (enableIndicator.boolValue)
{
AddElementHeight(ref rect);
autoIndicator.boolValue = EditorGUI.Toggle(rect, "Auto Generator Indicator", autoIndicator.boolValue);
if (!autoIndicator.boolValue)
{
AddElementHeight(ref rect);
indicatorObject.objectReferenceValue = (GameObject)EditorGUI.ObjectField(rect, "Indicator", (GameObject)indicatorObject.objectReferenceValue, typeof(GameObject), true);
}
}
else
{
m_ShowAllIndicator.boolValue = false;
}
}
private void DrawMirrorButton(ref Rect rect)
{
AddElementHeight(ref rect);
Rect labelRect = new Rect(rect.x, rect.y, EditorGUIUtility.labelWidth, rect.height);
EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Mirror Pose"));
Rect mirrorXRect = new Rect(rect.x + EditorGUIUtility.labelWidth + EditorGUIUtility.standardVerticalSpacing, rect.y,
(rect.width - EditorGUIUtility.labelWidth - EditorGUIUtility.standardVerticalSpacing * 4) / 3, rect.height);
Rect mirrorYRect = new Rect(mirrorXRect.x + mirrorXRect.width + EditorGUIUtility.standardVerticalSpacing, rect.y, mirrorXRect.width, rect.height);
Rect mirrorZRect = new Rect(mirrorYRect.x + mirrorYRect.width + EditorGUIUtility.standardVerticalSpacing, rect.y, mirrorYRect.width, rect.height);
if (GUI.Button(mirrorXRect, "Align X axis"))
{
MirrorPose(ref grabOffset, Vector3.right);
}
if (GUI.Button(mirrorYRect, "Align Y axis"))
{
MirrorPose(ref grabOffset, Vector3.up);
}
if (GUI.Button(mirrorZRect, "Align Z axis"))
{
MirrorPose(ref grabOffset, Vector3.forward);
}
}
private void DrawPoseOffset(ref Rect rect)
{
SerializedProperty srcPos = grabOffset.FindPropertyRelative("sourcePosition");
SerializedProperty srcRot = grabOffset.FindPropertyRelative("sourceRotation");
SerializedProperty tgtPos = grabOffset.FindPropertyRelative("targetPosition");
SerializedProperty tgtRot = grabOffset.FindPropertyRelative("targetRotation");
AddElementHeight(ref rect);
EditorGUI.Vector3Field(rect, "Position Offset", tgtPos.vector3Value - srcPos.vector3Value);
AddElementHeight(ref rect);
Vector3 rotEulerAngles = (Quaternion.Inverse(srcRot.quaternionValue) * tgtRot.quaternionValue).eulerAngles;
for (int i = 0; i < 3; i++)
{
if (rotEulerAngles[i] > 180)
{
rotEulerAngles[i] = 360.0f - rotEulerAngles[i];
}
}
EditorGUI.Vector3Field(rect, "Rotation Offset", rotEulerAngles);
}
private void DrawFineTune(ref Rect rect, int index)
{
AddElementHeight(ref rect);
Rect labelRect = new Rect(rect.x, rect.y, EditorGUIUtility.labelWidth, rect.height);
EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Fine Tune"));
Rect previewRect = new Rect(rect.x + EditorGUIUtility.labelWidth + EditorGUIUtility.standardVerticalSpacing, rect.y,
(rect.width - EditorGUIUtility.labelWidth - EditorGUIUtility.standardVerticalSpacing * 4) / 2, rect.height);
Rect updateRect = new Rect(previewRect.x + previewRect.width + EditorGUIUtility.standardVerticalSpacing, rect.y, previewRect.width, rect.height);
if (GUI.Button(previewRect, "Preview Grab Pose") && Application.isPlaying)
{
activeGrabbable = handGrabbable;
m_PreviewIndex.intValue = index;
ShowMeshHandPose();
}
GUI.enabled = activeGrabbable == handGrabbable && m_PreviewIndex.intValue == index;
if (GUI.Button(updateRect, "Update Grab Pose"))
{
UpdateGrabPose();
}
GUI.enabled = true;
}
/// <summary>
/// Convert the rotation of joints of the current hand into those of another hand.
/// </summary>
/// <param name="rotations">Rotation of joints of the current hand.</param>
private void SwitchRotations(ref SerializedProperty rotations)
{
for (int i = 0; i < rotations.arraySize; i++)
{
Quaternion rotation = rotations.GetArrayElementAtIndex(i).quaternionValue;
Quaternion newRotation = Quaternion.Euler(rotation.eulerAngles.x, -rotation.eulerAngles.y, -rotation.eulerAngles.z);
rotations.GetArrayElementAtIndex(i).quaternionValue = newRotation;
}
}
/// <summary>
/// Mirrors the pose properties (position and rotation) of a serialized object along a specified mirror axis.
/// </summary>
/// <param name="pose">The serialized property representing the pose to be mirrored.</param>
/// <param name="mirrorAxis">The axis along which the mirroring should occur.</param>
private void MirrorPose(ref SerializedProperty pose, Vector3 mirrorAxis)
{
Vector3 sourcePosition = grabOffset.FindPropertyRelative("sourcePosition").vector3Value;
Quaternion sourceRotation = grabOffset.FindPropertyRelative("sourceRotation").quaternionValue;
Vector3 targetPosition = grabOffset.FindPropertyRelative("targetPosition").vector3Value;
Quaternion targetRotation = grabOffset.FindPropertyRelative("targetRotation").quaternionValue;
Vector3 reflectNormal = targetRotation * mirrorAxis;
Vector3 diffPos = sourcePosition - targetPosition;
Vector3 mirrorPosition = targetPosition + Vector3.Reflect(diffPos, reflectNormal);
pose.FindPropertyRelative("sourcePosition").vector3Value = mirrorPosition;
Vector3 sourceForward = sourceRotation * Vector3.forward;
Vector3 sourceUp = sourceRotation * Vector3.up;
Quaternion mirroredRotation = Quaternion.LookRotation(Vector3.Reflect(sourceForward, reflectNormal), Vector3.Reflect(sourceUp, reflectNormal));
pose.FindPropertyRelative("sourceRotation").quaternionValue = mirroredRotation;
}
/// <summary>
/// Obtain the MeshHand and set its position and rotation based on the grabOffset of grabpose.
/// </summary>
private void ShowMeshHandPose()
{
HandPose handPose = HandPoseProvider.GetHandPose(isLeft.boolValue ? HandPoseType.MESH_LEFT : HandPoseType.MESH_RIGHT);
if (handPose != null && handPose is MeshHandPose meshHandPose)
{
GrabOffset grabOffsetObj = handGrabbable.grabPoses[m_PreviewIndex.intValue].grabOffset;
Quaternion handRot = handGrabbable.transform.rotation * Quaternion.Inverse(grabOffsetObj.rotOffset);
Quaternion handRotDiff = handRot * Quaternion.Inverse(grabOffsetObj.sourceRotation);
Vector3 handPos = handGrabbable.transform.position - handRotDiff * grabOffsetObj.posOffset;
meshHandPose.SetJointPose(JointType.Wrist, new Pose(handPos, handRot));
foreach (JointType joint in Enum.GetValues(typeof(JointType)))
{
if (joint == JointType.Wrist || joint == JointType.Count) { continue; }
meshHandPose.GetPosition(joint, out Vector3 pos, local: true);
Quaternion rot = recordedGrabRotations.GetArrayElementAtIndex((int)joint).quaternionValue;
meshHandPose.SetJointPose(joint, new Pose(pos, rot), local: true);
}
}
}
/// <summary>
/// Update the grabpose based on position and rotation of the MeshHand and Object.
/// </summary>
private void UpdateGrabPose()
{
HandPose handPose = HandPoseProvider.GetHandPose(isLeft.boolValue ? HandPoseType.MESH_LEFT : HandPoseType.MESH_RIGHT);
if (handPose != null && handPose is MeshHandPose meshHandPose)
{
meshHandPose.GetPosition(JointType.Wrist, out Vector3 wristPosition);
meshHandPose.GetRotation(JointType.Wrist, out Quaternion wristRotation);
Quaternion[] fingerJointRotation = new Quaternion[(int)JointType.Count];
for (int i = 0; i < fingerJointRotation.Length; i++)
{
meshHandPose.GetRotation((JointType)i, out Quaternion jointRotation, local: true);
fingerJointRotation[i] = jointRotation;
}
GrabPose grabPose = handGrabbable.grabPoses[m_PreviewIndex.intValue];
grabPose.Update(grabPoseName.stringValue, fingerJointRotation, isLeft.boolValue);
grabPose.grabOffset.Update(wristPosition, wristRotation, handGrabbable.transform.position, handGrabbable.transform.rotation);
handGrabbable.grabPoses[m_PreviewIndex.intValue] = grabPose;
GrabbablePoseRecorder.SaveChanges();
}
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ba007edb5befc349a95aa1cfa7cb6c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandGrabInteractor))]
public class HandGrabInteractorEditor : UnityEditor.Editor
{
private SerializedProperty m_Handedness, m_GrabDistance, m_OnBeginGrab, m_OnEndGrab;
private bool showEvent = false;
private void OnEnable()
{
m_Handedness = serializedObject.FindProperty("m_Handedness");
m_GrabDistance = serializedObject.FindProperty("m_GrabDistance");
m_OnBeginGrab = serializedObject.FindProperty("m_OnBeginGrab");
m_OnEndGrab = serializedObject.FindProperty("m_OnEndGrab");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_Handedness);
EditorGUILayout.PropertyField(m_GrabDistance);
showEvent = EditorGUILayout.Foldout(showEvent, "Grab Event");
if (showEvent)
{
EditorGUILayout.PropertyField(m_OnBeginGrab);
EditorGUILayout.PropertyField(m_OnEndGrab);
}
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8e7c213fed939e4b859638cde97a31c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandMeshManager))]
public class HandMeshManagerEditor : Editor
{
private HandMeshManager m_HandMesh;
private SerializedProperty m_Handedness, m_EnableCollider, m_HandJoints;
private bool showJoints = false;
public static readonly GUIContent findJoints = EditorGUIUtility.TrTextContent("Find Joints");
public static readonly GUIContent clearJoints = EditorGUIUtility.TrTextContent("All Clear");
private void OnEnable()
{
m_HandMesh = target as HandMeshManager;
m_Handedness = serializedObject.FindProperty("m_Handedness");
m_EnableCollider = serializedObject.FindProperty("m_EnableCollider");
m_HandJoints = serializedObject.FindProperty("m_HandJoints");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("Please check if your model is used to bind left hand poses", MessageType.None);
EditorGUILayout.PropertyField(m_Handedness, new GUIContent("Handedness"));
EditorGUILayout.HelpBox("Please check if you want the hand model with collision enabled.", MessageType.None);
EditorGUILayout.PropertyField(m_EnableCollider, new GUIContent("Enable Collider"));
showJoints = EditorGUILayout.Foldout(showJoints, "Hand Bones Reference");
if (showJoints)
{
EditorGUILayout.HelpBox("Please change rotation to make sure your model should palm faces forward and fingers points up in global axis.", MessageType.Info);
using (new EditorGUILayout.HorizontalScope())
{
using (new EditorGUI.DisabledScope())
{
if (GUILayout.Button(findJoints))
{
m_HandMesh.FindJoints();
}
}
using (new EditorGUI.DisabledScope())
{
if (GUILayout.Button(clearJoints))
{
m_HandMesh.ClearJoints();
}
}
}
bool isDetected = false;
for (int i = 0; i < m_HandJoints.arraySize; i++)
{
SerializedProperty bone = m_HandJoints.GetArrayElementAtIndex(i);
Transform boneTransform = (Transform)bone.objectReferenceValue;
if (boneTransform != null)
{
isDetected = true;
break;
}
}
if (isDetected)
{
for (int i = 0; i < m_HandJoints.arraySize; i++)
{
SerializedProperty bone = m_HandJoints.GetArrayElementAtIndex(i);
EditorGUILayout.PropertyField(bone, new GUIContent(((JointType)i).ToString()));
}
}
}
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d606d68d81c647241b90f1060786476c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e312e5246287a2546944bb77519270c4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3744e08fd384e154c82d53686f0c82a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44fadaef2720de846af62a7bbb2ec370
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: LeftHandGrab
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 9113736214857964471}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1102 &83218441301297147
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: LeftHandGrab
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 44fadaef2720de846af62a7bbb2ec370, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &9113736214857964471
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 83218441301297147}
m_Position: {x: 200, y: 0, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 83218441301297147}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1504c5149524a8a44a3854dfd4f94d1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a10ddbfcbce64f84787e026f4fc003a4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1107 &-3394168293143139300
AnimatorStateMachine:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 3187830036533893518}
m_Position: {x: 200, y: 0, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 3187830036533893518}
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: RightHandGrab
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: -3394168293143139300}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1102 &3187830036533893518
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: RightHandGrab
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: a10ddbfcbce64f84787e026f4fc003a4, type: 2}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 950573398b7eb5149bea536bfa6107ca
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 32007394341a17c44859030ef5b809ee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 010c0098d232cb0428f42a48488a6255
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9731229184277b54ba66bffd1633169b
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9b7f1f69b8b23e9459efda715941fbee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea1b2b1a3faba024aa3df5391529aec0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
// "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 HTCs SDK license agreement terms and
// conditions signed by you and all SDK and API requirements,
// specifications, and documentation provided by HTC to You."
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to automatically generate indicators.
/// </summary>
public class AutoGenIndicator : MonoBehaviour
{
private MeshRenderer meshRenderer;
private MeshFilter meshFilter;
private readonly Color indicatorColor = new Color(1f, 0.7960785f, 0.09411766f, 1f);
private const float k_Length = 0.05f;
private const float k_Width = 0.05f;
private void Start()
{
meshRenderer = transform.gameObject.AddComponent<MeshRenderer>();
meshFilter = transform.gameObject.AddComponent<MeshFilter>();
MeshInitialize();
}
/// <summary>
/// Initialize the mesh for the indicator.
/// </summary>
private void MeshInitialize()
{
Shader shader = Shader.Find("Sprites/Default");
meshRenderer.material = new Material(shader);
meshRenderer.material.SetColor("_Color", indicatorColor);
meshRenderer.sortingOrder = 1;
Mesh arrowMesh = new Mesh();
Vector3[] vertices = new Vector3[4];
int[] triangles = new int[3 * 2];
vertices[0] = new Vector3(0, 0f, 0f);
vertices[1] = new Vector3(0f, k_Length * 0.8f, 0f);
vertices[2] = new Vector3(-k_Width * 0.5f, k_Length, 0f);
vertices[3] = new Vector3(k_Width * 0.5f, k_Length, 0f);
triangles[0] = 0;
triangles[1] = 2;
triangles[2] = 1;
triangles[3] = 0;
triangles[4] = 1;
triangles[5] = 3;
arrowMesh.vertices = vertices;
arrowMesh.triangles = triangles;
arrowMesh.RecalculateNormals();
meshFilter.mesh = arrowMesh;
}
/// <summary>
/// Set the pose of the indicator.
/// </summary>
/// <param name="position">The position vector to set.</param>
/// <param name="direction">The direction vector to set.</param>
public void SetPose(Vector3 position, Vector3 direction)
{
transform.position = position;
transform.up = direction.normalized;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4694bdada589dce4c9b7096c7169833f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,378 @@
// "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 HTCs 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 System;
using UnityEditor;
using UnityEngine;
using System.Linq;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to edit grab gestures.
/// </summary>
[RequireComponent(typeof(HandMeshManager))]
public class CustomGrabPose : MonoBehaviour
{
#if UNITY_EDITOR
#region Log
const string LOG_TAG = "Wave.Essence.Hand.Interaction.CustomGrabPose";
private StringBuilder m_sb = null;
internal StringBuilder sb
{
get
{
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
private void DEBUG(StringBuilder msg) { Debug.Log($"{LOG_TAG}, {msg}"); }
private void WARNING(StringBuilder msg) { Debug.LogWarning($"{LOG_TAG}, {msg}"); }
private void ERROR(StringBuilder msg) { Debug.LogError($"{LOG_TAG}, {msg}"); }
int logFrame = 0;
bool printIntervalLog => logFrame == 0;
#endregion
/// <summary>
/// This structure is designed to record the rotation values of each joint at different bend angles.
/// </summary>
private struct JointBendingRotation
{
public FingerId fingerId;
public string jointPath;
public int bending;
public Quaternion rotation;
public JointBendingRotation(FingerId in_FingerId, string in_JointPath, int in_Bending, Quaternion in_Rotation)
{
fingerId = in_FingerId;
jointPath = in_JointPath;
bending = in_Bending;
rotation = in_Rotation;
}
public JointBendingRotation Identity => new JointBendingRotation(FingerId.Invalid, "", -1, Quaternion.identity);
}
[SerializeField]
private AnimationClip animationClip;
[SerializeField]
[Range(1, 30)]
private int thumbBending = 1;
[SerializeField]
[Range(1, 30)]
private int indexBending = 1;
[SerializeField]
[Range(1, 30)]
private int middleBending = 1;
[SerializeField]
[Range(1, 30)]
private int ringBending = 1;
[SerializeField]
[Range(1, 30)]
private int pinkyBending = 1;
private HandMeshManager m_HandMesh;
private Dictionary<FingerId, int> fingersBendingMapping = new Dictionary<FingerId, int>()
{
{FingerId.Thumb, 1 },
{FingerId.Index, 1 },
{FingerId.Middle, 1 },
{FingerId.Ring, 1 },
{FingerId.Pinky, 1 },
};
private static readonly Dictionary<string, FingerId> jointsPathMapping = new Dictionary<string, FingerId>()
{
{"WaveBone_0", FingerId.Invalid },
{"WaveBone_1", FingerId.Invalid },
{"WaveBone_2", FingerId.Thumb },
{"WaveBone_2/WaveBone_3", FingerId.Thumb },
{"WaveBone_2/WaveBone_3/WaveBone_4", FingerId.Thumb },
{"WaveBone_2/WaveBone_3/WaveBone_4/WaveBone_5", FingerId.Thumb },
{"WaveBone_6", FingerId.Index },
{"WaveBone_6/WaveBone_7", FingerId.Index },
{"WaveBone_6/WaveBone_7/WaveBone_8", FingerId.Index },
{"WaveBone_6/WaveBone_7/WaveBone_8/WaveBone_9", FingerId.Index },
{"WaveBone_6/WaveBone_7/WaveBone_8/WaveBone_9/WaveBone_10", FingerId.Index },
{"WaveBone_11", FingerId.Middle },
{"WaveBone_11/WaveBone_12", FingerId.Middle },
{"WaveBone_11/WaveBone_12/WaveBone_13", FingerId.Middle },
{"WaveBone_11/WaveBone_12/WaveBone_13/WaveBone_14", FingerId.Middle },
{"WaveBone_11/WaveBone_12/WaveBone_13/WaveBone_14/WaveBone_15", FingerId.Middle },
{"WaveBone_16", FingerId.Ring },
{"WaveBone_16/WaveBone_17", FingerId.Ring },
{"WaveBone_16/WaveBone_17/WaveBone_18", FingerId.Ring },
{"WaveBone_16/WaveBone_17/WaveBone_18/WaveBone_19", FingerId.Ring },
{"WaveBone_16/WaveBone_17/WaveBone_18/WaveBone_19/WaveBone_20", FingerId.Ring },
{"WaveBone_21", FingerId.Pinky },
{"WaveBone_21/WaveBone_22", FingerId.Pinky },
{"WaveBone_21/WaveBone_22/WaveBone_23", FingerId.Pinky },
{"WaveBone_21/WaveBone_22/WaveBone_23/WaveBone_24", FingerId.Pinky },
{"WaveBone_21/WaveBone_22/WaveBone_23/WaveBone_24/WaveBone_25", FingerId.Pinky },
};
private List<JointBendingRotation> jointsBending = new List<JointBendingRotation>();
private readonly float k_GrabDistance = 0.1f;
private HandGrabInteractable candidate = null;
private Pose wristPose = Pose.identity;
private Quaternion[] fingerJointRotation = new Quaternion[jointsPathMapping.Count];
#region MonoBehaviours
private void OnEnable()
{
m_HandMesh = transform.GetComponent<HandMeshManager>();
if (m_HandMesh == null)
{
sb.Clear().Append("Failed to find HandMeshRenderer.");
ERROR(sb);
}
if (animationClip != null)
{
EditorCurveBinding[] curveBindings = AnimationUtility.GetCurveBindings(animationClip);
foreach (string propertyName in jointsPathMapping.Keys)
{
SetEachFrameRotation(curveBindings, propertyName);
}
}
else
{
sb.Clear().Append("Failed to find hand grab animation. The hand model will not change when you change bend angles of finger.");
sb.Append("However, you still can record grab pose if you use direct preview mode.");
WARNING(sb);
}
}
private void OnDisable()
{
jointsBending.Clear();
}
private void Update()
{
if (IsFingerBendingUpdated())
{
for (int i = 0; i < fingerJointRotation.Length; i++)
{
var jointInfo = jointsPathMapping.ElementAt(i);
string jointPath = jointInfo.Key;
FingerId fingerId = jointInfo.Value;
int bending = -1;
if (fingersBendingMapping.ContainsKey(fingerId))
{
bending = fingersBendingMapping[fingerId] - 1;
}
if (jointsBending.Count(x => x.fingerId == fingerId && x.jointPath == jointPath && x.bending == bending) > 0)
{
JointBendingRotation jointRotation = jointsBending.FirstOrDefault(x => x.fingerId == fingerId && x.jointPath == jointPath && x.bending == bending);
fingerJointRotation[i] = jointRotation.rotation;
}
else
{
fingerJointRotation[i] = Quaternion.identity;
}
}
if (m_HandMesh != null)
{
for (int i = 0; i < fingerJointRotation.Length; i++)
{
JointType joint = (JointType)i;
m_HandMesh.GetJointPositionAndRotation(joint, out Vector3 jointPosition, out _, local: true);
m_HandMesh.SetJointPositionAndRotation(joint, jointPosition, fingerJointRotation[i], local: true);
}
}
}
if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))
{
FindNearInteractable();
SavePoseWithCandidate();
}
}
#endregion
/// <summary>
/// Reads the rotation of each joint frame by frame and records it.
/// </summary>
/// <param name="curveBindings">All the float curve bindings currently stored in the clip.</param>
/// <param name="jointPath">The path of the joint.</param>
private void SetEachFrameRotation(EditorCurveBinding[] curveBindings, string jointPath)
{
const int propertyCount = 4;
const int animeCount = 30;
const float animeFPS = 60.0f;
List<EditorCurveBinding> matchCurve = new List<EditorCurveBinding>();
foreach (EditorCurveBinding binding in curveBindings)
{
if (binding.path.Equals(jointPath))
{
matchCurve.Add(binding);
}
if (matchCurve.Count == propertyCount)
{
break;
}
}
if (matchCurve.Count == propertyCount)
{
for (int i = 0; i < animeCount; i++)
{
Quaternion rotation = Quaternion.identity;
foreach (var curveBinding in matchCurve)
{
AnimationCurve curve = AnimationUtility.GetEditorCurve(animationClip, curveBinding);
switch (curveBinding.propertyName)
{
case "m_LocalRotation.x":
rotation.x = curve.Evaluate(i / animeFPS);
break;
case "m_LocalRotation.y":
rotation.y = curve.Evaluate(i / animeFPS);
break;
case "m_LocalRotation.z":
rotation.z = curve.Evaluate(i / animeFPS);
break;
case "m_LocalRotation.w":
rotation.w = curve.Evaluate(i / animeFPS);
break;
}
}
jointsBending.Add(new JointBendingRotation(jointsPathMapping[jointPath], jointPath, i, rotation));
}
}
}
/// <summary>
/// Checks if the current finger bend angle has changed.
/// </summary>
/// <returns>True if the bend angle has changed; otherwise, false.</returns>
private bool IsFingerBendingUpdated()
{
bool updated = false;
if (fingersBendingMapping[FingerId.Thumb] != thumbBending)
{
fingersBendingMapping[FingerId.Thumb] = thumbBending;
updated = true;
}
if (fingersBendingMapping[FingerId.Index] != indexBending)
{
fingersBendingMapping[FingerId.Index] = indexBending;
updated = true;
}
if (fingersBendingMapping[FingerId.Middle] != middleBending)
{
fingersBendingMapping[FingerId.Middle] = middleBending;
updated = true;
}
if (fingersBendingMapping[FingerId.Ring] != ringBending)
{
fingersBendingMapping[FingerId.Ring] = ringBending;
updated = true;
}
if (fingersBendingMapping[FingerId.Pinky] != pinkyBending)
{
fingersBendingMapping[FingerId.Pinky] = pinkyBending;
updated = true;
}
return updated;
}
/// <summary>
/// Update hand pose from HandMeshRenderer.
/// </summary>
/// <returns>Return true if updating hand pose from HandMeshRenderer; otherwise.</returns>
private bool UpdateHandPose()
{
bool updated = false;
if (m_HandMesh != null)
{
for (int i = 0; i < fingerJointRotation.Length; i++)
{
if (i == (int)JointType.Wrist)
{
m_HandMesh.GetJointPositionAndRotation(JointType.Wrist, out wristPose.position, out wristPose.rotation);
}
m_HandMesh.GetJointPositionAndRotation((JointType)i, out _, out fingerJointRotation[i], local: true);
}
updated = true;
}
if (!updated)
{
sb.Clear().Append("Failed to update hand pose.");
DEBUG(sb);
}
return updated;
}
/// <summary>
/// Finds the nearest interactable object to the hand.
/// </summary>
public void FindNearInteractable()
{
if (!UpdateHandPose()) { return; }
candidate = null;
float maxScore = 0;
foreach (HandGrabInteractable interactable in GrabManager.handGrabbables)
{
float distanceScore = interactable.CalculateDistanceScore(wristPose.position, k_GrabDistance);
if (distanceScore > maxScore)
{
maxScore = distanceScore;
candidate = interactable;
}
}
if (candidate == null)
{
sb.Clear().Append("Unable to find a suitable candidate.");
WARNING(sb);
}
}
/// <summary>
/// Save the position and rotation offset with the candidate.
/// </summary>
public void SavePoseWithCandidate()
{
if (!UpdateHandPose() || candidate == null) { return; }
Quaternion[] clone = new Quaternion[fingerJointRotation.Length];
Array.Copy(fingerJointRotation, clone, fingerJointRotation.Length);
GrabPose grabPose = GrabPose.Identity;
grabPose.Update($"Grab Pose {candidate.grabPoses.Count + 1}", clone, m_HandMesh.isLeft);
grabPose.grabOffset = new GrabOffset(wristPose.position, wristPose.rotation, candidate.transform.position, candidate.transform.rotation);
if (!candidate.grabPoses.Contains(grabPose))
{
candidate.grabPoses.Add(grabPose);
}
GrabbablePoseRecorder.SaveChanges();
sb.Clear().Append("Save grab pose successfully.");
DEBUG(sb);
}
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd5957dc7b39bd249885b5bb53749b7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,173 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to manage all Grabbers and Grabbables.
/// </summary>
public static class GrabManager
{
private static List<IGrabber> m_GrabberRegistry = new List<IGrabber>();
public static IReadOnlyList<HandGrabInteractor> handGrabbers => m_GrabberRegistry.OfType<HandGrabInteractor>().ToList().AsReadOnly();
private static List<IGrabbable> m_GrabbableRegistry = new List<IGrabbable>();
public static IReadOnlyList<HandGrabInteractable> handGrabbables => m_GrabbableRegistry.OfType<HandGrabInteractable>().ToList().AsReadOnly();
#region IGrabber
/// <summary>
/// Register the grabber in the grabber registry.
/// </summary>
/// <param name="grabber">The grabber to register.</param>
/// <returns>True if the grabber is successfully registered; otherwise, false.</returns>
public static bool RegisterGrabber(IGrabber grabber)
{
if (!m_GrabberRegistry.Contains(grabber))
{
m_GrabberRegistry.Add(grabber);
}
return m_GrabberRegistry.Contains(grabber);
}
/// <summary>
/// Remove the grabber from the grabber registry.
/// </summary>
/// <param name="grabber">The grabber to remove.</param>
/// <returns>True if the grabber is successfully removed; otherwise, false.</returns>
public static bool UnregisterGrabber(IGrabber grabber)
{
if (m_GrabberRegistry.Contains(grabber))
{
m_GrabberRegistry.Remove(grabber);
}
return !m_GrabberRegistry.Contains(grabber);
}
/// <summary>
/// Get the first hand grabber component found in the child hierarchy of the GameObject.
/// </summary>
/// <param name="target">The target whose child hierarchy to search.</param>
/// <param name="grabber">The output parameter to store the first hand grabber component found.</param>
/// <returns>True if a hand grabber component is found; otherwise, false.</returns>
public static bool GetFirstHandGrabberFromChild(GameObject target, out HandGrabInteractor grabber)
{
grabber = TopDownFind<HandGrabInteractor>(target.transform);
return grabber != null;
}
/// <summary>
/// Get the first hand grabber component found in the parent hierarchy of the GameObject.
/// </summary>
/// <param name="target">The target whose parent hierarchy to search.</param>
/// <param name="grabber">The output parameter to store the first hand grabber component found.</param>
/// <returns>True if a hand grabber component is found; otherwise, false.</returns>
public static bool GetFirstHandGrabberFromParent(GameObject target, out HandGrabInteractor grabber)
{
grabber = BottomUpFind<HandGrabInteractor>(target.transform);
return grabber != null;
}
#endregion
#region GrabInteractable
/// <summary>
/// Register the grabbable in the grabbable registry.
/// </summary>
/// <param name="grabbable">The grabbable to register.</param>
/// <returns>True if the grabbable is successfully registered; otherwise, false.</returns>
public static bool RegisterGrabbable(IGrabbable grabbable)
{
if (!m_GrabbableRegistry.Contains(grabbable))
{
m_GrabbableRegistry.Add(grabbable);
}
return m_GrabbableRegistry.Contains(grabbable);
}
/// <summary>
/// Remove the grabbable from the grabbable registry.
/// </summary>
/// <param name="grabbable">The grabbable to remove.</param>
/// <returns>True if the grabbable is successfully removed; otherwise, false.</returns>
public static bool UnregisterGrabbable(IGrabbable grabbable)
{
if (m_GrabbableRegistry.Contains(grabbable))
{
m_GrabbableRegistry.Remove(grabbable);
}
return !m_GrabbableRegistry.Contains(grabbable);
}
/// <summary>
/// Get the first hand grabbable component found in the child hierarchy of the GameObject.
/// </summary>
/// <param name="target">The target whose child hierarchy to search.</param>
/// <param name="grabbable">The output parameter to store the first hand grabbable component found.</param>
/// <returns>True if a hand grabbable component is found; otherwise, false.</returns>
public static bool GetFirstHandGrabbableFromChild(GameObject target, out HandGrabInteractable grabbable)
{
grabbable = TopDownFind<HandGrabInteractable>(target.transform);
return grabbable != null;
}
/// <summary>
/// Get the first hand grabbable component found in the parent hierarchy of the GameObject.
/// </summary>
/// <param name="target">The target whose parent hierarchy to search.</param>
/// <param name="grabbable">The output parameter to store the first hand grabbable component found.</param>
/// <returns>True if a hand grabbable component is found; otherwise, false.</returns>
public static bool GetFirstHandGrabbableFromParent(GameObject target, out HandGrabInteractable grabbable)
{
grabbable = BottomUpFind<HandGrabInteractable>(target.transform);
return grabbable != null;
}
#endregion
/// <summary>
/// Find available components from self to children nodes.
/// </summary>
/// <param name="transform">The transform of the gameobject.</param>
/// <returns>Value for available component.</returns>
private static T TopDownFind<T>(Transform transform) where T : Component
{
T component = transform.GetComponent<T>();
if (component != null)
{
return component;
}
if (transform.childCount > 0)
{
for (int i = 0; i < transform.childCount; i++)
{
T childComponent = TopDownFind<T>(transform.GetChild(i));
if (childComponent != null)
{
return childComponent;
}
}
}
return null;
}
/// <summary>
/// Find available components from self to parent node.
/// </summary>
/// <param name="transform">The transform of the gameobject.</param>
/// <returns>Value for available component.</returns>
private static T BottomUpFind<T>(Transform transform) where T : Component
{
T component = transform.GetComponent<T>();
if (component != null)
{
return component;
}
if (transform.parent != null)
{
return BottomUpFind<T>(transform.parent);
}
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 252950eac28fb1f4cb1eae8e653f92ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,120 @@
// "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 WaveVR SDK(s).
// You shall fully comply with all of HTCs 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 UnityEditor;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
#if UNITY_EDITOR
/// <summary>
/// The class is designed to record all grab poses for all hand Grabbables.
/// </summary>
public class GrabPoseBinder : ScriptableObject
{
/// <summary>
/// This struct records the grab pose for grabbable object.
/// </summary>
[Serializable]
private struct GrabPoseBindFormat
{
[SerializeField]
public string grabbableName;
[SerializeField]
public List<GrabPose> grabPoses;
public GrabPoseBindFormat(string in_GrabbableName, List<GrabPose> in_GrabPoses)
{
grabbableName = in_GrabbableName;
grabPoses = in_GrabPoses;
}
public GrabPoseBindFormat Identity => new GrabPoseBindFormat(string.Empty, new List<GrabPose>());
public void Update(List<GrabPose> grabPoses)
{
this.grabPoses.Clear();
this.grabPoses.AddRange(grabPoses);
}
public void Reset()
{
grabbableName = string.Empty;
grabPoses.Clear();
}
public override bool Equals(object obj)
{
return obj is GrabPoseBindFormat grabPoseBindFormat &&
grabbableName == grabPoseBindFormat.grabbableName &&
grabPoses == grabPoseBindFormat.grabPoses;
}
public override int GetHashCode()
{
return grabbableName.GetHashCode() ^ grabPoses.GetHashCode();
}
public static bool operator ==(GrabPoseBindFormat source, GrabPoseBindFormat target) => source.Equals(target);
public static bool operator !=(GrabPoseBindFormat source, GrabPoseBindFormat target) => !(source == target);
}
[SerializeField]
private List<GrabPoseBindFormat> m_BindingInfos = new List<GrabPoseBindFormat>();
/// <summary>
/// Update the binding information for each hand grabbable object.
/// </summary>
public void UpdateBindingInfos()
{
m_BindingInfos.Clear();
foreach (HandGrabInteractable grabbable in GrabManager.handGrabbables)
{
m_BindingInfos.Add(new GrabPoseBindFormat(grabbable.name, grabbable.grabPoses));
}
}
/// <summary>
/// Stores the binding information.
/// </summary>
/// <returns>True if storage is successful; otherwise, false.</returns>
public bool StorageData()
{
if (m_BindingInfos.Count == 0) { return false; }
EditorApplication.delayCall += () =>
{
AssetDatabase.Refresh();
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
};
return true;
}
/// <summary>
/// Finds grab poses associated with the specified hand grabbable object.
/// </summary>
/// <param name="grabbable">The hand grabbable object to search for.</param>
/// <param name="grabPoses">The output parameter to store the found grab poses.</param>
/// <returns>True if grab poses are found for the grabbable object; otherwise, false.</returns>
public bool FindGrabPosesWithGrabbable(HandGrabInteractable grabbable, out List<GrabPose> grabPoses)
{
grabPoses = new List<GrabPose>();
GrabPoseBindFormat bindingInfo = m_BindingInfos.Find(x => x.grabbableName == grabbable.name);
if (bindingInfo != null)
{
grabPoses = bindingInfo.grabPoses;
return true;
}
return false;
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 53503199deedf444e84f1714b700737d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,104 @@
// "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 WaveVR SDK(s).
// You shall fully comply with all of HTCs 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.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
#if UNITY_EDITOR
/// <summary>
/// The class is designed to update the grab poses of all hand Grabbables.
/// </summary>
[InitializeOnLoad]
public class GrabbablePoseRecorder
{
private static readonly string filepath = "Assets/GrabablePoseRecording.asset";
private static readonly string metaFilepath = filepath + ".meta";
private static bool IsFileExist => File.Exists(filepath);
static GrabbablePoseRecorder()
{
EditorApplication.playModeStateChanged += ApplyChanges;
}
/// <summary>
/// Apply changes to grab poses when entering edit mode.
/// </summary>
/// <param name="state">The state of the play mode.</param>
private static void ApplyChanges(PlayModeStateChange state)
{
if (IsFileExist && state == PlayModeStateChange.EnteredEditMode)
{
GrabPoseBinder binder = AssetDatabase.LoadAssetAtPath<GrabPoseBinder>(filepath);
if (binder != null)
{
HandGrabInteractable[] grabbables = Object.FindObjectsOfType<HandGrabInteractable>();
foreach (var grabbable in grabbables)
{
if (binder.FindGrabPosesWithGrabbable(grabbable, out List<GrabPose> updatedGrabPose))
{
for (int i = 0; i < updatedGrabPose.Count; i++)
{
GrabPose grabPose = updatedGrabPose[i];
GrabPose oldGrabPose = grabbable.grabPoses.Find(x => x.grabPoseName == grabPose.grabPoseName);
if (oldGrabPose != null)
{
grabPose.indicator.target = oldGrabPose.indicator.target;
}
updatedGrabPose[i] = grabPose;
}
grabbable.grabPoses.Clear();
grabbable.grabPoses.AddRange(updatedGrabPose);
}
}
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
}
File.Delete(filepath);
File.Delete(metaFilepath);
}
}
/// <summary>
/// Saves changes to grab pose bindings.
/// </summary>
public static void SaveChanges()
{
if (!IsFileExist)
{
GenerateAsset();
}
else
{
GrabPoseBinder binder = AssetDatabase.LoadAssetAtPath<GrabPoseBinder>(filepath);
if (binder != null)
{
binder.UpdateBindingInfos();
binder.StorageData();
}
}
}
/// <summary>
/// Generates a new asset for storing grab pose bindings.
/// </summary>
private static void GenerateAsset()
{
GrabPoseBinder binder = ScriptableObject.CreateInstance<GrabPoseBinder>();
binder.UpdateBindingInfos();
AssetDatabase.CreateAsset(binder, filepath);
AssetDatabase.SaveAssets();
}
}
#endif
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9119682127e09314ca250470f13db0f2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,452 @@
// "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 HTCs 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.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to implement IHandGrabbable, allowing objects to be grabbed.
/// </summary>
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;
public new Rigidbody rigidbody => m_Rigidbody;
[SerializeField]
private List<GrabPose> m_GrabPoses = new List<GrabPose>();
public List<GrabPose> grabPoses => m_GrabPoses;
private GrabPose m_BestGrabPose = GrabPose.Identity;
public GrabPose bestGrabPose => m_BestGrabPose;
#endregion
[SerializeField]
private bool m_ShowAllIndicator = false;
private List<Collider> allColliders = new List<Collider>();
private HandGrabInteractor closestGrabber = null;
private OnBeginGrabbed beginGrabbed;
private OnEndGrabbed endGrabbed;
[SerializeField]
private IOneHandContraintMovement m_OneHandContraintMovement;
public bool isContraint => m_OneHandContraintMovement != null;
[SerializeField]
private int m_PreviewIndex = -1;
#region MonoBehaviour
private void Awake()
{
allColliders.AddRange(transform.GetComponentsInChildren<Collider>(true));
}
private void OnEnable()
{
GrabManager.RegisterGrabbable(this);
Initialize();
}
private void OnDisable()
{
GrabManager.UnregisterGrabbable(this);
}
#endregion
#region Public Interface
/// <summary>
/// Set the grabber for the hand grabbable object.
/// </summary>
/// <param name="grabber">The grabber to set.</param>
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));
beginGrabbed?.Invoke(this);
m_OnBeginGrabbed?.Invoke(this);
DEBUG($"{transform.name} is grabbed by {handGrabber.name}");
}
else
{
m_Grabber = null;
m_BestGrabPose = GrabPose.Identity;
endGrabbed?.Invoke(this);
m_OnEndGrabbed?.Invoke(this);
DEBUG($"{transform.name} is released.");
}
}
/// <summary>
/// Enable/Disable indicators. If enabled, display the closest indicator based on grabber position.
/// </summary>
/// <param name="enable">True to show the indicator, false to hide it.</param>
/// <param name="grabber">The grabber for which to show or hide this indicator.</param>
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);
}
}
}
/// <summary>
/// Calculate the shortest distance between the grabber and the grabbable and convert it into a score based on grabDistance.
/// </summary>
/// <param name="grabberPos">The current pose of grabber.</param>
/// <param name="grabDistance">The maximum grab distance between the grabber and the grabbable object.</param>
/// <returns>The score represents the distance between the grabber and the grabbable.</returns>
public float CalculateDistanceScore(Vector3 grabberPos, float grabDistance = 0.03f)
{
if (!isGrabbable || isGrabbed) { return 0; }
Vector3 closestPoint = GetClosestPoint(grabberPos);
float distacne = Vector3.Distance(grabberPos, closestPoint);
return distacne > grabDistance ? 0 : 1 - (distacne / grabDistance);
}
/// <summary>
/// Add a listener for the event triggered when the grabbable object is grabbed.
/// </summary>
/// <param name="handler">The method to be called when the grabbable object is grabbed.</param>
[Obsolete("Please use onBeginGrabbed instead.")]
public void AddBeginGrabbedListener(OnBeginGrabbed handler)
{
beginGrabbed += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabbable object is grabbed.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onBeginGrabbed instead.")]
public void RemoveBeginGrabbedListener(OnBeginGrabbed handler)
{
beginGrabbed -= handler;
}
/// <summary>
/// Add a listener for the event triggered when the grabbable object is released.
/// </summary>
/// <param name="handler">The method to be called when the grabbable object is released.</param>
[Obsolete("Please use onEndGrabbed instead.")]
public void AddEndGrabbedListener(OnEndGrabbed handler)
{
endGrabbed += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabbable object is released.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onEndGrabbed instead.")]
public void RemoveEndGrabbedListener(OnEndGrabbed handler)
{
endGrabbed -= handler;
}
/// <summary>
/// Update the position and rotation of the self with the pose of the hand that is grabbing it.
/// </summary>
/// <param name="grabberPose">The pose of the hand.</param>
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
/// <summary>
/// Generate all indicators and calculate grab offsets.
/// </summary>
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);
}
/// <summary>
/// Automatically generate an indicator by the index of the grab pose.
/// </summary>
/// <param name="index">The index of the grab pose.</param>
private void AutoGenerateIndicator(int index)
{
AutoGenIndicator autoGenIndicator = new GameObject($"Indicator {index}", typeof(AutoGenIndicator)).GetComponent<AutoGenIndicator>();
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;
}
/// <summary>
/// Calculate the point closest to the source position.
/// </summary>
/// <param name="sourcePos">The position of source.</param>
/// <returns>The position which closest to the source position.</returns>
private Vector3 GetClosestPoint(Vector3 sourcePos)
{
Vector3 closestPoint = Vector3.zero;
float shortDistance = float.MaxValue;
foreach (var collider in allColliders)
{
Vector3 closePoint = collider.ClosestPointOnBounds(sourcePos);
float distance = Vector3.Distance(sourcePos, closePoint);
if (collider.bounds.Contains(closePoint))
{
Vector3 direction = (closePoint - sourcePos).normalized;
RaycastHit[] hits = Physics.RaycastAll(sourcePos, direction, distance);
foreach (var hit in hits)
{
if (hit.collider == collider)
{
float hitDistance = Vector3.Distance(sourcePos, hit.point);
if (distance > hitDistance)
{
distance = hitDistance;
closePoint = hit.point;
}
}
}
}
if (shortDistance > distance)
{
shortDistance = distance;
closestPoint = closePoint;
}
}
return closestPoint;
}
/// <summary>
/// Find the best grab pose for the grabber and updates the bestGrabPoseId.
/// </summary>
/// <param name="isLeft">Whether the grabber is the left hand.</param>
/// <param name="grabberPose">The pose of the grabber.</param>
/// <returns>True if a best grab pose is found; otherwise, false.</returns>
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;
}
}
}
/// <summary>
/// Find the best grab pose for the grabber.
/// </summary>
/// <param name="isLeft">Whether the grabber is the left hand.</param>
/// <param name="grabberPose">The pose of the grabber.</param>
/// <returns>The index of the best grab pose among the grab poses.</returns>
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;
}
/// <summary>
/// Show the indicator corresponding to the specified index and hides others.
/// </summary>
/// <param name="index">The index of the indicator to show.</param>
private void ShowIndicatorByIndex(int index)
{
foreach (var grabPose in m_GrabPoses)
{
grabPose.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);
}
}
/// <summary>
/// Show all indicators corresponding to the specified hand side and hides others.
/// </summary>
/// <param name="isLeft">Whether the hand side is left.</param>
private void ShowAllIndicator(bool isLeft)
{
foreach (var grabPose in m_GrabPoses)
{
grabPose.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);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1c0e40da1ab9014c89d359be00fffb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,309 @@
// "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 HTCs 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.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to implement IHandGrabber, allowing objects to grab grabbable objects.
/// </summary>
public class HandGrabInteractor : MonoBehaviour, IHandGrabber
{
#region Log
const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.HandGrabInteractor";
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
private enum GrabState
{
None,
Hover,
Grabbing,
};
#region Public States
private HandGrabInteractable m_Grabbable = null;
public IGrabbable grabbable => m_Grabbable;
public bool isGrabbing => m_Grabbable != null;
[SerializeField]
private Handedness m_Handedness = Handedness.Left;
public Handedness handedness => m_Handedness;
private HandGrabState m_HandGrabState = null;
public HandGrabState handGrabState => m_HandGrabState;
[SerializeField]
private float m_GrabDistance = 0.03f;
public float grabDistance { get { return m_GrabDistance; } set { m_GrabDistance = value; } }
public bool isLeft => handedness == Handedness.Left;
[SerializeField]
private HandGrabberEvent m_OnBeginGrab = new HandGrabberEvent();
public HandGrabberEvent onBeginGrab => m_OnBeginGrab;
[SerializeField]
private HandGrabberEvent m_OnEndGrab = new HandGrabberEvent();
public HandGrabberEvent onEndGrab => m_OnEndGrab;
#endregion
private readonly float MinGrabScore = 0.25f;
private readonly float MinDistanceScore = 0.25f;
private HandGrabInteractable currentCandidate = null;
private GrabState m_State = GrabState.None;
private Pose wristPose = Pose.identity;
private Vector3[] fingerTipPosition = new Vector3[(int)FingerId.Count];
private OnBeginGrab beginGrabHandler;
private OnEndGrab endGrabHandler;
#region MonoBehaviour
private void Awake()
{
m_HandGrabState = new HandGrabState(isLeft);
}
private void OnEnable()
{
GrabManager.RegisterGrabber(this);
}
private void OnDisable()
{
GrabManager.UnregisterGrabber(this);
}
private void Update()
{
m_HandGrabState.UpdateState();
HandPose handPose = HandPoseProvider.GetHandPose(isLeft ? HandPoseType.HAND_LEFT : HandPoseType.HAND_RIGHT);
if (handPose != null)
{
handPose.GetPosition(JointType.Wrist, out wristPose.position);
handPose.GetRotation(JointType.Wrist, out wristPose.rotation);
handPose.GetPosition(JointType.Thumb_Tip, out fingerTipPosition[(int)FingerId.Thumb]);
handPose.GetPosition(JointType.Index_Tip, out fingerTipPosition[(int)FingerId.Index]);
handPose.GetPosition(JointType.Middle_Tip, out fingerTipPosition[(int)FingerId.Middle]);
handPose.GetPosition(JointType.Ring_Tip, out fingerTipPosition[(int)FingerId.Ring]);
handPose.GetPosition(JointType.Pinky_Tip, out fingerTipPosition[(int)FingerId.Pinky]);
}
if (m_State != GrabState.Grabbing)
{
FindCandidate();
}
switch (m_State)
{
case GrabState.None:
NoneUpdate();
break;
case GrabState.Hover:
HoverUpdate();
break;
case GrabState.Grabbing:
GrabbingUpdate();
break;
}
}
#endregion
#region Public Interface
/// <summary>
/// Checks if the specified joint type is required.
/// </summary>
/// <param name="joint">The joint that need to check.</param>
/// <returns>True if the joint is required; otherwise, false.</returns>
public bool IsRequiredJoint(JointType joint)
{
if (m_Grabbable != null)
{
HandData.GetJointIndex(joint, out int group, out _);
switch (group)
{
case 2: return m_Grabbable.fingerRequirement.thumb == GrabRequirement.Required;
case 3: return m_Grabbable.fingerRequirement.index == GrabRequirement.Required;
case 4: return m_Grabbable.fingerRequirement.middle == GrabRequirement.Required;
case 5: return m_Grabbable.fingerRequirement.ring == GrabRequirement.Required;
case 6: return m_Grabbable.fingerRequirement.pinky == GrabRequirement.Required;
}
}
return false;
}
/// <summary>
/// Add a listener for the event triggered when the grabber begins grabbing.
/// </summary>
/// <param name="handler">The method to be called when the grabber begins grabbing.</param>
[Obsolete("Please use onBeginGrab instead.")]
public void AddBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabber begins grabbing.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onBeginGrab instead.")]
public void RemoveBeginGrabListener(OnBeginGrab handler)
{
beginGrabHandler -= handler;
}
/// <summary>
/// Add a listener for the event triggered when the grabber ends grabbing.
/// </summary>
/// <param name="handler">The method to be called when the grabber ends grabbing.</param>
[Obsolete("Please use onEndGrab instead.")]
public void AddEndGrabListener(OnEndGrab handler)
{
endGrabHandler += handler;
}
/// <summary>
/// Remove a listener for the event triggered when the grabber ends grabbing.
/// </summary>
/// <param name="handler">The method to be removed from the event listeners.</param>
[Obsolete("Please use onEndGrab instead.")]
public void RemoveEndGrabListener(OnEndGrab handler)
{
endGrabHandler -= handler;
}
#endregion
/// <summary>
/// Find the candidate grabbable object for grabber.
/// </summary>
private void FindCandidate()
{
currentCandidate = null;
float distanceScore = float.MinValue;
if (GetClosestGrabbable(m_GrabDistance, out HandGrabInteractable grabbable, out float score) && score > distanceScore)
{
distanceScore = score;
currentCandidate = grabbable;
}
if (currentCandidate != null)
{
float grabScore = Grab.CalculateHandGrabScore(this, currentCandidate);
if (distanceScore < MinDistanceScore || grabScore < MinGrabScore)
{
currentCandidate = null;
}
}
}
/// <summary>
/// Get the closest grabbable object for grabber.
/// </summary>
/// <param name="grabDistance">The maximum grab distance between the grabber and the grabbable object.</param>
/// <param name="grabbable">The closest grabbable object.</param>
/// <param name="maxScore">The maximum score indicating the closeness of the grabbable object.</param>
/// <returns>True if a grabbable object is found within the grab distance; otherwise, false.</returns>
private bool GetClosestGrabbable(float grabDistance, out HandGrabInteractable grabbable, out float maxScore)
{
grabbable = null;
maxScore = 0f;
foreach (HandGrabInteractable interactable in GrabManager.handGrabbables)
{
interactable.ShowIndicator(false, this);
foreach (Vector3 tipPos in fingerTipPosition)
{
float distanceScore = interactable.CalculateDistanceScore(tipPos, grabDistance);
if (distanceScore > maxScore)
{
maxScore = distanceScore;
grabbable = interactable;
}
}
}
if (grabbable != null)
{
grabbable.ShowIndicator(true, this);
}
return grabbable != null;
}
/// <summary>
/// Set the state to GrabState.Hover if a candidate is found.
/// </summary>
private void NoneUpdate()
{
if (currentCandidate != null)
{
m_State = GrabState.Hover;
}
}
/// <summary>
/// Update the state and related information when the grabber begins grabbing the grabbable.
/// </summary>
private void HoverUpdate()
{
if (currentCandidate == null)
{
m_State = GrabState.None;
return;
}
if (Grab.HandBeginGrab(this, currentCandidate))
{
m_State = GrabState.Grabbing;
m_Grabbable = currentCandidate;
m_Grabbable.SetGrabber(this);
m_Grabbable.ShowIndicator(false, this);
beginGrabHandler?.Invoke(this);
onBeginGrab?.Invoke(this);
DEBUG($"The {(m_Handedness == Handedness.Left ? "left" : "right")} hand begins to grab the {m_Grabbable.name}");
}
}
/// <summary>
/// Update the position of grabbable object according to the movement of the grabber.
/// </summary>
private void GrabbingUpdate()
{
if (Grab.HandDoneGrab(this, m_Grabbable) || !Grab.HandIsGrabbing(this, m_Grabbable))
{
DEBUG($"The {(m_Handedness == Handedness.Left ? "left" : "right")} hand ends to grab the {m_Grabbable.name}");
endGrabHandler?.Invoke(this);
onEndGrab?.Invoke(this);
m_Grabbable.SetGrabber(null);
m_Grabbable = null;
m_State = GrabState.Hover;
return;
}
m_Grabbable.UpdatePositionAndRotation(wristPose);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3155365f073fdb45ba7a61887f8cf06
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,148 @@
using System.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class OneGrabMoveConstraint : IOneHandContraintMovement
{
#region Log
const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.OneGrabMoveConstraint";
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
[SerializeField]
private Transform m_Constraint;
[SerializeField]
private ConstraintInfo m_NegativeXMove = ConstraintInfo.Identity;
private float defaultNegativeXPos = 0.0f;
[SerializeField]
private ConstraintInfo m_PositiveXMove = ConstraintInfo.Identity;
private float defaultPositiveXPos = 0.0f;
[SerializeField]
private ConstraintInfo m_NegativeYMove = ConstraintInfo.Identity;
private float defaultNegativeYPos = 0.0f;
[SerializeField]
private ConstraintInfo m_PositiveYMove = ConstraintInfo.Identity;
private float defaultPositiveYPos = 0.0f;
[SerializeField]
private ConstraintInfo m_NegativeZMove = ConstraintInfo.Identity;
private float defaultNegativeZPos = 0.0f;
[SerializeField]
private ConstraintInfo m_PositiveZMove = ConstraintInfo.Identity;
private float defaultPositiveZPos = 0.0f;
private Pose previousHandPose = Pose.identity;
private GrabPose currentGrabPose = GrabPose.Identity;
public override void Initialize(IGrabbable grabbable)
{
if (grabbable is HandGrabInteractable handGrabbable)
{
if (m_Constraint == null)
{
m_Constraint = handGrabbable.transform;
WARNING("Since no constraint object is set, self will be used as the constraint object.");
}
}
if (m_NegativeXMove.enableConstraint) { defaultNegativeXPos = m_Constraint.position.x - m_NegativeXMove.value; }
if (m_PositiveXMove.enableConstraint) { defaultPositiveXPos = m_Constraint.position.x + m_PositiveXMove.value; }
if (m_NegativeYMove.enableConstraint) { defaultNegativeYPos = m_Constraint.position.y - m_NegativeYMove.value; }
if (m_PositiveYMove.enableConstraint) { defaultPositiveYPos = m_Constraint.position.y + m_PositiveYMove.value; }
if (m_NegativeZMove.enableConstraint) { defaultNegativeZPos = m_Constraint.position.z - m_NegativeZMove.value; }
if (m_PositiveZMove.enableConstraint) { defaultPositiveZPos = m_Constraint.position.z + m_PositiveZMove.value; }
}
public override void OnBeginGrabbed(IGrabbable grabbable)
{
if (grabbable is HandGrabInteractable handGrabbable)
{
currentGrabPose = handGrabbable.bestGrabPose;
}
if (grabbable.grabber is HandGrabInteractor 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);
previousHandPose = new Pose(wristPos, wristRot);
}
}
public override void UpdatePose(Pose handPose)
{
if (previousHandPose == Pose.identity)
{
previousHandPose = handPose;
return;
}
Quaternion previousRotOffset = previousHandPose.rotation * Quaternion.Inverse(currentGrabPose.grabOffset.sourceRotation);
Vector3 previousPos = previousHandPose.position + previousRotOffset * currentGrabPose.grabOffset.posOffset;
Quaternion currentRotOffset = handPose.rotation * Quaternion.Inverse(currentGrabPose.grabOffset.sourceRotation);
Vector3 currentPos = handPose.position + currentRotOffset * currentGrabPose.grabOffset.posOffset;
Vector3 handOffset = currentPos - previousPos;
if (m_NegativeXMove.enableConstraint)
{
float x = (m_Constraint.position + handOffset).x;
x = Mathf.Max(defaultNegativeXPos, x);
m_Constraint.position = new Vector3(x, m_Constraint.position.y, m_Constraint.position.z);
}
if (m_PositiveXMove.enableConstraint)
{
float x = (m_Constraint.position + handOffset).x;
x = Mathf.Min(defaultPositiveXPos, x);
m_Constraint.position = new Vector3(x, m_Constraint.position.y, m_Constraint.position.z);
}
if (m_NegativeYMove.enableConstraint)
{
float y = (m_Constraint.position + handOffset).y;
y = Mathf.Max(defaultNegativeYPos, y);
m_Constraint.position = new Vector3(m_Constraint.position.x, y, m_Constraint.position.z);
}
if (m_PositiveYMove.enableConstraint)
{
float y = (m_Constraint.position + handOffset).y;
y = Mathf.Min(defaultPositiveYPos, y);
m_Constraint.position = new Vector3(m_Constraint.position.x, y, m_Constraint.position.z);
}
if (m_NegativeZMove.enableConstraint)
{
float z = (m_Constraint.position + handOffset).z;
z = Mathf.Max(defaultNegativeZPos, z);
m_Constraint.position = new Vector3(m_Constraint.position.x, m_Constraint.position.y, z);
}
if (m_PositiveZMove.enableConstraint)
{
float z = (m_Constraint.position + handOffset).z;
z = Mathf.Min(defaultPositiveZPos, z);
m_Constraint.position = new Vector3(m_Constraint.position.x, m_Constraint.position.y, z);
}
previousHandPose = handPose;
}
public override void OnEndGrabbed(IGrabbable grabbable)
{
currentGrabPose = GrabPose.Identity;
previousHandPose = Pose.identity;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d521c71dbb8287409daf6ef81005d79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,123 @@
using System.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class OneGrabRotateConstraint : IOneHandContraintMovement
{
#region Log
const string LOG_TAG = "VIVE.OpenXR.Toolkits.RealisticHandInteraction.OneGrabRotateConstraint";
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
private enum RotationAxis
{
XAxis = 0,
YAxis = 1,
ZAxis = 2,
}
[SerializeField]
private Transform m_Constraint;
[SerializeField]
private Transform m_Pivot;
[SerializeField]
private RotationAxis m_RotationAxis = RotationAxis.XAxis;
[SerializeField]
private ConstraintInfo m_ClockwiseAngle = ConstraintInfo.Identity;
[SerializeField]
private ConstraintInfo m_CounterclockwiseAngle = ConstraintInfo.Identity;
private float totalRotationAngle = 0.0f;
private Pose previousHandPose = Pose.identity;
public override void Initialize(IGrabbable grabbable)
{
if (grabbable is HandGrabInteractable handGrabbable)
{
if (m_Constraint == null)
{
m_Constraint = handGrabbable.transform;
WARNING("Since no constraint object is set, self will be used as the constraint object.");
}
if (m_Pivot == null)
{
m_Pivot = handGrabbable.transform;
WARNING("Since no pivot is set, self will be used as the pivot.");
}
}
}
public override void OnBeginGrabbed(IGrabbable grabbable)
{
if (grabbable.grabber is HandGrabInteractor 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);
previousHandPose = new Pose(wristPos, wristRot);
}
}
public override void UpdatePose(Pose handPose)
{
if (previousHandPose == Pose.identity)
{
previousHandPose = handPose;
return;
}
Vector3 axis = Vector3.zero;
switch (m_RotationAxis)
{
case RotationAxis.XAxis: axis = Vector3.right; break;
case RotationAxis.YAxis: axis = Vector3.up; break;
case RotationAxis.ZAxis: axis = Vector3.forward; break;
}
Vector3 worldAxis = m_Pivot.TransformDirection(axis);
Vector3 previousOffset = previousHandPose.position - m_Pivot.position;
Vector3 previousVector = Vector3.ProjectOnPlane(previousOffset, worldAxis);
Vector3 targetOffset = handPose.position - m_Pivot.position;
Vector3 targetVector = Vector3.ProjectOnPlane(targetOffset, worldAxis);
float angleDelta = Vector3.Angle(previousVector, targetVector);
angleDelta *= Vector3.Dot(Vector3.Cross(previousVector, targetVector), worldAxis) > 0.0f ? 1.0f : -1.0f;
float previousAngle = totalRotationAngle;
totalRotationAngle += angleDelta;
if (m_CounterclockwiseAngle.enableConstraint)
{
totalRotationAngle = Mathf.Max(totalRotationAngle, -m_CounterclockwiseAngle.value);
}
if (m_ClockwiseAngle.enableConstraint)
{
totalRotationAngle = Mathf.Min(totalRotationAngle, m_ClockwiseAngle.value);
}
angleDelta = totalRotationAngle - previousAngle;
m_Constraint.RotateAround(m_Pivot.position, worldAxis, angleDelta);
previousHandPose = handPose;
}
public override void OnEndGrabbed(IGrabbable grabbable)
{
previousHandPose = Pose.identity;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7f25051021522804d90dc5448b7657a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,371 @@
// "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 HTCs 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 System.Text;
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 List<Vector3> collisionDirections = new List<Vector3>();
private bool isGrabbing = false;
#region MonoBehaviour
private void OnEnable()
{
StartCoroutine(WaitForInit());
}
private void OnDisable()
{
if (rootJoint != null)
{
JointCollider jointCollider = rootJoint.GetComponent<JointCollider>();
if (jointCollider != null)
{
jointCollider.RemoveJointCollisionListener(OnJointCollision);
}
Destroy(rootJoint.gameObject);
}
isInit = false;
}
private void Start()
{
JointCollider[] fullJointsCollider = FindObjectsOfType<JointCollider>(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)
{
rootJointRigidbody.velocity = Vector3.zero;
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>();
jointCollider.AddJointCollisionListener(OnJointCollision);
rootJointRigidbody = jointCollider.InitRigidbody(handMass);
isInit = true;
DEBUG("Hand Collider init successfully.");
}
}
/// <summary>
/// Initializes by copying the structure of the hand mesh and sequentially adding joint collider.
/// </summary>
public void InitJointColliders(Transform handRootJoint)
{
rootJointParent = handRootJoint.parent;
rootJoint = Instantiate(handRootJoint, rootJointParent);
rootJoint.name = handRootJoint.name;
List<Transform> totalTransforms = new List<Transform>() { 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>();
jointCollider.SetJointId(i);
jointsCollider[i] = jointCollider;
}
}
}
private void GetAllChildrenTransforms(Transform parent, ref List<Transform> childrenTransforms)
{
foreach (Transform child in parent)
{
childrenTransforms.Add(child);
GetAllChildrenTransforms(child, ref childrenTransforms);
}
}
/// <summary>
/// Updates the velocity of a Rigidbody.
/// </summary>
private void UpdateVelocity()
{
Vector3 vel = (lastRootPos - rootJoint.position) / Time.deltaTime;
if (IsValidVelocity(vel))
{
if (collisionDirections.Count > 0)
{
float minAngle = float.MaxValue;
Vector3 closestDirection = Vector3.zero;
foreach (Vector3 direction in collisionDirections.ToList())
{
float angle = Mathf.Abs(Vector3.Angle(direction, vel));
if (angle < minAngle)
{
minAngle = angle;
closestDirection = direction;
}
}
collisionDirections.Clear();
Vector3 adjustedDirection = closestDirection;
if (Vector3.Dot(vel, closestDirection) > 0)
{
adjustedDirection *= -1f;
}
vel = Vector3.ProjectOnPlane(vel, adjustedDirection);
if (vel.magnitude > 1)
{
vel.Normalize();
}
}
rootJointRigidbody.velocity = vel;
}
}
/// <summary>
/// Updates the angularVelocity of a Rigidbody.
/// </summary>
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;
}
}
/// <summary>
/// Checks if the vector is valid.
/// </summary>
/// <param name="vector">The vector to be checked.</param>
/// <returns>rue if the vector is valid; otherwise, false.</returns>
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
/// <summary>
/// When tracking state changing, reset the pose and enable/disable collider.
/// </summary>
/// <param name="isTracked">The bool that tracking state.</param>
private void ChangeTrackingState(bool isTracked)
{
if (!isTracked)
{
lastRootPos = Vector3.zero;
lastRotation = Quaternion.identity;
rootJointRigidbody.velocity = Vector3.zero;
rootJointRigidbody.angularVelocity = Vector3.zero;
}
foreach (JointCollider jointCollider in jointsCollider)
{
jointCollider.Collider.enabled = m_EnableCollider && isTracked;
}
}
/// <summary>
/// Called when the hand begins grabbing an object.
/// </summary>
/// <param name="grabber">The grabber that grabbing something.</param>
public void OnHandBeginGrab(IGrabber grabber)
{
EnableKinematic(true);
isGrabbing = true;
}
/// <summary>
/// Called when the hand ends grabbing an object.
/// </summary>
/// <param name="grabber">The grabber that releasing something.</param>
public void OnHandEndGrab(IGrabber grabber)
{
EnableKinematic(false);
isGrabbing = false;
}
/// <summary>
/// Enable/Disable the rigidbody of hand collider.
/// </summary>
/// <param name="enable">The bool that enable or disable the rigidbody.</param>
private void EnableKinematic(bool enable)
{
if (rootJointRigidbody != null)
{
rootJointRigidbody.isKinematic = enable;
}
}
/// <summary>
/// Called when a joint collider collides with another object.
/// </summary>
/// <param name="collision">The collision data.</param>
/// <param name="state">The state of the collision.</param>
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))
{
ContactPoint[] contactPoints = new ContactPoint[collision.contactCount];
collision.GetContacts(contactPoints);
foreach (ContactPoint contactPoint in contactPoints)
{
collisionDirections.Add(contactPoint.normal * -1f);
}
}
break;
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 199fd8ec06e6e7e49a3fab9a9b7e7988
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35c93bc2a4bb4334e9fc24c4dbac1b63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,376 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to manage the positions of various joint nodes in the hand model.
/// </summary>
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>();
meshHandPose.SetHandMeshRenderer(this);
}
private void OnDisable()
{
if (m_HandCollider != null)
{
Destroy(m_HandCollider);
}
MeshHandPose meshHandPose = transform.GetComponent<MeshHandPose>();
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);
}
}
/// <summary>
/// Gets the position and rotation of the specified joint.
/// </summary>
/// <param name="joint">The joint type to get position and rotation from.</param>
/// <param name="position">The position of the joint.</param>
/// <param name="rotation">The rotation of the joint.</param>
/// <param name="local">Whether to get the local position and rotation.</param>
/// <returns>True if the joint position and rotation are successfully obtained; otherwise, false.</returns>
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;
}
/// <summary>
/// Sets the position and rotation of the specified joint.
/// </summary>
/// <param name="joint">The joint type to set position and rotation for.</param>
/// <param name="position">The new position of the joint.</param>
/// <param name="rotation">The new rotation of the joint.</param>
/// <param name="local">Whether to set the local position and rotation.</param>
/// <returns>True if the joint position and rotation are successfully set; otherwise, false.</returns>
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<Transform> childrenTransforms)
{
foreach (Transform child in parent)
{
childrenTransforms.Add(child);
GetAllChildrenTransforms(child, ref childrenTransforms);
}
}
public void FindJoints()
{
List<Transform> totalTransforms = new List<Transform>() { 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<HandColliderController>();
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);
}
}
/// <summary>
/// Calculate whether the current rotation exceeds the maximum rotation.
/// If the product is greater than 0, it exceeds.
/// </summary>
/// <param name="currentRot">Current rotation</param>
/// <param name="maxRot">Maximum rotation</param>
/// <returns>The return value represents the dot product between the cross product of two rotations and the -x axis direction of the current rotation.</returns>
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));
}
/// <summary>
/// Calculate the angle between the y-axis directions of two rotations.
/// </summary>
/// <param name="currentRot">Current rotation</param>
/// <param name="maxRot">Maximum rotation</param>
/// <returns>The return value represents the angle between the up directions of the two rotation</returns>
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;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 062de99b5e677f34b8f4f6429d5178cc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e199f8a96e0fdf7498bbb0bf45e5b11a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using System.Linq;
using System.Text;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public abstract class HandPose : MonoBehaviour
{
#region Log
private const string LOG_TAG = "Wave.Essence.Hand.Interaction.HandPose";
private static StringBuilder m_sb = null;
protected static StringBuilder sb
{
get
{
if (m_sb == null) { m_sb = new StringBuilder(); }
return m_sb;
}
}
protected void DEBUG(string msg) { Debug.Log($"{LOG_TAG}.{m_PoseType}, {msg}"); }
protected void WARNING(string msg) { Debug.LogWarning($"{LOG_TAG}.{m_PoseType}, {msg}"); }
protected void ERROR(string msg) { Debug.LogError($"{LOG_TAG}.{m_PoseType}, {msg}"); }
#endregion
protected HandPoseType m_PoseType = HandPoseType.UNKNOWN;
protected bool m_Initialized = false;
protected bool m_IsTracked = false;
protected const int poseCount = (int)JointType.Count;
protected Vector3[] m_Position = Enumerable.Repeat(Vector3.zero, poseCount).ToArray();
protected Vector3[] m_LocalPosition = Enumerable.Repeat(Vector3.zero, poseCount).ToArray();
protected Quaternion[] m_Rotation = Enumerable.Repeat(Quaternion.identity, poseCount).ToArray();
protected Quaternion[] m_LocalRotation = Enumerable.Repeat(Quaternion.identity, poseCount).ToArray();
protected virtual void OnEnable()
{
HandPoseProvider.RegisterHandPose(m_PoseType, this);
}
protected virtual void OnDisable()
{
HandPoseProvider.UnregisterHandPose(m_PoseType);
}
public virtual void SetType(HandPoseType poseType)
{
m_PoseType = poseType;
m_Initialized = true;
}
public virtual bool IsTracked()
{
return m_IsTracked;
}
public virtual bool GetRotation(JointType joint, out Quaternion value, bool local = false)
{
value = Quaternion.identity;
if (joint != JointType.Count)
{
value = local ? m_LocalRotation[(int)joint] : m_Rotation[(int)joint];
return true;
}
return false;
}
public virtual bool GetPosition(JointType joint, out Vector3 value, bool local = false)
{
value = Vector3.zero;
if (joint != JointType.Count)
{
value = local ? m_LocalPosition[(int)joint] : m_Position[(int)joint];
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ced5448f0990b040b82475106e1b1d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public enum HandPoseType : UInt32
{
UNKNOWN = 0x7FFFFFFF,
HAND_LEFT = 100,
HAND_RIGHT = 101,
MESH_LEFT = 200,
MESH_RIGHT = 201,
}
public static class HandPoseProvider
{
private static Dictionary<HandPoseType, HandPose> m_HandPoseMap = new Dictionary<HandPoseType, HandPose>();
public static Dictionary<HandPoseType, HandPose> HandPoseMap
{
get
{
if (m_HandPoseMap == null) { m_HandPoseMap = new Dictionary<HandPoseType, HandPose>(); }
return m_HandPoseMap;
}
private set { m_HandPoseMap = value; }
}
public static bool RegisterHandPose(in HandPoseType poseType, in HandPose handPose)
{
if (!HandPoseMap.ContainsKey(poseType))
{
HandPoseMap.Add(poseType, handPose);
return true;
}
return false;
}
public static bool UnregisterHandPose(in HandPoseType poseType)
{
if (HandPoseMap.ContainsKey(poseType))
{
HandPoseMap.Remove(poseType);
return true;
}
return false;
}
public static HandPose GetHandPose(in HandPoseType poseType)
{
if (HandPoseMap.ContainsKey(poseType))
{
return HandPoseMap[poseType];
}
if (poseType == HandPoseType.HAND_LEFT || poseType == HandPoseType.MESH_LEFT)
{
return GetDefaultHandPose("LeftHandPose", HandPoseType.HAND_LEFT);
}
else if (poseType == HandPoseType.HAND_RIGHT || poseType == HandPoseType.MESH_RIGHT)
{
return GetDefaultHandPose("RightHandPose", HandPoseType.HAND_RIGHT);
}
return null;
}
public static string Name(this HandPoseType poseType)
{
string name = "";
switch (poseType)
{
case HandPoseType.HAND_LEFT: name = "HAND_LEFT"; break;
case HandPoseType.HAND_RIGHT: name = "HAND_LEFT"; break;
case HandPoseType.MESH_LEFT: name = "MESH_LEFT"; break;
case HandPoseType.MESH_RIGHT: name = "MESH_RIGHT"; break;
}
return name;
}
private static HandPose GetDefaultHandPose(string poseName, HandPoseType poseType)
{
if (!HandPoseMap.ContainsKey(poseType))
{
GameObject handPoseObject = new GameObject(poseName);
RealHandPose realHandPose = handPoseObject.AddComponent<RealHandPose>();
realHandPose.SetType(poseType);
RegisterHandPose(poseType, realHandPose);
return realHandPose;
}
return HandPoseMap[poseType];
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 059d07a57fa09e543bf2f2b6d5788bb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
using System.Collections;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class MeshHandPose : HandPose
{
[SerializeField]
private HandMeshManager m_HandMesh;
private bool keepUpdate = false;
protected override void OnEnable()
{
StartCoroutine(WaitInit());
}
protected override void OnDisable()
{
base.OnDisable();
if (keepUpdate)
{
keepUpdate = false;
StopCoroutine(UpdatePose());
}
}
public void SetHandMeshRenderer(HandMeshManager handMeshRenderer)
{
m_HandMesh = handMeshRenderer;
SetType(handMeshRenderer.isLeft ? HandPoseType.MESH_LEFT : HandPoseType.MESH_RIGHT);
}
public bool SetJointPose(JointType joint, Pose jointPose, bool local = false)
{
if (m_HandMesh != null)
{
return m_HandMesh.SetJointPositionAndRotation(joint, jointPose.position, jointPose.rotation, local);
}
return false;
}
private IEnumerator WaitInit()
{
yield return new WaitUntil(() => m_Initialized);
base.OnEnable();
if (!keepUpdate)
{
keepUpdate = true;
StartCoroutine(UpdatePose());
}
}
private IEnumerator UpdatePose()
{
while (keepUpdate)
{
yield return new WaitForFixedUpdate();
HandPose handPose = HandPoseProvider.GetHandPose(m_HandMesh.isLeft ? HandPoseType.HAND_LEFT : HandPoseType.HAND_RIGHT);
m_IsTracked = handPose.IsTracked();
for (int i = 0; i < poseCount; i++)
{
if (m_HandMesh.GetJointPositionAndRotation((JointType)i, out Vector3 position, out Quaternion rotation) &&
m_HandMesh.GetJointPositionAndRotation((JointType)i, out Vector3 localPosition, out Quaternion localRotation, local: true))
{
m_Position[i] = position;
m_Rotation[i] = rotation;
m_LocalPosition[i] = localPosition;
m_LocalRotation[i] = localRotation;
}
else
{
m_Position[i] = Vector3.zero;
m_Rotation[i] = Quaternion.identity;
m_LocalPosition[i] = Vector3.zero;
m_LocalRotation[i] = Quaternion.identity;
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e78e2d8c82d5584faaf4b66d40d1057
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
using System.Collections;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class RealHandPose : HandPose
{
[SerializeField]
private Handedness m_Handedness;
private bool isLeft => m_Handedness == Handedness.Left;
private bool keepUpdate = false;
protected override void OnEnable()
{
StartCoroutine(WaitInit());
}
protected override void OnDisable()
{
base.OnDisable();
if (keepUpdate)
{
keepUpdate = false;
StopCoroutine(UpdatePose());
}
}
public override void SetType(HandPoseType poseType)
{
if (poseType == HandPoseType.HAND_LEFT)
{
m_Handedness = Handedness.Left;
}
else if (poseType == HandPoseType.HAND_RIGHT)
{
m_Handedness = Handedness.Right;
}
base.SetType(poseType);
}
private IEnumerator WaitInit()
{
yield return new WaitUntil(() => m_Initialized);
base.OnEnable();
if (!keepUpdate)
{
keepUpdate = true;
StartCoroutine(UpdatePose());
}
}
private IEnumerator UpdatePose()
{
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
while (keepUpdate)
{
yield return new WaitForEndOfFrame();
HandData handData = CachedHand.Get(isLeft);
m_IsTracked = handData.isTracked;
if (!m_IsTracked) { continue; }
for (int i = 0; i < poseCount; i++)
{
if (handData.GetJointPosition((JointType)i, ref position) && handData.GetJointRotation((JointType)i, ref rotation))
{
m_Position[i] = position;
m_Rotation[i] = rotation;
m_LocalPosition[i] = position;
m_LocalRotation[i] = rotation;
}
else
{
m_Position[i] = Vector3.zero;
m_Rotation[i] = Quaternion.identity;
m_LocalPosition[i] = Vector3.zero;
m_LocalRotation[i] = Quaternion.identity;
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 52790aba0e3d55f4fb27aded6c698d8b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,251 @@
// "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 HTCs SDK license agreement terms and
// conditions signed by you and all SDK and API requirements,
// specifications, and documentation provided by HTC to You."
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
/// <summary>
/// This class is designed to generate appropriately sized colliders for each joint.
/// </summary>
public class JointCollider : MonoBehaviour
{
public enum CollisionState
{
Enter = 0,
Stay = 1,
Exit = 2,
}
private CapsuleCollider m_Collider = null;
public Collider Collider => m_Collider;
public delegate void OnJointCollision(Collision collision, CollisionState state);
private OnJointCollision onJointCollision;
private const float k_ColliderRadius = 1E-6f;
private const float k_ColliderHeight = 1E-6f;
private JointType jointType = JointType.Count;
private void OnEnable()
{
InitCollider();
}
/// <summary>
/// Set the joint id and adjust collider size..
/// </summary>
/// <param name="id">JointType of joint.</param>
public void SetJointId(int id)
{
InitCollider();
jointType = (JointType)id;
switch (jointType)
{
case JointType.Palm:
m_Collider.center = new Vector3(0f, -0.006f, -0.0025f);
m_Collider.radius = 0.016f;
m_Collider.height = 0.075f;
m_Collider.direction = 0;
break;
case JointType.Wrist:
m_Collider.center = new Vector3(0f, -0.003f, 0f);
m_Collider.radius = 0.02f;
m_Collider.height = 0.055f;
m_Collider.direction = 0;
break;
case JointType.Thumb_Joint0:
m_Collider.center = new Vector3(0f, -0.005f, 0f);
m_Collider.radius = 0.02f;
m_Collider.height = 0.05f;
break;
case JointType.Thumb_Joint1:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.012f;
m_Collider.height = 0.040f;
break;
case JointType.Thumb_Joint2:
m_Collider.center = new Vector3(0f, -0.003f, 0f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Thumb_Tip:
m_Collider.center = new Vector3(0f, 0f, -0.005f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.025f;
break;
case JointType.Index_Joint0:
m_Collider.center = new Vector3(0f, 0f, 0.02f);
m_Collider.radius = 0.012f;
m_Collider.height = 0.1f;
break;
case JointType.Index_Joint1:
m_Collider.center = new Vector3(0f, 0f, 0.01f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.04f;
break;
case JointType.Index_Joint2:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Index_Joint3:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Index_Tip:
m_Collider.center = new Vector3(0f, -0.001f, -0.0025f);
m_Collider.radius = 0.008f;
m_Collider.height = 0.015f;
break;
case JointType.Middle_Joint0:
m_Collider.center = new Vector3(0f, 0f, 0.02f);
m_Collider.radius = 0.012f;
m_Collider.height = 0.1f;
break;
case JointType.Middle_Joint1:
m_Collider.center = new Vector3(0f, 0f, 0.01f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.04f;
break;
case JointType.Middle_Joint2:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Middle_Joint3:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Middle_Tip:
m_Collider.center = new Vector3(0f, -0.002f, -0.0025f);
m_Collider.radius = 0.008f;
m_Collider.height = 0.015f;
break;
case JointType.Ring_Joint0:
m_Collider.center = new Vector3(0f, 0f, 0.02f);
m_Collider.radius = 0.012f;
m_Collider.height = 0.1f;
break;
case JointType.Ring_Joint1:
m_Collider.center = new Vector3(0f, 0f, 0.01f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.04f;
break;
case JointType.Ring_Joint2:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Ring_Joint3:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Ring_Tip:
m_Collider.center = new Vector3(0f, -0.002f, -0.0025f);
m_Collider.radius = 0.008f;
m_Collider.height = 0.015f;
break;
case JointType.Pinky_Joint0:
m_Collider.center = new Vector3(0f, 0f, 0.02f);
m_Collider.radius = 0.012f;
m_Collider.height = 0.1f;
break;
case JointType.Pinky_Joint1:
m_Collider.center = new Vector3(0f, 0f, 0.01f);
m_Collider.radius = 0.01f;
m_Collider.height = 0.04f;
break;
case JointType.Pinky_Joint2:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Pinky_Joint3:
m_Collider.center = Vector3.zero;
m_Collider.radius = 0.01f;
m_Collider.height = 0.01f;
break;
case JointType.Pinky_Tip:
m_Collider.center = new Vector3(0f, -0.002f, -0.0025f);
m_Collider.radius = 0.006f;
m_Collider.height = 0.015f;
break;
}
}
public Rigidbody InitRigidbody(float mass)
{
Rigidbody rigidbody = gameObject.AddComponent<Rigidbody>();
rigidbody.mass = mass;
rigidbody.useGravity = false;
rigidbody.interpolation = RigidbodyInterpolation.Interpolate;
rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
rigidbody.solverIterations = 100;
rigidbody.solverVelocityIterations = 15;
return rigidbody;
}
private void InitCollider()
{
if (m_Collider == null)
{
m_Collider = gameObject.AddComponent<CapsuleCollider>();
m_Collider.radius = k_ColliderRadius;
m_Collider.height = k_ColliderHeight;
m_Collider.direction = 2;
}
}
private void OnCollisionEnter(Collision collision)
{
if (!IsJointCollider(collision.collider))
{
onJointCollision?.Invoke(collision, CollisionState.Enter);
}
}
private void OnCollisionStay(Collision collision)
{
if (!IsJointCollider(collision.collider))
{
onJointCollision?.Invoke(collision, CollisionState.Stay);
}
}
private void OnCollisionExit(Collision collision)
{
if (!IsJointCollider(collision.collider))
{
onJointCollision?.Invoke(collision, CollisionState.Exit);
}
}
private bool IsJointCollider(Collider collider)
{
JointCollider jointCollider = collider.GetComponent<JointCollider>();
return jointCollider != null;
}
public void AddJointCollisionListener(OnJointCollision handler)
{
onJointCollision += handler;
}
public void RemoveJointCollisionListener(OnJointCollision handler)
{
onJointCollision -= handler;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ca9a78f9ae24b3f4a92881fa9a680081
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,162 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
public class PhysicsInteractable : MonoBehaviour
{
[SerializeField]
private float forceMultiplier = 1.0f;
private readonly int MIN_POSE_SAMPLES = 2;
private readonly int MAX_POSE_SAMPLES = 10;
private readonly float MIN_VELOCITY = 0.5f;
private Rigidbody interactableRigidbody;
private List<Pose> movementPoses = new List<Pose>();
private List<float> timestamps = new List<float>();
private bool isBegin = false;
private bool isEnd = false;
private object lockVel = new object();
private void Update()
{
if (interactableRigidbody == null) { return; }
if (isBegin)
{
RecordMovement();
}
}
private void FixedUpdate()
{
if (interactableRigidbody == null) { return; }
if (isEnd)
{
interactableRigidbody.velocity = Vector3.zero;
interactableRigidbody.angularVelocity = Vector3.zero;
Vector3 velocity = CalculateVelocity();
if (velocity.magnitude > MIN_VELOCITY)
{
interactableRigidbody.AddForce(velocity * forceMultiplier, ForceMode.Impulse);
}
interactableRigidbody = null;
movementPoses.Clear();
timestamps.Clear();
isEnd = false;
}
}
private void RecordMovement()
{
float time = Time.time;
if (movementPoses.Count == 0 ||
timestamps[movementPoses.Count - 1] != time)
{
movementPoses.Add(new Pose(interactableRigidbody.position, interactableRigidbody.rotation));
timestamps.Add(time);
}
if (movementPoses.Count > MAX_POSE_SAMPLES)
{
movementPoses.RemoveAt(0);
timestamps.RemoveAt(0);
}
}
private Vector3 CalculateVelocity()
{
if (movementPoses.Count >= MIN_POSE_SAMPLES)
{
List<Vector3> velocities = new List<Vector3>();
for (int i = 0; i < movementPoses.Count - 1; i++)
{
for (int j = i + 1; j < movementPoses.Count; j++)
{
velocities.Add(GetVelocity(i, j));
}
}
Vector3 finalVelocity = FindBestVelocity(velocities);
return finalVelocity;
}
return Vector3.zero;
}
private Vector3 GetVelocity(int idx1, int idx2)
{
if (idx1 < 0 || idx1 >= movementPoses.Count
|| idx2 < 0 || idx2 >= movementPoses.Count
|| movementPoses.Count < MIN_POSE_SAMPLES)
{
return Vector3.zero;
}
if (idx2 < idx1)
{
(idx1, idx2) = (idx2, idx1);
}
Vector3 currentPos = movementPoses[idx2].position;
Vector3 previousPos = movementPoses[idx1].position;
float currentTime = timestamps[idx2];
float previousTime = timestamps[idx1];
float timeDelta = currentTime - previousTime;
if (currentPos == null || previousPos == null || timeDelta == 0)
{
return Vector3.zero;
}
Vector3 velocity = (currentPos - previousPos) / timeDelta;
return velocity;
}
private Vector3 FindBestVelocity(List<Vector3> velocities)
{
Vector3 bestVelocity = Vector3.zero;
float bestScore = float.PositiveInfinity;
Parallel.For(0, velocities.Count, i =>
{
float score = 0f;
for (int j = 0; j < velocities.Count; j++)
{
if (i != j)
{
score += (velocities[i] - velocities[j]).magnitude;
}
}
lock (lockVel)
{
if (score < bestScore)
{
bestVelocity = velocities[i];
bestScore = score;
}
}
});
return bestVelocity;
}
public void OnBeginInteractabled(IGrabbable grabbable)
{
if (grabbable is HandGrabInteractable handGrabbable)
{
interactableRigidbody = handGrabbable.rigidbody;
}
isBegin = true;
}
public void OnEndInteractabled(IGrabbable grabbable)
{
isBegin = false;
isEnd = true;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23de72f64d9c93540b53c64f183e4efa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: