// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System.Collections.Generic; using UnityEngine; using UnityEngine.Assertions; namespace HoloToolkit.Unity.InputModule.Utilities.Interactions { /// <summary> /// Implements common logic for rotating holograms using a handlebar metaphor. /// each frame, object_rotation_delta = rotation_delta(current_hands_vector, previous_hands_vector) /// where hands_vector is the vector between two hand/controller positions. /// /// Usage: /// When a manipulation starts, call Setup. /// Call Update any time to update the move logic and get a new rotation for the object. /// </summary> public class TwoHandRotateLogic { /// <summary> /// Private variables /// </summary> private const float MinHandDistanceForPitchM = 0.1f; /// <summary> /// a scalar applied to Rotation angle generated by handlebar calculations. /// </summary> private const float RotationMultiplier = 2f; /// <summary> /// This enum value stores the initial constraint specified as an argument in the Constructor. /// It may be overridden by runtime conditions. The actual constraint is stored in the /// variable currentRotationConstraint. /// </summary> private AxisConstraint rotationConstraint; /// <summary> /// Vector storing last handlebar Rotation. The handlebar is the line imagined between the controllers/hands. /// </summary> private Vector3 previousHandlebarRotation; /// <summary> /// The current rotation constraint might be modified based on disambiguation logic, for example /// XOrYBasedOnInitialHandPosition might change the current rotation constraint based on the /// initial hand positions at the start /// </summary> private AxisConstraint currentRotationConstraint; /// <summary> /// ProjectHandlebarGivenConstraint internal function to account for axis constraint /// </summary> /// <param name="constraint">Enum value describing the axis to which the rotation is constrained/param> /// <param name="handlebarRotation">A Vector3 describing the rotation of the line connecting the inputSources</param> /// <param name="manipulationRoot">Transform of gameObject to be two hand manipulated</param> /// <returns>a Vector3 describing handlebar after constraint is applied</returns> private Vector3 ProjectHandlebarGivenConstraint(AxisConstraint constraint, Vector3 handlebarRotation, Transform manipulationRoot) { Vector3 result = handlebarRotation; switch (constraint) { case AxisConstraint.XAxisOnly: result.x = 0; break; case AxisConstraint.YAxisOnly: result.y = 0; break; case AxisConstraint.ZAxisOnly: result.z = 0; break; } return CameraCache.Main.transform.TransformDirection(result); } /// <summary> /// GetHandlebarDirection internal function to get rotation described by inputSources. /// </summary> /// <param name="handsPressedMap">Dictionary listing inputSourceStates</param> /// <param name="manipulationRoot">Transform of gameObject to be two hand manipulated</param> /// <returns>A Vector3 describing the direction of the line connecting the inputSources</returns> private Vector3 GetHandlebarDirection(Dictionary<uint, Vector3> handsPressedMap, Transform manipulationRoot) { var handsEnumerator = handsPressedMap.Values.GetEnumerator(); handsEnumerator.MoveNext(); var hand1 = handsEnumerator.Current; handsEnumerator.MoveNext(); var hand2 = handsEnumerator.Current; // We project the handlebar direction into camera space because otherwise when we move our body the handlebard will move even // though, relative to our heads, the handlebar is not moving. hand1 = CameraCache.Main.transform.InverseTransformPoint(hand1); hand2 = CameraCache.Main.transform.InverseTransformPoint(hand2); return hand2 - hand1; } /// <summary> /// TwoHandRotateLogic Constructor /// </summary> /// <param name="constrainRotation">Enum describing to which axis the rotation is constrained</param> public TwoHandRotateLogic(AxisConstraint constrainRotation) { rotationConstraint = constrainRotation; } /// <summary> /// Initializes twohand system with controller/hand source info: /// the Dictionary collection already filled with controller/hand info, /// and the Transform of the GameObject to be manipulated. /// </summary> /// <param name="handsPressedMap">Dictionary listing inputSourceStates</param> /// <param name="manipulationRoot">Transform of gameObject to be two hand manipulated</param> public void Setup(Dictionary<uint, Vector3> handsPressedMap, Transform manipulationRoot) { currentRotationConstraint = rotationConstraint; previousHandlebarRotation = GetHandlebarDirection(handsPressedMap, manipulationRoot); } /// <summary> /// Updates internal states based on current Controller/hand states. /// </summary> /// <param name="handsPressedMap">Dictionary listing inputSourceStates</param> /// <param name="manipulationRoot">Transform of gameObject to be two hand manipulated</param> /// <param name="currentRotation">New rotation to be applied</param> /// <returns>Quaternion describing rotation based on position of inputSources - Controllers/Hands</returns> public Quaternion Update(Dictionary<uint, Vector3> handsPressedMap, Transform manipulationRoot, Quaternion currentRotation) { var handlebarDirection = GetHandlebarDirection(handsPressedMap, manipulationRoot); var handlebarDirectionProjected = ProjectHandlebarGivenConstraint(currentRotationConstraint, handlebarDirection, manipulationRoot); var prevHandlebarDirectionProjected = ProjectHandlebarGivenConstraint(currentRotationConstraint, previousHandlebarRotation, manipulationRoot); previousHandlebarRotation = handlebarDirection; var rotationDelta = Quaternion.FromToRotation(prevHandlebarDirectionProjected, handlebarDirectionProjected); var angle = 0f; var axis = Vector3.zero; rotationDelta.ToAngleAxis(out angle, out axis); angle *= RotationMultiplier; if (currentRotationConstraint == AxisConstraint.YAxisOnly) { // If we are rotating about Y axis, then make sure we rotate about global Y axis. // Since the angle is obtained from a quaternion, we need to properly orient it (up or down) based // on the original axis-angle representation. axis = Vector3.up * Vector3.Dot(axis, Vector3.up); } return Quaternion.AngleAxis(angle, axis) * currentRotation; } } }