// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Experimental.Physics; using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Physics; using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Serialization; namespace Microsoft.MixedReality.Toolkit.UI { /// /// This script allows for an object to be movable, scalable, and rotatable with one or two hands. /// You may also configure the script on only enable certain manipulations. The script works with /// both HoloLens' gesture input and immersive headset's motion controller input. /// [HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/ux-building-blocks/object-manipulator")] [RequireComponent(typeof(ConstraintManager))] public class ObjectManipulator : MonoBehaviour, IMixedRealityPointerHandler, IMixedRealityFocusChangedHandler, IMixedRealitySourcePoseHandler { #region Public Enums /// /// Describes what pivot the manipulated object will rotate about when /// you rotate your hand. This is not a description of any limits or /// additional rotation logic. If no other factors (such as constraints) /// are involved, rotating your hand by an amount should rotate the object /// by the same amount. /// For example a possible future value here is RotateAboutUserDefinedPoint /// where the user could specify a pivot that the object is to rotate /// around. /// An example of a value that should not be found here is MaintainRotationToUser /// as this restricts rotation of the object when we rotate the hand. /// public enum RotateInOneHandType { RotateAboutObjectCenter, RotateAboutGrabPoint } [Flags] public enum ReleaseBehaviorType { KeepVelocity = 1 << 0, KeepAngularVelocity = 1 << 1 } #endregion Public Enums #region Serialized Fields [SerializeField] [Tooltip("Transform that will be dragged. Defaults to the object of the component.")] private Transform hostTransform = null; /// /// Transform that will be dragged. Defaults to the object of the component. /// public Transform HostTransform { get { if (hostTransform == null) { hostTransform = gameObject.transform; } return hostTransform; } set => hostTransform = value; } [SerializeField] [EnumFlags] [Tooltip("Can manipulation be done only with one hand, only with two hands, or with both?")] private ManipulationHandFlags manipulationType = ManipulationHandFlags.OneHanded | ManipulationHandFlags.TwoHanded; /// /// Can manipulation be done only with one hand, only with two hands, or with both? /// public ManipulationHandFlags ManipulationType { get => manipulationType; set => manipulationType = value; } [SerializeField] [EnumFlags] [Tooltip("What manipulation will two hands perform?")] private TransformFlags twoHandedManipulationType = TransformFlags.Move | TransformFlags.Rotate | TransformFlags.Scale; /// /// What manipulation will two hands perform? /// public TransformFlags TwoHandedManipulationType { get => twoHandedManipulationType; set => twoHandedManipulationType = value; } [SerializeField] [Tooltip("Specifies whether manipulation can be done using far interaction with pointers.")] private bool allowFarManipulation = true; /// /// Specifies whether manipulation can be done using far interaction with pointers. /// public bool AllowFarManipulation { get => allowFarManipulation; set => allowFarManipulation = value; } [SerializeField] [Tooltip( "Whether physics forces are used to move the object when performing near manipulations. " + "Off will make the object feel more directly connected to the hand. On will honor the mass and inertia of the object. " + "The default is off.")] private bool useForcesForNearManipulation = false; /// /// Whether physics forces are used to move the object when performing near manipulations. /// /// /// Setting this to false will make the object feel more directly connected to the /// users hand. Setting this to true will honor the mass and inertia of the object, /// but may feel as though the object is connected through a spring. The default is false. /// public bool UseForcesForNearManipulation { get => useForcesForNearManipulation; set => useForcesForNearManipulation = value; } [SerializeField] [Tooltip("Rotation behavior of object when using one hand near")] private RotateInOneHandType oneHandRotationModeNear = RotateInOneHandType.RotateAboutGrabPoint; /// /// Rotation behavior of object when using one hand near /// public RotateInOneHandType OneHandRotationModeNear { get => oneHandRotationModeNear; set => oneHandRotationModeNear = value; } [SerializeField] [Tooltip("Rotation behavior of object when using one hand at distance")] private RotateInOneHandType oneHandRotationModeFar = RotateInOneHandType.RotateAboutGrabPoint; /// /// Rotation behavior of object when using one hand at distance /// public RotateInOneHandType OneHandRotationModeFar { get => oneHandRotationModeFar; set => oneHandRotationModeFar = value; } [SerializeField] [EnumFlags] [Tooltip("Rigid body behavior of the dragged object when releasing it.")] private ReleaseBehaviorType releaseBehavior = ReleaseBehaviorType.KeepVelocity | ReleaseBehaviorType.KeepAngularVelocity; /// /// Rigid body behavior of the dragged object when releasing it. /// public ReleaseBehaviorType ReleaseBehavior { get => releaseBehavior; set => releaseBehavior = value; } /// /// Obsolete: Whether to enable frame-rate independent smoothing. /// [Obsolete("SmoothingActive is obsolete and will be removed in a future version. Applications should use SmoothingFar, SmoothingNear or a combination of the two.")] public bool SmoothingActive { get => smoothingFar; set => smoothingFar = value; } [SerializeField] [Tooltip("The concrete type of TransformSmoothingLogic to use for smoothing between transforms.")] [Implements(typeof(ITransformSmoothingLogic), TypeGrouping.ByNamespaceFlat)] private SystemType transformSmoothingLogicType = typeof(DefaultTransformSmoothingLogic); [FormerlySerializedAs("smoothingActive")] [SerializeField] [Tooltip("Frame-rate independent smoothing for far interactions. Far smoothing is enabled by default.")] private bool smoothingFar = true; /// /// Whether to enable frame-rate independent smoothing for far interactions. /// /// /// Far smoothing is enabled by default. /// public bool SmoothingFar { get => smoothingFar; set => smoothingFar = value; } [SerializeField] [Tooltip("Frame-rate independent smoothing for near interactions. Note that enabling near smoothing may be perceived as being 'disconnected' from the hand.")] private bool smoothingNear = true; /// /// Whether to enable frame-rate independent smoothing for near interactions. /// /// /// Note that enabling near smoothing may be perceived as being 'disconnected' from the hand. /// public bool SmoothingNear { get => smoothingNear; set => smoothingNear = value; } [SerializeField] [Range(0, 1)] [Tooltip("Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value.")] private float moveLerpTime = 0.001f; /// /// Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float MoveLerpTime { get => moveLerpTime; set => moveLerpTime = value; } [SerializeField] [Range(0, 1)] [Tooltip("Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.")] private float rotateLerpTime = 0.001f; /// /// Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float RotateLerpTime { get => rotateLerpTime; set => rotateLerpTime = value; } [SerializeField] [Range(0, 1)] [Tooltip("Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.")] private float scaleLerpTime = 0.001f; /// /// Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float ScaleLerpTime { get => scaleLerpTime; set => scaleLerpTime = value; } [SerializeField] [Tooltip("Enable or disable constraint support of this component. When enabled transform " + "changes will be post processed by the linked constraint manager.")] private bool enableConstraints = true; /// /// Enable or disable constraint support of this component. When enabled, transform /// changes will be post processed by the linked constraint manager. /// public bool EnableConstraints { get => enableConstraints; set => enableConstraints = value; } [SerializeField] [Tooltip("Constraint manager slot to enable constraints when manipulating the object.")] private ConstraintManager constraintsManager; /// /// Constraint manager slot to enable constraints when manipulating the object. /// public ConstraintManager ConstraintsManager { get => constraintsManager; set => constraintsManager = value; } [SerializeField] [Tooltip("Elastics Manager slot to enable elastics simulation when manipulating the object.")] private ElasticsManager elasticsManager; /// /// Elastics Manager slot to enable elastics simulation when manipulating the object. /// public ElasticsManager ElasticsManager { get => elasticsManager; set => elasticsManager = value; } #endregion Serialized Fields #region Event handlers [Header("Manipulation Events")] [SerializeField] [FormerlySerializedAs("OnManipulationStarted")] private ManipulationEvent onManipulationStarted = new ManipulationEvent(); /// /// Unity event raised on manipulation started /// public ManipulationEvent OnManipulationStarted { get => onManipulationStarted; set => onManipulationStarted = value; } [SerializeField] [FormerlySerializedAs("OnManipulationEnded")] private ManipulationEvent onManipulationEnded = new ManipulationEvent(); /// /// Unity event raised on manipulation ended /// public ManipulationEvent OnManipulationEnded { get => onManipulationEnded; set => onManipulationEnded = value; } [SerializeField] [FormerlySerializedAs("OnHoverEntered")] private ManipulationEvent onHoverEntered = new ManipulationEvent(); /// /// Unity event raised on hover started /// public ManipulationEvent OnHoverEntered { get => onHoverEntered; set => onHoverEntered = value; } [SerializeField] [FormerlySerializedAs("OnHoverExited")] private ManipulationEvent onHoverExited = new ManipulationEvent(); /// /// Unity event raised on hover ended /// public ManipulationEvent OnHoverExited { get => onHoverExited; set => onHoverExited = value; } #endregion Event Handlers #region Private Properties private ManipulationMoveLogic moveLogic; private TwoHandScaleLogic scaleLogic; private TwoHandRotateLogic rotateLogic; private ITransformSmoothingLogic smoothingLogic; /// /// Holds the pointer and the initial intersection point of the pointer ray /// with the object on pointer down in pointer space /// private readonly struct PointerData { public PointerData(IMixedRealityPointer pointer, Vector3 worldGrabPoint) : this() { initialGrabPointInPointer = Quaternion.Inverse(pointer.Rotation) * (worldGrabPoint - pointer.Position); Pointer = pointer; IsNearPointer = pointer is IMixedRealityNearPointer; } private readonly Vector3 initialGrabPointInPointer; public IMixedRealityPointer Pointer { get; } public bool IsNearPointer { get; } /// /// Returns the grab point on the manipulated object in world space. /// public Vector3 GrabPoint => (Pointer.Rotation * initialGrabPointInPointer) + Pointer.Position; } private List pointerDataList = new List(); private Quaternion objectToGripRotation; private bool isNearManipulation; private bool isManipulationStarted; private bool isSmoothing; private Rigidbody rigidBody; private bool wasGravity = false; private bool wasKinematic = false; private bool IsOneHandedManipulationEnabled => manipulationType.IsMaskSet(ManipulationHandFlags.OneHanded) && pointerDataList.Count == 1; private bool IsTwoHandedManipulationEnabled => manipulationType.IsMaskSet(ManipulationHandFlags.TwoHanded) && pointerDataList.Count > 1; private Quaternion leftHandRotation; private Quaternion rightHandRotation; #endregion Private Properties #region MonoBehaviour Functions private void Awake() { moveLogic = new ManipulationMoveLogic(); rotateLogic = new TwoHandRotateLogic(); scaleLogic = new TwoHandScaleLogic(); smoothingLogic = Activator.CreateInstance(transformSmoothingLogicType) as ITransformSmoothingLogic; if (elasticsManager) { elasticsManager.InitializeElastics(HostTransform); } } protected virtual void Start() { rigidBody = HostTransform.GetComponent(); if (constraintsManager == null && EnableConstraints) { constraintsManager = gameObject.EnsureComponent(); } // Get child objects with NearInteractionGrabbable attached var children = GetComponentsInChildren(); if (children.Length == 0) { Debug.Log($"Near interactions are not enabled for {gameObject.name}. To enable near interactions, add a " + $"{nameof(NearInteractionGrabbable)} component to {gameObject.name} or to a child object of {gameObject.name} that contains a collider."); } } #endregion #region Private Methods /// /// Calculates the unweighted average, or centroid, of all pointers' /// grab points, as defined by the PointerData.GrabPoint property. /// Does not use the rotation of each pointer; represents a pure /// geometric centroid of the grab points in world space. /// /// /// Worldspace grab point centroid of all pointers /// in pointerIdToPointerMap. /// private Vector3 GetPointersGrabPoint() { Vector3 sum = Vector3.zero; int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { PointerData pointerData = pointerDataList[i]; sum += pointerData.GrabPoint; } return sum / Math.Max(1, pointerDataList.Count); } /// /// Calculates the multiple-handed pointer pose, used for /// far-interaction hand-ray-based manipulations. Uses the /// unweighted vector average of the pointers' forward vectors /// to calculate a compound pose that takes into account the /// pointing direction of each pointer. /// /// /// Compound pose calculated as the average of the poses /// corresponding to all of the pointers in pointerIdToPointerMap. /// private MixedRealityPose GetPointersPose() { Vector3 sumPos = Vector3.zero; Vector3 sumDir = Vector3.zero; int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { PointerData pointerData = pointerDataList[i]; sumPos += pointerData.Pointer.Position; sumDir += pointerData.Pointer.Rotation * Vector3.forward; } int divisor = Math.Max(1, pointerDataList.Count); return new MixedRealityPose { Position = sumPos / divisor, Rotation = Quaternion.LookRotation(sumDir / divisor) }; } private Vector3 GetPointersVelocity() { Vector3 sum = Vector3.zero; int numControllers = 0; int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { IMixedRealityController controller = pointerDataList[i].Pointer.Controller; // Check pointer has a valid controller (e.g. gaze pointer doesn't) if (controller != null) { numControllers++; sum += controller.Velocity; } } return sum / Math.Max(1, numControllers); } private Vector3 GetPointersAngularVelocity() { Vector3 sum = Vector3.zero; int numControllers = 0; int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { IMixedRealityController controller = pointerDataList[i].Pointer.Controller; // Check pointer has a valid controller (e.g. gaze pointer doesn't) if (controller != null) { numControllers++; sum += controller.AngularVelocity; } } return sum / Math.Max(1, numControllers); } private bool IsNearManipulation() { int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { if (pointerDataList[i].IsNearPointer) { return true; } } return false; } #endregion Private Methods #region Public Methods /// /// Releases the object that is currently manipulated /// public void ForceEndManipulation() { // end manipulation if (isManipulationStarted) { HandleManipulationEnded(GetPointersGrabPoint(), GetPointersVelocity(), GetPointersAngularVelocity()); } pointerDataList.Clear(); } /// /// Gets the grab point for the given pointer id. /// Only use if you know that your given pointer id corresponds to a pointer that has grabbed /// this component. /// public Vector3 GetPointerGrabPoint(uint pointerId) { if (TryGetPointerDataWithId(pointerId, out PointerData pointerData)) { return pointerData.GrabPoint; } return Vector3.zero; } #endregion Public Methods #region Hand Event Handlers /// public virtual void OnPointerDown(MixedRealityPointerEventData eventData) { if (eventData.used || eventData.Pointer == null || eventData.Pointer.Result == null || (!allowFarManipulation && eventData.Pointer as IMixedRealityNearPointer == null)) { return; } // If we only allow one handed manipulations, check there is no hand interacting yet. if (manipulationType != ManipulationHandFlags.OneHanded || pointerDataList.Count == 0) { if (!TryGetPointerDataWithId(eventData.Pointer.PointerId, out _)) { pointerDataList.Add(new PointerData(eventData.Pointer, eventData.Pointer.Result.Details.Point)); // Re-initialize elastic systems. if (elasticsManager) { elasticsManager.InitializeElastics(HostTransform); } // Call manipulation started handlers if (IsTwoHandedManipulationEnabled) { if (!isManipulationStarted) { HandleManipulationStarted(); } HandleTwoHandManipulationStarted(); } else if (IsOneHandedManipulationEnabled) { if (!isManipulationStarted) { HandleManipulationStarted(); } HandleOneHandMoveStarted(); } } } if (pointerDataList.Count > 0) { // Always mark the pointer data as used to prevent any other behavior to handle pointer events // as long as the ObjectManipulator is active. // This is due to us reacting to both "Select" and "Grip" events. eventData.Use(); } } bool hasFirstPointerDraggedThisFrame = false; public virtual void OnPointerDragged(MixedRealityPointerEventData eventData) { // Call manipulation updated handlers if (IsOneHandedManipulationEnabled) { HandleOneHandMoveUpdated(); } else if (IsTwoHandedManipulationEnabled) { if (hasFirstPointerDraggedThisFrame) { HandleTwoHandManipulationUpdated(); hasFirstPointerDraggedThisFrame = false; } else { hasFirstPointerDraggedThisFrame = true; } } } /// public virtual void OnPointerUp(MixedRealityPointerEventData eventData) { // Get pointer data before they are removed from the map Vector3 grabPoint = GetPointersGrabPoint(); Vector3 velocity = GetPointersVelocity(); Vector3 angularVelocity = GetPointersAngularVelocity(); if (TryGetPointerDataWithId(eventData.Pointer.PointerId, out PointerData pointerDataToRemove)) { pointerDataList.Remove(pointerDataToRemove); } // Call manipulation ended handlers if (manipulationType.IsMaskSet(ManipulationHandFlags.TwoHanded) && pointerDataList.Count == 1) { if (manipulationType.IsMaskSet(ManipulationHandFlags.OneHanded)) { HandleOneHandMoveStarted(); hasFirstPointerDraggedThisFrame = false; } else { HandleManipulationEnded(grabPoint, velocity, angularVelocity); } } else if (isManipulationStarted && pointerDataList.Count == 0) { HandleManipulationEnded(grabPoint, velocity, angularVelocity); } eventData.Use(); } #endregion Hand Event Handlers #region Private Event Handlers private void HandleTwoHandManipulationStarted() { Vector3[] handPositionArray = GetHandPositionArray(); if (twoHandedManipulationType.IsMaskSet(TransformFlags.Rotate)) { rotateLogic.Setup(handPositionArray, HostTransform); } if (twoHandedManipulationType.IsMaskSet(TransformFlags.Move)) { // If near manipulation, a pure grab point centroid is used for // the initial pointer pose; if far manipulation, a more complex // look-rotation-based pointer pose is used. MixedRealityPose pointerPose = IsNearManipulation() ? new MixedRealityPose(GetPointersGrabPoint()) : GetPointersPose(); MixedRealityPose hostPose = new MixedRealityPose(HostTransform.position, HostTransform.rotation); moveLogic.Setup(pointerPose, GetPointersGrabPoint(), hostPose, HostTransform.localScale); } if (twoHandedManipulationType.IsMaskSet(TransformFlags.Scale)) { scaleLogic.Setup(handPositionArray, HostTransform); } } private void HandleTwoHandManipulationUpdated() { var targetTransform = new MixedRealityTransform(HostTransform.position, HostTransform.rotation, HostTransform.localScale); Vector3[] handPositionArray = GetHandPositionArray(); if (twoHandedManipulationType.IsMaskSet(TransformFlags.Scale)) { targetTransform.Scale = scaleLogic.UpdateMap(handPositionArray); if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyScaleConstraints(ref targetTransform, false, IsNearManipulation()); } } if (twoHandedManipulationType.IsMaskSet(TransformFlags.Rotate)) { targetTransform.Rotation = rotateLogic.Update(handPositionArray, targetTransform.Rotation); if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyRotationConstraints(ref targetTransform, false, IsNearManipulation()); } } if (twoHandedManipulationType.IsMaskSet(TransformFlags.Move)) { // If near manipulation, a pure GrabPoint centroid is used for // the pointer pose; if far manipulation, a more complex // look-rotation-based pointer pose is used. MixedRealityPose pose = IsNearManipulation() ? new MixedRealityPose(GetPointersGrabPoint()) : GetPointersPose(); targetTransform.Position = moveLogic.UpdateTransform(pose, targetTransform, true, IsNearManipulation()); if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyTranslationConstraints(ref targetTransform, false, IsNearManipulation()); } } ApplyTargetTransform(targetTransform); } private void HandleOneHandMoveStarted() { Assert.IsTrue(pointerDataList.Count == 1); PointerData pointerData = pointerDataList[0]; IMixedRealityPointer pointer = pointerData.Pointer; // Calculate relative transform from object to grip. TryGetGripRotation(pointer, out Quaternion gripRotation); Quaternion worldToGripRotation = Quaternion.Inverse(gripRotation); objectToGripRotation = worldToGripRotation * HostTransform.rotation; MixedRealityPose pointerPose = new MixedRealityPose(pointer.Position, pointer.Rotation); MixedRealityPose hostPose = new MixedRealityPose(HostTransform.position, HostTransform.rotation); moveLogic.Setup(pointerPose, pointerData.GrabPoint, hostPose, HostTransform.localScale); } private void HandleOneHandMoveUpdated() { Debug.Assert(pointerDataList.Count == 1); IMixedRealityPointer pointer = pointerDataList[0].Pointer; var targetTransform = new MixedRealityTransform(HostTransform.position, HostTransform.rotation, HostTransform.localScale); if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyScaleConstraints(ref targetTransform, true, IsNearManipulation()); } TryGetGripRotation(pointer, out Quaternion gripRotation); targetTransform.Rotation = gripRotation * objectToGripRotation; if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyRotationConstraints(ref targetTransform, true, IsNearManipulation()); } RotateInOneHandType rotateInOneHandType = isNearManipulation ? oneHandRotationModeNear : oneHandRotationModeFar; MixedRealityPose pointerPose = new MixedRealityPose(pointer.Position, pointer.Rotation); targetTransform.Position = moveLogic.UpdateTransform(pointerPose, targetTransform, rotateInOneHandType != RotateInOneHandType.RotateAboutObjectCenter, IsNearManipulation()); if (EnableConstraints && constraintsManager != null) { constraintsManager.ApplyTranslationConstraints(ref targetTransform, true, IsNearManipulation()); } ApplyTargetTransform(targetTransform); } private void HandleManipulationStarted() { isManipulationStarted = true; isNearManipulation = IsNearManipulation(); isSmoothing = (isNearManipulation ? smoothingNear : smoothingFar); // TODO: If we are on HoloLens 1, push and pop modal input handler so that we can use old // gaze/gesture/voice manipulation. For HoloLens 2, we don't want to do this. if (OnManipulationStarted != null) { OnManipulationStarted.Invoke(new ManipulationEventData { ManipulationSource = gameObject, IsNearInteraction = isNearManipulation, Pointer = pointerDataList[0].Pointer, PointerCentroid = GetPointersGrabPoint(), PointerVelocity = GetPointersVelocity(), PointerAngularVelocity = GetPointersAngularVelocity() }); } if (rigidBody != null) { wasGravity = rigidBody.useGravity; wasKinematic = rigidBody.isKinematic; rigidBody.useGravity = false; rigidBody.isKinematic = false; } if (EnableConstraints && constraintsManager != null) { constraintsManager.Initialize(new MixedRealityTransform(HostTransform)); } if (elasticsManager != null) { elasticsManager.EnableElasticsUpdate = false; } } private void HandleManipulationEnded(Vector3 pointerGrabPoint, Vector3 pointerVelocity, Vector3 pointerAnglularVelocity) { isManipulationStarted = false; hasFirstPointerDraggedThisFrame = false; // TODO: If we are on HoloLens 1, push and pop modal input handler so that we can use old // gaze/gesture/voice manipulation. For HoloLens 2, we don't want to do this. if (OnManipulationEnded != null) { OnManipulationEnded.Invoke(new ManipulationEventData { ManipulationSource = gameObject, IsNearInteraction = isNearManipulation, PointerCentroid = pointerGrabPoint, PointerVelocity = pointerVelocity, PointerAngularVelocity = pointerAnglularVelocity }); } ReleaseRigidBody(pointerVelocity, pointerAnglularVelocity); if (elasticsManager != null) { elasticsManager.EnableElasticsUpdate = true; } } #endregion Private Event Handlers #region Unused Event Handlers /// public virtual void OnPointerClicked(MixedRealityPointerEventData eventData) { } /// public void OnBeforeFocusChange(FocusEventData eventData) { } #endregion Unused Event Handlers #region Private methods private bool TryGetPointerDataWithId(uint id, out PointerData pointerData) { int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { PointerData data = pointerDataList[i]; if (data.Pointer.PointerId == id) { pointerData = data; return true; } } pointerData = default(PointerData); return false; } private void ApplyTargetTransform(MixedRealityTransform targetTransform) { bool applySmoothing = isSmoothing && smoothingLogic != null; if (rigidBody == null) { TransformFlags transformUpdated = 0; if (elasticsManager != null) { transformUpdated = elasticsManager.ApplyTargetTransform(targetTransform); } if (!transformUpdated.IsMaskSet(TransformFlags.Move)) { HostTransform.position = applySmoothing ? smoothingLogic.SmoothPosition(HostTransform.position, targetTransform.Position, moveLerpTime, Time.deltaTime) : targetTransform.Position; } if (!transformUpdated.IsMaskSet(TransformFlags.Rotate)) { HostTransform.rotation = applySmoothing ? smoothingLogic.SmoothRotation(HostTransform.rotation, targetTransform.Rotation, rotateLerpTime, Time.deltaTime) : targetTransform.Rotation; } if (!transformUpdated.IsMaskSet(TransformFlags.Scale)) { HostTransform.localScale = applySmoothing ? smoothingLogic.SmoothScale(HostTransform.localScale, targetTransform.Scale, scaleLerpTime, Time.deltaTime) : targetTransform.Scale; } } else { // There is a RigidBody. Potential different paths for near vs far manipulation if (isNearManipulation && !useForcesForNearManipulation) { // This is a near manipulation and we're not using forces // Apply direct updates but account for smoothing if (applySmoothing) { rigidBody.MovePosition(smoothingLogic.SmoothPosition(rigidBody.position, targetTransform.Position, moveLerpTime, Time.deltaTime)); rigidBody.MoveRotation(smoothingLogic.SmoothRotation(rigidBody.rotation, targetTransform.Rotation, rotateLerpTime, Time.deltaTime)); } else { rigidBody.MovePosition(targetTransform.Position); rigidBody.MoveRotation(targetTransform.Rotation); } } else { // We are using forces rigidBody.velocity = ((1f - Mathf.Pow(moveLerpTime, Time.deltaTime)) / Time.deltaTime) * (targetTransform.Position - HostTransform.position); var relativeRotation = targetTransform.Rotation * Quaternion.Inverse(HostTransform.rotation); relativeRotation.ToAngleAxis(out float angle, out Vector3 axis); if (axis.IsValidVector()) { if (angle > 180f) { angle -= 360f; } rigidBody.angularVelocity = ((1f - Mathf.Pow(rotateLerpTime, Time.deltaTime)) / Time.deltaTime) * (axis.normalized * angle * Mathf.Deg2Rad); } } HostTransform.localScale = applySmoothing ? smoothingLogic.SmoothScale(HostTransform.localScale, targetTransform.Scale, scaleLerpTime, Time.deltaTime) : targetTransform.Scale; } } private Vector3[] handPositionMap = null; private Vector3[] GetHandPositionArray() { if (handPositionMap?.Length != pointerDataList.Count) { handPositionMap = new Vector3[pointerDataList.Count]; } uint index = 0; int pointerDataListCount = pointerDataList.Count; for (int i = 0; i < pointerDataListCount; i++) { handPositionMap[index++] = pointerDataList[i].Pointer.Position; } return handPositionMap; } public void OnFocusChanged(FocusEventData eventData) { bool isFar = !(eventData.Pointer is IMixedRealityNearPointer); if (isFar && !AllowFarManipulation) { return; } if (eventData.OldFocusedObject == null || !eventData.OldFocusedObject.transform.IsChildOf(transform)) { if (OnHoverEntered != null) { OnHoverEntered.Invoke(new ManipulationEventData { ManipulationSource = gameObject, Pointer = eventData.Pointer, IsNearInteraction = !isFar }); } } else if (eventData.NewFocusedObject == null || !eventData.NewFocusedObject.transform.IsChildOf(transform)) { if (OnHoverExited != null) { OnHoverExited.Invoke(new ManipulationEventData { ManipulationSource = gameObject, Pointer = eventData.Pointer, IsNearInteraction = !isFar }); } } } private void ReleaseRigidBody(Vector3 velocity, Vector3 angularVelocity) { if (rigidBody != null) { rigidBody.useGravity = wasGravity; rigidBody.isKinematic = wasKinematic; // Match the object's velocity to the controller for near interactions // Otherwise keep the objects current velocity so that it's not dampened unnaturally if (isNearManipulation) { if (releaseBehavior.IsMaskSet(ReleaseBehaviorType.KeepVelocity)) { rigidBody.velocity = velocity; } if (releaseBehavior.IsMaskSet(ReleaseBehaviorType.KeepAngularVelocity)) { rigidBody.angularVelocity = angularVelocity; } } } } private bool TryGetGripRotation(IMixedRealityPointer pointer, out Quaternion rotation) { rotation = Quaternion.identity; switch (pointer.Controller?.ControllerHandedness) { case Handedness.Left: rotation = leftHandRotation; break; case Handedness.Right: rotation = rightHandRotation; break; default: return false; } return true; } #endregion #region Source Pose Handler Implementation /// /// Raised when the source pose tracking state is changed. /// public void OnSourcePoseChanged(SourcePoseEventData eventData) { } /// /// Raised when the source position is changed. /// public void OnSourcePoseChanged(SourcePoseEventData eventData) { } /// /// Raised when the source position is changed. /// public void OnSourcePoseChanged(SourcePoseEventData eventData) { } /// /// Raised when the source rotation is changed. /// public void OnSourcePoseChanged(SourcePoseEventData eventData) { } /// /// Raised when the source pose is changed. /// public void OnSourcePoseChanged(SourcePoseEventData eventData) { switch (eventData.Controller?.ControllerHandedness) { case Handedness.Left: leftHandRotation = eventData.SourceData.Rotation; break; case Handedness.Right: rightHandRotation = eventData.SourceData.Rotation; break; default: break; } } public void OnSourceDetected(SourceStateEventData eventData) { } public void OnSourceLost(SourceStateEventData eventData) { } #endregion } }