// -------------------------------------------------------------------------------------------------------------------- // // Copyright (c) VRMADA, All rights reserved. // // -------------------------------------------------------------------------------------------------------------------- using System.Collections.Generic; using System.Linq; using UltimateXR.Animation.Interpolation; using UltimateXR.Avatar; using UltimateXR.Core; using UltimateXR.Core.Math; using UltimateXR.Core.Settings; using UltimateXR.Core.StateSync; using UltimateXR.Devices.Visualization; using UltimateXR.Extensions.System.Math; using UltimateXR.Extensions.Unity; using UltimateXR.Extensions.Unity.Math; using UltimateXR.Haptics; using UnityEngine; namespace UltimateXR.Manipulation { public partial class UxrGrabManager { #region Public Types & Data /// /// Gets the currently grabbed objects. /// public IEnumerable CurrentGrabbedObjects => _currentManipulations.Keys; /// /// Gets or sets whether grabbing is allowed. /// public bool IsGrabbingAllowed { get; set; } = true; #endregion #region Public Methods /// /// Tries to grab something. An object will be grabbed if there is any in reach. /// /// Avatar that tried to grab /// Whether it is trying to grab using the left hand or right hand /// /// The grabber component that grabbed an object, if an object was grabbed. /// public UxrGrabber TryGrab(UxrAvatar avatar, UxrHandSide handSide) { foreach (UxrGrabber grabber in UxrGrabber.GetComponents(avatar).Where(grabber => grabber.Side == handSide)) { bool wasGrabbing = grabber.GrabbedObject != null; TryGrab(grabber); if (!wasGrabbing && grabber.GrabbedObject != null) { return grabber; } } return null; } /// /// Tries to release an object being grabbed. /// /// Avatar that tried to release /// Whether it is trying to release using the left hand or right hand /// /// The grabber component that released an object, if an object was released. /// public UxrGrabber TryRelease(UxrAvatar avatar, UxrHandSide handSide) { foreach (UxrGrabber grabber in UxrGrabber.GetComponents(avatar).Where(grabber => grabber.Side == handSide)) { bool wasGrabbing = grabber.GrabbedObject != null; NotifyReleaseGrab(grabber); if (wasGrabbing && grabber.GrabbedObject == null) { return grabber; } } return null; } /// /// Grabs an object. /// /// Grabber that will grab the object /// Object to grab /// Grab point to grab the object from /// Whether to propagate events public void GrabObject(UxrGrabber grabber, UxrGrabbableObject grabbableObject, int grabPoint, bool propagateEvents) { GrabObject(grabber, grabbableObject, grabPoint, null, propagateEvents); } /// /// Releases an object from a hand. /// /// /// If non-null it will tell the grabber that releases the object. If it is null any grabber that is holding the object /// will release it /// /// Object being released /// Whether to propagate events public void ReleaseObject(UxrGrabber grabber, UxrGrabbableObject grabbableObject, bool propagateEvents) { ReleaseObject(grabber, grabbableObject, null, null, null, null, propagateEvents); } /// /// Releases all grabs on a given . /// /// The object to release /// Whether to propagate events public void ReleaseGrabs(UxrGrabbableObject grabbableObject, bool propagateEvents) { if (grabbableObject == null) { return; } foreach (RuntimeGrabInfo grabInfo in GetGrabs(grabbableObject)) { ReleaseObject(grabInfo.Grabber, grabbableObject, propagateEvents); } } /// /// Places a on an . /// /// It can be placed either instantly or smoothly depending on . /// If the object is currently being grabbed, can also decide /// whether the grips are released or not. /// /// /// The object to place /// The anchor to place it on /// Placement options /// Whether to propagate potential Removing/Removed/Placing/Placed events. /// /// Whether the object was placed or not. The placement can fail if there was a null argument or if the anchor has /// already an object on it. /// public bool PlaceObject(UxrGrabbableObject grabbableObject, UxrGrabbableObjectAnchor anchor, UxrPlacementOptions placementOptions, bool propagateEvents) { if (grabbableObject == null) { return false; } if (grabbableObject.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(PlaceObject)}, {nameof(UxrGrabbableObject)} component on {grabbableObject.name} is disabled."); } } if (anchor == null || (anchor != null && anchor.CurrentPlacedObject != null)) { return false; } if (anchor.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(PlaceObject)}, {nameof(UxrGrabbableObjectAnchor)} component on {anchor.name} is disabled."); } } // This method will be synchronized. It will generate a new frame when recording a replay to ensure smooth interpolation when re-parenting. BeginSync(UxrStateSyncOptions.Default | UxrStateSyncOptions.GenerateNewFrame); grabbableObject.PlacementOptions = placementOptions; UxrGrabber grabber = null; int grabbedPoint = -1; UxrGrabbableObjectAnchor oldAnchor = grabbableObject.CurrentAnchor; bool releaseGrip = !placementOptions.HasFlag(UxrPlacementOptions.DontRelease); if (releaseGrip) { // Release the grips if there are any. if (_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { // TODO: Ideally we would send events from all grabbers later, not just the first grabber = manipulationInfo.Grabs.First().Grabber; grabbedPoint = manipulationInfo.Grabs.First().GrabbedPoint; while (manipulationInfo.Grabs.Any()) { // Don't propagate Release events, because Place and Release are mutually exclusive ReleaseObject(manipulationInfo.Grabs.First().Grabber, grabbableObject, false); } } } // Remove and raise events if (oldAnchor != null) { UxrManipulationEventArgs removeEventArgs = UxrManipulationEventArgs.FromRemove(grabbableObject, oldAnchor, grabber, grabbedPoint); OnObjectRemoving(removeEventArgs, propagateEvents); if (propagateEvents) { oldAnchor.RaiseRemovingEvent(removeEventArgs); } // Activate/deactivate objects if (oldAnchor.ActivateOnPlaced) { oldAnchor.ActivateOnPlaced.SetActive(true); } if (oldAnchor.ActivateOnEmpty) { oldAnchor.ActivateOnEmpty.SetActive(false); } if (oldAnchor.ActivateOnCompatibleNear != null) { oldAnchor.ActivateOnCompatibleNear.SetActive(false); } if (oldAnchor.ActivateOnCompatibleNotNear != null) { oldAnchor.ActivateOnCompatibleNotNear.SetActive(false); } oldAnchor.CurrentPlacedObject = null; grabbableObject.CurrentAnchor = null; OnObjectRemoved(removeEventArgs, propagateEvents); if (propagateEvents) { oldAnchor.RaiseRemovedEvent(removeEventArgs); } } UxrManipulationEventArgs placeEventArgs = UxrManipulationEventArgs.FromPlace(grabbableObject, anchor, grabber, grabbedPoint, placementOptions); OnObjectPlacing(placeEventArgs, propagateEvents); if (propagateEvents) { grabbableObject.RaisePlacingEvent(placeEventArgs); anchor.RaisePlacingEvent(placeEventArgs); } // Setup if (grabbableObject.RigidBodySource != null) { grabbableObject.RigidBodySource.isKinematic = true; } if (grabbableObject.UseParenting) { ChangeGrabbableObjectParent(grabbableObject, anchor.AlignTransform); } grabbableObject.LocalPositionBeforeUpdate = grabbableObject.transform.localPosition; grabbableObject.LocalRotationBeforeUpdate = grabbableObject.transform.localRotation; // Activate/deactivate objects if (anchor.ActivateOnPlaced) { anchor.ActivateOnPlaced.SetActive(true); } if (anchor.ActivateOnEmpty) { anchor.ActivateOnEmpty.SetActive(false); } if (anchor.ActivateOnCompatibleNear != null) { anchor.ActivateOnCompatibleNear.SetActive(false); } if (anchor.ActivateOnCompatibleNotNear != null) { anchor.ActivateOnCompatibleNotNear.SetActive(false); } // Update references grabbableObject.CurrentAnchor = anchor; anchor.CurrentPlacedObject = grabbableObject; if (releaseGrip && grabber != null && grabber.GrabbedObject != null && _currentManipulations.ContainsKey(grabber.GrabbedObject)) { _currentManipulations.Remove(grabbableObject); } // Start smooth transitions to final position/orientation if necessary if (placementOptions.HasFlag(UxrPlacementOptions.Smooth) && Features.HasFlag(UxrManipulationFeatures.SmoothTransitions)) { grabbableObject.StartSmoothAnchorPlacement(); } else { grabbableObject.StopSmoothAnchorPlacement(); grabbableObject.transform.ApplyAlignment(grabbableObject.DropAlignTransform, grabbableObject.CurrentAnchor.AlignTransform, UxrUtils.BuildTransformations(UxrGrabbableObject.GetSnapModeAffectsPosition(grabbableObject.DropSnapMode), UxrGrabbableObject.GetSnapModeAffectsRotation(grabbableObject.DropSnapMode))); } if (grabbableObject.IsConstrained && Features.HasFlag(UxrManipulationFeatures.SmoothTransitions)) { grabbableObject.StartSmoothConstrain(); } // Raise events OnObjectPlaced(placeEventArgs, propagateEvents); if (propagateEvents) { grabbableObject.RaisePlacedEvent(placeEventArgs); anchor.RaisePlacedEvent(placeEventArgs); } EndSyncMethod(new object[] { grabbableObject, anchor, placementOptions, propagateEvents }); return true; } /// /// Removes a placed on an . /// /// Grabbable object to remove from the anchor /// Whether to propagate events public void RemoveObjectFromAnchor(UxrGrabbableObject grabbableObject, bool propagateEvents) { if (grabbableObject == null || grabbableObject.CurrentAnchor == null) { return; } if (grabbableObject.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(RemoveObjectFromAnchor)}, {nameof(UxrGrabbableObject)} component on {nameof(grabbableObject.name)} is disabled."); } } if (grabbableObject.CurrentAnchor.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(RemoveObjectFromAnchor)}, {nameof(UxrGrabbableObjectAnchor)} component on {nameof(grabbableObject.CurrentAnchor.name)} is disabled."); } } // This method will be synchronized. It will generate a new frame when recording a replay to ensure smooth interpolation when re-parenting. BeginSync(UxrStateSyncOptions.Default | UxrStateSyncOptions.GenerateNewFrame); // Raise events UxrGrabbableObjectAnchor sourceAnchor = grabbableObject.CurrentAnchor; UxrManipulationEventArgs removeEventArgs = UxrManipulationEventArgs.FromRemove(grabbableObject, sourceAnchor, null, -1); OnObjectRemoving(removeEventArgs, propagateEvents); if (propagateEvents) { sourceAnchor.RaiseRemovingEvent(removeEventArgs); } // Perform removal grabbableObject.StopSmoothConstrain(); grabbableObject.StopSmoothAnchorPlacement(); if (grabbableObject.RigidBodySource != null && grabbableObject.CanUseRigidBody && !IsBeingGrabbed(grabbableObject) && !GetDirectChildrenLookAtManipulations(grabbableObject).Any()) { grabbableObject.RigidBodySource.isKinematic = !grabbableObject.RigidBodyDynamicOnRelease; } if (grabbableObject.UseParenting) { if (grabbableObject.IsBeingGrabbed) { AssignGrabbedObjectParent(grabbableObject); } else { ChangeGrabbableObjectParent(grabbableObject, grabbableObject.CurrentAnchor.transform.parent); } } if (grabbableObject.IsConstrained && Features.HasFlag(UxrManipulationFeatures.SmoothTransitions)) { // Smoothly transition from the constrained state to the grabbed state grabbableObject.StartSmoothManipulationTransition(); } if (sourceAnchor != null) { sourceAnchor.CurrentPlacedObject = null; } grabbableObject.CurrentAnchor = null; // Raise events OnObjectRemoved(removeEventArgs, propagateEvents); if (propagateEvents) { sourceAnchor.RaiseRemovedEvent(removeEventArgs); } EndSyncMethod(new object[] { grabbableObject, propagateEvents }); } /// /// Moves the object to a new world position and optionally applies resistance and propagates user-defined constraining /// events. /// /// The object to move /// New world-space position /// /// Whether to apply resistance to the new position using /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetPositionUsingConstraints(UxrGrabbableObject grabbableObject, Vector3 position, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, position, grabbableObject.transform.rotation, Space.World, useResistance, propagateEvents); } /// /// Rotates the object to a new world rotation and optionally applies resistance and propagates user-defined /// constraining events. /// /// The object to rotate /// New world-space rotation /// /// Whether to apply resistance to the new rotation using /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetRotationUsingConstraints(UxrGrabbableObject grabbableObject, Quaternion rotation, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, grabbableObject.transform.position, rotation, Space.World, useResistance, propagateEvents); } /// /// Moves and rotates the object to a new world position/rotation and optionally applies resistance and propagates /// user-defined constraining events. /// /// The object to rotate /// New world-space position /// New world-space rotation /// /// Whether to apply resistance to the new position and rotation using /// and /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetPositionAndRotationUsingConstraints(UxrGrabbableObject grabbableObject, Vector3 position, Quaternion rotation, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, position, rotation, Space.World, useResistance, propagateEvents); } /// /// Moves the object to a new local position and optionally applies resistance and propagates user-defined constraining /// events. /// /// The object to move /// New local-space position /// /// Whether to apply resistance to the new position using /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetLocalPositionUsingConstraints(UxrGrabbableObject grabbableObject, Vector3 localPosition, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, localPosition, grabbableObject.transform.rotation, Space.Self, useResistance, propagateEvents); } /// /// Rotates the object to a new local rotation and optionally applies resistance and propagates user-defined /// constraining events. /// /// The object to rotate /// New local-space rotation /// /// Whether to apply resistance to the new rotation using /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetLocalRotationUsingConstraints(UxrGrabbableObject grabbableObject, Quaternion localRotation, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, grabbableObject.transform.position, localRotation, Space.Self, useResistance, propagateEvents); } /// /// Moves and rotates the object to a new local position/rotation and optionally applies resistance and propagates /// user-defined constraining events. /// /// The object to rotate /// New local-space position /// New local-space rotation /// /// Whether to apply resistance to the new position and rotation using /// and /// /// /// Whether to propagate constraining events ( /// // /// ) /// public void SetLocalPositionAndRotationUsingConstraints(UxrGrabbableObject grabbableObject, Vector3 localPosition, Quaternion localRotation, bool useResistance, bool propagateEvents) { SetPositionAndRotationUsingConstraintsInternal(grabbableObject, localPosition, localRotation, Space.Self, useResistance, propagateEvents); } /// /// Places the grips of a grabbed object again in the correct position after the object was moved/rotated. /// This is required because the hands are not parented to the object and if the object is moved or rotated, the hands /// will not keep the correct relative position/rotation anymore. /// For grips that have position/rotation snapping, it makes sure to snap to the correct position. /// For grips that don't have snapping, it will make sure to keep the same grip position/rotation used at the moment /// the object was grabbed. /// /// /// When using / there is no need to call this method. /// UltimateX will call it internally to make sure that the grips stay in place after any user-defined constraints are /// applied. /// public void KeepGripsInPlace(UxrGrabbableObject grabbableObject) { foreach (UxrGrabber grabber in GetGrabbingHands(grabbableObject)) { KeepGripInPlace(grabber); } } /// /// Like but for a single grip. /// public void KeepGripInPlace(UxrGrabber grabber) { if (grabber.HandBone == null || !Features.HasFlag(UxrManipulationFeatures.KeepGripsInPlace)) { return; } if (grabber.GrabbedObject != null && _currentManipulations.ContainsKey(grabber.GrabbedObject)) { grabber.HandBone.ApplyAlignment(grabber.transform.position, grabber.transform.rotation, GetGrabbedPointGrabAlignPosition(grabber), GetGrabbedPointGrabAlignRotation(grabber)); } } /// /// Gets or rotation angle in degrees for objects that have a single rotational degree of freedom. /// /// The object to get the information from /// Angle in degrees public float GetObjectSingleRotationAxisDegrees(UxrGrabbableObject grabbableObject) { int singleRotationAxisIndex = grabbableObject.SingleRotationAxisIndex; if (singleRotationAxisIndex == -1) { return 0.0f; } return (grabbableObject.SingleRotationAngleCumulative + GetCurrentSingleRotationAngleContributions(grabbableObject)).Clamped(grabbableObject.RotationAngleLimitsMin[singleRotationAxisIndex], grabbableObject.RotationAngleLimitsMax[singleRotationAxisIndex]); } /// /// Sets the rotation angle in degrees for objects that have a single rotational degree of freedom. Use this method to /// change the angle at runtime so that it keeps track of the rotation limits and also works while the object is being /// grabbed. /// /// The object to apply the new rotation to /// Angle in degrees public void SetObjectSingleRotationAxisDegrees(UxrGrabbableObject grabbableObject, float degrees) { int singleRotationAxisIndex = grabbableObject.SingleRotationAxisIndex; if (singleRotationAxisIndex == -1) { return; } float currentGrabContributions = GetCurrentSingleRotationAngleContributions(grabbableObject); float newValue = degrees.Clamped(grabbableObject.RotationAngleLimitsMin[singleRotationAxisIndex], grabbableObject.RotationAngleLimitsMax[singleRotationAxisIndex]); grabbableObject.SingleRotationAngleCumulative = newValue - currentGrabContributions; grabbableObject.transform.localRotation = grabbableObject.InitialLocalRotation * Quaternion.AngleAxis(newValue, (UxrAxis)singleRotationAxisIndex); } #endregion #region Private Methods /// /// Updates the grabbable objects using manipulation logic. /// private void UpdateManipulation() { ICollection sortedGrabbableObjects = null; if (_currentManipulations.Any()) { // Sort objects for manipulation so that parents are always processed before the children. sortedGrabbableObjects = _currentManipulations.Keys; if (_currentManipulations.Count > 1) { List sortedList = new List(sortedGrabbableObjects); sortedList.Sort((a, b) => b.AllChildren.Count.CompareTo(a.AllChildren.Count)); sortedGrabbableObjects = sortedList; } // Compute manipulation update. Check ProcessManipulation's summary for detailed explanation. foreach (UxrGrabbableObject grabbableObject in sortedGrabbableObjects) { if (grabbableObject == null || grabbableObject.IsBeingDestroyed) { continue; } ProcessManipulation(grabbableObject); } } // Update smooth object transitions. if (Features.HasFlag(UxrManipulationFeatures.SmoothTransitions)) { UpdateSmoothObjectTransitions(); } // Apply user-defined constraints if (sortedGrabbableObjects != null) { // Apply user defined manipulation constraints foreach (UxrGrabbableObject grabbableObject in sortedGrabbableObjects) { if (grabbableObject == null || grabbableObject.IsBeingDestroyed) { continue; } UxrApplyConstraintsEventArgs constrainEventArgs = new UxrApplyConstraintsEventArgs(grabbableObject); grabbableObject.RaiseConstraintsApplying(constrainEventArgs); grabbableObject.RaiseConstraintsApplied(constrainEventArgs); grabbableObject.KeepGripsInPlace(); grabbableObject.RaiseConstraintsFinished(constrainEventArgs); } } // Release constrained grips that are too far away from the real hand. // Do it in two passes to avoid deleting elements in the collection being iterated. List gripsToRelease = null; foreach (var manipulation in _currentManipulations) { foreach (UxrGrabber grabber in manipulation.Value.Grabbers) { if (ShouldGrabberReleaseFarAwayGrip(grabber)) { if (gripsToRelease == null) { gripsToRelease = new List(); } gripsToRelease.Add(grabber); } } } if (gripsToRelease != null) { foreach (UxrGrabber grabber in gripsToRelease) { grabber.Avatar.ControllerInput.SendHapticFeedback(grabber.Side, UxrHapticClipType.Click, 1.0f); ReleaseObject(grabber, grabber.GrabbedObject, true); } } // Update smooth grabber transitions. if (Features.HasFlag(UxrManipulationFeatures.SmoothTransitions)) { foreach (UxrGrabber grabber in UxrGrabber.EnabledComponents) { grabber.UpdateSmoothManipulationTransition(Time.unscaledDeltaTime); } } } /// /// Grabs an object. Optionally if is not null, the grab synchronizes with a grab /// coming from an external event. /// /// Grabber that will grab the object /// Object to grab /// Grab point to grab the object from /// /// If not null, it will contain the event args from an external grab that this grab will /// have to mimic. This is useful in multi-player environments to ensure that the grip ends up being the same. /// /// Whether to propagate events private void GrabObject(UxrGrabber grabber, UxrGrabbableObject grabbableObject, int grabPoint, UxrManipulationEventArgs sourceEventArgs, bool propagateEvents) { if (grabber == null || grabbableObject == null) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Errors) { Debug.LogError($"{UxrConstants.ManipulationModule} {nameof(GrabObject)}, {nameof(UxrGrabber)} component or {nameof(UxrGrabbableObject)} component is null."); } return; } if (grabber.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(GrabObject)}, {nameof(UxrGrabber)} component on {grabber.name} is disabled."); } } if (grabbableObject.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(GrabObject)}, {nameof(UxrGrabbableObject)} component on {grabbableObject.name} is disabled."); } } // This method will be synchronized. It will generate a new frame when recording a replay to ensure smooth interpolation when re-parenting. BeginSync(UxrStateSyncOptions.Default | UxrStateSyncOptions.GenerateNewFrame); UxrGrabbableObjectAnchor sourceAnchor = grabbableObject.CurrentAnchor; // Were we swapping hands, are we grabbing with more than one hand or is it a new grab? bool handSwapSamePoint = false; bool handSwapDifferentPoints = false; bool moreThanOneHand = false; UxrGrabber releasingGrabber = null; RuntimeManipulationInfo manipulationInfo = null; UxrManipulationEventArgs releaseEventArgs = null; foreach (UxrGrabber otherGrabberCandidate in UxrGrabber.EnabledComponents) { if (otherGrabberCandidate != grabber && otherGrabberCandidate.GrabbedObject == grabbableObject) { // Other grabber is already grabbing this object. Check if it is the same grabbing point or not. _currentManipulations.TryGetValue(grabbableObject, out manipulationInfo); if (manipulationInfo != null && manipulationInfo.GrabbedPoints.Contains(grabPoint)) { // It is the same grabbing point. Now there are two options: // -If the grabbing point has an UxrGrabPointShape component associated, it will be grabbed with the new hand at the same time if MultiGrab is enabled. // -If it does not have an UxrGrabPointShape it will mean that the object will swap from one hand to the other. if (grabbableObject.GetGrabPointShape(grabPoint) != null && grabbableObject.AllowMultiGrab) { moreThanOneHand = true; break; } // We are swapping hands because there is already other hand grabbing this same point // Raise release event for the other hand releasingGrabber = otherGrabberCandidate; handSwapSamePoint = true; releaseEventArgs = UxrManipulationEventArgs.FromRelease(releasingGrabber.GrabbedObject, sourceAnchor, releasingGrabber, grabPoint, false, true); break; } if (manipulationInfo != null && !manipulationInfo.GrabbedPoints.Contains(grabPoint)) { // Other hand grabbing another point of the same object: both hands will grab the object if MultiGrab is enabled, or the // other hand will be released if not releasingGrabber = otherGrabberCandidate; if (!grabbableObject.AllowMultiGrab) { handSwapDifferentPoints = true; releaseEventArgs = UxrManipulationEventArgs.FromRelease(releasingGrabber.GrabbedObject, sourceAnchor, releasingGrabber, grabPoint, false, true); } else { moreThanOneHand = true; } break; } } } // Raise release event if (releaseEventArgs != null) { OnObjectReleasing(releaseEventArgs, propagateEvents); if (propagateEvents) { releaseEventArgs.GrabbableObject.RaiseReleasingEvent(releaseEventArgs); } manipulationInfo.NotifyEndGrab(releaseEventArgs.Grabber, releaseEventArgs.GrabbableObject, releaseEventArgs.GrabPointIndex); } // Compute grabber snap position/orientation grabbableObject.ComputeRequiredGrabberTransform(grabber, grabPoint, out Vector3 grabberPosition, out Quaternion grabberRotation, false); Vector3 grabberLocalSnapPosition = sourceEventArgs?.GrabberLocalSnapPosition ?? grabber.transform.InverseTransformPoint(grabberPosition); Quaternion grabberLocalSnapRotation = sourceEventArgs?.GrabberLocalSnapRotation ?? Quaternion.Inverse(grabber.transform.rotation) * grabberRotation; // Raise grabbing/removing events UxrManipulationEventArgs grabEventArgs = UxrManipulationEventArgs.FromGrab(grabbableObject, sourceAnchor, grabber, grabPoint, moreThanOneHand, handSwapSamePoint || handSwapDifferentPoints, grabberLocalSnapPosition, grabberLocalSnapRotation); UxrManipulationEventArgs removeEventArgs = UxrManipulationEventArgs.FromRemove(grabbableObject, sourceAnchor, grabber, grabPoint, moreThanOneHand, handSwapSamePoint || handSwapDifferentPoints); OnObjectGrabbing(grabEventArgs, propagateEvents); if (propagateEvents) { grabbableObject.RaiseGrabbingEvent(grabEventArgs); } if (sourceAnchor && !grabbableObject.IsConstrained) { OnObjectRemoving(removeEventArgs, propagateEvents); if (propagateEvents) { sourceAnchor.RaiseRemovingEvent(removeEventArgs); } } // Link it to the hand grabber.GrabbedObject = grabbableObject; if (sourceAnchor && !grabbableObject.IsConstrained) { sourceAnchor.CurrentPlacedObject = null; grabber.GrabbedObject.CurrentAnchor = null; if (sourceAnchor.ActivateOnPlaced) { sourceAnchor.ActivateOnPlaced.SetActive(false); } if (sourceAnchor.ActivateOnEmpty) { sourceAnchor.ActivateOnEmpty.SetActive(true); } if (sourceAnchor.ActivateOnCompatibleNear != null) { sourceAnchor.ActivateOnCompatibleNear.SetActive(false); } if (sourceAnchor.ActivateOnCompatibleNotNear != null) { sourceAnchor.ActivateOnCompatibleNotNear.SetActive(false); } } // If it is a dynamic object, make it kinematic while it is grabbed. Also check the parent grabbable if it exists. Rigidbody rigidBodyToGrab = null; if (grabbableObject.RigidBodySource != null && grabbableObject.CanUseRigidBody) { rigidBodyToGrab = grabbableObject.RigidBodySource; } UxrGrabbableObject grabbableParentLookAt = grabbableObject.ParentLookAts.FirstOrDefault(); if (rigidBodyToGrab == null && grabbableParentLookAt != null && grabbableParentLookAt.RigidBodySource != null && grabbableParentLookAt.CanUseRigidBody) { if (!grabbableParentLookAt.IsBeingGrabbed && !GetDirectChildrenLookAtBeingGrabbed(grabbableParentLookAt).Any()) { rigidBodyToGrab = grabbableParentLookAt.RigidBodySource; } } if (rigidBodyToGrab) { rigidBodyToGrab.isKinematic = true; } if (handSwapSamePoint) { manipulationInfo.SwapGrabber(manipulationInfo.GetGrabberGrabbingPoint(grabPoint), grabber); } else if (handSwapDifferentPoints) { manipulationInfo.SwapGrabber(releasingGrabber, manipulationInfo.GetGrabbedPoint(releasingGrabber), grabber, grabPoint); } else if (moreThanOneHand) { manipulationInfo.RegisterNewGrab(grabber, grabPoint); } else { manipulationInfo = new RuntimeManipulationInfo(grabber, grabPoint, sourceAnchor); _currentManipulations.Add(grabbableObject, manipulationInfo); } // Re-parent if (!grabbableObject.IsConstrained && !moreThanOneHand) { if (grabbableObject.UseParenting) { AssignGrabbedObjectParent(grabbableObject); } } manipulationInfo.NotifyBeginGrab(grabber, grabbableObject, grabPoint, grabberPosition, grabberRotation, sourceEventArgs); // Raise events if (releaseEventArgs != null) { OnObjectReleased(releaseEventArgs, propagateEvents); if (propagateEvents) { releasingGrabber.GrabbedObject.RaiseReleasedEvent(releaseEventArgs); } releasingGrabber.GrabbedObject = null; } OnObjectGrabbed(grabEventArgs, propagateEvents); if (propagateEvents) { grabbableObject.RaiseGrabbedEvent(grabEventArgs); } if (sourceAnchor && !grabbableObject.IsConstrained) { OnObjectRemoved(removeEventArgs, propagateEvents); if (propagateEvents) { sourceAnchor.RaiseRemovedEvent(removeEventArgs); } } // Here we use grabEventArgs instead of sourceEventArgs so that all clients are synchronized using the exact same grab position/orientation as // computed by the the original grab. This ensures a deterministic grab, otherwise a plain grab would compute the grab position/orientation // on each client and the different hand position/orientation would cause different results. EndSyncMethod(new object[] { grabber, grabbableObject, grabPoint, grabEventArgs, propagateEvents }); } /// /// Releases an object from a hand. /// /// /// If non-null it will tell the grabber that releases the object. If it is null any grabber that is holding the object /// will release it /// /// Object being released /// The position to use when releasing, or null to compute it locally /// The rotation to use when releasing, or null to compute it locally /// The velocity to use when releasing, or null to compute it locally /// The angular velocity to use when releasing, or null to compute it locally /// Whether to propagate events private void ReleaseObject(UxrGrabber grabber, UxrGrabbableObject grabbableObject, Vector3? position, Quaternion? rotation, Vector3? velocity, Vector3? angularVelocity, bool propagateEvents) { int grabbedPoint = GetGrabbedPoint(grabber); if (!UxrGrabber.EnabledComponents.Any(grb => grb.GrabbedObject == grabbableObject && (grb == grabber || grabber == null))) { return; } if (!_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Errors) { Debug.LogError($"{UxrConstants.ManipulationModule} {nameof(RuntimeManipulationInfo)} not found for object {grabbableObject.name}. This should not be happening."); } return; } if (grabbableObject.enabled == false) { if (UxrGlobalSettings.Instance.LogLevelManipulation >= UxrLogLevel.Warnings) { Debug.LogWarning($"{UxrConstants.ManipulationModule} {nameof(ReleaseObject)}, {nameof(UxrGrabbableObject)} component on {nameof(grabbableObject.name)} is disabled."); } } // This method will be synchronized. It will generate a new frame when recording a replay to ensure smooth interpolation. BeginSync(UxrStateSyncOptions.Default | UxrStateSyncOptions.GenerateNewFrame); UxrGrabbableObjectAnchor sourceAnchor = manipulationInfo.SourceAnchor; bool isMultiHands = manipulationInfo.Grabs.Count > 1; Vector3 releasePosition = position ?? transform.position; Quaternion releaseRotation = rotation ?? transform.rotation; Vector3 releaseVelocity = velocity ?? (grabber != null && !grabber.IsInSmoothManipulationTransition ? grabber.SmoothVelocity : Vector3.zero); Vector3 releaseAngularVelocity = angularVelocity ?? (grabber != null && !grabber.IsInSmoothManipulationTransition ? grabber.SmoothAngularVelocity * Mathf.Deg2Rad : Vector3.zero); Vector2 horizontal = new Vector2(releaseVelocity.x, releaseVelocity.z); // Minimum amount of velocity in units/sec a component (hor/ver) needs to have in order to have the multiplier applied. // This will avoid objects being thrown super fast at low velocities. float multiplierVelocityThreshold = 2.5f; // In order to avoid the sudden change of velocity if an object is thrown just over/under the speed threshold, the // multiplier is applied using a gradient measured in units/second. float multiplierVelocityGradient = 2.0f; if (grabbableObject.HorizontalReleaseMultiplier > 1.0f) { // we will apply the multiplier gradually depending on the release velocity starting from multiplierVelocityThreshold. // This is measured in units/sec so objects going below multiplierVelocityThreshold units/sec will have the normal // release velocity and objects above or equal to (multiplierVelocityThreshold + multiplierVelocityGradient) units/sec will // get the maximum multiplier. Velocities in between will have a smooth transition. if (horizontal.magnitude > multiplierVelocityThreshold) { float lerp = Mathf.Clamp01((horizontal.magnitude - multiplierVelocityThreshold) * (1.0f / multiplierVelocityGradient)); horizontal = Vector3.Lerp(horizontal, horizontal * grabbableObject.HorizontalReleaseMultiplier, lerp); } } else { horizontal *= grabbableObject.HorizontalReleaseMultiplier; } releaseVelocity.x = horizontal.x; releaseVelocity.z = horizontal.y; if (grabbableObject.VerticalReleaseMultiplier > 1.0f) { // Apply multiplier in the same gradual way as the horizontal component. if (Mathf.Abs(releaseVelocity.y) > multiplierVelocityThreshold) { float lerp = Mathf.Clamp01((Mathf.Abs(releaseVelocity.y) - multiplierVelocityThreshold) * (1.0f / multiplierVelocityGradient)); releaseVelocity.y = Mathf.Lerp(releaseVelocity.y, releaseVelocity.y * grabbableObject.VerticalReleaseMultiplier, lerp); } } else { releaseVelocity.y *= grabbableObject.VerticalReleaseMultiplier; } // Check if the object's rigidbody needs to be made dynamic Rigidbody rigidBodyToRelease = null; if (isMultiHands == false || grabber == null) { _currentManipulations.Remove(grabbableObject); if (grabbableObject.RigidBodySource != null && grabbableObject.CanUseRigidBody && grabbableObject.RigidBodyDynamicOnRelease) { if (!GetDirectChildrenLookAtBeingGrabbed(grabbableObject).Any()) { rigidBodyToRelease = grabbableObject.RigidBodySource; } } } // Check if the object's parent grabbable rigidbody needs to be made dynamic, if this is the last grab that keeps holding it UxrGrabbableObject grabbableParentLookAt = grabbableObject.ParentLookAts.FirstOrDefault(); if (rigidBodyToRelease == null && grabbableParentLookAt != null && grabbableParentLookAt.RigidBodySource != null && grabbableParentLookAt.CanUseRigidBody && grabbableParentLookAt.RigidBodyDynamicOnRelease) { if (!grabbableParentLookAt.IsBeingGrabbed && !GetDirectChildrenLookAtBeingGrabbed(grabbableParentLookAt).Any()) { rigidBodyToRelease = grabbableParentLookAt.RigidBodySource; } } if (rigidBodyToRelease != null && releaseVelocity.IsValid()) { if (position == null) { // Locally, update the position update for this frame when releasing because physics are still not enabled releasePosition += releaseVelocity * Time.deltaTime; rigidBodyToRelease.position += releasePosition; } else { // Use the parameters passed rigidBodyToRelease.position = releasePosition; rigidBodyToRelease.rotation = releaseRotation; } } // Process and raise event(s) // Avoid creating list of multiple releases if the release is just a single grabber List<(UxrGrabber, int)> multipleReleases = grabber != null ? new List<(UxrGrabber, int)>() : null; foreach (UxrGrabber grb in manipulationInfo.Grabbers) { if (grb == grabber || grabber == null) { int grbPoint = GetGrabbedPoint(grb); UxrManipulationEventArgs releasingEventArgs = UxrManipulationEventArgs.FromRelease(grabbableObject, sourceAnchor, grb, grbPoint, isMultiHands && grabber != null, isMultiHands && grabber != null, releaseVelocity, releaseAngularVelocity); OnObjectReleasing(releasingEventArgs, propagateEvents); if (grabbableObject) { if (propagateEvents) { grabbableObject.RaiseReleasingEvent(releasingEventArgs); } manipulationInfo.NotifyEndGrab(grb, grabbableObject, grbPoint); } grb.GrabbedObject = null; multipleReleases?.Add((grb, grbPoint)); } } // Make rigidbody dynamic if there is one if (rigidBodyToRelease != null) { rigidBodyToRelease.isKinematic = false; if (releaseVelocity.IsValid()) { rigidBodyToRelease.AddForce(releaseVelocity, ForceMode.VelocityChange); } if (releaseAngularVelocity.IsValid()) { rigidBodyToRelease.AddTorque(releaseAngularVelocity, ForceMode.VelocityChange); } } // Remove grabber(s) if (grabber == null) { manipulationInfo.RemoveAll(); } else { manipulationInfo.RemoveGrab(grabber); } // Raise event(s) if (multipleReleases != null) { foreach ((UxrGrabber, int) releaseInfo in multipleReleases) { RaiseReleaseEvent(releaseInfo.Item1, releaseInfo.Item2); } } else { RaiseReleaseEvent(grabber, grabbedPoint); } void RaiseReleaseEvent(UxrGrabber targetGrabber, int targetGrabPoint) { UxrManipulationEventArgs releasedEventArgs = UxrManipulationEventArgs.FromRelease(grabbableObject, sourceAnchor, targetGrabber, targetGrabPoint, isMultiHands && grabber != null, isMultiHands && grabber != null, releaseVelocity, releaseAngularVelocity); OnObjectReleased(releasedEventArgs, propagateEvents); if (grabbableObject && propagateEvents) { grabbableObject.RaiseReleasedEvent(releasedEventArgs); } } EndSyncMethod(new object[] { grabber, grabbableObject, releasePosition, releaseRotation, releaseVelocity, releaseAngularVelocity, propagateEvents }); } /// /// Assigns the new parent of a grabbable object that was grabbed. /// /// Grabbable object to assign a new parent to private void AssignGrabbedObjectParent(UxrGrabbableObject grabbableObject) { if (_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { // Will assign the parent of the first avatar that grabbed the object. ChangeGrabbableObjectParent(grabbableObject, manipulationInfo.Grabbers.First().Avatar.transform.parent); } } /// /// Changes the parent of a grabbable object. We do it using this method to keep track of re-parenting /// so that we can recalculate internal data if necessary. /// /// Grabbable object to change the parent of /// New parent private void ChangeGrabbableObjectParent(UxrGrabbableObject grabbableObject, Transform newParent) { Vector3 positionBeforeUpdate = TransformExt.GetWorldPosition(grabbableObject.transform.parent, grabbableObject.LocalPositionBeforeUpdate); Quaternion rotationBeforeUpdate = TransformExt.GetWorldRotation(grabbableObject.transform.parent, grabbableObject.LocalRotationBeforeUpdate); grabbableObject.transform.SetParent(newParent, true); grabbableObject.LocalPositionBeforeUpdate = TransformExt.GetLocalPosition(newParent, positionBeforeUpdate); grabbableObject.LocalRotationBeforeUpdate = TransformExt.GetLocalRotation(newParent, rotationBeforeUpdate); } /// /// Tries to grab something using the given grabber. /// /// Grabber to try grabbing with private void TryGrab(UxrGrabber grabber) { if (grabber.GrabbedObject != null) { // We have already something grabbed, this means we have a grab point with toggle grab mode or keep always foreach (RuntimeGrabInfo grabInfo in GetGrabs(grabber.GrabbedObject)) { if (grabInfo.Grabber == grabber && grabber.GrabbedObject.GetGrabPoint(grabInfo.GrabbedPoint).GrabMode == UxrGrabMode.GrabAndKeepAlways) { // Grab is in "Keep always" mode return; } } // It is in Toggle grab mode NotifyReleaseGrab(grabber, true); return; } OnGrabTrying(UxrManipulationEventArgs.FromOther(UxrManipulationEventType.GrabTrying, null, null, grabber), true); if (IsGrabbingAllowed == false) { return; } // A grab gesture has been made. Try to find possible objects that have been grabbed if (GetClosestGrabbableObject(grabber.Avatar, grabber.Side, out UxrGrabbableObject grabbableObject, out int grabPoint)) { // There's a grabbed object! GrabObject(grabber, grabbableObject, grabPoint, true); } } /// /// Notifies that a grip is released and checks if the object needs to be released or placed on an anchor. /// /// Grabber to release the object from /// Whether the release was from a private void NotifyReleaseGrab(UxrGrabber grabber, bool fromToggle = false) { // A release gesture has been made. Check for possible object placements / drop if (grabber.GrabbedObject != null) { // First check if the grabbed point has toggle mode or keep always mode. In that case we should not release the object but keep it in the grabbed list if (_currentManipulations.TryGetValue(grabber.GrabbedObject, out RuntimeManipulationInfo manipulationInfo) && !fromToggle) { int grabbedPoint = manipulationInfo.GetGrabbedPoint(grabber); if (grabbedPoint != -1) { UxrGrabPointInfo grabPointInfo = grabber.GrabbedObject.GetGrabPoint(grabbedPoint); if (grabPointInfo != null && (grabPointInfo.GrabMode == UxrGrabMode.GrabToggle || grabPointInfo.GrabMode == UxrGrabMode.GrabAndKeepAlways)) { // Ignore release. We will keep grabbing it until another TryGrab or keep it grabbed always unless another grabber gets it. return; } } } // If we only have a single hand left grabbing, find the closest compatible anchor candidate within the influence radius UxrGrabbableObjectAnchor anchorCandidate = null; if (manipulationInfo != null && manipulationInfo.Grabs.Count == 1 && grabber.GrabbedObject.IsPlaceable) { float minDistance = float.MaxValue; if (grabber.GrabbedObject.UsesGrabbableParentDependency == false) { foreach (KeyValuePair anchorPair in _grabbableObjectAnchors) { if (grabber.GrabbedObject.CanBePlacedOnAnchor(anchorPair.Key, out float distance) && distance < minDistance) { anchorCandidate = anchorPair.Key; minDistance = distance; } } } } // Execute if (anchorCandidate == null) { ReleaseObject(grabber, grabber.GrabbedObject, true); } else { PlaceObject(grabber.GrabbedObject, anchorCandidate, UxrPlacementOptions.Smooth, true); } grabber.GrabbedObject = null; } } /// /// /// Handles an object being manipulated. This method is called by with objects /// sorted from top to bottom grabbable hierarchy, starting with those with the most grabbable children /// in decreasing count order. /// /// /// The algorithm has the following steps: /// /// /// If the object uses , it is solved. If it uses /// , is is only moved without any rotation. It /// can't be fully solved at this point without knowing the rotation pivot, which may depend on a parent, /// and the parent only can be solved after all children. Moving it allows the parent later to know at /// least where to rotate to. /// /// /// If there are any parent grabbable objects whose direction when grabbed is controlled by children ( /// used in children grabbable objects), these /// directions (look-at) in the parent are solved by the last grabbed child's /// . /// /// /// Parent look-ats will be: /// /// /// Parent grab to child grabs look-at when the parent is also grabbed and uses /// . /// /// /// Keep position/rotation relative to the children when the parent is not grabbed and uses /// . /// /// /// Parent pivot to child grabs look-at when the parent (grabbed or not) uses /// . /// /// /// /// /// A post-process is performed for all objects in this order: /// /// /// The object has grabbed children and /// the last child was processed. /// /// /// The object controls a parent direction and the parent's last child was processed. /// /// /// The object doesn't control any parent nor has any child that controls it. /// /// /// /// /// Post-processing steps: /// /// /// Solve object. /// /// /// Apply object constraints and rotation/position resistance. /// /// /// /// /// /// /// The object being grabbed private void ProcessManipulation(UxrGrabbableObject grabbableObject) { if (!_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { return; } UxrGrabbableObject grabbableParent = grabbableObject.GrabbableParent; bool controlsParentDirection = grabbableParent != null && grabbableObject.UsesGrabbableParentDependency && grabbableObject.ControlParentDirection; if (controlsParentDirection) { foreach (RuntimeGrabInfo grabInfo in manipulationInfo.Grabs) { grabInfo.ParentLocalGrabPositionBeforeUpdate = grabbableParent.transform.InverseTransformPoint(GetGrabbedPointGrabAlignPosition(grabInfo.Grabber)); grabInfo.ChildLocalParentPosition = grabbableObject.transform.InverseTransformPoint(grabbableParent.transform.position); grabInfo.ChildLocalParentRotation = Quaternion.Inverse(grabbableObject.transform.rotation) * grabbableParent.transform.rotation; } } switch (grabbableObject.RotationProvider) { case UxrRotationProvider.HandOrientation: SolveHandOrientationManipulation(grabbableObject, manipulationInfo); break; case UxrRotationProvider.HandPositionAroundPivot: SolveSimplePositionManipulation(grabbableObject, manipulationInfo); break; } if (controlsParentDirection) { foreach (RuntimeGrabInfo grabInfo in manipulationInfo.Grabs) { grabInfo.ParentLocalGrabPositionAfterUpdate = grabbableParent.transform.InverseTransformPoint(GetGrabbedPointGrabAlignPosition(grabInfo.Grabber)); } grabbableParent.DirectLookAtChildProcessedCount++; } // Check if we need to solve the parent if (controlsParentDirection && grabbableParent.DirectLookAtChildProcessedCount == grabbableParent.DirectLookAtChildGrabbedCount) { // This was the last child processed that controls a grabbable parent direction: Perform look-at. // Store currently grabbed child transforms to compute constraints correctly. foreach (RuntimeManipulationInfo childManipulation in GetDirectChildrenLookAtManipulations(grabbableParent)) { childManipulation.GrabbableObject.PushTransform(); } // Handle the parent grabbable object _currentManipulations.TryGetValue(grabbableParent, out RuntimeManipulationInfo parentManipulationInfo); if ((parentManipulationInfo != null && grabbableParent.RotationProvider == UxrRotationProvider.HandOrientation) || grabbableParent.RotationProvider == UxrRotationProvider.HandPositionAroundPivot) { // Perform lookAt if parent is being grabbed or if not grabbed but rotation provider is HandPositionAroundPivot if (grabbableParent.RotationProvider == UxrRotationProvider.HandPositionAroundPivot) { // In this path we're going to average all individual rotations foreach (RuntimeManipulationInfo childManipulation in GetDirectChildrenLookAtManipulations(grabbableParent)) { foreach (RuntimeGrabInfo grabInfo in childManipulation.Grabs) { grabInfo.ParentGrabbableLeverageContribution = (childManipulation.GrabbableObject.transform.TransformPoint(grabInfo.ParentGrabbableLookAtLocalLeveragePoint) - grabbableParent.transform.position).normalized; } } grabbableParent.transform.localRotation = grabbableParent.InitialLocalRotation; Vector3 singleRotationAxis = grabbableParent.SingleRotationAxisIndex != -1 ? grabbableParent.transform.TransformDirection((UxrAxis)grabbableParent.SingleRotationAxisIndex) : Vector3.zero; foreach (RuntimeManipulationInfo childManipulation in GetDirectChildrenLookAtManipulations(grabbableParent)) { foreach (RuntimeGrabInfo grabInfo in childManipulation.Grabs) { Vector3 originalLookAt = (grabbableParent.transform.TransformPoint(grabInfo.ParentGrabbableLookAtParentLeveragePoint) - grabbableParent.transform.position).normalized; Vector3 desiredLookAt = grabInfo.ParentGrabbableLeverageContribution; if (grabbableParent.SingleRotationAxisIndex != -1) { // This makes sure that in parents where there is a single rotation axis, the axis is aligned correctly. originalLookAt = Vector3.ProjectOnPlane(originalLookAt, singleRotationAxis).normalized; desiredLookAt = Vector3.ProjectOnPlane(desiredLookAt, singleRotationAxis).normalized; float angle = Vector3.SignedAngle(originalLookAt, desiredLookAt, singleRotationAxis); grabInfo.ParentGrabbableLookAtRotationContribution = Quaternion.AngleAxis(angle, singleRotationAxis) * grabbableParent.transform.rotation; } else { Vector3 rotationAxis = Vector3.Cross(originalLookAt, desiredLookAt); grabInfo.ParentGrabbableLookAtRotationContribution = Quaternion.AngleAxis(Vector3.SignedAngle(originalLookAt, desiredLookAt, singleRotationAxis), rotationAxis); } } } Quaternion averageRotation = QuaternionExt.Average(GetDirectChildrenLookAtManipulations(grabbableParent).SelectMany(m => m.Grabs.Select(g => g.ParentGrabbableLookAtRotationContribution))); grabbableParent.transform.rotation = averageRotation; } else { // Parent is grabbed and rotation provider is HandOrientation: Average all child dependencies for the lookAt // TODO: Ideally we would average all rotation contributions to avoid opposite directions cancelling each other. Vector3 oldTotal = Vector3.zero; Vector3 newTotal = Vector3.zero; int contributions = 0; foreach (RuntimeManipulationInfo childManipulation in GetDirectChildrenLookAtManipulations(grabbableParent)) { foreach (RuntimeGrabInfo grabInfo in childManipulation.Grabs) { oldTotal += grabInfo.ParentLocalGrabPositionBeforeUpdate; newTotal += grabInfo.ParentLocalGrabPositionAfterUpdate; contributions++; } } // Perform the lookAt.The rotation pivot will be the parent grabs. if (contributions > 0) { Vector3 worldLookAtPivot = grabbableParent.transform.TransformPoint(parentManipulationInfo.LocalManipulationRotationPivot); Vector3 currentLookAt = grabbableParent.transform.TransformPoint(oldTotal / contributions) - worldLookAtPivot; Vector3 desiredLookAt = grabbableParent.transform.TransformPoint(newTotal / contributions) - worldLookAtPivot; Vector3 rotationAxis = Vector3.Cross(currentLookAt, desiredLookAt); grabbableParent.transform.RotateAround(worldLookAtPivot, rotationAxis, Vector3.SignedAngle(currentLookAt, desiredLookAt, rotationAxis)); } } } else { // Parent is not being grabbed and rotation provider is not HandPositionAroundPivot: average using look ats and position averaging. IEnumerable childGrabContributions = GetDirectChildrenLookAtManipulations(grabbableParent).SelectMany(m => m.Grabs); if (childGrabContributions.Any()) { SolveUsingLookAtAveraging(grabbableParent, null, childGrabContributions.First().Grabber, childGrabContributions, true); } } // Finalize parent FinalizeManipulation(grabbableParent, parentManipulationInfo); // Finalize children foreach (RuntimeManipulationInfo childManipulation in GetDirectChildrenLookAtManipulations(grabbableParent)) { childManipulation.GrabbableObject.PopTransform(); FinalizeManipulation(childManipulation.GrabbableObject, childManipulation); } } // If the object has grabbable parent dependencies, and no child dependencies, the object will be finalized by the last parent's child dependency // If the object has grabbable child dependencies the object will be finalized by the last child // If there are no dependencies, finalize now if (!controlsParentDirection && grabbableObject.DirectLookAtChildGrabbedCount == 0) { FinalizeManipulation(grabbableObject, manipulationInfo); } } /// /// Finalizes a manipulation: /// /// Solves rotation constraint mode /// Applies constraints /// /// /// Grabbable object /// Manipulation info private void FinalizeManipulation(UxrGrabbableObject grabbableObject, RuntimeManipulationInfo manipulationInfo) { if (grabbableObject.RotationProvider == UxrRotationProvider.HandPositionAroundPivot) { // Constrain translation in a first pass. In grabbed objects, the local position was moved by SolveSimplePositionManipulation to // help the parent lookAt if it exists. In parent grabbable objects that are not being grabbed, it clamps translation due to // grabbed children. // It also is useful if the object position isn't locked since SolveHandAroundPivotManipulation() only takes care of the rotation. ConstrainTransform(grabbableObject, UxrTransformations.Translate); SolveHandAroundPivotManipulation(grabbableObject, manipulationInfo); } else { // Constraints in HandPositionAroundPivot are already computed by SolveHandAroundPivotManipulation(). ConstrainTransform(grabbableObject); } // Apply resistance if (Features.HasFlag(UxrManipulationFeatures.ObjectResistance)) { grabbableObject.transform.SetLocalPositionAndRotation(UxrInterpolator.SmoothDampPosition(grabbableObject.LocalPositionBeforeUpdate, grabbableObject.transform.localPosition, grabbableObject.TranslationResistance), UxrInterpolator.SmoothDampRotation(grabbableObject.LocalRotationBeforeUpdate, grabbableObject.transform.localRotation, grabbableObject.RotationResistance)); } } /// /// Solves the manipulation for an object applying simple translation without rotation or snapping. This is used for /// in a first pass to place the object approximately in /// space for potential parent objects to know where to rotate to. /// /// Grabbable object to solve /// Manipulation information /// Object is assigned a new position/rotation without considering any constraints private void SolveSimplePositionManipulation(UxrGrabbableObject grabbableObject, RuntimeManipulationInfo manipulationInfo) { Vector3 total = Vector3.zero; int contributions = 0; foreach (RuntimeGrabInfo grabInfo in manipulationInfo.Grabs) { total += grabInfo.Grabber.transform.TransformPoint(GetGrabPointRelativeGrabPosition(grabInfo.Grabber)); contributions++; } if (contributions > 0) { grabbableObject.transform.position = total / contributions; } } /// /// Solves the manipulation for an object that has as rotation /// provider. /// /// Grabbable object to solve /// Manipulation information /// Object is assigned a new position/rotation without considering any constraints private void SolveHandOrientationManipulation(UxrGrabbableObject grabbableObject, RuntimeManipulationInfo manipulationInfo) { UxrGrabber firstGrabber = manipulationInfo.Grabbers.First(); UxrGrabber mainGrabber = grabbableObject.FirstGrabPointIsMain ? manipulationInfo.GetGrabberGrabbingPoint(0) : null; bool recenterEachGrab = true; if (mainGrabber != null) { // If the first grab point is considered as main, the grabber that holds the main grab point will keep its position while the others // will only affect the rotation. // If the first grab point is not the main, and all grabs have the same importance, then the object will be centered between them. firstGrabber = mainGrabber; recenterEachGrab = false; } SolveUsingLookAtAveraging(grabbableObject, manipulationInfo, firstGrabber, manipulationInfo.Grabs, recenterEachGrab); } /// /// Solves an object manipulation so that as more grabs are added, the object will rotate towards those grabs to /// average each contribution. /// To solve the position, is used. /// /// Grabbable object to solve /// Grabbable object manipulation info. Can be null when solving grabbable parents that are not being grabbed, but whose children are /// First grabber to be solved in the order /// Total grabs, which may include or not /// /// If true, all contributions will be averaged. If false, the first grab will be used to /// solve the position and all the other grabs will only contribute with the rotation averaging /// private void SolveUsingLookAtAveraging(UxrGrabbableObject grabbableObject, RuntimeManipulationInfo manipulationInfo, UxrGrabber firstGrabber, IEnumerable grabs, bool averagePosition) { if (grabs == null || !grabs.Any()) { return; } // Place object in first grabber to process if (manipulationInfo != null) { // The object itself is being grabbed PlaceObjectInHand(grabbableObject, firstGrabber); } else { // It's a parent object, not being grabbed, with children being grabbed. Use the child relative position to place it in the first hand. RuntimeGrabInfo firstGrabInfo = grabs.FirstOrDefault(g => g.Grabber == firstGrabber); if (firstGrabInfo != null && firstGrabber.GrabbedObject != null && Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.position = firstGrabber.GrabbedObject.transform.TransformPoint(firstGrabInfo.ChildLocalParentPosition); grabbableObject.transform.rotation = firstGrabber.GrabbedObject.transform.rotation * firstGrabInfo.ChildLocalParentRotation; } } UxrGrabber previousGrabber = firstGrabber; Vector3 pivotSum = firstGrabber.transform.position; int grabSumCount = 1; // Now iterate over the rest of the grabs, rotating the object towards each grab. // The first 3 grabs will place the object on the plane determined by the 3 points. // Subsequent grabs will rotate the object less each time. foreach (RuntimeGrabInfo grabInfo in grabs) { if (grabInfo.Grabber == firstGrabber) { continue; } Vector3 rotationPivot = pivotSum / grabSumCount; float rotationAmount = grabSumCount < 3 ? 1.0f : 1.0f / grabSumCount; if (manipulationInfo == null) { // The parent is not being grabbed. We need to constrain the child transform early so that RotateObjectTowardsGrab // works using the correct lookAt direction. ConstrainTransform(grabInfo.Grabber.GrabbedObject); } if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { RotateObjectTowardsGrab(grabbableObject, pivotSum / grabSumCount, grabInfo.Grabber, rotationAmount); } if (averagePosition) { // Recenter object so that it keeps centered among grabs Vector3 snapPosition = GetGrabbedPointGrabAlignPosition(grabInfo.Grabber); float grabberDistance = Vector3.Distance(previousGrabber.transform.position, grabInfo.Grabber.transform.position); float snapDistance = Vector3.Distance(snapPosition, GetGrabbedPointGrabAlignPosition(previousGrabber)); if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.position += (snapPosition - rotationPivot).normalized * ((grabberDistance - snapDistance) * 0.5f * rotationAmount); } } pivotSum += grabInfo.Grabber.transform.position; grabSumCount++; previousGrabber = grabInfo.Grabber; } // Compute the rotation pivot used when child objects control this object's direction. if (manipulationInfo != null) { manipulationInfo.LocalManipulationRotationPivot = grabbableObject.transform.InverseTransformPoint(pivotSum / grabSumCount); } } /// /// Solves the manipulation for an object that has as /// rotation provider. /// /// Grabbable object to solve /// /// Grabbable object manipulation information. Can be null for parent grabbable objects that /// aren't grabbed but are grabbed by child objects /// /// Object is assigned a new position/rotation without considering any constraints private void SolveHandAroundPivotManipulation(UxrGrabbableObject grabbableObject, RuntimeManipulationInfo manipulationInfo) { Vector3 worldPosition = grabbableObject.transform.position; int rangeOfMotionAxisCount = grabbableObject.RangeOfMotionRotationAxisCount; // Rotation: We use the angle between V1(pivot, initial grab position) and V2 (pivot, current grab position). // This method works better for levers, steering wheels, etc. It won't work well with elements // like knobs or similar because the point where the torque is applied lies in the rotation axis itself. // In this cases we recommend using ManipulationMode.GrabAndMove instead. if (rangeOfMotionAxisCount == 1) { // Object rotation is constrained to a single axis UxrAxis rotationAxis = grabbableObject.SingleRotationAxisIndex; // We iterate over all current grabbers to compute each contribution IEnumerable grabs = GetGrabs(grabbableObject); int contributionCount = grabs.Count(); if (grabbableObject.NeedsTwoHandsToRotate && contributionCount < 2) { return; } foreach (RuntimeGrabInfo grabInfo in grabs) { // Compute values in world coordinates first Vector3 grabDirection = grabInfo.Grabber.transform.TransformPoint(grabInfo.GrabberLocalLeverageSource) - worldPosition; Vector3 initialGrabDirection = TransformExt.GetWorldPosition(grabbableObject.transform.parent, grabInfo.GrabberLocalParentLeverageSourceOnGrab) - worldPosition; // Transform to local coordinates grabDirection = Quaternion.Inverse(grabbableObject.transform.GetParentRotation()) * grabDirection; initialGrabDirection = Quaternion.Inverse(grabbableObject.transform.GetParentRotation()) * initialGrabDirection; // When there's a single axis with range of motion, we use additional computations to be able to specify ranges below/above -180/180 degrees Vector3 projectedGrabDirection = Vector3.ProjectOnPlane(grabDirection, grabbableObject.LocalRotationBeforeUpdate * rotationAxis); Vector3 projectedInitialGrabDirection = Vector3.ProjectOnPlane(initialGrabDirection, grabbableObject.LocalRotationBeforeUpdate * rotationAxis); float angle = Vector3.SignedAngle(projectedInitialGrabDirection, projectedGrabDirection, grabbableObject.LocalRotationBeforeUpdate * rotationAxis); float angleDelta = angle - grabInfo.SingleRotationAngleContribution.ToEuler180(); // Keep track of turns below/above -360/360 degrees. if (angleDelta > 180.0f) { grabInfo.SingleRotationAngleContribution -= 360.0f - angleDelta; } else if (angleDelta < -180.0f) { grabInfo.SingleRotationAngleContribution += 360.0f + angleDelta; } else { grabInfo.SingleRotationAngleContribution += angleDelta; } } if (manipulationInfo != null) { // Object is grabbed. // Clamp inside valid range. float unclampedAngle = grabbableObject.SingleRotationAngleCumulative + manipulationInfo.CurrentSingleRotationAngleContributions; float clampedAngle = unclampedAngle.Clamped(grabbableObject.RotationAngleLimitsMin[rotationAxis], grabbableObject.RotationAngleLimitsMax[rotationAxis]); // Rotate using absolute current rotation to preserve precision if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.localRotation = grabbableObject.InitialLocalRotation * Quaternion.AngleAxis(clampedAngle, rotationAxis); } // Subtract clamping equally among contributors float contributionExcess = unclampedAngle - clampedAngle; if (contributionCount > 1) { contributionExcess /= contributionCount; } foreach (RuntimeGrabInfo grabInfo in grabs) { grabInfo.SingleRotationAngleContribution -= contributionExcess; } } else { // Object is not grabbed directly, but through children that have already been constrained. Keep track of rotation. float singleRotationAngleCumulative = grabbableObject.SingleRotationAngleCumulative; if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.localRotation = ClampRotation(grabbableObject, grabbableObject.transform.localRotation, grabbableObject.LocalRotationBeforeUpdate, grabbableObject.InitialLocalRotation, grabbableObject.RotationAngleLimitsMin, grabbableObject.RotationAngleLimitsMax, false, ref singleRotationAngleCumulative); } grabbableObject.SingleRotationAngleCumulative = singleRotationAngleCumulative; } } else { // More than one rotational degree of motion: Compute all grabs and average the result. IEnumerable GetAllLocalRotationContributions() { foreach (RuntimeGrabInfo grabInfo in GetGrabs(grabbableObject)) { // Here we can potentially have up to 3 rotational ranges of motion but we use the hand position around the pivot to rotate the object, so we need to be // extra careful with not losing any information when computing the rotation and clamping. Vector3 grabDirection = grabInfo.Grabber.transform.TransformPoint(grabInfo.GrabberLocalLeverageSource) - worldPosition; Vector3 initialGrabDirection = TransformExt.GetWorldPosition(grabbableObject.transform.parent, grabInfo.GrabberLocalParentLeverageSourceOnGrab) - worldPosition; // First compute the rotation of the grabbed object if it were to be controlled by the hand orientation Quaternion rotUsingHandOrientation = grabInfo.Grabber.transform.rotation * GetGrabPointRelativeGrabRotation(grabInfo.Grabber); // Now compute the rotation of the grabbed object if we were to use the hand position around the axis. But we do not use this directly because we // potentially lose the rotation around the longitudinal axis if there is one. We use it instead to know where the longitudinal axis will point, // and correct rotUsingHandOrientation. Quaternion rotationOnGrab = grabbableObject.transform.GetParentRotation() * grabInfo.LocalRotationOnGrab; Quaternion rotUsingHandPosAroundAxis = Quaternion.FromToRotation(initialGrabDirection.normalized, grabDirection.normalized) * rotationOnGrab; Quaternion rotCorrection = Quaternion.FromToRotation(rotUsingHandOrientation * grabbableObject.RotationLongitudinalAxis, rotUsingHandPosAroundAxis * grabbableObject.RotationLongitudinalAxis); Quaternion localRotation = Quaternion.Inverse(grabbableObject.transform.GetParentRotation()) * rotCorrection * rotUsingHandOrientation; yield return localRotation; } } Quaternion localRotationAverage = QuaternionExt.Average(GetAllLocalRotationContributions(), grabbableObject.transform.localRotation); if (grabbableObject.RotationConstraint == UxrRotationConstraintMode.Free) { if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.localRotation = localRotationAverage; } } else { float singleRotationAngleCumulative = grabbableObject.SingleRotationAngleCumulative; if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.localRotation = ClampRotation(grabbableObject, localRotationAverage, grabbableObject.LocalRotationBeforeUpdate, grabbableObject.InitialLocalRotation, grabbableObject.RotationAngleLimitsMin, grabbableObject.RotationAngleLimitsMax, false, ref singleRotationAngleCumulative); } grabbableObject.SingleRotationAngleCumulative = singleRotationAngleCumulative; } } } /// /// Places the object in a given hand, considering the object snapping parameters. /// /// Grabbable object to place /// Grabber to place the object in /// /// Object is assigned a new position/rotation and constraints are applied only if it doesn't control a grabbable /// parent direction. /// The reason constraints are applied in this case is because it's better to do it in two separate steps while /// snapping to the grabber than in a single step afterwards. More info commented in code inside. /// private void PlaceObjectInHand(UxrGrabbableObject grabbableObject, UxrGrabber grabber) { if (!_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { return; } // Default positioning without any snapping if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.SetPositionAndRotation(grabber.transform.TransformPoint(GetGrabPointRelativeGrabPosition(grabber)), grabber.transform.rotation * GetGrabPointRelativeGrabRotation(grabber)); } // Now process snapping. We compute the required transformations from the current object snapping transform to the grabber. RuntimeGrabInfo grabInfo = manipulationInfo.GetGrabInfo(grabber); UxrGrabPointInfo grabPointInfo = grabbableObject.GetGrabPoint(grabInfo.GrabbedPoint); Vector3 sourcePosition = TransformExt.GetWorldPosition(grabInfo.GrabAlignParentTransformUsed, grabInfo.RelativeGrabAlignPosition); Quaternion sourceRotation = TransformExt.GetWorldRotation(grabInfo.GrabAlignParentTransformUsed, grabInfo.RelativeGrabAlignRotation); Quaternion targetRotation = grabber.transform.rotation; if (grabPointInfo.AlignToController && grabber != null && grabber.Avatar != null && grabber.Avatar.AvatarMode == UxrAvatarMode.Local) { // Align the object to the controller. Useful for weapons or things that need directional precision. // In externally updated avatars (multiplayer, replays...), this doesn't need to be computed because the hand position/rotation is already sampled. sourceRotation = grabPointInfo.AlignToControllerAxes != null ? grabPointInfo.AlignToControllerAxes.rotation : grabbableObject.transform.rotation; UxrController3DModel controller3DModel = grabber.Avatar.ControllerInput.GetController3DModel(grabber.Side); if (controller3DModel != null) { targetRotation = controller3DModel.ForwardTrackingRotation; } } // This is a rare place where constraints might be applied early. // We do it safely when objects do not have any parent dependencies to avoid breaking any manipulation chain. // The reason is constrained objects preferably need to be snapped in steps: // -Snap rotation // -Constrain rotation // -Snap position // -Constrain position // Otherwise you might get unpleasant manipulations when, for example, twisting your hand while grabbing // an object that can't rotate and has constrained translation. If it's performed in a single step the // twisting affects the object position, while doing it in steps removes the unwanted translation. // It can be seen in the example scene when grabbing a constrained battery and rotating the hand. if (grabbableObject.UsesGrabbableParentDependency && grabbableObject.ControlParentDirection) { UxrTransformations snapTransformations = UxrUtils.BuildTransformations(grabbableObject.GetGrabPointSnapModeAffectsPosition(grabInfo.GrabbedPoint, UxrHandSnapDirection.ObjectToHand), grabbableObject.GetGrabPointSnapModeAffectsRotation(grabInfo.GrabbedPoint, UxrHandSnapDirection.ObjectToHand)); Vector3 targetPosition = grabber.transform.position; if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.ApplyAlignment(sourcePosition, sourceRotation, targetPosition, targetRotation, snapTransformations); } } else { if (grabbableObject.GetGrabPointSnapModeAffectsRotation(grabInfo.GrabbedPoint, UxrHandSnapDirection.ObjectToHand)) { if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.ApplyAlignment(sourcePosition, sourceRotation, grabber.transform.position, targetRotation, UxrTransformations.Rotate); } ConstrainTransform(grabbableObject, UxrTransformations.Rotate); } if (grabbableObject.GetGrabPointSnapModeAffectsPosition(grabInfo.GrabbedPoint, UxrHandSnapDirection.ObjectToHand)) { sourcePosition = TransformExt.GetWorldPosition(grabInfo.GrabAlignParentTransformUsed, grabInfo.RelativeGrabAlignPosition); sourceRotation = TransformExt.GetWorldRotation(grabInfo.GrabAlignParentTransformUsed, grabInfo.RelativeGrabAlignRotation); if (Features.HasFlag(UxrManipulationFeatures.ObjectManipulation)) { grabbableObject.transform.ApplyAlignment(sourcePosition, sourceRotation, grabber.transform.position, targetRotation, UxrTransformations.Translate); } } ConstrainTransform(grabbableObject); } } /// /// Rotates an object towards another grabber. /// /// Object to rotate /// Rotation pivot /// Destination grabber /// Interpolation parameter [0.0, 1.0], to control whether to perform the rotation partially or totally private void RotateObjectTowardsGrab(UxrGrabbableObject grabbableObject, Vector3 rotationPivot, UxrGrabber grabber, float t) { Vector3 grabPosition = GetGrabbedPointGrabAlignPosition(grabber); Vector3 currentVectorToSecondHand = grabPosition - rotationPivot; Vector3 desiredVectorToSecondHand = grabber.transform.position - rotationPivot; Vector3 rotationAxis = Vector3.Cross(currentVectorToSecondHand, desiredVectorToSecondHand); grabbableObject.transform.RotateAround(rotationPivot, rotationAxis, Vector3.SignedAngle(currentVectorToSecondHand, desiredVectorToSecondHand, rotationAxis) * t); } /// /// Moves/rotates the object and optionally applies resistance and propagates user-defined constraining events. /// /// The object to move/rotate /// New position or local position, depending on /// New rotation or local rotation, depending on /// Whether the new position/rotation is specified in local or world space /// /// Whether to apply resistance to the new position/rotation using /// and /// /// /// Whether to propagate constraining events ( /// // /// ) /// private void SetPositionAndRotationUsingConstraintsInternal(UxrGrabbableObject grabbableObject, Vector3 position, Quaternion rotation, Space space, bool useResistance, bool propagateEvents) { UxrApplyConstraintsEventArgs constrainEventArgs = new UxrApplyConstraintsEventArgs(grabbableObject); // Store the current position/rotation grabbableObject.LocalPositionBeforeUpdate = grabbableObject.transform.localPosition; grabbableObject.LocalRotationBeforeUpdate = grabbableObject.transform.localRotation; // Pre-event if (propagateEvents) { grabbableObject.RaiseConstraintsApplying(constrainEventArgs); } // Move/Rotate if (space == Space.Self) { grabbableObject.transform.SetLocalPositionAndRotation(position, rotation); } else if (space == Space.World) { grabbableObject.transform.SetPositionAndRotation(position, rotation); } // Apply resistance if (useResistance && Features.HasFlag(UxrManipulationFeatures.ObjectResistance)) { grabbableObject.transform.SetLocalPositionAndRotation(UxrInterpolator.SmoothDampPosition(grabbableObject.LocalPositionBeforeUpdate, grabbableObject.transform.localPosition, grabbableObject.TranslationResistance), UxrInterpolator.SmoothDampRotation(grabbableObject.LocalRotationBeforeUpdate, grabbableObject.transform.localRotation, grabbableObject.RotationResistance)); } // Constrain ConstrainTransform(grabbableObject); // Apply user-defined constraints if (propagateEvents) { grabbableObject.RaiseConstraintsApplied(constrainEventArgs); } // Re-adjust grips to new position/orientation grabbableObject.KeepGripsInPlace(); // Finished event if (propagateEvents) { grabbableObject.RaiseConstraintsFinished(constrainEventArgs); } } /// /// Applies the constraints to a grabbable object's Transform. /// /// The grabbable object to apply the constraints to /// Which constraints to apply private void ConstrainTransform(UxrGrabbableObject grabbableObject, UxrTransformations transformations = UxrTransformations.All) { Vector3 localPosition = grabbableObject.transform.localPosition; Quaternion localRotation = grabbableObject.transform.localRotation; ConstrainTransform(grabbableObject, ref localPosition, ref localRotation, true, transformations); grabbableObject.transform.localPosition = localPosition; grabbableObject.transform.localRotation = localRotation; } /// /// Applies the constraints to a grabbable object's Transform. /// /// The grabbable object to apply the constraints to /// Current object local position, will return constrained object local position /// Current object local rotation, will return constrained object local rotation /// /// Whether to clamp the internal angle that stores the degrees for objects /// constrained to a single rotation axis. Usually this depends on if we want to constrain the actual object transform /// or if we want to compute the constrained transform without applying it. In the second case we don't want to /// modify the internal angle. /// /// Which constraints to apply private void ConstrainTransform(UxrGrabbableObject grabbableObject, ref Vector3 localPosition, ref Quaternion localRotation, bool clampSingleAxisRotation = true, UxrTransformations transformations = UxrTransformations.All) { if (!Features.HasFlag(UxrManipulationFeatures.ObjectConstraints)) { return; } // Rotation if (grabbableObject.IsLockedInPlace) { localRotation = grabbableObject.LocalRotationBeforeUpdate; } else if (transformations.HasFlag(UxrTransformations.Rotate) && grabbableObject.RotationConstraint != UxrRotationConstraintMode.Free) { Quaternion targetLocalRotation = grabbableObject.InitialLocalRotation; if (grabbableObject.RotationConstraint == UxrRotationConstraintMode.RestrictLocalRotation) { float singleRotationAngleCumulative = grabbableObject.SingleRotationAngleCumulative; targetLocalRotation = ClampRotation(grabbableObject, localRotation, grabbableObject.LocalRotationBeforeUpdate, grabbableObject.InitialLocalRotation, grabbableObject.RotationAngleLimitsMin, grabbableObject.RotationAngleLimitsMax, false, ref singleRotationAngleCumulative); if (clampSingleAxisRotation) { grabbableObject.SingleRotationAngleCumulative = singleRotationAngleCumulative; } } localRotation = targetLocalRotation; } // Translation if (grabbableObject.IsLockedInPlace) { localPosition = grabbableObject.LocalPositionBeforeUpdate; } else if (transformations.HasFlag(UxrTransformations.Translate) && grabbableObject.TranslationConstraint != UxrTranslationConstraintMode.Free) { Vector3 targetLocalPos = grabbableObject.InitialLocalPosition; if (grabbableObject.TranslationConstraint == UxrTranslationConstraintMode.RestrictToBox && grabbableObject.RestrictToBox != null) { Vector3 clampedWorldPos = grabbableObject.transform.GetParentWorldMatrix().MultiplyPoint(localPosition).ClampToBox(grabbableObject.RestrictToBox); targetLocalPos = grabbableObject.transform.GetParentWorldMatrix().inverse.MultiplyPoint(clampedWorldPos); } else if (grabbableObject.TranslationConstraint == UxrTranslationConstraintMode.RestrictToSphere && grabbableObject.RestrictToSphere != null) { Vector3 clampedWorldPos = grabbableObject.transform.GetParentWorldMatrix().MultiplyPoint(localPosition).ClampToSphere(grabbableObject.RestrictToSphere); targetLocalPos = grabbableObject.transform.GetParentWorldMatrix().inverse.MultiplyPoint(clampedWorldPos); } else if (grabbableObject.TranslationConstraint == UxrTranslationConstraintMode.RestrictLocalOffset) { if (grabbableObject.transform.parent != null || grabbableObject.InitialParent == null) { // Current local space -> Initial local space using the matrix at Awake() Vector3 localPosOffset = grabbableObject.InitialRelativeMatrix.inverse.MultiplyPoint3x4(localPosition); // Clamp in initial local space, transform to current local space targetLocalPos = grabbableObject.InitialRelativeMatrix.MultiplyPoint(localPosOffset.Clamp(grabbableObject.TranslationLimitsMin, grabbableObject.TranslationLimitsMax)); } else { targetLocalPos = localPosition; } } localPosition = targetLocalPos; } } /// /// Tries to clamp a rotation for a . /// /// GrabbableObject whose rotation to clamp /// Rotation to clamp /// Rotation before the manipulation update this frame /// Initial rotation /// Minimum euler values /// Maximum euler values /// Whether to invert the rotation angles /// /// The rotation angle if rotation is being constrained to a single axis. This improves /// constraining by allowing ranges over +-360 degrees. /// /// Clamped rotation private Quaternion ClampRotation(UxrGrabbableObject grabbableObject, Quaternion rot, Quaternion rotBeforeUpdate, Quaternion initialRot, Vector3 eulerMin, Vector3 eulerMax, bool invertRotation, ref float singleRotationAngle) { if (!Features.HasFlag(UxrManipulationFeatures.ObjectConstraints)) { return rot; } int rangeOfMotionAxisCount = grabbableObject.RangeOfMotionRotationAxisCount; if (grabbableObject.RangeOfMotionRotationAxisCount == 0) { return initialRot; } if (rangeOfMotionAxisCount > 1) { bool invertPitchYaw = false; int ignorePitchYaw = -1; bool clampLongitudinal = false; // Use classic yaw/pitch clamping when more than one axis has constrained range of motion. UxrAxis axis1 = grabbableObject.RangeOfMotionRotationAxes.First(); UxrAxis axis2 = grabbableObject.RangeOfMotionRotationAxes.Last(); UxrAxis longitudinalAxis = UxrAxis.OtherThan(axis1, axis2); if (rangeOfMotionAxisCount == 3) { // Pitch/yaw clamping will be on the other-than-longitudinal axes, when all 3 axes have constrained range of motion. axis1 = grabbableObject.RangeOfMotionRotationAxes.First(a => a != grabbableObject.RotationLongitudinalAxis); axis2 = grabbableObject.RangeOfMotionRotationAxes.Last(a => a != grabbableObject.RotationLongitudinalAxis); longitudinalAxis = grabbableObject.RotationLongitudinalAxis; clampLongitudinal = true; } else { // If there are only two rotation axes constrained, check if one of the constrained axes is actually the longitudinal axis. // In this case, we will zero either the pitch or yaw and perform longitudinal clamping later. if (!longitudinalAxis.Equals(grabbableObject.RotationLongitudinalAxis)) { // Ignore the incorrectly computed longitudinal axis, which in reality is either the pitch or yaw ignorePitchYaw = longitudinalAxis; // Assign the longitudinal axis correctly based on what the user selected longitudinalAxis = grabbableObject.RotationLongitudinalAxis; if (axis1 == longitudinalAxis) { axis1 = UxrAxis.OtherThan(longitudinalAxis, axis2); } else if (axis2 == longitudinalAxis) { axis2 = UxrAxis.OtherThan(longitudinalAxis, axis1); } // Clamp the rotation around longitudinal axis later clampLongitudinal = true; // Check need to invert invertPitchYaw = UxrAxis.OtherThan(ignorePitchYaw, longitudinalAxis) == UxrAxis.Y; } } Quaternion relativeRot = Quaternion.Inverse(initialRot) * rot; Vector3 targetDirection = relativeRot * longitudinalAxis; float pitch = -Mathf.Asin(targetDirection[axis2]) * Mathf.Rad2Deg; float yaw = Mathf.Atan2(targetDirection[axis1], targetDirection[longitudinalAxis]) * Mathf.Rad2Deg; pitch = longitudinalAxis == UxrAxis.Y ? Mathf.Clamp(pitch, -eulerMax[axis1], -eulerMin[axis1]) : Mathf.Clamp(pitch, eulerMin[axis1], eulerMax[axis1]); yaw = longitudinalAxis == UxrAxis.Y ? Mathf.Clamp(yaw, -eulerMax[axis2], -eulerMin[axis2]) : Mathf.Clamp(yaw, eulerMin[axis2], eulerMax[axis2]); // Detect cases where we need to invert angles due to math if (invertPitchYaw || longitudinalAxis == UxrAxis.Y) { pitch = -pitch; yaw = -yaw; } // Now invert if it was requested if (invertRotation) { pitch = -pitch; yaw = -yaw; } // Create clamped rotation using pitch/yaw Vector3 clampedEuler = Vector3.zero; clampedEuler[axis1] = pitch; clampedEuler[axis2] = yaw; if (ignorePitchYaw != -1) { clampedEuler[ignorePitchYaw] = 0.0f; } Quaternion clampedRot = Quaternion.Euler(clampedEuler); // Clamp rotation around the longitudinal axis if necessary if (clampLongitudinal) { Vector3 fixedLongitudinal = clampedRot * longitudinalAxis; Quaternion correctionRot = Quaternion.FromToRotation(targetDirection, fixedLongitudinal); Quaternion withRoll = correctionRot * relativeRot; float roll = Vector3.SignedAngle(clampedRot * axis1, withRoll * axis1, fixedLongitudinal) * (invertRotation ? -1.0f : 1.0f); float clampedRoll = roll.Clamped(eulerMin[longitudinalAxis], eulerMax[longitudinalAxis]); return initialRot * Quaternion.AngleAxis(clampedRoll, fixedLongitudinal) * clampedRot; } return initialRot * clampedRot; } // At this point we have a rotation constrained to a single axis. We will allow limits beyond +- 360 degrees by keeping track of the rotation angle. // Get a perpendicular vector to the rotation axis, compute the projection on the rotation plane and then get the angle increment. int singleAxisIndex = grabbableObject.SingleRotationAxisIndex; Vector3 rotationAxis = (UxrAxis)singleAxisIndex; Vector3 perpendicularAxis = ((UxrAxis)singleAxisIndex).Perpendicular; Vector3 initialPerpendicularVector = initialRot * perpendicularAxis; Vector3 currentPerpendicularVector = Vector3.ProjectOnPlane(rot * perpendicularAxis, initialRot * rotationAxis); float angle = Vector3.SignedAngle(initialPerpendicularVector, currentPerpendicularVector, initialRot * rotationAxis) * (invertRotation ? -1.0f : 1.0f); float angleDelta = angle - singleRotationAngle.ToEuler180(); // Keep track of turns below/above -360/360 degrees. if (angleDelta > 180.0f) { singleRotationAngle -= 360.0f - angleDelta; } else if (angleDelta < -180.0f) { singleRotationAngle += 360.0f + angleDelta; } else { singleRotationAngle += angleDelta; } // Clamp inside valid range singleRotationAngle.Clamp(grabbableObject.RotationAngleLimitsMin[singleAxisIndex], grabbableObject.RotationAngleLimitsMax[singleAxisIndex]); return initialRot * Quaternion.AngleAxis(singleRotationAngle, rotationAxis); } /// /// Gets the current rotation angle, in objects constrained to a single rotation axis, contributed by all the grabbers /// manipulating the object. /// /// The object to get the information from private float GetCurrentSingleRotationAngleContributions(UxrGrabbableObject grabbableObject) { if (_currentManipulations.TryGetValue(grabbableObject, out RuntimeManipulationInfo manipulationInfo)) { return manipulationInfo.CurrentSingleRotationAngleContributions; } return 0.0f; } /// /// If an object is constrained, sometimes the hand that grabs it can be at a different distance than the real hand. /// For example, if an object whose position is constrained is grabbed and the hand is pulled far away, there is a /// point where the real hand might be far enough to the hand that is grabbing the grip is automatically released. /// This method does the check. Only the local avatar is processed to keep consistency, the releases will be /// be requested from each client. /// /// Grabber to check /// Whether the grip should be released private bool ShouldGrabberReleaseFarAwayGrip(UxrGrabber grabber) { if (grabber.Avatar == UxrAvatar.LocalAvatar && grabber != null && grabber.GrabbedObject != null) { // Check if the user separated the hands too much to drop it from the hand that went too far if (Vector3.Distance(grabber.UnprocessedGrabberPosition, grabber.transform.position) > grabber.GrabbedObject.LockedGrabReleaseDistance) { return true; } } return false; } /// /// Updates the smooth grabbable object position/rotation transitions due to manipulation. /// private void UpdateSmoothObjectTransitions() { foreach (UxrGrabbableObject grabbableObject in UxrGrabbableObject.EnabledComponents) { grabbableObject.UpdateSmoothManipulationTransition(Time.unscaledDeltaTime); grabbableObject.UpdateSmoothAnchorPlacement(Time.unscaledDeltaTime); grabbableObject.UpdateSmoothConstrainTransition(Time.unscaledDeltaTime); } } #endregion } }