一、框架视图
二、关键代码
NearInteractionTouchableUnityUI
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// Use a Unity UI RectTransform as touchable surface.
/// </summary>
[RequireComponent(typeof(RectTransform))]
[AddComponentMenu("Scripts/MRTK/Services/NearInteractionTouchableUnityUI")]
public class NearInteractionTouchableUnityUI : NearInteractionTouchableSurface
{
private Lazy<RectTransform> rectTransform;
public static IReadOnlyList<NearInteractionTouchableUnityUI> Instances => instances;
public override Vector3 LocalCenter => Vector3.zero;
public override Vector3 LocalPressDirection => Vector3.forward;
public override Vector2 Bounds => rectTransform.Value.rect.size;
private static readonly List<NearInteractionTouchableUnityUI> instances = new List<NearInteractionTouchableUnityUI>();
public NearInteractionTouchableUnityUI()
{
rectTransform = new Lazy<RectTransform>(GetComponent<RectTransform>);
}
/// <inheritdoc />
public override float DistanceToTouchable(Vector3 samplePoint, out Vector3 normal)
{
normal = transform.TransformDirection(-LocalPressDirection);
Vector3 localPoint = transform.InverseTransformPoint(samplePoint);
// touchables currently can only be touched within the bounds of the rectangle.
// We return infinity to ensure that any point outside the bounds does not get touched.
if (!rectTransform.Value.rect.Contains(localPoint))
{
return float.PositiveInfinity;
}
// Scale back to 3D space
localPoint = transform.TransformSize(localPoint);
return Math.Abs(localPoint.z);
}
protected void OnEnable()
{
instances.Add(this);
}
protected void OnDisable()
{
instances.Remove(this);
}
}
}
CanvasUtility
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using UnityEngine;
using UnityEngine.EventSystems;
namespace Microsoft.MixedReality.Toolkit.Input.Utilities
{
/// <summary>
/// Helper class for setting up canvases for use in the MRTK.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Canvas))]
[AddComponentMenu("Scripts/MRTK/Services/CanvasUtility")]
public class CanvasUtility : MonoBehaviour, IMixedRealityPointerHandler
{
private bool oldIsTargetPositionLockedOnFocusLock = false;
public void OnPointerClicked(MixedRealityPointerEventData eventData) {}
public void OnPointerDown(MixedRealityPointerEventData eventData)
{
oldIsTargetPositionLockedOnFocusLock = eventData.Pointer.IsTargetPositionLockedOnFocusLock;
if (!(eventData.Pointer is IMixedRealityNearPointer) && eventData.Pointer.Controller.IsRotationAvailable)
{
eventData.Pointer.IsTargetPositionLockedOnFocusLock = false;
}
}
public void OnPointerDragged(MixedRealityPointerEventData eventData) { }
public void OnPointerUp(MixedRealityPointerEventData eventData)
{
eventData.Pointer.IsTargetPositionLockedOnFocusLock = oldIsTargetPositionLockedOnFocusLock;
}
private void Start()
{
Canvas canvas = GetComponent<Canvas>();
Debug.Assert(canvas != null);
if (canvas.worldCamera == null)
{
Debug.Assert(CoreServices.InputSystem?.FocusProvider?.UIRaycastCamera != null, this);
canvas.worldCamera = CoreServices.InputSystem?.FocusProvider?.UIRaycastCamera;
if (EventSystem.current == null)
{
Debug.LogError("No EventSystem detected. UI events will not be propagated to Unity UI.");
}
}
else
{
Debug.LogError("World Space Canvas should have no camera set to work properly with Mixed Reality Toolkit. At runtime, they'll get their camera set automatically.");
}
}
}
}
VideoScreen
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
public class VideoScreen : MonoBehaviour
{
public VideoPlayer videoPlayer;
public RawImage videoImage;
private bool isPlay=true;
void Start()
{
videoPlayer.loopPointReached += EndReached;
}
void Update()
{
if (videoPlayer.texture == null) return;
videoImage.texture = videoPlayer.texture;
}
public void PlayOrPauseVideo()
{
if (isPlay) {
videoPlayer.transform.gameObject.SetActive(true);
videoPlayer.Play();
} else {
videoPlayer.transform.gameObject.SetActive(false);
videoPlayer.Pause();
}
isPlay = !isPlay;
// SendPause
}
private void EndReached(VideoPlayer source)
{
// gameObject.SetActive(false);
}
}
public void SetRotation() {
transform.localEulerAngles = new Vector3(0,transform.localEulerAngles.y,0);
}
ObjectManipulator
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.UI;
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.Experimental.UI
{
/// <summary>
/// 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.
/// </summary>
[HelpURL("https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/README_ManipulationHandler.html")]
public class ObjectManipulator : MonoBehaviour, IMixedRealityPointerHandler, IMixedRealityFocusChangedHandler
{
#region Public Enums
/// <summary>
/// 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.
/// </summary>
public enum RotateInOneHandType
{
RotateAboutObjectCenter,
RotateAboutGrabPoint
};
[System.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;
/// <summary>
/// Transform that will be dragged. Defaults to the object of the component.
/// </summary>
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;
/// <summary>
/// Can manipulation be done only with one hand, only with two hands, or with both?
/// </summary>
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;
/// <summary>
/// What manipulation will two hands perform?
/// </summary>
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;
/// <summary>
/// Specifies whether manipulation can be done using far interaction with pointers.
/// </summary>
public bool AllowFarManipulation
{
get => allowFarManipulation;
set => allowFarManipulation = value;
}
[SerializeField]
[Tooltip("Rotation behavior of object when using one hand near")]
private RotateInOneHandType oneHandRotationModeNear = RotateInOneHandType.RotateAboutGrabPoint;
/// <summary>
/// Rotation behavior of object when using one hand near
/// </summary>
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;
/// <summary>
/// Rotation behavior of object when using one hand at distance
/// </summary>
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;
/// <summary>
/// Rigid body behavior of the dragged object when releasing it.
/// </summary>
public ReleaseBehaviorType ReleaseBehavior
{
get => releaseBehavior;
set => releaseBehavior = value;
}
[SerializeField]
[Tooltip("Check to enable frame-rate independent smoothing.")]
private bool smoothingActive = true;
/// <summary>
/// Check to enable frame-rate independent smoothing.
/// </summary>
public bool SmoothingActive
{
get => smoothingActive;
set => smoothingActive = 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;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value.
/// </summary>
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;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.
/// </summary>
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;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.
/// </summary>
public float ScaleLerpTime
{
get => scaleLerpTime;
set => scaleLerpTime = value;
}
#endregion Serialized Fields
#region Event handlers
[Header("Manipulation Events")]
[SerializeField]
[FormerlySerializedAs("OnManipulationStarted")]
private ManipulationEvent onManipulationStarted = new ManipulationEvent();
/// <summary>
/// Unity event raised on manipulation started
/// </summary>
public ManipulationEvent OnManipulationStarted
{
get => onManipulationStarted;
set => onManipulationStarted = value;
}
[SerializeField]
[FormerlySerializedAs("OnManipulationEnded")]
private ManipulationEvent onManipulationEnded = new ManipulationEvent();
/// <summary>
/// Unity event raised on manipulation ended
/// </summary>
public ManipulationEvent OnManipulationEnded
{
get => onManipulationEnded;
set => onManipulationEnded = value;
}
[SerializeField]
[FormerlySerializedAs("OnHoverEntered")]
private ManipulationEvent onHoverEntered = new ManipulationEvent();
/// <summary>
/// Unity event raised on hover started
/// </summary>
public ManipulationEvent OnHoverEntered
{
get => onHoverEntered;
set => onHoverEntered = value;
}
[SerializeField]
[FormerlySerializedAs("OnHoverExited")]
private ManipulationEvent onHoverExited = new ManipulationEvent();
/// <summary>
/// Unity event raised on hover ended
/// </summary>
public ManipulationEvent OnHoverExited
{
get => onHoverExited;
set => onHoverExited = value;
}
#endregion
#region Private Properties
private ManipulationMoveLogic moveLogic;
private TwoHandScaleLogic scaleLogic;
private TwoHandRotateLogic rotateLogic;
/// <summary>
/// Holds the pointer and the initial intersection point of the pointer ray
/// with the object on pointer down in pointer space
/// </summary>
private struct PointerData
{
public IMixedRealityPointer pointer;
private Vector3 initialGrabPointInPointer;
public PointerData(IMixedRealityPointer pointer, Vector3 worldGrabPoint) : this()
{
this.pointer = pointer;
this.initialGrabPointInPointer = Quaternion.Inverse(pointer.Rotation) * (worldGrabPoint - pointer.Position);
}
public bool IsNearPointer => pointer is IMixedRealityNearPointer;
/// Returns the grab point on the manipulated object in world space
public Vector3 GrabPoint => (pointer.Rotation * initialGrabPointInPointer) + pointer.Position;
}
private Dictionary<uint, PointerData> pointerIdToPointerMap = new Dictionary<uint, PointerData>();
private Quaternion objectToGripRotation;
private bool isNearManipulation;
private bool isManipulationStarted;
private Rigidbody rigidBody;
private bool wasKinematic = false;
private ConstraintManager constraints;
private bool IsOneHandedManipulationEnabled => manipulationType.HasFlag(ManipulationHandFlags.OneHanded) && pointerIdToPointerMap.Count == 1;
private bool IsTwoHandedManipulationEnabled => manipulationType.HasFlag(ManipulationHandFlags.TwoHanded) && pointerIdToPointerMap.Count > 1;
#endregion
#region MonoBehaviour Functions
private void Awake()
{
moveLogic = new ManipulationMoveLogic();
rotateLogic = new TwoHandRotateLogic();
scaleLogic = new TwoHandScaleLogic();
}
private void Start()
{
rigidBody = HostTransform.GetComponent<Rigidbody>();
constraints = new ConstraintManager(gameObject);
}
#endregion MonoBehaviour Functions
#region Private Methods
private Vector3 GetPointersGrabPoint()
{
Vector3 sum = Vector3.zero;
int count = 0;
foreach (var p in pointerIdToPointerMap.Values)
{
sum += p.GrabPoint;
count++;
}
return sum / Math.Max(1, count);
}
private MixedRealityPose GetPointersPose()
{
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++;
}
return new MixedRealityPose
{
Position = sumPos / Math.Max(1, count),
Rotation = Quaternion.LookRotation(sumDir / Math.Max(1, count))
};
}
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;
}
#endregion Private Methods
#region Public Methods
/// <summary>
/// Releases the object that is currently manipulated
/// </summary>
public void ForceEndManipulation()
{
// end manipulation
if (isManipulationStarted)
{
HandleManipulationEnded(GetPointersGrabPoint(), GetPointersVelocity(), GetPointersAngularVelocity());
}
pointerIdToPointerMap.Clear();
}
/// <summary>
/// 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.
/// </summary>
public Vector3 GetPointerGrabPoint(uint pointerId)
{
Assert.IsTrue(pointerIdToPointerMap.ContainsKey(pointerId));
return pointerIdToPointerMap[pointerId].GrabPoint;
}
#endregion Public Methods
#region Hand Event Handlers
/// <inheritdoc />
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 != ManipulationHandFlags.OneHanded || pointerIdToPointerMap.Count == 0)
{
uint id = eventData.Pointer.PointerId;
// Ignore poke pointer events
if (!pointerIdToPointerMap.ContainsKey(id))
{
// cache start ptr grab point
pointerIdToPointerMap.Add(id, new PointerData(eventData.Pointer, eventData.Pointer.Result.Details.Point));
// Call manipulation started handlers
if (IsTwoHandedManipulationEnabled)
{
if (!isManipulationStarted)
{
HandleManipulationStarted();
}
HandleTwoHandManipulationStarted();
}
else if (IsOneHandedManipulationEnabled)
{
if (!isManipulationStarted)
{
HandleManipulationStarted();
}
HandleOneHandMoveStarted();
}
}
}
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)
{
// Call manipulation updated handlers
if (IsOneHandedManipulationEnabled)
{
HandleOneHandMoveUpdated();
}
else if (IsTwoHandedManipulationEnabled)
{
HandleTwoHandManipulationUpdated();
}
}
/// <inheritdoc />
public void OnPointerUp(MixedRealityPointerEventData eventData)
{
// Get pointer data before they are removed from the map
Vector3 grabPoint = GetPointersGrabPoint();
Vector3 velocity = GetPointersVelocity();
Vector3 angularVelocity = GetPointersAngularVelocity();
uint id = eventData.Pointer.PointerId;
if (pointerIdToPointerMap.ContainsKey(id))
{
pointerIdToPointerMap.Remove(id);
}
// Call manipulation ended handlers
var handsPressedCount = pointerIdToPointerMap.Count;
if (manipulationType.HasFlag(ManipulationHandFlags.TwoHanded) && handsPressedCount == 1)
{
if (manipulationType.HasFlag(ManipulationHandFlags.OneHanded))
{
HandleOneHandMoveStarted();
}
else
{
HandleManipulationEnded(grabPoint, velocity, angularVelocity);
}
}
else if (isManipulationStarted && handsPressedCount == 0)
{
HandleManipulationEnded(grabPoint, velocity, angularVelocity);
}
eventData.Use();
}
#endregion Hand Event Handlers
#region Private Event Handlers
private void HandleTwoHandManipulationStarted()
{
var handPositionArray = GetHandPositionArray();
if (twoHandedManipulationType.HasFlag(TransformFlags.Rotate))
{
rotateLogic.Setup(handPositionArray, HostTransform);
}
if (twoHandedManipulationType.HasFlag(TransformFlags.Move))
{
MixedRealityPose pointerPose = GetPointersPose();
MixedRealityPose hostPose = new MixedRealityPose(HostTransform.position, HostTransform.rotation);
moveLogic.Setup(pointerPose, GetPointersGrabPoint(), hostPose, HostTransform.localScale);
}
if (twoHandedManipulationType.HasFlag(TransformFlags.Scale))
{
scaleLogic.Setup(handPositionArray, HostTransform);
}
}
private void HandleTwoHandManipulationUpdated()
{
var targetTransform = new MixedRealityTransform(HostTransform.position, HostTransform.rotation, HostTransform.localScale);
var handPositionArray = GetHandPositionArray();
if (twoHandedManipulationType.HasFlag(TransformFlags.Scale))
{
targetTransform.Scale = scaleLogic.UpdateMap(handPositionArray);
constraints.ApplyScaleConstraints(ref targetTransform, false, IsNearManipulation());
}
if (twoHandedManipulationType.HasFlag(TransformFlags.Rotate))
{
targetTransform.Rotation = rotateLogic.Update(handPositionArray, targetTransform.Rotation);
constraints.ApplyRotationConstraints(ref targetTransform, false, IsNearManipulation());
}
if (twoHandedManipulationType.HasFlag(TransformFlags.Move))
{
MixedRealityPose pose = GetPointersPose();
targetTransform.Position = moveLogic.Update(pose, targetTransform.Rotation, targetTransform.Scale, true);
constraints.ApplyTranslationConstraints(ref targetTransform, false, IsNearManipulation());
}
ApplyTargetTransform(targetTransform);
}
private void HandleOneHandMoveStarted()
{
Assert.IsTrue(pointerIdToPointerMap.Count == 1);
PointerData pointerData = GetFirstPointer();
IMixedRealityPointer pointer = pointerData.pointer;
// 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);
}
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);
constraints.ApplyScaleConstraints(ref targetTransform, true, IsNearManipulation());
Quaternion gripRotation;
TryGetGripRotation(pointer, out gripRotation);
targetTransform.Rotation = gripRotation * objectToGripRotation;
constraints.ApplyRotationConstraints(ref targetTransform, true, IsNearManipulation());
RotateInOneHandType rotateInOneHandType = isNearManipulation ? oneHandRotationModeNear : oneHandRotationModeFar;
MixedRealityPose pointerPose = new MixedRealityPose(pointer.Position, pointer.Rotation);
targetTransform.Position = moveLogic.Update(pointerPose, targetTransform.Rotation, targetTransform.Scale, rotateInOneHandType != RotateInOneHandType.RotateAboutObjectCenter);
constraints.ApplyTranslationConstraints(ref targetTransform, true, IsNearManipulation());
ApplyTargetTransform(targetTransform);
}
private void HandleManipulationStarted()
{
isManipulationStarted = true;
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 = GetPointersGrabPoint(),
PointerVelocity = GetPointersVelocity(),
PointerAngularVelocity = GetPointersAngularVelocity()
});
}
if (rigidBody != null)
{
wasKinematic = rigidBody.isKinematic;
rigidBody.isKinematic = false;
}
constraints.Initialize(new MixedRealityPose(HostTransform.position, HostTransform.rotation));
}
private void HandleManipulationEnded(Vector3 pointerGrabPoint, Vector3 pointerVelocity, Vector3 pointerAnglularVelocity)
{
isManipulationStarted = 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);
}
#endregion Private Event Handlers
#region Unused Event Handlers
/// <inheritdoc />
public void OnPointerClicked(MixedRealityPointerEventData eventData) { }
public void OnBeforeFocusChange(FocusEventData eventData) { }
#endregion Unused Event Handlers
#region Private methods
private void ApplyTargetTransform(MixedRealityTransform targetTransform)
{
if (rigidBody == null)
{
HostTransform.position = SmoothTo(HostTransform.position, targetTransform.Position, moveLerpTime);
HostTransform.rotation = SmoothTo(HostTransform.rotation, targetTransform.Rotation, rotateLerpTime);
HostTransform.localScale = SmoothTo(HostTransform.localScale, targetTransform.Scale, scaleLerpTime);
}
else
{
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 (angle > 180f)
angle -= 360f;
if (axis.IsValidVector())
{
rigidBody.angularVelocity = ((1f - Mathf.Pow(rotateLerpTime, Time.deltaTime)) / Time.deltaTime) * (axis.normalized * angle * Mathf.Deg2Rad);
}
HostTransform.localScale = SmoothTo(HostTransform.localScale, targetTransform.Scale, scaleLerpTime);
}
}
private Vector3 SmoothTo(Vector3 source, Vector3 goal, float lerpTime)
{
return Vector3.Lerp(source, goal, (!smoothingActive || lerpTime == 0f) ? 1f : 1f - Mathf.Pow(lerpTime, Time.deltaTime));
}
private Quaternion SmoothTo(Quaternion source, Quaternion goal, float slerpTime)
{
return Quaternion.Slerp(source, goal, (!smoothingActive || slerpTime == 0f) ? 1f : 1f - Mathf.Pow(slerpTime, 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 (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.isKinematic = wasKinematic;
if (releaseBehavior.HasFlag(ReleaseBehaviorType.KeepVelocity))
{
rigidBody.velocity = velocity;
}
if (releaseBehavior.HasFlag(ReleaseBehaviorType.KeepAngularVelocity))
{
rigidBody.angularVelocity = angularVelocity;
}
}
}
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; i++)
{
if (pointer.Controller.Interactions[i].InputType == DeviceInputType.SpatialGrip)
{
rotation = pointer.Controller.Interactions[i].RotationData;
return true;
}
}
rotation = Quaternion.identity;
return false;
}
#endregion
}
}
NearInteractionGrabbable
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using System;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// Add a NearInteractionGrabbable component to any GameObject that has a collidable
/// on it in order to make that collidable near grabbable.
///
/// Any IMixedRealityNearPointer will then dispatch pointer events
/// to the closest near grabbable objects.
///
/// Additionally, the near pointer will send focus enter and exit events when the
/// decorated object is the closest object to the near pointer
/// </summary>
[AddComponentMenu("Scripts/MRTK/Services/NearInteractionGrabbable")]
public class NearInteractionGrabbable : MonoBehaviour
{
[Tooltip("Check to show a tether from the position where object was grabbed to the hand when manipulating. Useful for things like bounding boxes where resizing/rotating might be constrained.")]
public bool ShowTetherWhenManipulating = false;
void OnEnable()
{
// As of https://docs.unity3d.com/ScriptReference/Physics.ClosestPoint.html
// ClosestPoint call will only work on specific types of colliders.
// Using incorrect type of collider will emit warning from FocusProvider,
// but grab behavior will be broken at this point.
// Emit exception on initialization, when we know grab interaction is used
// on this object to make an error clearly visible.
var collider = gameObject.GetComponent<Collider>();
if((collider as BoxCollider) == null &&
(collider as CapsuleCollider) == null &&
(collider as SphereCollider) == null &&
((collider as MeshCollider) == null || (collider as MeshCollider).convex == false))
{
Debug.LogException(new InvalidOperationException("NearInteractionGrabbable requires a " +
"BoxCollider, SphereCollider, CapsuleCollider or a convex MeshCollider on an object. " +
"Otherwise grab interaction will not work correctly."));
}
}
}
}
LockRotation
using UnityEngine;
using System.Collections;
public class LockRotation : MonoBehaviour {
Quaternion rotation;
void Awake()
{
rotation = transform.rotation;
}
void LateUpdate()
{
transform.rotation = rotation;
}
}
ConstraintManager
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.UI
{
/// <summary>
/// Manages constraints for a given object and ensures that Scale/Rotation/Translation
/// constraints are executed separately.
/// </summary>
internal class ConstraintManager
{
private List<TransformConstraint> constraints;
public ConstraintManager(GameObject gameObject)
{
constraints = gameObject.GetComponents<TransformConstraint>().ToList();
}
public void ApplyScaleConstraints(ref MixedRealityTransform transform, bool isOneHanded, bool isNear)
{
ApplyConstraintsForType(ref transform, isOneHanded, isNear, TransformFlags.Scale);
}
public void ApplyRotationConstraints(ref MixedRealityTransform transform, bool isOneHanded, bool isNear)
{
ApplyConstraintsForType(ref transform, isOneHanded, isNear, TransformFlags.Rotate);
}
public void ApplyTranslationConstraints(ref MixedRealityTransform transform, bool isOneHanded, bool isNear)
{
ApplyConstraintsForType(ref transform, isOneHanded, isNear, TransformFlags.Move);
}
public void Initialize(MixedRealityPose worldPose)
{
foreach (var constraint in constraints)
{
constraint.Initialize(worldPose);
}
}
private void ApplyConstraintsForType(ref MixedRealityTransform transform, bool isOneHanded, bool isNear, TransformFlags transformType)
{
ManipulationHandFlags handMode = isOneHanded ? ManipulationHandFlags.OneHanded : ManipulationHandFlags.TwoHanded;
ManipulationProximityFlags proximityMode = isNear ? ManipulationProximityFlags.Near : ManipulationProximityFlags.Far;
foreach (var constraint in constraints)
{
if (constraint.ConstraintType == transformType &&
constraint.HandType.HasFlag(handMode) &&
constraint.ProximityType.HasFlag(proximityMode))
{
constraint.ApplyConstraint(ref transform);
}
}
}
}
}
ManipulationHandler
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
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
{
/// <summary>
/// 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.
/// </summary>
[HelpURL("https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/README_ManipulationHandler.html")]
[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
};
[System.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;
/// <summary>
/// Gets or sets whether the constraints should be applied in local space of the object being manipulated or world space.
/// </summary>
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;
/// <summary>
/// Holds the pointer and the initial intersection point of the pointer ray
/// with the object on pointer down in pointer space
/// </summary>
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<uint, PointerData> pointerIdToPointerMap = new Dictionary<uint, PointerData>();
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<FixedDistanceConstraint>();
moveConstraint.TargetTransform = hostTransform;
moveConstraint.ConstraintTransform = CameraCache.Main.transform;
rotateConstraint = this.EnsureComponent<RotationAxisConstraint>();
rotateConstraint.TargetTransform = hostTransform;
rotateConstraint.ConstraintOnRotation = RotationConstraintHelper.ConvertToAxisFlags(constraintOnRotation);
rotateConstraint.UseLocalSpaceForConstraint = useLocalSpaceForConstraint;
scaleHandler = this.GetComponent<MinMaxScaleConstraint>();
}
#endregion MonoBehaviour Functions
#region Private Methods
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);
}
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
/// <summary>
/// Releases the object that is currently manipulated
/// </summary>
public void ForceEndManipulation()
{
// release rigidbody and clear pointers
ReleaseRigidBody();
pointerIdToPointerMap.Clear();
// end manipulation
State newState = State.Start;
InvokeStateUpdateFunctions(currentState, newState);
currentState = newState;
}
/// <summary>
/// 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.
/// </summary>
public Vector3 GetPointerGrabPoint(uint pointerId)
{
Assert.IsTrue(pointerIdToPointerMap.ContainsKey(pointerId));
return pointerIdToPointerMap[pointerId].GrabPoint;
}
#endregion Public Methods
#region Hand Event Handlers
/// <inheritdoc />
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<Rigidbody>();
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();
}
}
/// <inheritdoc />
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)
{
MixedRealityPose pose = GetAveragePointerPose();
targetTransform.Position = moveLogic.Update(pose, targetTransform.Rotation, targetTransform.Scale, true);
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);
targetTransform.Position = moveLogic.Update(pointerPose, targetTransform.Rotation, targetTransform.Scale, rotateInOneHandType != RotateInOneHandType.RotateAboutObjectCenter);
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)
{
MixedRealityPose pointerPose = 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 MixedRealityPose(hostTransform.position, hostTransform.rotation);
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
/// <inheritdoc />
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.HasFlag(ReleaseBehaviorType.KeepVelocity))
{
rigidBody.velocity = GetPointersVelocity();
}
if (releaseBehavior.HasFlag(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; i++)
{
if (pointer.Controller.Interactions[i].InputType == DeviceInputType.SpatialGrip)
{
rotation = pointer.Controller.Interactions[i].RotationData;
return true;
}
}
rotation = Quaternion.identity;
return false;
}
#endregion
}
}
网友评论