version 2.3.0

This commit is contained in:
Sean Lu(呂祥榮)
2024-05-15 14:09:18 +08:00
parent 45b4e46f74
commit 7f2a459592
289 changed files with 116381 additions and 5440 deletions

View File

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

View File

@@ -0,0 +1,180 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using UnityEngine;
using UnityEngine.XR.OpenXR;
using VIVE.OpenXR.Feature;
using VIVE.OpenXR.Anchor;
using static VIVE.OpenXR.Anchor.ViveAnchor;
namespace VIVE.OpenXR.Toolkits.Anchor
{
public static class AnchorManager
{
static ViveAnchor feature = null;
static bool isSupported = false;
static void CheckFeature()
{
if (feature != null) return;
feature = OpenXRSettings.Instance.GetFeature<ViveAnchor>();
if (feature == null)
throw new NotSupportedException("ViveAnchor feature is not enabled");
}
/// <summary>
/// Helper to get the extention feature instance.
/// </summary>
/// <returns></returns>
public static ViveAnchor GetFeature()
{
try
{
CheckFeature();
}
catch (NotSupportedException)
{
Debug.LogWarning("ViveAnchor feature is not enabled");
return null;
}
return feature;
}
/// <summary>
/// Check if the extension is supported.
/// </summary>
/// <returns></returns>
public static bool IsSupported()
{
if (GetFeature() == null) return false;
if (isSupported) return true;
var ret = false;
if (feature.GetProperties(out XrSystemAnchorPropertiesHTC properties) == XrResult.XR_SUCCESS)
{
Debug.Log("Anchor: IsSupported() properties.supportedFeatures: " + properties.supportsAnchor);
ret = properties.supportsAnchor;
isSupported = ret;
}
else
{
Debug.Log("Anchor: IsSupported() GetSystemProperties failed.");
}
return ret;
}
/// <summary>
/// Create a spatial anchor at tracking space (Camera Rig).
/// </summary>
/// <param name="pose">The related pose to the tracking space (Camera Rig)</param>
/// <returns>Anchor container</returns>
public static Anchor CreateAnchor(Pose pose, string name)
{
try
{
CheckFeature();
XrSpace baseSpace = feature.GetTrackingSpace();
XrSpatialAnchorCreateInfoHTC createInfo = new XrSpatialAnchorCreateInfoHTC();
createInfo.type = XrStructureType.XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_HTC;
createInfo.poseInSpace = new XrPosef();
createInfo.poseInSpace.position = pose.position.ToOpenXRVector();
createInfo.poseInSpace.orientation = pose.rotation.ToOpenXRQuaternion();
createInfo.name.name = name;
createInfo.space = baseSpace;
if (feature.CreateSpatialAnchor(createInfo, out XrSpace anchor) == XrResult.XR_SUCCESS)
{
return new Anchor(anchor, name);
}
} catch (Exception) { }
return null;
}
public static bool GetSpatialAnchorName(Anchor anchor, out string name)
{
return GetSpatialAnchorName(anchor.GetXrSpace(), out name);
}
public static bool GetSpatialAnchorName(XrSpace anchor, out string name)
{
name = "";
CheckFeature();
XrResult ret = feature.GetSpatialAnchorName(anchor, out XrSpatialAnchorNameHTC xrName);
if (ret == XrResult.XR_SUCCESS)
name = xrName.name;
return ret == XrResult.XR_SUCCESS;
}
/// <summary>
/// Get the XrSpace stand for current tracking space.
/// </summary>
/// <returns></returns>
public static XrSpace GetTrackingSpace()
{
CheckFeature();
return feature.GetTrackingSpace();
}
/// <summary>
/// Get the pose related to current tracking space. Only when position and orientation are both valid, the pose is valid.
/// </summary>
/// <param name="anchor"></param>
/// <param name="pose"></param>
/// <returns>true if both position and rotation are valid.</returns>
public static bool GetTrackingSpacePose(Anchor anchor, out Pose pose)
{
var sw = SpaceWrapper.Instance;
return anchor.GetRelatedPose(feature.GetTrackingSpace(), ViveInterceptors.Instance.GetPredictTime(), out pose);
}
/// <summary>
/// Anchor is a named Space. It can be used to create a spatial anchor, or get the anchor's name.
/// After use it, you should call Dispose() to release the anchor.
/// </summary>
public class Anchor : VIVE.OpenXR.Feature.Space
{
/// <summary>
/// The anchor's name
/// </summary>
string name;
/// <summary>
/// The anchor's name
/// </summary>
public string Name
{
get
{
if (string.IsNullOrEmpty(name))
name = GetSpatialAnchorName();
return name;
}
}
internal Anchor(XrSpace anchor, string name) : base(anchor)
{
// Get the current tracking space.
this.name = name;
}
internal Anchor(Anchor other) : base(other.space)
{
// Get the current tracking space.
name = other.name;
}
/// <summary>
/// Get the anchor's name by using this anchor's handle, instead of the anchor's Name. This will update the anchor's Name.
/// </summary>
/// <returns></returns>
public string GetSpatialAnchorName()
{
AnchorManager.CheckFeature();
if (AnchorManager.GetSpatialAnchorName(this, out string name))
return name;
return null;
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
using System;
using System.Runtime.InteropServices;
namespace VIVE.OpenXR
{
public class MemoryTools
{
public static IntPtr ToIntPtr<T>(T[] array) where T : Enum
{
int size = Marshal.SizeOf(typeof(T)) * array.Length;
IntPtr ptr = Marshal.AllocHGlobal(size);
int[] intArray = new int[array.Length];
for (int i = 0; i < array.Length; i++)
intArray[i] = (int)(object)array[i];
Marshal.Copy(intArray, 0, ptr, array.Length);
return ptr;
}
// Make the same size raw buffer from input array.
public static IntPtr MakeRawMemory<T>(T[] refArray)
{
int size = Marshal.SizeOf(typeof(T)) * refArray.Length;
return Marshal.AllocHGlobal(size);
}
// Make the same size raw buffer from input array.
public static void CopyFromRawMemory<T>(T[] array, IntPtr raw)
{
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < array.Length; i++)
{
array[i] = Marshal.PtrToStructure<T>(IntPtr.Add(raw, i * step));
}
}
// Make the same size raw buffer from input array. Make sure the raw has enough size.
public static void CopyToRawMemory<T>(IntPtr raw, T[] array)
{
int step = Marshal.SizeOf(typeof(T));
for (int i = 0; i < array.Length; i++)
{
Marshal.StructureToPtr<T>(array[i], IntPtr.Add(raw, i * step), false);
}
}
public static void ReleaseRawMemory(IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
}
}
}

View File

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

View File

@@ -0,0 +1,373 @@
// Copyright HTC Corporation All Rights Reserved.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.OpenXR;
using VIVE.OpenXR.PlaneDetection;
using static VIVE.OpenXR.PlaneDetection.VivePlaneDetection;
namespace VIVE.OpenXR.Toolkits.PlaneDetection
{
/// <summary>
/// The detected plane's data.
/// See <see cref="VivePlaneDetection.XrPlaneDetectorLocationEXT"/>
/// </summary>
public class PlaneDetectorLocation
{
public ulong planeId;
public XrSpaceLocationFlags locationFlags;
public Pose pose;
public Vector3 size; // Only width(X) and height(Y) are valid, Z is always 0.
public XrPlaneDetectorOrientationEXT orientation;
public XrPlaneDetectorSemanticTypeEXT semanticType;
public uint polygonBufferCount;
public XrPlaneDetectorLocationEXT locationRaw;
}
/// <summary>
/// The information for creating Mesh.
/// Plane's normal is facing +Z in Unity's coordination.
/// </summary>
public class Plane
{
public Vector3 scale; // Only width(X) and height(Y) are valid, Z is always 1.
public Vector3 center; // Should always be Vector3.Zero.
public Vector3[] verticesRaw; // The original vertices from <see cref="XrPlaneDetectorPolygonBufferEXT"/>
public Vector3[] verticesGenerated; // generated vertices for creating Mesh.
public Vector2[] uvsGenerated;
public int[] indicesGenerated;
/// <summary>
/// According to the input vertices, calculate the rectangle, and create the plane.
/// </summary>
/// <param name="vertices">The vertices from <see cref="XrPlaneDetectorPolygonBufferEXT"/></param>
public static Plane CreateFromVertices(Vector2[] vertices)
{
// Assume the polygon is a rectangle.
if (vertices.Length != 4)
return null;
// Check the size from vertices.
Vector2 min, max;
min = max = new Vector2(vertices[0].x, vertices[0].y);
for (int i = 1; i < vertices.Length; i++)
{
min.x = Mathf.Min(min.x, vertices[i].x);
min.y = Mathf.Min(min.y, vertices[i].y);
max.x = Mathf.Max(max.x, vertices[i].x);
max.y = Mathf.Max(max.y, vertices[i].y);
}
var verticesRaw = new Vector3[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
verticesRaw[i] = new Vector3(vertices[i].x, 0, vertices[i].y);
var verticesGenerated = new Vector3[4];
verticesGenerated[0] = new Vector3(min.x, min.y, 0);
verticesGenerated[1] = new Vector3(max.x, min.y, 0);
verticesGenerated[2] = new Vector3(min.x, max.y, 0);
verticesGenerated[3] = new Vector3(max.x, max.y, 0);
var indicesGenerated = new int[] { 0, 3, 2, 0, 1, 3 };
var uvsGenerated = new Vector2[4];
uvsGenerated[0] = new Vector2(0, 0);
uvsGenerated[1] = new Vector2(1, 0);
uvsGenerated[2] = new Vector2(0, 1);
uvsGenerated[3] = new Vector2(1, 1);
return new Plane()
{
scale = max - min,
center = (max + min) / 2,
verticesRaw = verticesRaw,
verticesGenerated = verticesGenerated,
indicesGenerated = indicesGenerated,
uvsGenerated = uvsGenerated
};
}
}
/// <summary>
/// The PlaneDetector is created by <see cref="PlaneDetectionManager.CreatePlaneDetector" />.
/// </summary>
public class PlaneDetector
{
IntPtr planeDetector = IntPtr.Zero;
VivePlaneDetection feature = null;
public PlaneDetector(IntPtr pd, VivePlaneDetection f)
{
feature = f;
planeDetector = pd;
}
/// <summary>
/// Get the raw plane detector handle. See <see cref="VivePlaneDetection.CreatePlaneDetector"/>
/// </summary>
/// <returns>The raw plane detector handle</returns>
public IntPtr GetDetectorRaw()
{
return planeDetector;
}
/// <summary>
/// Begin detect plane. In VIVE's implementation, planes are predefined in Room Setup.
/// However you have to call this function to get the plane information.
/// See <see cref="VivePlaneDetection.BeginPlaneDetection"/>
/// </summary>
public XrResult BeginPlaneDetection()
{
if (feature == null)
return XrResult.XR_ERROR_FEATURE_UNSUPPORTED;
Debug.Log("BeginPlaneDetection()");
var beginInfo = feature.MakeGetAllXrPlaneDetectorBeginInfoEXT();
return feature.BeginPlaneDetection(planeDetector, beginInfo);
}
/// <summary>
/// Get the state of plane detection.
/// See <see cref="VivePlaneDetection.GetPlaneDetectionState"/>
/// </summary>
/// <returns></returns>
public XrPlaneDetectionStateEXT GetPlaneDetectionState()
{
if (feature == null)
return XrPlaneDetectionStateEXT.NONE_EXT;
Debug.Log("GetPlaneDetectionState()");
XrPlaneDetectionStateEXT state = XrPlaneDetectionStateEXT.NONE_EXT;
feature.GetPlaneDetectionState(planeDetector, ref state);
return state;
}
/// <summary>
/// Get result of plane detection.
/// See <see cref="VivePlaneDetection.GetPlaneDetections"/>
/// </summary>
/// <param name="locations">The detected planes.</param>
/// <returns></returns>
public XrResult GetPlaneDetections(out List<PlaneDetectorLocation> locations)
{
locations = null;
if (feature == null)
return XrResult.XR_ERROR_FEATURE_UNSUPPORTED;
Debug.Log("GetPlaneDetections()");
XrPlaneDetectorGetInfoEXT info = new XrPlaneDetectorGetInfoEXT
{
type = XrStructureType.XR_TYPE_PLANE_DETECTOR_GET_INFO_EXT,
baseSpace = feature.GetTrackingSpace(),
time = feature.GetPredictTime(),
};
XrPlaneDetectorLocationsEXT locationsRaw = new XrPlaneDetectorLocationsEXT
{
type = XrStructureType.XR_TYPE_PLANE_DETECTOR_LOCATIONS_EXT,
planeLocationCapacityInput = 0,
planeLocationCountOutput = 0,
planeLocations = IntPtr.Zero,
};
var ret = feature.GetPlaneDetections(planeDetector, ref info, ref locationsRaw);
if (ret != XrResult.XR_SUCCESS || locationsRaw.planeLocationCountOutput == 0)
return ret;
Debug.Log("GetPlaneDetections() locations.planeLocationCountOutput: " + locationsRaw.planeLocationCountOutput);
var locationsArray = new XrPlaneDetectorLocationEXT[locationsRaw.planeLocationCountOutput];
var locationsPtr = MemoryTools.MakeRawMemory(locationsArray);
locationsRaw.planeLocationCapacityInput = locationsRaw.planeLocationCountOutput;
locationsRaw.planeLocationCountOutput = 0;
locationsRaw.planeLocations = locationsPtr;
ret = feature.GetPlaneDetections(planeDetector, ref info, ref locationsRaw);
if (ret != XrResult.XR_SUCCESS)
{
MemoryTools.ReleaseRawMemory(locationsPtr);
return ret;
}
MemoryTools.CopyFromRawMemory(locationsArray, locationsPtr);
locations = new List<PlaneDetectorLocation>();
// The plane's neutral pose is horizontal, and not like the plane pose in unity which is vertical.
// Therefore, we wil perform a rotation to convert from the OpenXR's forward to unity's forward.
// In Unity, the rotation applied order is in ZXY order.
Quaternion forward = Quaternion.Euler(-90, 180, 0);
for (int i = 0; i < locationsRaw.planeLocationCountOutput; i++)
{
XrPlaneDetectorLocationEXT location = locationsArray[i];
PlaneDetectorLocation pdl = new PlaneDetectorLocation
{
planeId = location.planeId,
locationFlags = location.locationFlags,
pose = new Pose(OpenXRHelper.ToUnityVector(location.pose.position), OpenXRHelper.ToUnityQuaternion(location.pose.orientation) * forward),
// Because the pose is converted, we will apply extent to X and Y.
size = new Vector3(location.extents.width, location.extents.height, 0),
orientation = location.orientation,
semanticType = location.semanticType,
polygonBufferCount = location.polygonBufferCount,
locationRaw = location,
};
locations.Add(pdl);
}
MemoryTools.ReleaseRawMemory(locationsPtr);
for (int i = 0; i < locationsRaw.planeLocationCountOutput; i++)
{
var location = locations[i];
Debug.Log("GetPlaneDetections() location.planeId: " + location.planeId);
Debug.Log("GetPlaneDetections() location.locationFlags: " + location.locationFlags);
Debug.Log("GetPlaneDetections() location.pose.position: " + location.pose.position);
Debug.Log("GetPlaneDetections() location.pose.rotation: " + location.pose.rotation);
Debug.Log("GetPlaneDetections() location.pose.rotation.eulerAngles: " + location.pose.rotation.eulerAngles);
var rot = location.locationRaw.pose.orientation;
Debug.Log($"GetPlaneDetections() locationRaw.pose.rotation: {rot.x}, {rot.y}, {rot.z}, {rot.w}");
}
return ret;
}
/// <summary>
/// Get the vertices of the plane from extension. Because there is no triangle
/// information from extension, it is hard to generate a mesh from only these vertices.
/// However VIVE only have rectangle plane. In this function, it will return the
/// <see cref="Plane"/> class which contains generated information for creating Mesh.
/// </summary>
/// <param name="planeId">The planeId from <see cref="PlaneDetectorLocation"/></param>
/// <returns>The information for creating Mesh.</returns>
public Plane GetPlane(ulong planeId)
{
if (feature == null)
return null;
XrPlaneDetectorPolygonBufferEXT polygonBuffer = new XrPlaneDetectorPolygonBufferEXT
{
type = XrStructureType.XR_TYPE_PLANE_DETECTOR_POLYGON_BUFFER_EXT,
vertexCapacityInput = 0,
vertexCountOutput = 0,
vertices = IntPtr.Zero,
};
var ret = feature.GetPlanePolygonBuffer(planeDetector, planeId, 0, ref polygonBuffer);
Debug.Log("GetPlane() polygonBuffer.vertexCountOutput: " + polygonBuffer.vertexCountOutput);
if (ret != XrResult.XR_SUCCESS || polygonBuffer.vertexCountOutput == 0)
return null;
var verticesArray = new Vector2[polygonBuffer.vertexCountOutput];
var verticesPtr = MemoryTools.MakeRawMemory(verticesArray);
polygonBuffer.vertexCapacityInput = polygonBuffer.vertexCountOutput;
polygonBuffer.vertexCountOutput = 0;
polygonBuffer.vertices = verticesPtr;
if (feature.GetPlanePolygonBuffer(planeDetector, planeId, 0, ref polygonBuffer) != XrResult.XR_SUCCESS)
{
MemoryTools.ReleaseRawMemory(verticesPtr);
return null;
}
MemoryTools.CopyFromRawMemory(verticesArray, verticesPtr);
MemoryTools.ReleaseRawMemory(verticesPtr);
for (int j = 0; j < verticesArray.Length; j++)
{
var v = verticesArray[j];
Debug.Log($"GetPlane() verticesArray[{j}]: ({v.x}, {v.y})");
}
return Plane.CreateFromVertices(verticesArray);
}
}
public static class PlaneDetectionManager
{
static VivePlaneDetection feature = null;
static bool isSupported = false;
static void CheckFeature()
{
if (feature != null) return;
feature = OpenXRSettings.Instance.GetFeature<VivePlaneDetection>();
if (feature == null)
throw new NotSupportedException("PlaneDetection feature is not enabled");
}
/// <summary>
/// Helper to get the extention feature instance.
/// </summary>
/// <returns></returns>
public static VivePlaneDetection GetFeature()
{
try
{
CheckFeature();
}
catch (NotSupportedException)
{
Debug.LogWarning("PlaneDetection feature is not enabled");
return null;
}
return feature;
}
/// <summary>
/// Check if the extension is supported.
/// </summary>
/// <returns></returns>
public static bool IsSupported()
{
if (GetFeature() == null) return false;
if (isSupported) return true;
if (feature == null) return false;
bool ret = false;
if (feature.GetProperties(out var properties) == XrResult.XR_SUCCESS)
{
Debug.Log("PlaneDetection: IsSupported() properties.supportedFeatures: " + properties.supportedFeatures);
ret = (properties.supportedFeatures & CAPABILITY_PLANE_DETECTION_BIT_EXT) > 0;
isSupported = ret;
}
else
{
Debug.Log("PlaneDetection: IsSupported() GetSystemProperties failed.");
}
return ret;
}
/// <summary>
/// This is a helper function. Currently only one createInfo is available. Developepr should create their own
/// </summary>
/// <returns></returns>
public static XrPlaneDetectorCreateInfoEXT MakeXrPlaneDetectorCreateInfoEXT()
{
return new XrPlaneDetectorCreateInfoEXT
{
type = XrStructureType.XR_TYPE_PLANE_DETECTOR_CREATE_INFO_EXT,
flags = XR_PLANE_DETECTOR_ENABLE_CONTOUR_BIT_EXT,
};
}
/// <summary>
/// Plane detector is a session of detect plane. You don't need to create multiple plane detector in VIVE's implemention. You need destroy it.
/// </summary>
/// <returns>PlaneDetector's handle</returns>
public static PlaneDetector CreatePlaneDetector()
{
CheckFeature();
if (feature == null)
return null;
var createInfo = MakeXrPlaneDetectorCreateInfoEXT();
var ret = feature.CreatePlaneDetector(createInfo, out var planeDetector);
if (ret != XrResult.XR_SUCCESS)
return null;
return new PlaneDetector(planeDetector, feature);
}
/// <summary>
/// Destroy the plane detector to release resource.
/// </summary>
public static void DestroyPlaneDetector(PlaneDetector pd)
{
if (pd == null)
return;
CheckFeature();
if (feature == null)
return;
feature.DestroyPlaneDetector(pd.GetDetectorRaw());
}
}
}

View File

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

View File

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

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,246 @@
// "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;
using UnityEditorInternal;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandGrabInteractable))]
public class HandGrabInteractableEditor : UnityEditor.Editor
{
private SerializedProperty m_IsGrabbable, m_ForceMovable, m_FingerRequirement, m_GrabPoses, m_ShowAllIndicator;
private SerializedProperty grabPoseName, gestureThumbPose, gestureIndexPose, gestureMiddlePose, gestureRingPose, gesturePinkyPose,
recordedGrabRotations, isLeft, enableIndicator, autoIndicator, indicatorObject, grabOffset;
private ReorderableList grabPoses;
private bool showGrabPoses = false;
private void OnEnable()
{
m_IsGrabbable = serializedObject.FindProperty("m_IsGrabbable");
m_ForceMovable = serializedObject.FindProperty("m_ForceMovable");
m_FingerRequirement = serializedObject.FindProperty("m_FingerRequirement");
m_GrabPoses = serializedObject.FindProperty("m_GrabPoses");
m_ShowAllIndicator = serializedObject.FindProperty("m_ShowAllIndicator");
#region ReorderableList
grabPoses = new ReorderableList(serializedObject, m_GrabPoses, true, true, true, true);
grabPoses.drawHeaderCallback = (Rect rect) =>
{
EditorGUI.LabelField(rect, "Grab Pose List");
};
grabPoses.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 gestureRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
grabPoseName.stringValue = EditorGUI.TextField(gestureRect, grabPoseName.stringValue);
if (recordedGrabRotations.arraySize == 0)
{
// Draw GrabGesture fields
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, gestureThumbPose);
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, gestureIndexPose);
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, gestureMiddlePose);
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, gestureRingPose);
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, gesturePinkyPose);
}
// Draw Handness fields
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
bool isToggle = EditorGUI.Toggle(gestureRect, "Is Left", isLeft.boolValue);
if (isToggle != isLeft.boolValue)
{
isLeft.boolValue = isToggle;
SwitchRotations(ref recordedGrabRotations);
}
// Draw Indicator fields
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
enableIndicator.boolValue = EditorGUI.Toggle(gestureRect, "Show Indicator", enableIndicator.boolValue);
if (enableIndicator.boolValue)
{
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
autoIndicator.boolValue = EditorGUI.Toggle(gestureRect, "Auto Generator Indicator", autoIndicator.boolValue);
if (!autoIndicator.boolValue)
{
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
indicatorObject.objectReferenceValue = (GameObject)EditorGUI.ObjectField(gestureRect, "Indicator", (GameObject)indicatorObject.objectReferenceValue, typeof(GameObject), true);
}
}
else
{
m_ShowAllIndicator.boolValue = false;
}
// Draw Mirror Pose fields
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
Rect labelRect = new Rect(gestureRect.x, gestureRect.y, EditorGUIUtility.labelWidth, gestureRect.height);
EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), new GUIContent("Mirror Pose"));
Rect buttonRect1 = new Rect(gestureRect.x + EditorGUIUtility.labelWidth + EditorGUIUtility.standardVerticalSpacing, gestureRect.y, (gestureRect.width - EditorGUIUtility.labelWidth - EditorGUIUtility.standardVerticalSpacing * 4) / 3, gestureRect.height);
Rect buttonRect2 = new Rect(buttonRect1.x + buttonRect1.width + EditorGUIUtility.standardVerticalSpacing, gestureRect.y, buttonRect1.width, gestureRect.height);
Rect buttonRect3 = new Rect(buttonRect2.x + buttonRect2.width + EditorGUIUtility.standardVerticalSpacing, gestureRect.y, buttonRect1.width, gestureRect.height);
if (GUI.Button(buttonRect1, "Align X axis"))
{
MirrorPose(ref grabOffset, Vector3.right);
}
if (GUI.Button(buttonRect2, "Align Y axis"))
{
MirrorPose(ref grabOffset, Vector3.up);
}
if (GUI.Button(buttonRect3, "Align Z axis"))
{
MirrorPose(ref grabOffset, Vector3.forward);
}
// Draw Position fields
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(gestureRect, grabOffset.FindPropertyRelative("position"));
// Draw Rotation fields
SerializedProperty rotationProperty = grabOffset.FindPropertyRelative("rotation");
Vector4 rotationVector = new Vector4(rotationProperty.quaternionValue.x, rotationProperty.quaternionValue.y, rotationProperty.quaternionValue.z, rotationProperty.quaternionValue.w);
gestureRect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
rotationVector = EditorGUI.Vector4Field(gestureRect, "Rotation", rotationVector);
rotationProperty.quaternionValue = new Quaternion(rotationVector.x, rotationVector.y, rotationVector.z, rotationVector.w);
};
grabPoses.elementHeightCallback = (int index) =>
{
if (!UpdateGrabPose(m_GrabPoses.GetArrayElementAtIndex(index))) { return EditorGUIUtility.singleLineHeight; }
// Including Title, Handness, Show Indicator, Mirror Pose, Position, Rotation
int minHeight = 6;
// 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 + 2);
};
grabPoses.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_IsGrabbable);
EditorGUILayout.PropertyField(m_ForceMovable);
EditorGUILayout.PropertyField(m_FingerRequirement);
showGrabPoses = EditorGUILayout.Foldout(showGrabPoses, "Grab Pose Settings");
if (showGrabPoses)
{
if(m_GrabPoses.arraySize == 0)
{
grabPoses.elementHeight = EditorGUIUtility.singleLineHeight;
}
grabPoses.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;
}
}
}
}
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;
}
/// <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 sourcePos = pose.FindPropertyRelative("position").vector3Value;
Quaternion sourceRot = pose.FindPropertyRelative("rotation").quaternionValue;
Vector3 sourceFwd = sourceRot * Vector3.forward;
Vector3 sourceUp = sourceRot * Vector3.up;
// Calculate the mirrored position using Vector3.Reflect along the specified mirror axis.
Vector3 mirroredPosition = Vector3.Reflect(sourcePos, mirrorAxis);
// Calculate the mirrored rotation using Quaternion.LookRotation and Vector3.Reflect for the forward and up vectors.
Quaternion mirroredRotation = Quaternion.LookRotation(Vector3.Reflect(sourceFwd, mirrorAxis), Vector3.Reflect(sourceUp, mirrorAxis));
pose.FindPropertyRelative("position").vector3Value = mirroredPosition;
pose.FindPropertyRelative("rotation").quaternionValue = mirroredRotation;
}
}
}
#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,69 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
namespace VIVE.OpenXR.Toolkits.RealisticHandInteraction
{
[CustomEditor(typeof(HandMeshManager))]
public class HandMeshManagerEditor : Editor
{
private HandMeshManager m_HandJointManager;
private SerializedProperty m_Handedness, m_HandGrabber, m_RootJointType, m_HandRootJoint, m_HandJoints;
private bool showJoints = false;
public static readonly GUIContent findJoints = EditorGUIUtility.TrTextContent("Find Joints");
public static readonly GUIContent clearJoints = EditorGUIUtility.TrTextContent("Clear");
private void OnEnable()
{
m_Handedness = serializedObject.FindProperty("m_Handedness");
m_HandGrabber = serializedObject.FindProperty("m_HandGrabber");
m_RootJointType = serializedObject.FindProperty("m_RootJointType");
m_HandRootJoint = serializedObject.FindProperty("m_HandRootJoint");
m_HandJoints = serializedObject.FindProperty("m_HandJoints");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(m_Handedness);
EditorGUILayout.PropertyField(m_HandGrabber);
EditorGUILayout.HelpBox("Without HandGrabber, it still works but won't stop when colliding with Immovable objects.", MessageType.Info);
EditorGUILayout.PropertyField(m_RootJointType);
EditorGUILayout.PropertyField(m_HandRootJoint);
showJoints = EditorGUILayout.Foldout(showJoints, "Hand Joints");
if (showJoints)
{
for (int i = 0; i < m_HandJoints.arraySize; i++)
{
SerializedProperty joint = m_HandJoints.GetArrayElementAtIndex(i);
JointType jointType = (JointType)i;
EditorGUILayout.PropertyField(joint, new GUIContent(jointType.ToString()));
}
using (new EditorGUILayout.HorizontalScope())
{
m_HandJointManager = target as HandMeshManager;
using (new EditorGUI.DisabledScope())
{
if (GUILayout.Button(findJoints))
{
m_HandJointManager.FindJoints();
}
}
using (new EditorGUI.DisabledScope())
{
if (GUILayout.Button(clearJoints))
{
m_HandJointManager.ClearJoints();
}
}
}
}
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: 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,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,93 @@
// "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 edit grab gestures.
/// </summary>
[RequireComponent(typeof(HandMeshManager))]
public class CustomGrabPose : MonoBehaviour
{
#if UNITY_EDITOR
[SerializeField]
private HandGrabGesture m_GrabGesture;
private HandMeshManager jointManager;
private Transform palmTransform;
private HandGrabGesture currentGesture;
private HandGrabInteractable candidate = null;
private readonly float k_GrabDistance = 0.1f;
private Pose[] fingerTipPoses => new Pose[5];
private void OnEnable()
{
if (jointManager == null)
{
jointManager = transform.GetComponent<HandMeshManager>();
}
}
private void Update()
{
// Non-DirectPreview mode.
if (m_GrabGesture != currentGesture)
{
currentGesture = m_GrabGesture;
jointManager.SetJointsFromGrabGesture(currentGesture);
}
}
/// <summary>
/// Finds the nearest interactable object to the hand.
/// </summary>
public void FindNearInteractable()
{
if (jointManager.GetJointTransform(JointType.Palm, out palmTransform))
{
float maxScore = 0;
foreach (HandGrabInteractable interactable in GrabManager.handGrabbables)
{
float distanceScore = interactable.CalculateDistanceScore(palmTransform.position, k_GrabDistance);
if (distanceScore > maxScore)
{
maxScore = distanceScore;
candidate = interactable;
}
}
}
}
/// <summary>
/// Save the position and rotation offset with the candidate.
/// </summary>
public void SavePoseWithCandidate()
{
if (candidate != null &&
jointManager.GetJointTransform(JointType.Palm, out palmTransform))
{
Vector3 posOffset = candidate.transform.position - palmTransform.position;
Quaternion rotOffset = palmTransform.rotation;
GrabPose grabPose = GrabPose.Identity;
grabPose.Update($"Grab Pose {candidate.grabPoses.Count + 1}", currentGesture, jointManager.IsLeft);
grabPose.grabOffset = new GrabOffset(candidate.transform.position, candidate.transform.rotation, posOffset, rotOffset);
if (!candidate.grabPoses.Contains(grabPose))
{
candidate.grabPoses.Add(grabPose);
}
GrabbablePoseRecorder.SaveChanges();
}
}
#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,141 @@
// "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 GrabCollider : MonoBehaviour
{
public enum CollisionState
{
start = 0,
keep = 1,
end = 2,
}
private CapsuleCollider m_Collider = null;
public Collider Collider => m_Collider;
private bool m_IsCollision = false;
public bool IsCollision { get { return m_IsCollision; } set { m_IsCollision = value; } }
private const float k_ColliderRadius = 0.01f;
private const float k_ColliderHeight = 0.01f;
private JointType jointType = JointType.Count;
public delegate void CollisionHandler(JointType joint, Collision collision, CollisionState state);
private CollisionHandler m_CollisionHandler;
private void OnEnable()
{
m_Collider = transform.GetComponent<CapsuleCollider>();
if (m_Collider == null)
{
m_Collider = transform.gameObject.AddComponent<CapsuleCollider>();
}
m_Collider.radius = k_ColliderRadius;
m_Collider.height = k_ColliderHeight;
m_Collider.direction = 2;
Rigidbody rigidbody = transform.GetComponent<Rigidbody>();
if (rigidbody == null)
{
rigidbody = transform.gameObject.AddComponent<Rigidbody>();
}
rigidbody.useGravity = false;
rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
rigidbody.constraints = RigidbodyConstraints.FreezeAll;
}
/// <summary>
/// Set the joint id and adjust collider size..
/// </summary>
/// <param name="id">JointType of joint.</param>
public void SetJointId(int id)
{
jointType = (JointType)id;
if (m_Collider)
{
// Adjust the size and position of the collider based on jointId.
switch (jointType)
{
case JointType.Thumb_Joint0:
case JointType.Thumb_Joint1:
m_Collider.height = 0.03f;
break;
case JointType.Index_Joint0:
case JointType.Middle_Joint0:
case JointType.Ring_Joint0:
case JointType.Pinky_Joint0:
m_Collider.height = 0.08f;
m_Collider.center = new Vector3(0f, 0f, 0.02f);
break;
case JointType.Index_Joint1:
case JointType.Middle_Joint1:
case JointType.Ring_Joint1:
case JointType.Pinky_Joint1:
m_Collider.height = 0.05f;
m_Collider.center = new Vector3(0f, 0f, 0.02f);
break;
case JointType.Index_Tip:
case JointType.Middle_Tip:
case JointType.Ring_Tip:
case JointType.Pinky_Tip:
m_Collider.radius = 0.005f;
break;
}
}
}
public void AddListener(CollisionHandler handler)
{
m_CollisionHandler += handler;
}
public void RemoveListener(CollisionHandler handler)
{
m_CollisionHandler -= handler;
}
private void OnCollisionEnter(Collision collision)
{
if (!IsGrabCollider(collision.collider) && m_CollisionHandler != null)
{
m_CollisionHandler.Invoke(jointType, collision, CollisionState.start);
}
}
private void OnCollisionStay(Collision collision)
{
if (!IsGrabCollider(collision.collider) && m_CollisionHandler != null)
{
m_CollisionHandler.Invoke(jointType, collision, CollisionState.keep);
}
}
private void OnCollisionExit(Collision collision)
{
if (!IsGrabCollider(collision.collider) && m_CollisionHandler != null)
{
m_CollisionHandler.Invoke(jointType, collision, CollisionState.end);
}
}
private bool IsGrabCollider(Collider collider)
{
GrabCollider grabCollider = collider.gameObject.GetComponent<GrabCollider>();
return grabCollider != null;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b9b8d210c92da6a49ac85755f7b15cbb
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,102 @@
// "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;
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);
}
}
}
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,344 @@
// "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 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 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 bool m_ForceMovable = true;
public bool forceMovable { get { return m_ForceMovable; } set { m_ForceMovable = value; } }
[SerializeField]
private FingerRequirement m_FingerRequirement;
public FingerRequirement fingerRequirement => m_FingerRequirement;
#endregion
#region Public State
[SerializeField]
private List<GrabPose> m_GrabPoses = new List<GrabPose>();
public List<GrabPose> grabPoses => m_GrabPoses;
public GrabPose bestGrabPose => bestGrabPoseId != -1 ? m_GrabPoses[bestGrabPoseId] : GrabPose.Identity;
#endregion
[SerializeField]
private bool m_ShowAllIndicator = false;
private List<Collider> allColliders = new List<Collider>();
private HandGrabInteractor closestGrabber = null;
private int bestGrabPoseId = -1;
private OnBeginGrabbed onBeginGrabbed;
private OnEndGrabbed onEndGrabbed;
#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)
{
HandGrabInteractor handGrabber = grabber as HandGrabInteractor;
m_Grabber = handGrabber;
UpdateBestGrabPose(handGrabber.isLeft, handGrabber.handGrabState.GetJointPose(JointType.Palm));
onBeginGrabbed?.Invoke(this);
}
else
{
m_Grabber = null;
bestGrabPoseId = -1;
onEndGrabbed?.Invoke(this);
}
}
/// <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
{
int index = FindBestGrabPose(grabber.isLeft, grabber.handGrabState.GetJointPose((int)JointType.Palm));
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>
public void AddBeginGrabbedListener(OnBeginGrabbed handler)
{
onBeginGrabbed += 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>
public void RemoveBeginGrabbedListener(OnBeginGrabbed handler)
{
onBeginGrabbed -= 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>
public void AddEndGrabbedListener(OnEndGrabbed handler)
{
onEndGrabbed += 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>
public void RemoveEndGrabbedListener(OnEndGrabbed handler)
{
onEndGrabbed -= handler;
}
#endregion
/// <summary>
/// Generate all indicators and calculate grab offsets.
/// </summary>
private void Initialize()
{
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);
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];
// The grabPose.grabOffset was calculated as the position of the object minus the position of the hand,
// so inverse calculation is needed here to infer the position of the hand.
Vector3 offset = transform.rotation * Quaternion.Inverse(grabPose.grabOffset.targetRotation) * -grabPose.grabOffset.position;
Vector3 defaultPosition = transform.position + offset;
Vector3 closestPoint = GetClosestPoint(defaultPosition);
autoGenIndicator.SetPose(closestPoint, offset.normalized);
grabPose.indicator.Update(true, true, autoGenIndicator.gameObject);
grabPose.indicator.CalculateGrabOffset(transform);
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 hitDistnace = Vector3.Distance(sourcePos, hit.point);
if (distance > hitDistnace)
{
distance = hitDistnace;
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 bool UpdateBestGrabPose(bool isLeft, Pose grabberPose)
{
int index = FindBestGrabPose(isLeft, grabberPose);
if (index != -1 && index < m_GrabPoses.Count)
{
bestGrabPoseId = index;
return true;
}
index = -1;
return false;
}
/// <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)
{
Vector3 grabDirection = transform.rotation * Quaternion.Inverse(m_GrabPoses[i].grabOffset.targetRotation) * -m_GrabPoses[i].grabOffset.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);
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);
grabPose.indicator.SetActive(true);
}
}
}
}
}

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,390 @@
// "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 implement IHandGrabber, allowing objects to grab grabbable objects.
/// </summary>
public class HandGrabInteractor : MonoBehaviour, IHandGrabber
{
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; } }
[SerializeField]
private bool m_EnableCollider = true;
public bool enableCollider
{
get { return m_EnableCollider; }
set
{
m_EnableCollider = value;
if (colliderManager != null)
{
colliderManager.EnableCollider(m_EnableCollider);
}
}
}
public bool isLeft => handedness == Handedness.Left;
#endregion
[SerializeField]
private GrabColliderManager colliderManager;
private readonly float MinGrabScore = 0.25f;
private readonly float MinDistanceScore = 0.25f;
private HandGrabInteractable currentCaidate = null;
private GrabPose grabPose = GrabPose.Identity;
private GrabState m_State = GrabState.None;
private Pose[] fingerTipPoses => new Pose[]
{
m_HandGrabState.GetJointPose(JointType.Thumb_Tip),
m_HandGrabState.GetJointPose(JointType.Index_Tip),
m_HandGrabState.GetJointPose(JointType.Middle_Tip),
m_HandGrabState.GetJointPose(JointType.Ring_Tip),
m_HandGrabState.GetJointPose(JointType.Pinky_Tip),
};
private Pose palmPose => m_HandGrabState.GetJointPose(JointType.Palm);
private Pose[] frozenPoses = new Pose[(int)JointType.Count];
private bool isFrozen = false;
private OnBeginGrab beginGrabHandler;
private OnEndGrab endGrabHandler;
#region MonoBehaviour
private void Awake()
{
m_HandGrabState = new HandGrabState(isLeft);
}
private void Start()
{
if (colliderManager != null)
{
colliderManager.EnableCollider(m_EnableCollider);
}
}
private void OnEnable()
{
GrabManager.RegisterGrabber(this);
if (colliderManager != null)
{
colliderManager.AddImmovableCollisionListener(FreezeHandPose);
}
}
private void OnDisable()
{
GrabManager.UnregisterGrabber(this);
if (colliderManager != null)
{
colliderManager.RemoveImmovableCollisionListener(FreezeHandPose);
}
}
private void Update()
{
m_HandGrabState.UpdateState();
if (isFrozen)
{
return;
}
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>
/// Get the current joint pose of the grabber.
/// </summary>
/// <param name="jointId">The id of the joint for which to get the pose.</param>
/// <returns>The current pose of the specified joint.</returns>
public Pose GetCurrentJointPose(int jointId)
{
if (isFrozen)
{
return frozenPoses[jointId];
}
return m_HandGrabState.GetJointPose(jointId);
}
/// <summary>
/// Get the rotation of the joint in the grab pose.
/// </summary>
/// <param name="jointId">The id of the joint for which to get the rotation.</param>
/// <returns>The rotation of the joint in the grab pose.</returns>
public bool GetGrabPoseJointRotation(int jointId, out Quaternion rotation)
{
rotation = Quaternion.identity;
if (m_Grabbable == null) { return false; }
if (jointId >= 0 && grabPose.recordedGrabRotations.Length > jointId)
{
rotation = grabPose.recordedGrabRotations[jointId];
return true;
}
else if (grabPose.handGrabGesture != HandGrabGesture.Identity)
{
rotation = m_HandGrabState.GetDefaultJointRotationInGesture(grabPose.handGrabGesture, jointId);
return true;
}
return false;
}
/// <summary>
/// Check if the specific joint is necessary for grabbing.
/// </summary>
/// <param name="joint">JointType of the specified joint.</param>
/// <returns>Return true if this joint is needed for grabbing, otherwise false.</returns>
public bool IsRequiredJoint(JointType joint)
{
if (m_Grabbable != null)
{
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>
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>
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>
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>
public void RemoveEndGrabListener(OnEndGrab handler)
{
endGrabHandler -= handler;
}
#endregion
/// <summary>
/// Find the candidate grabbable object for grabber.
/// </summary>
private void FindCandidate()
{
float distanceScore = float.MinValue;
if (GetClosestGrabbable(m_GrabDistance, out HandGrabInteractable grabbable, out float score) && score > distanceScore)
{
distanceScore = score;
currentCaidate = grabbable;
}
if (currentCaidate != null)
{
float grabScore = Grab.CalculateHandGrabScore(this, currentCaidate);
if (distanceScore < MinDistanceScore || grabScore < MinGrabScore)
{
currentCaidate = 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 (Pose fingerTipPose in fingerTipPoses)
{
float distanceScore = interactable.CalculateDistanceScore(fingerTipPose.position, 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 (currentCaidate != null)
{
m_State = GrabState.Hover;
}
}
/// <summary>
/// Update the state and related information when the grabber begins grabbing the grabbable.
/// </summary>
private void HoverUpdate()
{
if (currentCaidate == null)
{
m_State = GrabState.None;
return;
}
if (Grab.HandBeginGrab(this, currentCaidate))
{
m_State = GrabState.Grabbing;
m_Grabbable = currentCaidate;
m_Grabbable.SetGrabber(this);
m_Grabbable.ShowIndicator(false, this);
grabPose = m_Grabbable.bestGrabPose;
if (grabPose == GrabPose.Identity)
{
Vector3 posOffset = m_Grabbable.transform.position - palmPose.position;
Quaternion rotOffset = palmPose.rotation;
grabPose.grabOffset = new GrabOffset(m_Grabbable.transform.position, m_Grabbable.transform.rotation, posOffset, rotOffset);
}
beginGrabHandler?.Invoke(this);
}
}
/// <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))
{
m_Grabbable.SetGrabber(null);
m_Grabbable = null;
m_State = GrabState.Hover;
endGrabHandler?.Invoke(this);
return;
}
Quaternion handRotOffset = palmPose.rotation * Quaternion.Inverse(grabPose.grabOffset.rotation);
Vector3 currentPos = palmPose.position + handRotOffset * grabPose.grabOffset.position;
Quaternion currentRot = handRotOffset * grabPose.grabOffset.targetRotation;
m_Grabbable.transform.SetPositionAndRotation(currentPos, currentRot);
}
/// <summary>
/// Freezes or unfreezes the hand pose.
/// </summary>
/// <param name="enable">True to freeze the hand pose; False to unfreeze.</param>
private void FreezeHandPose(bool enable)
{
isFrozen = enable;
if (isFrozen)
{
for (int i = 0; i < frozenPoses.Length; i++)
{
frozenPoses[i] = m_HandGrabState.GetJointPose(i);
}
}
}
/// <summary>
/// Get the position of a specific joint.
/// </summary>
/// <param name="joint">The type of joint to get.</param>
/// <param name="position">The reference to store the position of the joint.</param>
/// <returns>True if the joint position is successfully retrieved; otherwise, false.</returns>
private void GetJointIndex(JointType joint, out int group, out int index)
{
int jointId = (int)joint + 1;
group = 0;
index = jointId;
// palm, wrist, thumb, index, middle, ring, pinky
int[] fingerGroup = { 1, 1, 4, 5, 5, 5, 5 };
while (index > fingerGroup[group])
{
index -= fingerGroup[group];
group += 1;
}
index -= 1;
}
}
}

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,11 @@
fileFormatVersion: 2
guid: 35c93bc2a4bb4334e9fc24c4dbac1b63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,251 @@
using System;
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
{
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}"); }
private enum RootType
{
Palm = JointType.Palm,
Wrist = JointType.Wrist,
}
[SerializeField]
private Handedness m_Handedness;
public bool IsLeft { get { return m_Handedness == Handedness.Left; } }
[SerializeField]
private HandGrabInteractor m_HandGrabber;
public HandGrabInteractor HandGrabber { get { return m_HandGrabber; } }
[SerializeField]
private RootType m_RootJointType;
public JointType RootJointType { get { return (JointType)m_RootJointType; } }
[SerializeField]
private Transform m_HandRootJoint;
public Transform HandRootJoint { get { return m_HandRootJoint; } }
[SerializeField]
private Transform[] m_HandJoints = new Transform[(int)JointType.Count];
public Transform[] HandJoints { get { return m_HandJoints; } }
private const int k_JointCount = 26;
private const int k_JointChildCount = 6;
private void OnEnable()
{
if (m_HandGrabber == null)
{
WARNING("Not to set HandGrabInteractor so it won't stop when colliding with Immovable objects.");
}
}
private void Update()
{
HandData hand = CachedHand.Get(IsLeft);
if (!hand.isTracked)
{
return;
}
var parentTransform = m_HandRootJoint.parent;
int rootId = m_RootJointType == RootType.Palm ? 0 : 1;
Pose jointPose = GetJointPose(hand, rootId);
m_HandJoints[rootId].rotation = parentTransform.rotation * jointPose.rotation;
m_HandJoints[rootId].localPosition = jointPose.position;
m_HandJoints[rootId].localRotation = jointPose.rotation;
for (int i = 0; i < m_HandJoints.Length; i++)
{
if (m_HandJoints[i] == null || i == rootId) { continue; }
jointPose = GetJointPose(hand, i);
m_HandJoints[i].rotation = parentTransform.rotation * jointPose.rotation;
if (m_HandGrabber != null && m_HandGrabber.isGrabbing &&
m_HandGrabber.GetGrabPoseJointRotation(i, out Quaternion localStaticRot))
{
Quaternion currentRotation = m_HandJoints[i].rotation;
Quaternion maxRotation = m_HandJoints[i].parent.rotation * localStaticRot;
if (m_HandGrabber.IsRequiredJoint((JointType)i) ||
OverFlex(currentRotation, maxRotation) >= 0 ||
FlexAngle(currentRotation, maxRotation) >= 100)
{
m_HandJoints[i].rotation = maxRotation;
}
}
}
}
/// <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;
}
/// <summary>
/// Get the pose of the joint based on the joint ID.
/// </summary>
/// <param name="hand">The current result of hand tracking.</param>
/// <param name="jointId">ID of the specified joint.</param>
/// <returns>Return the pose of the specified joint.</returns>
private Pose GetJointPose(HandData hand, int jointId)
{
if (m_HandGrabber != null)
{
return m_HandGrabber.GetCurrentJointPose(jointId);
}
Vector3 jointPosition = Vector3.zero;
Quaternion jointRotation = Quaternion.identity;
hand.GetJointPosition((JointType)jointId, ref jointPosition);
hand.GetJointRotation((JointType)jointId, ref jointRotation);
return new Pose(jointPosition, jointRotation);
}
/// <summary>
/// Get the transform of the joint.
/// </summary>
/// <param name="joint">JointType of the specified joint.</param>
/// <param name="jointTransform">Output the transform of the joint.</param>
/// <returns>Return true if successfully get the transform, otherwise false.</returns>
public bool GetJointTransform(JointType joint, out Transform jointTransform)
{
jointTransform = null;
int id = (int)joint;
if (id >= 0 && id < m_HandJoints.Length)
{
if (m_HandJoints[id] != null)
{
jointTransform = m_HandJoints[id];
return true;
}
}
return false;
}
/// <summary>
/// Set all joints through gesture.
/// </summary>
/// <param name="handGrabGesture">The gesture of grabbing.</param>
/// <returns>Return true if successfully set the gesture, otherwise false.</returns>
public bool SetJointsFromGrabGesture(HandGrabGesture handGrabGesture)
{
if (m_HandGrabber == null) { return false; }
for (int i = 0; i < m_HandJoints.Length; i++)
{
m_HandJoints[i].localRotation = m_HandGrabber.handGrabState.GetDefaultJointRotationInGesture(handGrabGesture, i);
}
return true;
}
#if UNITY_EDITOR
public void FindJoints()
{
if (m_HandRootJoint != null)
{
int fingerJoint = (int)JointType.Thumb_Joint0;
if (m_HandRootJoint.childCount == k_JointChildCount)
{
for (int i = 0; i < m_HandRootJoint.childCount; i++)
{
Transform child = m_HandRootJoint.GetChild(i);
switch (child.childCount)
{
case 0:
if (m_RootJointType == RootType.Palm)
{
m_HandJoints[(int)JointType.Palm] = m_HandRootJoint;
m_HandJoints[(int)JointType.Wrist] = child;
}
else
{
m_HandJoints[(int)JointType.Palm] = child;
m_HandJoints[(int)JointType.Wrist] = m_HandRootJoint;
}
break;
case 4:
case 5:
for (int j = 0; j < child.childCount; j++)
{
m_HandJoints[fingerJoint] = child.GetChild(j);
fingerJoint++;
}
break;
default:
fingerJoint = RecursiveFind(fingerJoint, child);
break;
}
}
}
else if (m_HandRootJoint.childCount == k_JointCount - 1)
{
Transform child = m_HandRootJoint.GetChild(0);
if (m_RootJointType == RootType.Palm)
{
m_HandJoints[(int)JointType.Palm] = m_HandRootJoint;
m_HandJoints[(int)JointType.Wrist] = child;
}
else
{
m_HandJoints[(int)JointType.Palm] = child;
m_HandJoints[(int)JointType.Wrist] = m_HandRootJoint;
}
for (int i = 1; i < m_HandRootJoint.childCount; i++)
{
m_HandJoints[i + 1] = m_HandRootJoint.GetChild(i);
}
}
}
}
private int RecursiveFind(int jointId, Transform transform)
{
m_HandJoints[jointId] = transform;
jointId++;
if (transform.childCount > 0)
{
for (int i = 0; i < transform.childCount; i++)
{
jointId = RecursiveFind(jointId, transform.GetChild(i));
}
}
return jointId;
}
public void ClearJoints()
{
Array.Clear(m_HandJoints, 0, m_HandJoints.Length);
}
#endif
}
}

View File

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