This commit is contained in:
2026-01-06 17:47:11 +01:00
commit 8423e2b790
1313 changed files with 295355 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
#if XR_HANDS_1_2_OR_NEWER
using System.Collections.Generic;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Hands.Processing;
#endif
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// A post processor for XR hand tracking data, using the One Euro filter to smooth hand positions.
/// </summary>
#if XR_HANDS_1_2_OR_NEWER
public class HandsOneEuroFilterPostProcessor : MonoBehaviour, IXRHandProcessor
#else
public class HandsOneEuroFilterPostProcessor : MonoBehaviour
#endif
{
[SerializeField]
[Tooltip("Smoothing amount at low speeds.")]
#pragma warning disable CS0414 // Field assigned but its value is never used -- Keep to retain serialized value when XR Hands is not installed
float m_FilterMinCutoff = 0.1f;
#pragma warning restore CS0414
[SerializeField]
[Tooltip("Filter's responsiveness to speed changes.")]
#pragma warning disable CS0414 // Field assigned but its value is never used -- Keep to retain serialized value when XR Hands is not installed
float m_FilterBeta = 0.2f;
#pragma warning restore CS0414
#if XR_HANDS_1_2_OR_NEWER
/// <inheritdoc />
public int callbackOrder => 0;
readonly OneEuroFilterVector3 m_LeftHandFilter = new OneEuroFilterVector3(Vector3.zero);
readonly OneEuroFilterVector3 m_RightHandFilter = new OneEuroFilterVector3(Vector3.zero);
bool m_WasLeftHandTrackedLastFrame;
bool m_WasRightHandTrackedLastFrame;
XRHandSubsystem m_Subsystem;
static readonly List<XRHandSubsystem> s_SubsystemsReuse = new List<XRHandSubsystem>();
#endif
#if XR_HANDS_1_2_OR_NEWER
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
if (m_Subsystem != null)
{
m_Subsystem.UnregisterProcessor(this);
m_Subsystem = null;
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Update()
{
if (m_Subsystem != null && m_Subsystem.running)
return;
SubsystemManager.GetSubsystems(s_SubsystemsReuse);
var foundRunningHandSubsystem = false;
for (var i = 0; i < s_SubsystemsReuse.Count; ++i)
{
var handSubsystem = s_SubsystemsReuse[i];
if (handSubsystem.running)
{
m_Subsystem?.UnregisterProcessor(this);
m_Subsystem = handSubsystem;
foundRunningHandSubsystem = true;
break;
}
}
if (!foundRunningHandSubsystem)
return;
m_WasLeftHandTrackedLastFrame = false;
m_WasRightHandTrackedLastFrame = false;
m_Subsystem.RegisterProcessor(this);
}
/// <inheritdoc />
public void ProcessJoints(XRHandSubsystem subsystem, XRHandSubsystem.UpdateSuccessFlags successFlags, XRHandSubsystem.UpdateType updateType)
{
var leftHand = subsystem.leftHand;
if (leftHand.isTracked)
{
var leftHandPose = leftHand.rootPose;
if (!m_WasLeftHandTrackedLastFrame)
{
m_LeftHandFilter.Initialize(leftHandPose.position);
}
else
{
var newLeftPosition = m_LeftHandFilter.Filter(leftHandPose.position, Time.deltaTime, m_FilterMinCutoff, m_FilterBeta);
var newLeftPose = new Pose(newLeftPosition, leftHandPose.rotation);
leftHand.SetRootPose(newLeftPose);
subsystem.SetCorrespondingHand(leftHand);
}
}
m_WasLeftHandTrackedLastFrame = leftHand.isTracked;
var rightHand = subsystem.rightHand;
if (rightHand.isTracked)
{
var rightHandPose = rightHand.rootPose;
if (!m_WasRightHandTrackedLastFrame)
{
m_RightHandFilter.Initialize(rightHandPose.position);
}
else
{
var newRightPosition = m_RightHandFilter.Filter(rightHandPose.position, Time.deltaTime, m_FilterMinCutoff, m_FilterBeta);
var newRightPose = new Pose(newRightPosition, rightHandPose.rotation);
rightHand.SetRootPose(newRightPose);
subsystem.SetCorrespondingHand(rightHand);
}
}
m_WasRightHandTrackedLastFrame = rightHand.isTracked;
}
#else
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Awake()
{
Debug.LogWarning("HandsOneEuroFilterPostProcessor requires XR Hands (com.unity.xr.hands) 1.2.0 or newer. Disabling component.", this);
enabled = false;
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,49 @@
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.Interactors.Visuals;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Hides the specified GameObject when the associated interactor is blocked by an interaction within its group.
/// </summary>
public class HideObjectWhenInteractorBlocked : MonoBehaviour
{
[SerializeField]
[Tooltip("The interactor that this component monitors for blockages.")]
XRBaseInteractor m_Interactor;
[SerializeField]
[Tooltip("The GameObject to hide when the interactor is blocked.")]
GameObject m_ObjectToHide;
ICurveInteractionDataProvider m_CurveInteractionDataProvider;
bool m_HasCurveDataProvider;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
if (m_Interactor == null || m_ObjectToHide == null)
enabled = false;
m_HasCurveDataProvider = false;
if (m_Interactor is ICurveInteractionDataProvider provider)
{
m_CurveInteractionDataProvider = provider;
m_HasCurveDataProvider = true;
}
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Update()
{
if (m_HasCurveDataProvider)
m_ObjectToHide.SetActive(m_CurveInteractionDataProvider.isActive);
else
m_ObjectToHide.SetActive(m_Interactor.isActiveAndEnabled && !m_Interactor.IsBlockedByInteractionWithinGroup());
}
}
}

View File

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

View File

@@ -0,0 +1,73 @@
using System;
using Unity.Mathematics;
using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.Receiver.Primitives;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Affordance receiver applying a Vector3 (Float3) affordance theme to a Transform local position.
/// Broadcasts new affordance value with Unity Event.
/// </summary>
[AddComponentMenu("Affordance System/Receiver/Transformation/Local Position Offset Affordance Receiver", 12)]
[Obsolete("The Affordance System namespace and all associated classes have been deprecated. The existing affordance system will be moved, replaced and updated with a new interaction feedback system in a future version of XRI.")]
public class LocalPositionOffsetAffordanceReceiver : Vector3AffordanceReceiver
{
[SerializeField]
[Tooltip("Transform on which to apply a local translation value.")]
Transform m_TransformToTranslate;
/// <summary>
/// Transform on which to apply a local translation value.
/// </summary>
public Transform transformToTranslate
{
get => m_TransformToTranslate;
set
{
m_TransformToTranslate = value;
m_HasTransformToTranslate = m_TransformToTranslate != null;
}
}
bool m_HasTransformToTranslate;
float3 m_InitialOffset = float3.zero;
/// <inheritdoc/>
protected override void OnEnable()
{
base.OnEnable();
m_HasTransformToTranslate = m_TransformToTranslate != null;
}
/// <inheritdoc/>
protected override float3 GetCurrentValueForCapture()
{
if (m_HasTransformToTranslate)
{
m_InitialOffset = m_TransformToTranslate.localPosition;
}
return float3.zero;
}
/// <inheritdoc/>
protected override void OnAffordanceValueUpdated(float3 newValue)
{
if (m_HasTransformToTranslate)
{
m_TransformToTranslate.localPosition = m_InitialOffset + newValue;
}
base.OnAffordanceValueUpdated(newValue);
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnValidate()
{
if (m_TransformToTranslate == null)
m_TransformToTranslate = transform;
}
}
}

View File

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

View File

@@ -0,0 +1,263 @@
using System;
using Unity.XR.CoreUtils.Bindings.Variables;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit.Inputs;
#if XR_HANDS_1_1_OR_NEWER
using UnityEngine.XR.Hands;
#endif
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Behavior that provides events for when the system gesture starts and ends and when the
/// menu palm pinch gesture occurs while hand tracking is in use.
/// </summary>
/// <remarks>
/// See <see href="https://docs.unity3d.com/Packages/com.unity.xr.hands@1.1/manual/features/metahandtrackingaim.html">Meta Hand Tracking Aim</see>.
/// </remarks>
/// <seealso cref="MetaAimHand"/>
public class MetaSystemGestureDetector : MonoBehaviour
{
/// <summary>
/// The state of the system gesture.
/// </summary>
/// <seealso cref="systemGestureState"/>
public enum SystemGestureState
{
/// <summary>
/// The system gesture has fully ended.
/// </summary>
Ended,
/// <summary>
/// The system gesture has started or is ongoing. Typically, this means the user is looking at
/// their palm at eye level or has not yet released the palm pinch gesture or turned their hand around.
/// </summary>
Started,
}
[SerializeField]
InputActionProperty m_AimFlagsAction = new InputActionProperty(new InputAction(expectedControlType: "Integer"));
/// <summary>
/// The Input System action to read the Aim Flags.
/// </summary>
/// <remarks>
/// Typically a <b>Value</b> action type with an <b>Integer</b> control type with a binding to either:
/// <list type="bullet">
/// <item>
/// <description><c>&lt;MetaAimHand&gt;{LeftHand}/aimFlags</c></description>
/// </item>
/// <item>
/// <description><c>&lt;MetaAimHand&gt;{RightHand}/aimFlags</c></description>
/// </item>
/// </list>
/// </remarks>
public InputActionProperty aimFlagsAction
{
get => m_AimFlagsAction;
set
{
if (Application.isPlaying)
UnbindAimFlags();
m_AimFlagsAction = value;
if (Application.isPlaying && isActiveAndEnabled)
BindAimFlags();
}
}
[SerializeField]
UnityEvent m_SystemGestureStarted;
/// <summary>
/// Calls the methods in its invocation list when the system gesture starts, which typically occurs when
/// the user looks at their palm at eye level.
/// </summary>
/// <seealso cref="systemGestureEnded"/>
/// <seealso cref="MetaAimFlags.SystemGesture"/>
public UnityEvent systemGestureStarted
{
get => m_SystemGestureStarted;
set => m_SystemGestureStarted = value;
}
[SerializeField]
UnityEvent m_SystemGestureEnded;
/// <summary>
/// Calls the methods in its invocation list when the system gesture ends.
/// </summary>
/// <remarks>
/// This behavior postpones ending the system gesture until the user has turned their hand around.
/// In other words, it isn't purely based on the <see cref="MetaAimFlags.SystemGesture"/>
/// being cleared from the aim flags in order to better replicate the native visual feedback in the Meta Home menu.
/// </remarks>
/// <seealso cref="systemGestureStarted"/>
/// <seealso cref="MetaAimFlags.SystemGesture"/>
public UnityEvent systemGestureEnded
{
get => m_SystemGestureEnded;
set => m_SystemGestureEnded = value;
}
[SerializeField]
UnityEvent m_MenuPressed;
/// <summary>
/// Calls the methods in its invocation list when the menu button is triggered by a palm pinch gesture.
/// </summary>
/// <remarks>
/// This is triggered by the non-dominant hand, which is the one with the menu icon (&#x2630;).
/// The universal menu (Oculus icon) on the dominant hand does not trigger this event.
/// </remarks>
/// <seealso cref="MetaAimFlags.MenuPressed"/>
public UnityEvent menuPressed
{
get => m_MenuPressed;
set => m_MenuPressed = value;
}
/// <summary>
/// The state of the system gesture.
/// </summary>
/// <seealso cref="SystemGestureState"/>
/// <seealso cref="systemGestureStarted"/>
/// <seealso cref="systemGestureEnded"/>
public IReadOnlyBindableVariable<SystemGestureState> systemGestureState => m_SystemGestureState;
readonly BindableEnum<SystemGestureState> m_SystemGestureState = new BindableEnum<SystemGestureState>(checkEquality: false);
#if XR_HANDS_1_1_OR_NEWER && (ENABLE_VR || UNITY_GAMECORE)
[NonSerialized] // NonSerialized is required to avoid an "Unsupported enum base type" error about the Flags enum being ulong
MetaAimFlags m_AimFlags;
#endif
bool m_AimFlagsBound;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnEnable()
{
BindAimFlags();
#if XR_HANDS_1_1_OR_NEWER
#if ENABLE_VR || UNITY_GAMECORE
var action = m_AimFlagsAction.action;
if (action != null)
// Force invoking the events upon initialization to simplify making sure the callback's desired results are synced
UpdateAimFlags((MetaAimFlags)action.ReadValue<int>(), true);
#endif
#else
Debug.LogWarning("Script requires XR Hands (com.unity.xr.hands) package to monitor Meta Aim Flags. Install using Window > Package Manager or click Fix on the related issue in Edit > Project Settings > XR Plug-in Management > Project Validation.", this);
SetGestureState(SystemGestureState.Ended, true);
#endif
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnDisable()
{
UnbindAimFlags();
}
void BindAimFlags()
{
if (m_AimFlagsBound)
return;
var action = m_AimFlagsAction.action;
if (action == null)
return;
action.performed += OnAimFlagsActionPerformedOrCanceled;
action.canceled += OnAimFlagsActionPerformedOrCanceled;
m_AimFlagsBound = true;
m_AimFlagsAction.EnableDirectAction();
}
void UnbindAimFlags()
{
if (!m_AimFlagsBound)
return;
var action = m_AimFlagsAction.action;
if (action == null)
return;
m_AimFlagsAction.DisableDirectAction();
action.performed -= OnAimFlagsActionPerformedOrCanceled;
action.canceled -= OnAimFlagsActionPerformedOrCanceled;
m_AimFlagsBound = false;
}
void SetGestureState(SystemGestureState state, bool forceInvoke)
{
if (!forceInvoke && m_SystemGestureState.Value == state)
return;
m_SystemGestureState.Value = state;
switch (state)
{
case SystemGestureState.Ended:
m_SystemGestureEnded?.Invoke();
break;
case SystemGestureState.Started:
m_SystemGestureStarted?.Invoke();
break;
}
}
#if XR_HANDS_1_1_OR_NEWER && (ENABLE_VR || UNITY_GAMECORE)
void UpdateAimFlags(MetaAimFlags value, bool forceInvoke = false)
{
var hadMenuPressed = (m_AimFlags & MetaAimFlags.MenuPressed) != 0;
m_AimFlags = value;
var hasSystemGesture = (m_AimFlags & MetaAimFlags.SystemGesture) != 0;
var hasMenuPressed = (m_AimFlags & MetaAimFlags.MenuPressed) != 0;
var hasValid = (m_AimFlags & MetaAimFlags.Valid) != 0;
var hasIndexPinching = (m_AimFlags & MetaAimFlags.IndexPinching) != 0;
if (!hadMenuPressed && hasMenuPressed)
{
m_MenuPressed?.Invoke();
}
if (hasSystemGesture || hasMenuPressed)
{
SetGestureState(SystemGestureState.Started, forceInvoke);
return;
}
if (hasValid)
{
SetGestureState(SystemGestureState.Ended, forceInvoke);
return;
}
// We want to keep the system gesture going when the user is still index pinching
// even though the SystemGesture flag is no longer set.
if (hasIndexPinching && m_SystemGestureState.Value != SystemGestureState.Ended)
{
SetGestureState(SystemGestureState.Started, forceInvoke);
return;
}
SetGestureState(SystemGestureState.Ended, forceInvoke);
}
#endif
void OnAimFlagsActionPerformedOrCanceled(InputAction.CallbackContext context)
{
#if XR_HANDS_1_1_OR_NEWER && (ENABLE_VR || UNITY_GAMECORE)
UpdateAimFlags((MetaAimFlags)context.ReadValue<int>());
#endif
}
}
}

View File

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

View File

@@ -0,0 +1,126 @@
using UnityEngine.XR.Interaction.Toolkit.Utilities;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Provides a means to smooth jittery <see cref="Vector3"/> signals.
/// This filter is particularly effective for small and rapid movements,
/// making it useful for applications like motion tracking or gesture recognition.
/// </summary>
/// <remarks>
/// The filtering process relies on two main parameters: <c>minCutoff</c> and <c>beta</c>.
/// <list type="bullet">
/// <item>
/// <term><c>minCutoff</c></term>
/// <description> primarily influences the smoothing at low speeds.</description>
/// </item>
/// <item>
/// <term><c>beta</c></term>
/// <description> determines the filter's responsiveness to speed changes.</description>
/// </item>
/// </list>
/// </remarks>
public class OneEuroFilterVector3
{
Vector3 m_LastRawValue;
Vector3 m_LastFilteredValue;
readonly float m_MinCutoff;
readonly float m_Beta;
/// <summary>
/// Initializes a new instance of the <see cref="OneEuroFilterVector3"/> with specified cutoff and beta values.
/// </summary>
/// <param name="initialRawValue">The initial raw value for the filter.</param>
/// <param name="minCutoff">The minimum cutoff value for the filter. Default is 0.1f.</param>
/// <param name="beta">The beta value for the filter. Default is 0.02f.</param>
/// <remarks>
/// Filter parameters:
/// <list type="bullet">
/// <item>
/// <term><paramref name="minCutoff"/></term>
/// <description>
/// Controls the amount of smoothing at low speeds. A smaller value will introduce
/// more smoothing and potential lag, helping to reduce low-frequency jitter. A larger value
/// may feel more responsive but can let through more jitter. It's advised to start with a
/// value around 0.1 for masking jitter in movements of about 1 cm.
/// </description>
/// </item>
/// <item>
/// <term><paramref name="beta"/></term>
/// <description>
/// Determines the filter's adjustment to speed changes. A smaller value provides consistent
/// smoothing, while a larger one introduces more aggressive adjustments for speed changes, offering
/// responsive filtering at high speeds. A starting value of 0.02 is recommended, but fine-tuning
/// might be necessary based on specific use cases.
/// </description>
/// </item>
/// </list>
/// </remarks>
/// <seealso cref="Initialize"/>
public OneEuroFilterVector3(Vector3 initialRawValue, float minCutoff = 0.1f, float beta = 0.02f)
{
m_LastRawValue = initialRawValue;
m_LastFilteredValue = initialRawValue;
m_MinCutoff = minCutoff;
m_Beta = beta;
}
/// <summary>
/// Resets the initial raw value. Useful to recover from tracking loss.
/// </summary>
/// <param name="initialRawValue">Raw value to reset filtering basis to.</param>
public void Initialize(Vector3 initialRawValue)
{
m_LastRawValue = initialRawValue;
m_LastFilteredValue = initialRawValue;
}
/// <summary>
/// Filters the given <see cref="Vector3"/> rawValue using the internal minCutoff and beta parameters.
/// </summary>
/// <param name="rawValue">The raw <see cref="Vector3"/> value to be filtered.</param>
/// <param name="deltaTime">The time since the last filter update.</param>
/// <returns>The filtered <see cref="Vector3"/> value.</returns>
public Vector3 Filter(Vector3 rawValue, float deltaTime)
{
return Filter(rawValue, deltaTime, m_MinCutoff, m_Beta);
}
/// <summary>
/// Filters the given <see cref="Vector3"/> rawValue using provided minCutoff and beta parameters.
/// This method computes the speed of change in the signal and dynamically adjusts the amount of smoothing
/// based on the speed and the provided minCutoff and beta values.
/// </summary>
/// <param name="rawValue">The raw <see cref="Vector3"/> value to be filtered.</param>
/// <param name="deltaTime">The time since the last filter update.</param>
/// <param name="minCutoff">The minimum cutoff value for the filter. Influences the amount of smoothing at low speeds.</param>
/// <param name="beta">Determines the filter's adjustment to speed changes, influencing its responsiveness.</param>
/// <returns>The filtered <see cref="Vector3"/> value.</returns>
public Vector3 Filter(Vector3 rawValue, float deltaTime, float minCutoff, float beta)
{
// Calculate speed as a Vector3
Vector3 speed = (rawValue - m_LastRawValue) / deltaTime;
// Compute cutoffs for x, y, and z
Vector3 cutoffs = new Vector3(minCutoff, minCutoff, minCutoff);
Vector3 betaValues = new Vector3(beta, beta, beta);
// Incorporate speed into the cutoffs
Vector3 combinedCutoffs = cutoffs + Vector3.Scale(betaValues, speed);
// Compute alpha for x, y, and z
BurstMathUtility.FastSafeDivide(Vector3.one, Vector3.one + combinedCutoffs, out Vector3 alpha);
Vector3 rawFiltered = Vector3.Scale(alpha, rawValue);
Vector3 lastFiltered = Vector3.Scale(Vector3.one - alpha, m_LastFilteredValue);
// Calculate the final filtered value
Vector3 filteredValue = rawFiltered + lastFiltered;
m_LastRawValue = rawValue;
m_LastFilteredValue = filteredValue;
return filteredValue;
}
}
}

View File

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

View File

@@ -0,0 +1,185 @@
#if XR_HANDS_1_2_OR_NEWER
using Unity.XR.CoreUtils.Bindings;
using UnityEngine.XR.Hands;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
#endif
using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// A class that follows the pinch point between the thumb and index finger using XR Hand Tracking.
/// It updates its position to the midpoint between the thumb and index tip while optionally adjusting its rotation
/// to look at a specified target. The rotation towards the target can also be smoothly interpolated over time.
/// </summary>
public class PinchPointFollow : MonoBehaviour
{
[Header("Events")]
[SerializeField]
[Tooltip("The XR Hand Tracking Events component that will be used to subscribe to hand tracking events.")]
#if XR_HANDS_1_2_OR_NEWER
XRHandTrackingEvents m_XRHandTrackingEvents;
#else
Object m_XRHandTrackingEvents;
#endif
[Header("Interactor reference (Pick one)")]
[SerializeField]
[Tooltip("The transform will use the XRRayInteractor endpoint position to calculate the transform rotation.")]
XRRayInteractor m_RayInteractor;
[SerializeField]
[Tooltip("The transform will use the NearFarInteractor endpoint position to calculate the transform rotation.")]
NearFarInteractor m_NearFarInteractor;
[Header("Rotation Config")]
[SerializeField]
[Tooltip("The transform to match the rotation of.")]
Transform m_TargetRotation;
[SerializeField]
[Tooltip("How fast to match rotation (0 means no rotation smoothing.)")]
[Range(0f, 32f)]
#pragma warning disable CS0414 // Field assigned but its value is never used -- Keep to retain serialized value when XR Hands is not installed
float m_RotationSmoothingSpeed = 12f;
#pragma warning restore CS0414
#if XR_HANDS_1_2_OR_NEWER
bool m_HasTargetRotationTransform;
IXRRayProvider m_RayProvider;
bool m_HasRayProvider;
OneEuroFilterVector3 m_OneEuroFilterVector3;
#pragma warning disable CS0618 // Type or member is obsolete
readonly QuaternionTweenableVariable m_QuaternionTweenableVariable = new QuaternionTweenableVariable();
#pragma warning restore CS0618 // Type or member is obsolete
readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
#endif
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
#if XR_HANDS_1_2_OR_NEWER
if (m_XRHandTrackingEvents != null)
m_XRHandTrackingEvents.jointsUpdated.AddListener(OnJointsUpdated);
m_OneEuroFilterVector3 = new OneEuroFilterVector3(transform.localPosition);
if (m_RayInteractor != null)
{
m_RayProvider = m_RayInteractor;
m_HasRayProvider = true;
}
if (m_NearFarInteractor != null)
{
m_RayProvider = m_NearFarInteractor;
m_HasRayProvider = true;
}
m_HasTargetRotationTransform = m_TargetRotation != null;
m_BindingsGroup.AddBinding(m_QuaternionTweenableVariable.Subscribe(newValue => transform.rotation = newValue));
#else
Debug.LogWarning("PinchPointFollow requires XR Hands (com.unity.xr.hands) 1.2.0 or newer. Disabling component.", this);
enabled = false;
#endif
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
#if XR_HANDS_1_2_OR_NEWER
m_BindingsGroup.Clear();
if (m_XRHandTrackingEvents != null)
m_XRHandTrackingEvents.jointsUpdated.RemoveListener(OnJointsUpdated);
#endif
}
#if XR_HANDS_1_2_OR_NEWER
static bool TryGetPinchPosition(XRHandJointsUpdatedEventArgs args, out Vector3 position)
{
#if XR_HANDS_1_5_OR_NEWER
if (args.subsystem != null)
{
var commonHandGestures = args.hand.handedness == Handedness.Left
? args.subsystem.leftHandCommonGestures
: args.hand.handedness == Handedness.Right
? args.subsystem.rightHandCommonGestures
: null;
if (commonHandGestures != null && commonHandGestures.TryGetPinchPose(out var pinchPose))
{
// Protect against platforms returning bad data like (NaN, NaN, NaN)
if (!float.IsNaN(pinchPose.position.x) &&
!float.IsNaN(pinchPose.position.y) &&
!float.IsNaN(pinchPose.position.z))
{
position = pinchPose.position;
return true;
}
}
}
#endif
var thumbTip = args.hand.GetJoint(XRHandJointID.ThumbTip);
if (!thumbTip.TryGetPose(out var thumbTipPose))
{
position = Vector3.zero;
return false;
}
var indexTip = args.hand.GetJoint(XRHandJointID.IndexTip);
if (!indexTip.TryGetPose(out var indexTipPose))
{
position = Vector3.zero;
return false;
}
position = Vector3.Lerp(thumbTipPose.position, indexTipPose.position, 0.5f);
return true;
}
public void OnJointsUpdated(XRHandJointsUpdatedEventArgs args)
{
if (!TryGetPinchPosition(args, out var targetPos))
return;
var filteredTargetPos = m_OneEuroFilterVector3.Filter(targetPos, Time.deltaTime);
// Hand pose data is in local space relative to the XR Origin.
transform.localPosition = filteredTargetPos;
if (m_HasTargetRotationTransform && m_HasRayProvider)
{
// Given that the ray endpoint is in world space, we need to use the world space transform of this point to determine the target rotation.
// This allows us to keep orientation consistent when moving the XR Origin for locomotion.
var targetDir = (m_RayProvider.rayEndPoint - transform.position).normalized;
if (targetDir != Vector3.zero)
{
// Use the parent Transform's up vector if available, otherwise use the world up vector.
// The assumption is the parent Transform matches the XR Origin rotation.
// This allows the XR Origin to teleport to angled surfaces or upside down surfaces
// and the visual will still be correct relative to the application's ground.
var upwards = Vector3.up;
var parentTransform = transform.parent;
if (!(parentTransform is null))
upwards = parentTransform.up;
var targetRot = Quaternion.LookRotation(targetDir, upwards);
// If there aren't any major swings in rotation, follow the target rotation.
if (Vector3.Dot(m_TargetRotation.forward, targetDir) > 0.5f)
m_QuaternionTweenableVariable.target = targetRot;
}
else
{
m_QuaternionTweenableVariable.target = m_TargetRotation.rotation;
}
var tweenTarget = m_RotationSmoothingSpeed > 0f ? m_RotationSmoothingSpeed * Time.deltaTime : 1f;
m_QuaternionTweenableVariable.HandleTween(tweenTarget);
}
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,200 @@
using System.Collections.Generic;
using UnityEngine.Events;
#if XR_HANDS_1_1_OR_NEWER
using UnityEngine.XR.Hands;
#endif
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Behavior that provides events for when an <see cref="XRHand"/> starts and ends a poke gesture. The gesture is
/// detected if the index finger is extended and the middle, ring, and little fingers are curled in.
/// </summary>
public class PokeGestureDetector : MonoBehaviour
{
[SerializeField]
[Tooltip("Which hand to check for the poke gesture.")]
#if XR_HANDS_1_1_OR_NEWER
Handedness m_Handedness;
#else
int m_Handedness;
#endif
[SerializeField]
[Tooltip("Called when the hand has started a poke gesture.")]
UnityEvent m_PokeGestureStarted;
[SerializeField]
[Tooltip("Called when the hand has ended a poke gesture.")]
UnityEvent m_PokeGestureEnded;
#if XR_HANDS_1_1_OR_NEWER
XRHandSubsystem m_Subsystem;
bool m_IsPoking;
static readonly List<XRHandSubsystem> s_Subsystems = new List<XRHandSubsystem>();
#endif
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnEnable()
{
#if XR_HANDS_1_1_OR_NEWER
SubsystemManager.GetSubsystems(s_Subsystems);
if (s_Subsystems.Count == 0)
return;
m_Subsystem = s_Subsystems[0];
m_Subsystem.updatedHands += OnUpdatedHands;
#else
Debug.LogError("Script requires XR Hands (com.unity.xr.hands) package. Install using Window > Package Manager or click Fix on the related issue in Edit > Project Settings > XR Plug-in Management > Project Validation.", this);
#endif
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
protected void OnDisable()
{
#if XR_HANDS_1_1_OR_NEWER
if (m_Subsystem == null)
return;
m_Subsystem.updatedHands -= OnUpdatedHands;
m_Subsystem = null;
#endif
}
#if XR_HANDS_1_1_OR_NEWER
void OnUpdatedHands(XRHandSubsystem subsystem, XRHandSubsystem.UpdateSuccessFlags updateSuccessFlags, XRHandSubsystem.UpdateType updateType)
{
var wasPoking = m_IsPoking;
switch (m_Handedness)
{
case Handedness.Left:
if (!HasUpdateSuccessFlag(updateSuccessFlags, XRHandSubsystem.UpdateSuccessFlags.LeftHandJoints))
return;
var leftHand = subsystem.leftHand;
m_IsPoking = IsIndexExtended(leftHand) && IsMiddleGrabbing(leftHand) && IsRingGrabbing(leftHand) &&
IsLittleGrabbing(leftHand);
break;
case Handedness.Right:
if (!HasUpdateSuccessFlag(updateSuccessFlags, XRHandSubsystem.UpdateSuccessFlags.RightHandJoints))
return;
var rightHand = subsystem.rightHand;
m_IsPoking = IsIndexExtended(rightHand) && IsMiddleGrabbing(rightHand) && IsRingGrabbing(rightHand) &&
IsLittleGrabbing(rightHand);
break;
}
if (m_IsPoking && !wasPoking)
StartPokeGesture();
else if (!m_IsPoking && wasPoking)
EndPokeGesture();
}
/// <summary>
/// Determines whether one or more bit fields are set in the flags.
/// Non-boxing version of <c>HasFlag</c> for <see cref="XRHandSubsystem.UpdateSuccessFlags"/>.
/// </summary>
/// <param name="successFlags">The flags enum instance.</param>
/// <param name="successFlag">The flag to check if set.</param>
/// <returns>Returns <see langword="true"/> if the bit field or bit fields are set, otherwise returns <see langword="false"/>.</returns>
static bool HasUpdateSuccessFlag(XRHandSubsystem.UpdateSuccessFlags successFlags, XRHandSubsystem.UpdateSuccessFlags successFlag)
{
return (successFlags & successFlag) == successFlag;
}
/// <summary>
/// Returns true if the given hand's index finger tip is farther from the wrist than the index intermediate joint.
/// </summary>
/// <param name="hand">Hand to check for the required pose.</param>
/// <returns>True if the given hand's index finger tip is farther from the wrist than the index intermediate joint, false otherwise.</returns>
static bool IsIndexExtended(XRHand hand)
{
if (!(hand.GetJoint(XRHandJointID.Wrist).TryGetPose(out var wristPose) &&
hand.GetJoint(XRHandJointID.IndexTip).TryGetPose(out var tipPose) &&
hand.GetJoint(XRHandJointID.IndexIntermediate).TryGetPose(out var intermediatePose)))
{
return false;
}
var wristToTip = tipPose.position - wristPose.position;
var wristToIntermediate = intermediatePose.position - wristPose.position;
return wristToTip.sqrMagnitude > wristToIntermediate.sqrMagnitude;
}
/// <summary>
/// Returns true if the given hand's middle finger tip is closer to the wrist than the middle proximal joint.
/// </summary>
/// <param name="hand">Hand to check for the required pose.</param>
/// <returns>True if the given hand's middle finger tip is closer to the wrist than the middle proximal joint, false otherwise.</returns>
static bool IsMiddleGrabbing(XRHand hand)
{
if (!(hand.GetJoint(XRHandJointID.Wrist).TryGetPose(out var wristPose) &&
hand.GetJoint(XRHandJointID.MiddleTip).TryGetPose(out var tipPose) &&
hand.GetJoint(XRHandJointID.MiddleProximal).TryGetPose(out var proximalPose)))
{
return false;
}
var wristToTip = tipPose.position - wristPose.position;
var wristToProximal = proximalPose.position - wristPose.position;
return wristToProximal.sqrMagnitude >= wristToTip.sqrMagnitude;
}
/// <summary>
/// Returns true if the given hand's ring finger tip is closer to the wrist than the ring proximal joint.
/// </summary>
/// <param name="hand">Hand to check for the required pose.</param>
/// <returns>True if the given hand's ring finger tip is closer to the wrist than the ring proximal joint, false otherwise.</returns>
static bool IsRingGrabbing(XRHand hand)
{
if (!(hand.GetJoint(XRHandJointID.Wrist).TryGetPose(out var wristPose) &&
hand.GetJoint(XRHandJointID.RingTip).TryGetPose(out var tipPose) &&
hand.GetJoint(XRHandJointID.RingProximal).TryGetPose(out var proximalPose)))
{
return false;
}
var wristToTip = tipPose.position - wristPose.position;
var wristToProximal = proximalPose.position - wristPose.position;
return wristToProximal.sqrMagnitude >= wristToTip.sqrMagnitude;
}
/// <summary>
/// Returns true if the given hand's little finger tip is closer to the wrist than the little proximal joint.
/// </summary>
/// <param name="hand">Hand to check for the required pose.</param>
/// <returns>True if the given hand's little finger tip is closer to the wrist than the little proximal joint, false otherwise.</returns>
static bool IsLittleGrabbing(XRHand hand)
{
if (!(hand.GetJoint(XRHandJointID.Wrist).TryGetPose(out var wristPose) &&
hand.GetJoint(XRHandJointID.LittleTip).TryGetPose(out var tipPose) &&
hand.GetJoint(XRHandJointID.LittleProximal).TryGetPose(out var proximalPose)))
{
return false;
}
var wristToTip = tipPose.position - wristPose.position;
var wristToProximal = proximalPose.position - wristPose.position;
return wristToProximal.sqrMagnitude >= wristToTip.sqrMagnitude;
}
void StartPokeGesture()
{
m_IsPoking = true;
m_PokeGestureStarted.Invoke();
}
void EndPokeGesture()
{
m_IsPoking = false;
m_PokeGestureEnded.Invoke();
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,127 @@
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// An input button reader based on another <see cref="XRInputButtonReader"/> and holds it true until falling below a lower release threshold.
/// Useful with hand interaction because the bool select value can bounce when the hand is near the tight internal threshold,
/// so using this will keep the pinch true until moving the fingers much further away than the pinch activation threshold.
/// </summary>
[DefaultExecutionOrder(XRInteractionUpdateOrder.k_XRInputDeviceButtonReader)]
public class ReleaseThresholdButtonReader : MonoBehaviour, IXRInputButtonReader
{
[SerializeField]
[Tooltip("The source input that this component reads to create a processed button value.")]
XRInputButtonReader m_ValueInput = new XRInputButtonReader("Value");
/// <summary>
/// The source input that this component reads to create a processed button value.
/// </summary>
public XRInputButtonReader valueInput
{
get => m_ValueInput;
set => XRInputReaderUtility.SetInputProperty(ref m_ValueInput, value, this);
}
[SerializeField]
[Tooltip("The threshold value to use to determine when the button is pressed. Considered pressed equal to or greater than this value.")]
[Range(0f, 1f)]
float m_PressThreshold = 1f;
/// <summary>
/// The threshold value to use to determine when the button is pressed. Considered pressed equal to or greater than this value.
/// </summary>
/// <remarks>
/// This reader will also be considered performed if the source input is performed.
/// </remarks>
public float pressThreshold
{
get => m_PressThreshold;
set => m_PressThreshold = value;
}
[SerializeField]
[Tooltip("The threshold value to use to determine when the button is released when it was previously pressed. Keeps being pressed until falls back to a value of or below this value.")]
[Range(0f, 1f)]
float m_ReleaseThreshold = 0.9f;
/// <summary>
/// The threshold value to use to determine when the button is released when it was previously pressed.
/// Keeps being pressed until falls back to a value of or below this value.
/// </summary>
/// <remarks>
/// This reader will still be considered performed if the source input is still performed
/// when this threshold is reached.
/// </remarks>
public float releaseThreshold
{
get => m_ReleaseThreshold;
set => m_ReleaseThreshold = value;
}
bool m_IsPerformed;
bool m_WasPerformedThisFrame;
bool m_WasCompletedThisFrame;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
m_ValueInput?.EnableDirectActionIfModeUsed();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
m_ValueInput?.DisableDirectActionIfModeUsed();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Update()
{
// Go true when either the press threshold is reached or the bool is already performed.
// Only drop back to false when the release threshold is reached and the bool is no longer performed.
var prevPerformed = m_IsPerformed;
var pressAmount = m_ValueInput.ReadValue();
m_IsPerformed = m_ValueInput.ReadIsPerformed() || prevPerformed ? pressAmount > m_ReleaseThreshold : pressAmount >= m_PressThreshold;
m_WasPerformedThisFrame = !prevPerformed && m_IsPerformed;
m_WasCompletedThisFrame = prevPerformed && !m_IsPerformed;
}
/// <inheritdoc />
public bool ReadIsPerformed()
{
return m_IsPerformed;
}
/// <inheritdoc />
public bool ReadWasPerformedThisFrame()
{
return m_WasPerformedThisFrame;
}
/// <inheritdoc />
public bool ReadWasCompletedThisFrame()
{
return m_WasCompletedThisFrame;
}
/// <inheritdoc />
public float ReadValue()
{
return m_ValueInput.ReadValue();
}
/// <inheritdoc />
public bool TryReadValue(out float value)
{
return m_ValueInput.TryReadValue(out value);
}
}
}

View File

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

View File

@@ -0,0 +1,121 @@
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Example class that reads a float value from an <see cref="XRInputValueReader"/> and converts it to a bool.
/// Useful with hand interaction because the bool select value can be unreliable when the hand is near the tight internal threshold.
/// </summary>
[DefaultExecutionOrder(XRInteractionUpdateOrder.k_XRInputDeviceButtonReader)]
public class ValueDerivedButtonReader : MonoBehaviour, IXRInputButtonReader
{
[SerializeField]
[Tooltip("The input reader used to reference the float value to convert to a bool.")]
XRInputValueReader<float> m_ValueInput = new XRInputValueReader<float>("Value");
/// <summary>
/// The input reader used to reference the float value to convert to a bool.
/// </summary>
public XRInputValueReader<float> valueInput
{
get => m_ValueInput;
set => XRInputReaderUtility.SetInputProperty(ref m_ValueInput, value, this);
}
[SerializeField]
[Tooltip("The threshold value to use to determine when the button is pressed. Considered pressed equal to or greater than this value.")]
[Range(0f, 1f)]
float m_PressThreshold = 0.8f;
/// <summary>
/// The threshold value to use to determine when the button is pressed. Considered pressed equal to or greater than this value.
/// </summary>
public float pressThreshold
{
get => m_PressThreshold;
set => m_PressThreshold = value;
}
[SerializeField]
[Tooltip("The threshold value to use to determine when the button is released when it was previously pressed. Keeps being pressed until falls back to a value of or below this value.")]
[Range(0f, 1f)]
float m_ReleaseThreshold = 0.25f;
/// <summary>
/// The threshold value to use to determine when the button is released when it was previously pressed.
/// Keeps being pressed until falls back to a value of or below this value.
/// </summary>
public float releaseThreshold
{
get => m_ReleaseThreshold;
set => m_ReleaseThreshold = value;
}
bool m_IsPerformed;
bool m_WasPerformedThisFrame;
bool m_WasCompletedThisFrame;
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnEnable()
{
m_ValueInput?.EnableDirectActionIfModeUsed();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void OnDisable()
{
m_ValueInput?.DisableDirectActionIfModeUsed();
}
/// <summary>
/// See <see cref="MonoBehaviour"/>.
/// </summary>
void Update()
{
var prevPerformed = m_IsPerformed;
var pressAmount = m_ValueInput.ReadValue();
var newValue = pressAmount >= m_PressThreshold;
if (!newValue && prevPerformed)
newValue = pressAmount > m_ReleaseThreshold;
m_IsPerformed = newValue;
m_WasPerformedThisFrame = !prevPerformed && m_IsPerformed;
m_WasCompletedThisFrame = prevPerformed && !m_IsPerformed;
}
/// <inheritdoc />
public bool ReadIsPerformed()
{
return m_IsPerformed;
}
/// <inheritdoc />
public bool ReadWasPerformedThisFrame()
{
return m_WasPerformedThisFrame;
}
/// <inheritdoc />
public bool ReadWasCompletedThisFrame()
{
return m_WasCompletedThisFrame;
}
/// <inheritdoc />
public float ReadValue()
{
return m_ValueInput.ReadValue();
}
/// <inheritdoc />
public bool TryReadValue(out float value)
{
return m_ValueInput.TryReadValue(out value);
}
}
}

View File

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

View File

@@ -0,0 +1,33 @@
using System;
using Unity.Mathematics;
using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.Receiver.Primitives;
namespace UnityEngine.XR.Interaction.Toolkit.Samples.Hands
{
/// <summary>
/// Affordance receiver applying a Vector3 (Float3) affordance theme to a Transform local scale.
/// Broadcasts new affordance value with Unity Event.
/// </summary>
[Obsolete("The Affordance System namespace and all associated classes have been deprecated. The existing affordance system will be moved, replaced and updated with a new interaction feedback system in a future version of XRI.")]
public class Vector3ScaleAffordanceReceiver : Vector3AffordanceReceiver
{
[SerializeField]
[Tooltip("The transform to apply the scale value to.")]
Transform m_TargetTransform;
/// <inheritdoc />
protected override void OnEnable()
{
base.OnEnable();
if (m_TargetTransform == null)
m_TargetTransform = transform;
}
/// <inheritdoc />
protected override void OnAffordanceValueUpdated(float3 newValue)
{
base.OnAffordanceValueUpdated(newValue);
m_TargetTransform.localScale = newValue;
}
}
}

View File

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