mixedreality/com.microsoft.mixedreality..../Core/Providers/InputSimulation/SimulatedHandDataProvider.cs

317 lines
13 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using UnityEngine;
/// <summary>
/// Provides per-frame data access to simulated hand data
///
/// Controls for mouse/keyboard simulation:
/// - Press spacebar to turn right hand on/off
/// - Left mouse button brings index and thumb together
/// - Mouse moves left and right hand.
/// </summary>
namespace Microsoft.MixedReality.Toolkit.Input
{
/// <summary>
/// Internal class to define current gesture and smoothly animate hand data points.
/// </summary>
internal class SimulatedHandState : SimulatedControllerState
{
// Activate the pinch gesture
// Pinch is a special gesture that triggers the Select and TriggerPress input actions
// The pinch action doesn't occur until the gesture is completed.
public bool IsPinching => gesture == ArticulatedHandPose.GestureId.Pinch && gestureBlending == 1.0f;
private ArticulatedHandPose.GestureId gesture = ArticulatedHandPose.GestureId.None;
public ArticulatedHandPose.GestureId Gesture
{
get { return gesture; }
set
{
if (value != ArticulatedHandPose.GestureId.None && value != gesture)
{
gesture = value;
gestureBlending = 0.0f;
}
}
}
// Interpolation between current pose and target gesture
private float gestureBlending = 0.0f;
public float GestureBlending
{
get { return gestureBlending; }
set
{
gestureBlending = Mathf.Clamp(value, gestureBlending, 1.0f);
}
}
private float poseBlending = 0.0f;
private ArticulatedHandPose pose = new ArticulatedHandPose();
public SimulatedHandState(Handedness _handedness) : base(_handedness) { }
public void ResetGesture()
{
gestureBlending = 1.0f;
ArticulatedHandPose gesturePose = SimulatedArticulatedHandPoses.GetGesturePose(gesture);
if (gesturePose != null)
{
pose.Copy(gesturePose);
}
}
public override void ResetRotation()
{
ViewportRotation = Vector3.zero;
}
internal void FillCurrentFrame(MixedRealityPose[] jointsOut)
{
ArticulatedHandPose gesturePose = SimulatedArticulatedHandPoses.GetGesturePose(gesture);
if (gesturePose != null)
{
if (gestureBlending > poseBlending)
{
float range = Mathf.Clamp01(1.0f - poseBlending);
float lerpFactor = range > 0.0f ? (gestureBlending - poseBlending) / range : 1.0f;
pose.InterpolateOffsets(pose, gesturePose, lerpFactor);
}
}
poseBlending = gestureBlending;
Vector3 screenPosition = CameraCache.Main.ViewportToScreenPoint(ViewportPosition);
Vector3 worldPosition = CameraCache.Main.ScreenToWorldPoint(screenPosition + JitterOffset);
Quaternion worldRotation = CameraCache.Main.transform.rotation * Quaternion.Euler(ViewportRotation);
pose.ComputeJointPoses(handedness, worldRotation, worldPosition, jointsOut);
}
}
/// <summary>
/// Produces simulated data every frame that defines joint positions.
/// </summary>
public class SimulatedHandDataProvider : SimulatedControllerDataProvider
{
// Cached delegates for hand joint generation
private SimulatedHandData.HandJointDataGenerator generatorLeft;
private SimulatedHandData.HandJointDataGenerator generatorRight;
private SimulatedHandData.HandJointDataGenerator generatorGaze;
public SimulatedHandDataProvider(MixedRealityInputSimulationProfile _profile) : base(_profile)
{
InputStateLeft = new SimulatedHandState(Handedness.Left);
InputStateRight = new SimulatedHandState(Handedness.Right);
InputStateGaze = new SimulatedHandState(Handedness.None);
SimulatedHandState handStateLeft = InputStateLeft as SimulatedHandState;
SimulatedHandState handStateRight = InputStateRight as SimulatedHandState;
SimulatedHandState handStateGaze = InputStateGaze as SimulatedHandState;
handStateLeft.Gesture = profile.DefaultHandGesture;
handStateRight.Gesture = profile.DefaultHandGesture;
handStateGaze.Gesture = profile.DefaultHandGesture;
}
/// <summary>
/// Capture a snapshot of simulated hand data based on current state.
/// </summary>
public bool UpdateHandData(SimulatedHandData handDataLeft, SimulatedHandData handDataRight, SimulatedHandData handDataGaze, MouseDelta mouseDelta)
{
SimulateUserInput(mouseDelta);
SimulatedHandState handStateLeft = InputStateLeft as SimulatedHandState;
SimulatedHandState handStateRight = InputStateRight as SimulatedHandState;
SimulatedHandState handStateGaze = InputStateGaze as SimulatedHandState;
handStateLeft.Update();
handStateRight.Update();
handStateGaze.Update();
bool handDataChanged = false;
// Cache the generator delegates so we don't gc alloc every frame
if (generatorLeft == null)
{
generatorLeft = handStateLeft.FillCurrentFrame;
}
if (generatorRight == null)
{
generatorRight = handStateRight.FillCurrentFrame;
}
if (generatorGaze == null)
{
generatorGaze = handStateGaze.FillCurrentFrame;
}
handDataChanged |= handDataLeft.Update(handStateLeft.IsTracked, handStateLeft.IsPinching, generatorLeft);
handDataChanged |= handDataRight.Update(handStateRight.IsTracked, handStateRight.IsPinching, generatorRight);
handDataChanged |= handDataGaze.Update(handStateGaze.IsTracked, handStateGaze.IsPinching, generatorGaze);
return handDataChanged;
}
/// <summary>
/// Update hand state based on keyboard and mouse input
/// </summary>
protected override void SimulateUserInput(MouseDelta mouseDelta)
{
base.SimulateUserInput(mouseDelta);
SimulatedHandState handStateLeft = InputStateLeft as SimulatedHandState;
SimulatedHandState handStateRight = InputStateRight as SimulatedHandState;
SimulatedHandState handStateGaze = InputStateGaze as SimulatedHandState;
// This line explicitly uses unscaledDeltaTime because we don't want input simulation
// to lag when the time scale is set to a value other than 1. Input should still continue
// to move freely.
float gestureAnimDelta = profile.HandGestureAnimationSpeed * Time.unscaledDeltaTime;
handStateLeft.GestureBlending += gestureAnimDelta;
handStateRight.GestureBlending += gestureAnimDelta;
handStateGaze.GestureBlending = 1.0f;
}
/// Apply changes to one hand and update tracking
internal override void SimulateInput(
ref long lastHandTrackedTimestamp,
SimulatedControllerState state,
bool isSimulating,
bool isAlwaysVisible,
MouseDelta mouseDelta,
bool useMouseRotation)
{
var handState = state as SimulatedHandState;
bool enableTracking = isAlwaysVisible || isSimulating;
if (!handState.IsTracked && enableTracking)
{
ResetInput(handState, isSimulating);
}
if (isSimulating)
{
handState.SimulateInput(mouseDelta, useMouseRotation, profile.MouseRotationSensitivity, profile.MouseControllerRotationSpeed, profile.ControllerJitterAmount);
if (isAlwaysVisible)
{
// Toggle gestures on/off
handState.Gesture = ToggleGesture(handState.Gesture);
}
else
{
// Enable gesture while mouse button is pressed
handState.Gesture = SelectGesture();
}
}
// Update tracked state of a hand.
// If hideTimeout value is null, hands will stay visible after tracking stops.
// TODO: DateTime.UtcNow can be quite imprecise, better use Stopwatch.GetTimestamp
// https://stackoverflow.com/questions/2143140/c-sharp-datetime-now-precision
DateTime currentTime = DateTime.UtcNow;
if (enableTracking)
{
handState.IsTracked = true;
lastHandTrackedTimestamp = currentTime.Ticks;
}
else
{
float timeSinceTracking = (float)currentTime.Subtract(new DateTime(lastHandTrackedTimestamp)).TotalSeconds;
if (timeSinceTracking > profile.ControllerHideTimeout)
{
handState.IsTracked = false;
}
}
}
internal override void ResetInput(SimulatedControllerState state, bool isSimulating)
{
base.ResetInput(state, isSimulating);
var handState = state as SimulatedHandState;
handState.Gesture = profile.DefaultHandGesture;
handState.ResetGesture();
handState.ResetRotation();
}
/// <summary>
/// Gets the currently active gesture, according to the mouse configuration and mouse button that is down.
/// </summary>
private ArticulatedHandPose.GestureId SelectGesture()
{
// Each check needs to verify that both:
// 1) The corresponding mouse button is down (meaning the gesture, if defined, should be used)
// 2) The gesture is defined.
// If only #1 is checked and #2 is not checked, it's possible to "miss" transitions in cases where the user has
// the left mouse button down and then while it is down, presses the right button, and then lifts the left.
// It's not until both mouse buttons lift in that case, that the state finally "rests" to the DefaultHandGesture.
if (KeyInputSystem.GetKey(profile.InteractionButton) && profile.LeftMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return profile.LeftMouseHandGesture;
}
else if (KeyInputSystem.GetKey(profile.MouseLookButton) && profile.RightMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return profile.RightMouseHandGesture;
}
else if (KeyInputSystem.GetKey(KeyBinding.FromMouseButton(KeyBinding.MouseButton.Middle)) && profile.MiddleMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return profile.MiddleMouseHandGesture;
}
else
{
return profile.DefaultHandGesture;
}
}
private ArticulatedHandPose.GestureId ToggleGesture(ArticulatedHandPose.GestureId gesture)
{
// See comments in SelectGesture for why both the button down and gesture are checked.
if (KeyInputSystem.GetKeyDown(profile.InteractionButton) && profile.LeftMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return (gesture != profile.LeftMouseHandGesture ? profile.LeftMouseHandGesture : profile.DefaultHandGesture);
}
else if (KeyInputSystem.GetKeyDown(profile.MouseLookButton) && profile.RightMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return (gesture != profile.RightMouseHandGesture ? profile.RightMouseHandGesture : profile.DefaultHandGesture);
}
else if (KeyInputSystem.GetKeyDown(KeyBinding.FromMouseButton(KeyBinding.MouseButton.Middle)) && profile.MiddleMouseHandGesture != ArticulatedHandPose.GestureId.None)
{
return (gesture != profile.MiddleMouseHandGesture ? profile.MiddleMouseHandGesture : profile.DefaultHandGesture);
}
else
{
// 'None' will not change the gesture
return ArticulatedHandPose.GestureId.None;
}
}
#region Obsolete Fields
[Obsolete("Use InputStateLeft instead.")]
internal SimulatedHandState HandStateLeft
{
get => InputStateLeft as SimulatedHandState;
set { InputStateLeft = value; }
}
[Obsolete("Use InputStateRight instead.")]
internal SimulatedHandState HandStateRight
{
get => InputStateRight as SimulatedHandState;
set { InputStateRight = value; }
}
[Obsolete("Use InputStateGaze instead.")]
internal SimulatedHandState HandStateGaze
{
get => InputStateGaze as SimulatedHandState;
set { InputStateGaze = value; }
}
#endregion
}
}