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