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