// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (c) VRMADA, All rights reserved.
//
// --------------------------------------------------------------------------------------------------------------------
using System.Collections.Generic;
using System.Linq;
using UltimateXR.Avatar;
using UltimateXR.Avatar.Rig;
using UltimateXR.Core;
using UltimateXR.Core.Components.Composite;
using UltimateXR.Extensions.Unity;
using UltimateXR.Extensions.Unity.Math;
using UnityEngine;
namespace UltimateXR.Manipulation
{
///
///
/// Component that added to an allows to interact with
/// entities. Normally there are two per avatar, one on each hand. They are usually added to the hand object since
/// it is the transform where grabbable objects will be snapped to when snapping is used.
///
///
/// By default, the grabber transform is also used to compute distances to grabbable objects. Additional proximity
/// transforms can be specified on the grabber so that grabbable objects can choose which one is used. This can be
/// useful in some scenarios: In an aircraft cockpit most knobs and buttons will prefer the distance from the tip
/// of the index finger, while bigger objects will prefer from the palm of the hand.
///
///
public partial class UxrGrabber : UxrAvatarComponent
{
#region Inspector Properties/Serialized Fields
[SerializeField] private Renderer _handRenderer;
[SerializeField] private GameObject[] _objectsToDisableOnGrab;
[SerializeField] private List _optionalProximityTransforms;
#endregion
#region Public Types & Data
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in local-space that is pointing
/// to the fingers, excluding the thumb.
///
public Vector3 LocalFingerDirection
{
get
{
if (Avatar == null || Avatar.AvatarRigInfo == null)
{
return transform.forward;
}
return transform.GetClosestLocalAxis(Avatar.AvatarRigInfo.GetArmInfo(Side).HandUniversalLocalAxes.WorldForward);
}
}
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in world-space that is pointing
/// to the fingers, excluding the thumb.
///
public Vector3 FingerDirection => transform.TransformDirection(LocalFingerDirection);
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in local-space that is pointing
/// outwards from the palm.
///
public Vector3 LocalPalmOutDirection
{
get
{
if (Avatar == null || Avatar.AvatarRigInfo == null)
{
return -transform.up;
}
return transform.GetClosestLocalAxis(-Avatar.AvatarRigInfo.GetArmInfo(Side).HandUniversalLocalAxes.WorldUp);
}
}
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in world-space that is pointing
/// outwards from the palm..
///
public Vector3 PalmOutDirection => transform.TransformDirection(LocalPalmOutDirection);
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in local-space that is pointing
/// towards the thumb.
///
public Vector3 LocalPalmThumbDirection
{
get
{
Vector3 direction = transform.right;
if (Avatar != null && Avatar.AvatarRigInfo != null)
{
direction = transform.GetClosestLocalAxis(Avatar.AvatarRigInfo.GetArmInfo(Side).HandUniversalLocalAxes.WorldRight);
}
return Side == UxrHandSide.Left ? direction : -direction;
}
}
///
/// Gets from all the positive and negative axes in the grabber's transform, the axis in world-space that is pointing
/// towards the thumb.
///
public Vector3 PalmThumbDirection => transform.TransformDirection(LocalPalmThumbDirection);
///
///
/// Gets, based on and , which mirroring snap
/// transforms
/// should use with the grabber if they want to be mirrored.
///
/// Snap transforms are GameObjects in that determine where the hand should be placed
/// during grabs by making the 's transform align with the snap .
/// Mirroring snap transforms is used to quickly create/modify grab positions/orientations.
///
/// Which mirroring TransformExt.ApplyMirroring() should use
public TransformExt.MirrorType RequiredMirrorType
{
get
{
Vector3 other = Vector3.Cross(LocalPalmOutDirection, LocalFingerDirection);
if (Mathf.Abs(other.z) > 0.5)
{
return TransformExt.MirrorType.MirrorXY;
}
if (Mathf.Abs(other.y) > 0.5)
{
return TransformExt.MirrorType.MirrorXZ;
}
return TransformExt.MirrorType.MirrorYZ;
}
}
///
/// Gets whether the grabber component is on the left or right hand.
///
public UxrHandSide OppositeSide => Side == UxrHandSide.Left ? UxrHandSide.Right : UxrHandSide.Left;
///
/// Gets whether the grabber component is on the left or right hand.
///
public UxrHandSide Side
{
get
{
if (!_sideInitialized || (Application.isEditor && !Application.isPlaying))
{
InitializeSide();
}
return _side;
}
private set
{
_side = value;
_sideInitialized = true;
}
}
///
/// Gets the avatar hand bone that corresponds to the grabber.
///
public Transform HandBone => Avatar.GetHandBone(Side);
///
/// Gets the relative position of the hand bone to the grabber.
///
public Vector3 HandBoneRelativePos => HandBone != null ? transform.InverseTransformPoint(HandBone.position) : Vector3.zero;
///
/// Gets the relative rotation of the hand bone to the grabber.
///
public Quaternion HandBoneRelativeRot => HandBone != null ? Quaternion.Inverse(transform.rotation) * HandBone.rotation : Quaternion.identity;
///
/// Gets or sets the hand renderer.
///
public Renderer HandRenderer
{
get
{
// Try to get it automatically if it is unassigned or disabled.
if ((_handRenderer == null || !_handRenderer.gameObject.activeInHierarchy) && Avatar != null)
{
SkinnedMeshRenderer handRenderer = UxrAvatarRig.TryToGetHandRenderer(Avatar, Side);
if (handRenderer != null)
{
_handRenderer = handRenderer;
}
}
return _handRenderer;
}
set => _handRenderer = value;
}
///
/// Gets the opposite hand grabber in the same avatar.
///
public UxrGrabber OppositeHandGrabber { get; private set; }
///
/// The unprocessed grabber position. This is the position the grabber has taking only the hand controller tracking
/// sensor into account.
/// The hand position is updated by the and may be forced into a certain position if the
/// object being grabbed has constraints, altering also the position. Sometimes it is
/// preferred to use the unprocessed grabber position.
///
public Vector3 UnprocessedGrabberPosition { get; internal set; }
///
/// Gets the unprocessed grabber rotation. See .
///
public Quaternion UnprocessedGrabberRotation { get; internal set; }
///
/// Gets the currently grabbed object if there is one. null if no object is being grabbed.
///
public UxrGrabbableObject GrabbedObject
{
get => _grabbedObject;
set
{
_grabbedObject = value;
if (_objectsToDisableOnGrab != null)
{
foreach (GameObject go in _objectsToDisableOnGrab)
{
go.SetActive(value == null);
}
}
}
}
///
/// Gets 's current frame velocity.
///
public Vector3 Velocity { get; private set; } = Vector3.zero;
///
/// Gets 's current frame angular velocity.
///
public Vector3 AngularVelocity { get; private set; } = Vector3.zero;
///
/// Gets 's velocity smoothed using averaged previous frame data.
///
public Vector3 SmoothVelocity { get; private set; } = Vector3.zero;
///
/// Gets 's angular velocity smoothed using averaged previous frame data.
///
public Vector3 SmoothAngularVelocity { get; private set; } = Vector3.zero;
#endregion
#region Internal Types & Data
///
/// Gets whether the grabber is currently being smoothly interpolated in an object manipulation.
///
internal bool IsInSmoothManipulationTransition => SmoothManipulationTimer >= 0.0f;
#endregion
#region Public Overrides Object
///
public override string ToString()
{
string avatarName = Avatar != null ? $"{Avatar.name} " : string.Empty;
return $"{avatarName}{Side.ToString().ToLower()} hand grabber";
}
#endregion
#region Public Methods
///
/// Gets the given proximity transform, used to compute distances to entities
///
///
/// Proximity transform index. -1 for the default (the grabber's transform) and 0 to n for any
/// optional proximity transform.
///
/// Proximity transform. If the index is out of range it will return the default transform
public Transform GetProximityTransform(int proximityIndex = -1)
{
if (proximityIndex >= 0 && proximityIndex < _optionalProximityTransforms.Count)
{
return _optionalProximityTransforms[proximityIndex];
}
return transform;
}
#endregion
#region Internal Methods
///
/// Updates the hand renderer enabled state.
///
internal void UpdateHandGrabberRenderer()
{
if (_handRenderer != null && Avatar && (Avatar.RenderMode == UxrAvatarRenderModes.Avatar || Avatar.RenderMode == UxrAvatarRenderModes.AllControllersAndAvatar))
{
if (GrabbedObject == null)
{
_handRenderer.enabled = true;
}
else
{
_handRenderer.enabled = !UxrGrabManager.Instance.ShouldHideHandRenderer(this, GrabbedObject);
}
}
}
///
/// Updates the throw physics information.
///
internal void UpdateThrowPhysicsInfo()
{
Transform sampledTransform = GrabbedObject != null ? GrabbedObject.transform : transform;
Vector3 centerOfMassPosition = transform.TransformPoint(ThrowCenterOfMassLocalPosition);
Vector3 throwTipPosition = transform.TransformPoint(ThrowTipLocalPosition);
PhysicsSample newSample = new PhysicsSample(_physicsSampleWindow.LastOrDefault(), sampledTransform, centerOfMassPosition, throwTipPosition, Time.deltaTime);
// Update timers
_physicsSampleWindow.ForEach(s => s.Age += Time.deltaTime);
// Remove samples out of the time window
_physicsSampleWindow.RemoveAll(s => s.Age > SampleWindowSeconds);
// Add new sample
_physicsSampleWindow.Add(newSample);
// Compute instant and smoothed values:
Velocity = newSample.Velocity;
AngularVelocity = newSample.EulerSpeed;
SmoothVelocity = Vector3Ext.Average(_physicsSampleWindow.Select(s => s.TotalVelocity));
Quaternion relative = Quaternion.Inverse(_physicsSampleWindow.First().Rotation) * _physicsSampleWindow.Last().Rotation;
relative.ToAngleAxis(out float angle, out Vector3 axis);
SmoothAngularVelocity = angle * sampledTransform.TransformDirection(axis) / _physicsSampleWindow.First().Age;
}
///
/// Starts a smooth manipulation transition in a grab or a release, to make sure the hand transitions smoothly.
///
internal void StartSmoothManipulationTransition()
{
SmoothManipulationTimer = UxrConstants.SmoothManipulationTransitionSeconds;
SmoothTransitionLocalAvatarHandBonePos = Avatar.transform.InverseTransformPoint(transform.TransformPoint(HandBoneRelativePos));
SmoothTransitionLocalAvatarHandBoneRot = Quaternion.Inverse(Avatar.transform.rotation) * transform.rotation * HandBoneRelativeRot;
}
///
/// Updates the smooth manipulation transitions if they exist.
///
internal void UpdateSmoothManipulationTransition(float deltaTime)
{
if (SmoothManipulationTimer >= 0.0f)
{
SmoothManipulationTimer -= deltaTime;
if (SmoothManipulationTimer > 0.0f)
{
float t = SmoothManipulationT;
HandBone.SetPositionAndRotation(Vector3.Lerp(Avatar.transform.TransformPoint(SmoothTransitionLocalAvatarHandBonePos), transform.TransformPoint(HandBoneRelativePos), t),
Quaternion.Slerp(Avatar.transform.rotation * SmoothTransitionLocalAvatarHandBoneRot, transform.rotation * HandBoneRelativeRot, t));
}
}
}
#endregion
#region Unity
///
/// Initializes the component.
///
protected override void Awake()
{
base.Awake();
UxrAvatarRig.GetHandSide(transform, out UxrHandSide handSide);
Side = handSide;
if (Avatar != null)
{
// Compute grabber info
UxrGrabber[] avatarGrabbers = Avatar.GetComponentsInChildren();
OppositeHandGrabber = avatarGrabbers.FirstOrDefault(g => g != null && Side != g.Side);
GrabbedObject = null;
// Compute throw physics info
if (Avatar.GetHand(handSide).GetPalmCenter(out Vector3 palmCenter) && Avatar.GetHand(handSide).GetPalmToFingerDirection(out Vector3 palmToFinger))
{
ThrowCenterOfMassLocalPosition = transform.InverseTransformPoint(palmCenter);
ThrowTipLocalPosition = transform.InverseTransformPoint(palmCenter + palmToFinger * ThrowAxisLength);
}
}
}
///
/// Called when the object is destroyed. Releases any grabbed objects.
///
protected override void OnDestroy()
{
base.OnDestroy();
if (GrabbedObject != null)
{
UxrGrabManager.Instance.ReleaseObject(this, GrabbedObject, true);
}
}
///
/// Called when the object is disabled. Releases any grabbed objects.
///
protected override void OnDisable()
{
base.OnDisable();
if (GrabbedObject != null)
{
UxrGrabManager.Instance.ReleaseObject(this, GrabbedObject, true);
}
}
#endregion
#region Private Methods
///
/// Assigns the hand the grabber belongs to.
///
private void InitializeSide()
{
if (UxrAvatarRig.GetHandSide(transform, out UxrHandSide handSide))
{
Side = handSide;
}
}
#endregion
#region Private Types & Data
///
/// Gets the smooth manipulation transition interpolation value.
///
private float SmoothManipulationT
{
get
{
if (SmoothManipulationTimer <= 0.0f)
{
return 1.0f;
}
return 1.0f - Mathf.Clamp01(SmoothManipulationTimer / UxrConstants.SmoothManipulationTransitionSeconds);
}
}
///
/// Gets or sets the decreasing smooth manipulation transition timer.
///
private float SmoothManipulationTimer { get; set; } = -1.0f;
///
/// Gets or sets the position of the hand bone in local avatar space at the start of a smooth transition.
///
private Vector3 SmoothTransitionLocalAvatarHandBonePos { get; set; }
///
/// Gets or sets the rotation of the hand bone in local avatar space at the start of a smooth transition.
///
private Quaternion SmoothTransitionLocalAvatarHandBoneRot { get; set; }
///
/// Gets or sets the throw center of mass (palm center) in the grabber's local coordinate system.
///
private Vector3 ThrowCenterOfMassLocalPosition { get; set; } = Vector3.zero;
///
/// Gets or sets the throw center of mass (palm center) in the grabber local coordinate system.
///
private Vector3 ThrowTipLocalPosition { get; set; } = Vector3.zero;
///
/// Distance from the center of mass (palm) to the fingers to compute throw angular speed.
///
private const float ThrowAxisLength = 0.1f;
///
/// History physics sample window in seconds.
///
private const float SampleWindowSeconds = 0.15f;
private readonly List _physicsSampleWindow = new List();
private bool _sideInitialized;
private UxrHandSide _side;
private UxrGrabbableObject _grabbedObject;
#endregion
}
}