// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Physics; using Microsoft.MixedReality.Toolkit.Utilities; using System; using System.Collections.Generic; using System.Linq; 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/manipulation-handler")] [AddComponentMenu("Scripts/MRTK/SDK/ManipulationHandler")] public class ManipulationHandler : MonoBehaviour, IMixedRealityPointerHandler, IMixedRealityFocusChangedHandler { #region Public Enums public enum HandMovementType { OneHandedOnly = 0, TwoHandedOnly, OneAndTwoHanded } public enum TwoHandedManipulation { Scale, Rotate, MoveScale, MoveRotate, RotateScale, MoveRotateScale } public enum RotateInOneHandType { MaintainRotationToUser, GravityAlignedMaintainRotationToUser, FaceUser, FaceAwayFromUser, MaintainOriginalRotation, 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; public Transform HostTransform { get => hostTransform; set => hostTransform = value; } [SerializeField] [Tooltip("Can manipulation be done only with one hand, only with two hands, or with both?")] private HandMovementType manipulationType = HandMovementType.OneAndTwoHanded; public HandMovementType ManipulationType { get => manipulationType; set => manipulationType = value; } [SerializeField] [Tooltip("What manipulation will two hands perform?")] private TwoHandedManipulation twoHandedManipulationType = TwoHandedManipulation.MoveRotateScale; public TwoHandedManipulation TwoHandedManipulationType { get => twoHandedManipulationType; set => twoHandedManipulationType = value; } [SerializeField] [Tooltip("Specifies whether manipulation can be done using far interaction with pointers.")] private bool allowFarManipulation = true; public bool AllowFarManipulation { get => allowFarManipulation; set => allowFarManipulation = value; } [SerializeField] [Tooltip("Rotation behavior of object when using one hand near")] private RotateInOneHandType oneHandRotationModeNear = RotateInOneHandType.RotateAboutGrabPoint; 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; 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; public ReleaseBehaviorType ReleaseBehavior { get => releaseBehavior; set => releaseBehavior = value; } [SerializeField] [Tooltip("Constrain rotation along an axis")] private RotationConstraintType constraintOnRotation = RotationConstraintType.None; public RotationConstraintType ConstraintOnRotation { get => constraintOnRotation; set { constraintOnRotation = value; rotateConstraint.ConstraintOnRotation = RotationConstraintHelper.ConvertToAxisFlags(constraintOnRotation); } } [SerializeField] [Tooltip("Check if object rotation should be in local space of object being manipulated instead of world space.")] private bool useLocalSpaceForConstraint = false; /// /// Gets or sets whether the constraints should be applied in local space of the object being manipulated or world space. /// public bool UseLocalSpaceForConstraint { get => rotateConstraint != null && rotateConstraint.UseLocalSpaceForConstraint; set { if (rotateConstraint != null) { rotateConstraint.UseLocalSpaceForConstraint = value; } } } [SerializeField] [Tooltip("Constrain movement")] private MovementConstraintType constraintOnMovement = MovementConstraintType.None; public MovementConstraintType ConstraintOnMovement { get => constraintOnMovement; set => constraintOnMovement = value; } [SerializeField] [Tooltip("Check to enable frame-rate independent smoothing. ")] private bool smoothingActive = true; public bool SmoothingActive { get => smoothingActive; set => smoothingActive = value; } [SerializeField] [Range(0, 1)] [Tooltip("Enter amount representing amount of smoothing to apply to the movement, scale, rotation. Smoothing of 0 means no smoothing. Max value means no change to value.")] private float smoothingAmountOneHandManip = 0.001f; public float SmoothingAmoutOneHandManip { get => smoothingAmountOneHandManip; set => smoothingAmountOneHandManip = value; } #endregion Serialized Fields #region Event handlers [SerializeField] [FormerlySerializedAs("OnManipulationStarted")] private ManipulationEvent onManipulationStarted = new ManipulationEvent(); public ManipulationEvent OnManipulationStarted { get => onManipulationStarted; set => onManipulationStarted = value; } [SerializeField] [FormerlySerializedAs("OnManipulationEnded")] private ManipulationEvent onManipulationEnded = new ManipulationEvent(); public ManipulationEvent OnManipulationEnded { get => onManipulationEnded; set => onManipulationEnded = value; } [SerializeField] [FormerlySerializedAs("OnHoverEntered")] private ManipulationEvent onHoverEntered = new ManipulationEvent(); public ManipulationEvent OnHoverEntered { get => onHoverEntered; set => onHoverEntered = value; } [SerializeField] [FormerlySerializedAs("OnHoverExited")] private ManipulationEvent onHoverExited = new ManipulationEvent(); public ManipulationEvent OnHoverExited { get => onHoverExited; set => onHoverExited = value; } #endregion #region Private Properties [System.Flags] private enum State { Start = 0x000, Moving = 0x001, Scaling = 0x010, Rotating = 0x100, MovingRotating = Moving | Rotating, MovingScaling = Moving | Scaling, RotatingScaling = Rotating | Scaling, MovingRotatingScaling = Moving | Rotating | Scaling }; private State currentState = State.Start; private ManipulationMoveLogic moveLogic; private TwoHandScaleLogic scaleLogic; private TwoHandRotateLogic rotateLogic; /// /// Holds the pointer and the initial intersection point of the pointer ray /// with the object on pointer down in pointer space /// private struct PointerData { public IMixedRealityPointer pointer; private Vector3 initialGrabPointInPointer; public PointerData(IMixedRealityPointer pointer, Vector3 initialGrabPointInPointer) : this() { this.pointer = pointer; this.initialGrabPointInPointer = initialGrabPointInPointer; } public bool IsNearPointer() { return (pointer is IMixedRealityNearPointer); } /// Returns the grab point on the manipulated object in world space public Vector3 GrabPoint { get { return (pointer.Rotation * initialGrabPointInPointer) + pointer.Position; } } } private Dictionary pointerIdToPointerMap = new Dictionary(); private Quaternion objectToHandRotation; private Quaternion objectToGripRotation; private bool isNearManipulation; private Rigidbody rigidBody; private bool wasKinematic = false; private Quaternion startObjectRotationCameraSpace; private Quaternion startObjectRotationFlatCameraSpace; private Quaternion hostWorldRotationOnManipulationStart; private FixedDistanceConstraint moveConstraint; private RotationAxisConstraint rotateConstraint; private MinMaxScaleConstraint scaleHandler; #endregion #region MonoBehaviour Functions private void Awake() { moveLogic = new ManipulationMoveLogic(); rotateLogic = new TwoHandRotateLogic(); scaleLogic = new TwoHandScaleLogic(); } private void Start() { if (hostTransform == null) { hostTransform = transform; } moveConstraint = this.EnsureComponent(); moveConstraint.ConstraintTransform = CameraCache.Main.transform; rotateConstraint = this.EnsureComponent(); rotateConstraint.ConstraintOnRotation = RotationConstraintHelper.ConvertToAxisFlags(constraintOnRotation); rotateConstraint.UseLocalSpaceForConstraint = useLocalSpaceForConstraint; scaleHandler = this.GetComponent(); } #endregion MonoBehaviour Functions #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 GetPointersCentroid() { Vector3 sum = Vector3.zero; int count = 0; foreach (var p in pointerIdToPointerMap.Values) { sum += p.GrabPoint; count++; } return sum / Math.Max(1, 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 GetAveragePointerPose() { Vector3 sumPos = Vector3.zero; Vector3 sumDir = Vector3.zero; int count = 0; foreach (var p in pointerIdToPointerMap.Values) { sumPos += p.pointer.Position; sumDir += p.pointer.Rotation * Vector3.forward; count++; } MixedRealityPose pose = new MixedRealityPose(); if (count > 0) { pose.Position = sumPos / count; pose.Rotation = Quaternion.LookRotation(sumDir / count); } return pose; } private Vector3 GetPointersVelocity() { Vector3 sum = Vector3.zero; int numControllers = 0; foreach (var p in pointerIdToPointerMap.Values) { // Check pointer has a valid controller (e.g. gaze pointer doesn't) if (p.pointer.Controller != null) { numControllers++; sum += p.pointer.Controller.Velocity; } } return sum / Math.Max(1, numControllers); } private Vector3 GetPointersAngularVelocity() { Vector3 sum = Vector3.zero; int numControllers = 0; foreach (var p in pointerIdToPointerMap.Values) { // Check pointer has a valid controller (e.g. gaze pointer doesn't) if (p.pointer.Controller != null) { numControllers++; sum += p.pointer.Controller.AngularVelocity; } } return sum / Math.Max(1, numControllers); } private bool IsNearManipulation() { foreach (var item in pointerIdToPointerMap) { if (item.Value.IsNearPointer()) { return true; } } return false; } private void UpdateStateMachine() { var handsPressedCount = pointerIdToPointerMap.Count; State newState = currentState; // early out for no hands or one hand if TwoHandedOnly is active if (handsPressedCount == 0 || (handsPressedCount == 1 && manipulationType == HandMovementType.TwoHandedOnly)) { newState = State.Start; } else { switch (currentState) { case State.Start: case State.Moving: if (handsPressedCount == 1) { newState = State.Moving; } else if (handsPressedCount > 1 && manipulationType != HandMovementType.OneHandedOnly) { switch (twoHandedManipulationType) { case TwoHandedManipulation.Scale: newState = State.Scaling; break; case TwoHandedManipulation.Rotate: newState = State.Rotating; break; case TwoHandedManipulation.MoveRotate: newState = State.MovingRotating; break; case TwoHandedManipulation.MoveScale: newState = State.MovingScaling; break; case TwoHandedManipulation.RotateScale: newState = State.RotatingScaling; break; case TwoHandedManipulation.MoveRotateScale: newState = State.MovingRotatingScaling; break; default: throw new ArgumentOutOfRangeException(); } } break; case State.Scaling: case State.Rotating: case State.MovingScaling: case State.MovingRotating: case State.RotatingScaling: case State.MovingRotatingScaling: // one hand only supports move for now if (handsPressedCount == 1) { newState = State.Moving; } break; default: throw new ArgumentOutOfRangeException(); } } InvokeStateUpdateFunctions(currentState, newState); currentState = newState; } private void InvokeStateUpdateFunctions(State oldState, State newState) { if (newState != oldState) { switch (newState) { case State.Moving: HandleOneHandMoveStarted(); break; case State.Start: HandleManipulationEnded(); break; case State.RotatingScaling: case State.MovingRotating: case State.MovingRotatingScaling: case State.Scaling: case State.Rotating: case State.MovingScaling: HandleTwoHandManipulationStarted(newState); break; } switch (oldState) { case State.Start: HandleManipulationStarted(); break; case State.Scaling: case State.Rotating: case State.RotatingScaling: case State.MovingRotating: case State.MovingRotatingScaling: case State.MovingScaling: HandleTwoHandManipulationEnded(); break; } } else { switch (newState) { case State.Moving: HandleOneHandMoveUpdated(); break; case State.Scaling: case State.Rotating: case State.RotatingScaling: case State.MovingRotating: case State.MovingRotatingScaling: case State.MovingScaling: HandleTwoHandManipulationUpdated(); break; default: break; } } } #endregion Private Methods #region Public Methods /// /// Releases the object that is currently manipulated /// public void ForceEndManipulation() { // release rigidbody and clear pointers ReleaseRigidBody(); pointerIdToPointerMap.Clear(); // end manipulation State newState = State.Start; InvokeStateUpdateFunctions(currentState, newState); currentState = newState; } /// /// 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) { Assert.IsTrue(pointerIdToPointerMap.ContainsKey(pointerId)); return pointerIdToPointerMap[pointerId].GrabPoint; } #endregion Public Methods #region Hand Event Handlers /// public void OnPointerDown(MixedRealityPointerEventData eventData) { if (eventData.used || (!allowFarManipulation && eventData.Pointer as IMixedRealityNearPointer == null)) { return; } // If we only allow one handed manipulations, check there is no hand interacting yet. if (manipulationType != HandMovementType.OneHandedOnly || pointerIdToPointerMap.Count == 0) { uint id = eventData.Pointer.PointerId; // Ignore poke pointer events if (!pointerIdToPointerMap.ContainsKey(eventData.Pointer.PointerId)) { if (pointerIdToPointerMap.Count == 0) { rigidBody = GetComponent(); if (rigidBody != null) { wasKinematic = rigidBody.isKinematic; rigidBody.isKinematic = true; } } // cache start ptr grab point Vector3 initialGrabPoint = Quaternion.Inverse(eventData.Pointer.Rotation) * (eventData.Pointer.Result.Details.Point - eventData.Pointer.Position); pointerIdToPointerMap.Add(id, new PointerData(eventData.Pointer, initialGrabPoint)); UpdateStateMachine(); } } if (pointerIdToPointerMap.Count > 0) { // Always mark the pointer data as used to prevent any other behavior to handle pointer events // as long as the ManipulationHandler is active. // This is due to us reacting to both "Select" and "Grip" events. eventData.Use(); } } public void OnPointerDragged(MixedRealityPointerEventData eventData) { if (currentState != State.Start) { UpdateStateMachine(); } } /// public void OnPointerUp(MixedRealityPointerEventData eventData) { uint id = eventData.Pointer.PointerId; if (pointerIdToPointerMap.ContainsKey(id)) { if (pointerIdToPointerMap.Count == 1 && rigidBody != null) { ReleaseRigidBody(); } pointerIdToPointerMap.Remove(id); } UpdateStateMachine(); eventData.Use(); } #endregion Hand Event Handlers #region Private Event Handlers private void HandleTwoHandManipulationUpdated() { var targetTransform = new MixedRealityTransform(hostTransform.position, hostTransform.rotation, hostTransform.localScale); var handPositionArray = GetHandPositionArray(); if ((currentState & State.Scaling) > 0) { targetTransform.Scale = scaleLogic.UpdateMap(handPositionArray); if (scaleHandler != null) { scaleHandler.ApplyConstraint(ref targetTransform); } } if ((currentState & State.Rotating) > 0) { targetTransform.Rotation = rotateLogic.Update(handPositionArray, targetTransform.Rotation); if (rotateConstraint != null) { rotateConstraint.ApplyConstraint(ref targetTransform); } } if ((currentState & State.Moving) > 0) { // If near manipulation, a pure grabpoint centroid is used for // the initial pointer pose; if far manipulation, a more complex // look-rotation-based pointer pose is used. MixedRealityPose pose = IsNearManipulation() ? new MixedRealityPose(GetPointersCentroid()) : GetAveragePointerPose(); // The manipulation handler is not built to handle near manipulation properly, please use the object manipulator targetTransform.Position = moveLogic.UpdateTransform(pose, targetTransform, true, false); if (constraintOnMovement == MovementConstraintType.FixDistanceFromHead && moveConstraint != null) { moveConstraint.ApplyConstraint(ref targetTransform); } } float lerpAmount = GetLerpAmount(); hostTransform.position = Vector3.Lerp(hostTransform.position, targetTransform.Position, lerpAmount); hostTransform.rotation = Quaternion.Lerp(hostTransform.rotation, targetTransform.Rotation, lerpAmount); hostTransform.localScale = Vector3.Lerp(hostTransform.localScale, targetTransform.Scale, lerpAmount); } private void HandleOneHandMoveUpdated() { Debug.Assert(pointerIdToPointerMap.Count == 1); PointerData pointerData = GetFirstPointer(); IMixedRealityPointer pointer = pointerData.pointer; var targetTransform = new MixedRealityTransform(hostTransform.position, hostTransform.rotation, hostTransform.localScale); RotateInOneHandType rotateInOneHandType = isNearManipulation ? oneHandRotationModeNear : oneHandRotationModeFar; switch (rotateInOneHandType) { case RotateInOneHandType.MaintainOriginalRotation: targetTransform.Rotation = hostTransform.rotation; break; case RotateInOneHandType.MaintainRotationToUser: Vector3 euler = CameraCache.Main.transform.rotation.eulerAngles; // don't use roll (feels awkward) - just maintain yaw / pitch angle targetTransform.Rotation = Quaternion.Euler(euler.x, euler.y, 0) * startObjectRotationCameraSpace; break; case RotateInOneHandType.GravityAlignedMaintainRotationToUser: var cameraForwardFlat = CameraCache.Main.transform.forward; cameraForwardFlat.y = 0; targetTransform.Rotation = Quaternion.LookRotation(cameraForwardFlat, Vector3.up) * startObjectRotationFlatCameraSpace; break; case RotateInOneHandType.FaceUser: { Vector3 directionToTarget = pointerData.GrabPoint - CameraCache.Main.transform.position; // Vector3 directionToTarget = hostTransform.position - CameraCache.Main.transform.position; targetTransform.Rotation = Quaternion.LookRotation(-directionToTarget); break; } case RotateInOneHandType.FaceAwayFromUser: { Vector3 directionToTarget = pointerData.GrabPoint - CameraCache.Main.transform.position; targetTransform.Rotation = Quaternion.LookRotation(directionToTarget); break; } case RotateInOneHandType.RotateAboutObjectCenter: case RotateInOneHandType.RotateAboutGrabPoint: Quaternion gripRotation; TryGetGripRotation(pointer, out gripRotation); targetTransform.Rotation = gripRotation * objectToGripRotation; break; } if (rotateConstraint != null) { rotateConstraint.ApplyConstraint(ref targetTransform); } MixedRealityPose pointerPose = new MixedRealityPose(pointer.Position, pointer.Rotation); // The manipulation handler is not built to handle near manipulation properly, please use the object manipulator targetTransform.Position = moveLogic.UpdateTransform(pointerPose, targetTransform, rotateInOneHandType != RotateInOneHandType.RotateAboutObjectCenter, false); if (constraintOnMovement == MovementConstraintType.FixDistanceFromHead && moveConstraint != null) { moveConstraint.ApplyConstraint(ref targetTransform); } float lerpAmount = GetLerpAmount(); Quaternion smoothedRotation = Quaternion.Lerp(hostTransform.rotation, targetTransform.Rotation, lerpAmount); Vector3 smoothedPosition = Vector3.Lerp(hostTransform.position, targetTransform.Position, lerpAmount); hostTransform.SetPositionAndRotation(smoothedPosition, smoothedRotation); } private void HandleTwoHandManipulationStarted(State newState) { var handPositionArray = GetHandPositionArray(); if ((newState & State.Rotating) > 0) { rotateLogic.Setup(handPositionArray, hostTransform); } if ((newState & State.Moving) > 0) { // If near manipulation, a pure grabpoint 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(GetPointersCentroid()) : GetAveragePointerPose(); MixedRealityPose hostPose = new MixedRealityPose(hostTransform.position, hostTransform.rotation); moveLogic.Setup(pointerPose, GetPointersCentroid(), hostPose, hostTransform.localScale); } if ((newState & State.Scaling) > 0) { scaleLogic.Setup(handPositionArray, hostTransform); } } private void HandleTwoHandManipulationEnded() { } private void HandleOneHandMoveStarted() { Assert.IsTrue(pointerIdToPointerMap.Count == 1); PointerData pointerData = GetFirstPointer(); IMixedRealityPointer pointer = pointerData.pointer; // cache objects rotation on start to have a reference for constraint calculations // if we don't cache this on manipulation start the near rotation might drift off the hand // over time hostWorldRotationOnManipulationStart = hostTransform.rotation; // Calculate relative transform from object to hand. Quaternion worldToPalmRotation = Quaternion.Inverse(pointer.Rotation); objectToHandRotation = worldToPalmRotation * hostTransform.rotation; // Calculate relative transform from object to grip. Quaternion gripRotation; TryGetGripRotation(pointer, out 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); Vector3 worldGrabPoint = pointerData.GrabPoint; startObjectRotationCameraSpace = Quaternion.Inverse(CameraCache.Main.transform.rotation) * hostTransform.rotation; var cameraFlat = CameraCache.Main.transform.forward; cameraFlat.y = 0; var hostForwardFlat = hostTransform.forward; hostForwardFlat.y = 0; var hostRotFlat = Quaternion.LookRotation(hostForwardFlat, Vector3.up); startObjectRotationFlatCameraSpace = Quaternion.Inverse(Quaternion.LookRotation(cameraFlat, Vector3.up)) * hostRotFlat; } private void HandleManipulationStarted() { isNearManipulation = IsNearManipulation(); // 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 = GetFirstPointer().pointer, PointerCentroid = GetPointersCentroid(), PointerVelocity = GetPointersVelocity(), PointerAngularVelocity = GetPointersAngularVelocity() }); } var pose = new MixedRealityTransform(hostTransform); if (constraintOnMovement == MovementConstraintType.FixDistanceFromHead && moveConstraint != null) { moveConstraint.Initialize(pose); } if (rotateConstraint != null) { rotateConstraint.Initialize(pose); } if (scaleHandler != null) { scaleHandler.Initialize(pose); } } private void HandleManipulationEnded() { // 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 = GetPointersCentroid(), PointerVelocity = GetPointersVelocity(), PointerAngularVelocity = GetPointersAngularVelocity() }); } } #endregion Private Event Handlers #region Unused Event Handlers /// public void OnPointerClicked(MixedRealityPointerEventData eventData) { } public void OnBeforeFocusChange(FocusEventData eventData) { } #endregion Unused Event Handlers #region Private methods private float GetLerpAmount() { if (smoothingActive == false || smoothingAmountOneHandManip == 0) { return 1; } // Obtained from "Frame-rate independent smoothing" // www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/ // We divide by max value to give the slider a bit more sensitivity. return 1.0f - Mathf.Pow(smoothingAmountOneHandManip, Time.deltaTime); } private Vector3[] GetHandPositionArray() { var handPositionMap = new Vector3[pointerIdToPointerMap.Count]; int index = 0; foreach (var item in pointerIdToPointerMap) { handPositionMap[index++] = item.Value.pointer.Position; } return handPositionMap; } public void OnFocusChanged(FocusEventData eventData) { bool isFar = !(eventData.Pointer is IMixedRealityNearPointer); if (eventData.OldFocusedObject == null || !eventData.OldFocusedObject.transform.IsChildOf(transform)) { if (isFar && !AllowFarManipulation) { return; } 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 (isFar && !AllowFarManipulation) { return; } if (OnHoverExited != null) { OnHoverExited.Invoke(new ManipulationEventData { ManipulationSource = gameObject, Pointer = eventData.Pointer, IsNearInteraction = !isFar }); } } } private void ReleaseRigidBody() { if (rigidBody != null) { rigidBody.isKinematic = wasKinematic; if (releaseBehavior.IsMaskSet(ReleaseBehaviorType.KeepVelocity)) { rigidBody.velocity = GetPointersVelocity(); } if (releaseBehavior.IsMaskSet(ReleaseBehaviorType.KeepAngularVelocity)) { rigidBody.angularVelocity = GetPointersAngularVelocity(); } rigidBody = null; } } private PointerData GetFirstPointer() { // We may be able to do this without allocating memory. // Moving to a method for later investigation. return pointerIdToPointerMap.Values.First(); } private bool TryGetGripRotation(IMixedRealityPointer pointer, out Quaternion rotation) { for (int i = 0; i < (pointer.Controller?.Interactions?.Length ?? 0); i++) { if (pointer.Controller.Interactions[i].InputType == DeviceInputType.SpatialGrip) { rotation = pointer.Controller.Interactions[i].RotationData; return true; } } rotation = Quaternion.identity; return false; } #endregion } }