// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.Utilities; #if UNITY_WSA using Unity.Profiling; using UnityEngine; using UnityEngine.XR.WSA.Input; #endif namespace Microsoft.MixedReality.Toolkit.WindowsMixedReality.Input { /// /// A Windows Mixed Reality Source Instance. /// public abstract class BaseWindowsMixedRealitySource : BaseController { /// /// Constructor. /// protected BaseWindowsMixedRealitySource( TrackingState trackingState, Handedness sourceHandedness, IMixedRealityInputSource inputSource = null, MixedRealityInteractionMapping[] interactions = null, IMixedRealityInputSourceDefinition definition = null) : base(trackingState, sourceHandedness, inputSource, interactions, definition) { } #if UNITY_WSA /// /// The last updated source state reading for this Windows Mixed Reality Source. /// public InteractionSourceState LastSourceStateReading { get; protected set; } private Vector3 currentSourcePosition = Vector3.zero; private Quaternion currentSourceRotation = Quaternion.identity; private MixedRealityPose lastSourcePose = MixedRealityPose.ZeroIdentity; private MixedRealityPose currentSourcePose = MixedRealityPose.ZeroIdentity; private Vector3 currentPointerPosition = Vector3.zero; private Quaternion currentPointerRotation = Quaternion.identity; private MixedRealityPose currentPointerPose = MixedRealityPose.ZeroIdentity; private MixedRealityPose currentGripPose = MixedRealityPose.ZeroIdentity; #region Update data functions private static readonly ProfilerMarker UpdateControllerPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdateController"); /// /// Update the source data from the provided platform state. /// /// The InteractionSourceState retrieved from the platform. public virtual void UpdateController(InteractionSourceState interactionSourceState) { using (UpdateControllerPerfMarker.Auto()) { if (!Enabled) { return; } UpdateSixDofData(interactionSourceState); if (Interactions == null) { Debug.LogError($"No interaction configuration for Windows Mixed Reality {ControllerHandedness} Source"); Enabled = false; } for (int i = 0; i < Interactions?.Length; i++) { switch (Interactions[i].InputType) { case DeviceInputType.None: break; case DeviceInputType.Select: case DeviceInputType.Trigger: case DeviceInputType.TriggerTouch: case DeviceInputType.TriggerPress: case DeviceInputType.GripPress: UpdateTriggerData(interactionSourceState, Interactions[i]); break; } } LastSourceStateReading = interactionSourceState; } } protected void UpdateSixDofData(InteractionSourceState interactionSourceState) { UpdateSourceData(interactionSourceState); UpdateVelocity(interactionSourceState); for (int i = 0; i < Interactions?.Length; i++) { switch (Interactions[i].InputType) { case DeviceInputType.None: break; case DeviceInputType.SpatialPointer: UpdatePointerData(interactionSourceState, Interactions[i]); break; case DeviceInputType.SpatialGrip: UpdateGripData(interactionSourceState, Interactions[i]); break; } } } private static readonly ProfilerMarker UpdateVelocityPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdateVelocity"); public void UpdateVelocity(InteractionSourceState interactionSourceState) { using (UpdateVelocityPerfMarker.Auto()) { Vector3 newVelocity; if (interactionSourceState.sourcePose.TryGetVelocity(out newVelocity)) { Velocity = newVelocity; } Vector3 newAngularVelocity; if (interactionSourceState.sourcePose.TryGetAngularVelocity(out newAngularVelocity)) { AngularVelocity = newAngularVelocity; } } } private static readonly ProfilerMarker UpdateSourceDataPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdateSourceData"); /// /// Update the source input from the device. /// /// The InteractionSourceState retrieved from the platform. private void UpdateSourceData(InteractionSourceState interactionSourceState) { using (UpdateSourceDataPerfMarker.Auto()) { var lastState = TrackingState; var sourceKind = interactionSourceState.source.kind; lastSourcePose = currentSourcePose; if (sourceKind == InteractionSourceKind.Hand || (sourceKind == InteractionSourceKind.Controller && interactionSourceState.source.supportsPointing)) { // The source is either a hand or a controller that supports pointing. // We can now check for position and rotation. IsPositionAvailable = interactionSourceState.sourcePose.TryGetPosition(out currentSourcePosition); if (IsPositionAvailable) { IsPositionApproximate = (interactionSourceState.sourcePose.positionAccuracy == InteractionSourcePositionAccuracy.Approximate); } else { IsPositionApproximate = false; } IsRotationAvailable = interactionSourceState.sourcePose.TryGetRotation(out currentSourceRotation); // We want the source to follow the Playspace, so fold in the playspace transform here to // put the source pose into world space. currentSourcePosition = MixedRealityPlayspace.TransformPoint(currentSourcePosition); currentSourceRotation = MixedRealityPlayspace.Rotation * currentSourceRotation; // Devices are considered tracked if we receive position OR rotation data from the sensors. TrackingState = (IsPositionAvailable || IsRotationAvailable) ? TrackingState.Tracked : TrackingState.NotTracked; } else { // The input source does not support tracking. TrackingState = TrackingState.NotApplicable; } currentSourcePose.Position = currentSourcePosition; currentSourcePose.Rotation = currentSourceRotation; // Raise input system events if it is enabled. if (lastState != TrackingState) { CoreServices.InputSystem?.RaiseSourceTrackingStateChanged(InputSource, this, TrackingState); } if (TrackingState == TrackingState.Tracked && lastSourcePose != currentSourcePose) { if (IsPositionAvailable && IsRotationAvailable) { CoreServices.InputSystem?.RaiseSourcePoseChanged(InputSource, this, currentSourcePose); } else if (IsPositionAvailable && !IsRotationAvailable) { CoreServices.InputSystem?.RaiseSourcePositionChanged(InputSource, this, currentSourcePosition); } else if (!IsPositionAvailable && IsRotationAvailable) { CoreServices.InputSystem?.RaiseSourceRotationChanged(InputSource, this, currentSourceRotation); } } } } private static readonly ProfilerMarker UpdatePointerDataPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdatePointerData"); /// /// Update the spatial pointer input from the device. /// /// The InteractionSourceState retrieved from the platform. private void UpdatePointerData(InteractionSourceState interactionSourceState, MixedRealityInteractionMapping interactionMapping) { using (UpdatePointerDataPerfMarker.Auto()) { if (interactionSourceState.source.supportsPointing) { interactionSourceState.sourcePose.TryGetPosition(out currentPointerPosition, InteractionSourceNode.Pointer); interactionSourceState.sourcePose.TryGetRotation(out currentPointerRotation, InteractionSourceNode.Pointer); // We want the source to follow the Playspace, so fold in the playspace transform here to // put the source pose into world space. currentPointerPose.Position = MixedRealityPlayspace.TransformPoint(currentPointerPosition); currentPointerPose.Rotation = MixedRealityPlayspace.Rotation * currentPointerRotation; } // Update the interaction data source interactionMapping.PoseData = currentPointerPose; // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled CoreServices.InputSystem?.RaisePoseInputChanged(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction, currentPointerPose); } } } private static readonly ProfilerMarker UpdateGripDataPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdateGripData"); /// /// Update the spatial grip input from the device. /// /// The InteractionSourceState retrieved from the platform. private void UpdateGripData(InteractionSourceState interactionSourceState, MixedRealityInteractionMapping interactionMapping) { using (UpdateGripDataPerfMarker.Auto()) { switch (interactionMapping.AxisType) { case AxisType.SixDof: { // The data queried in UpdateSourceData is the grip pose. // Reuse that data to save two method calls and transforms. currentGripPose.Position = currentSourcePosition; currentGripPose.Rotation = currentSourceRotation; // Update the interaction data source interactionMapping.PoseData = currentGripPose; // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled CoreServices.InputSystem?.RaisePoseInputChanged(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction, currentGripPose); } } break; } } } private static readonly ProfilerMarker UpdateTriggerDataPerfMarker = new ProfilerMarker("[MRTK] BaseWindowsMixedRealitySource.UpdateTriggerData"); /// /// Update the trigger and grasped input from the device. /// /// The InteractionSourceState retrieved from the platform. private void UpdateTriggerData(InteractionSourceState interactionSourceState, MixedRealityInteractionMapping interactionMapping) { using (UpdateTriggerDataPerfMarker.Auto()) { switch (interactionMapping.InputType) { case DeviceInputType.TriggerPress: case DeviceInputType.GripPress: { // Update the interaction data source interactionMapping.BoolData = interactionSourceState.grasped; // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled if (interactionMapping.BoolData) { CoreServices.InputSystem?.RaiseOnInputDown(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } else { CoreServices.InputSystem?.RaiseOnInputUp(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } } break; } case DeviceInputType.Select: { // Get the select pressed state, factoring in a workaround for Unity issue #1033526. // When that issue is fixed, it should be possible change the line below to: // interactionMapping.BoolData = interactionSourceState.selectPressed; interactionMapping.BoolData = GetSelectPressedWorkaround(interactionSourceState); // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled if (interactionMapping.BoolData) { CoreServices.InputSystem?.RaiseOnInputDown(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } else { CoreServices.InputSystem?.RaiseOnInputUp(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } } break; } case DeviceInputType.Trigger: { // Update the interaction data source interactionMapping.FloatData = interactionSourceState.selectPressedAmount; // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled CoreServices.InputSystem?.RaiseFloatInputChanged(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction, interactionSourceState.selectPressedAmount); } break; } case DeviceInputType.TriggerTouch: { // Update the interaction data source interactionMapping.BoolData = interactionSourceState.selectPressedAmount > 0; // If our value changed raise it. if (interactionMapping.Changed) { // Raise input system event if it's enabled if (interactionSourceState.selectPressedAmount > 0) { CoreServices.InputSystem?.RaiseOnInputDown(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } else { CoreServices.InputSystem?.RaiseOnInputUp(InputSource, ControllerHandedness, interactionMapping.MixedRealityInputAction); } } break; } } } } /// /// Gets whether or not 'select' has been pressed. /// /// /// This includes a workaround to fix air-tap gestures in HoloLens 1 remoting, to work around the following Unity issue: /// https://issuetracker.unity3d.com/issues/hololens-interactionsourcestate-dot-selectpressed-is-false-when-air-tap-and-hold /// Bug was discovered May 2018 and still exists as of May 2019 in version 2018.3.11f1. This workaround is scoped to only /// cases where remoting is active. /// private bool GetSelectPressedWorkaround(InteractionSourceState interactionSourceState) { bool selectPressed = interactionSourceState.selectPressed; // Only do this workaround inside the Unity editor (in holographic remoting scenarios). // When this is invoked on device, this will display an error attempting to load the // remoting binaries. #if UNITY_EDITOR if (interactionSourceState.source.kind == InteractionSourceKind.Hand && UnityEngine.XR.WSA.HolographicRemoting.ConnectionState == UnityEngine.XR.WSA.HolographicStreamerConnectionState.Connected && !interactionSourceState.source.supportsGrasp) // Check that we're not remoting to a HoloLens 2 { // This workaround is safe as long as all these assumptions hold: Debug.Assert(!interactionSourceState.selectPressed, "Unity issue #1033526 seems to have been resolved. Please remove this workaround!"); Debug.Assert(!interactionSourceState.source.supportsGrasp); Debug.Assert(!interactionSourceState.source.supportsMenu); Debug.Assert(!interactionSourceState.source.supportsPointing); Debug.Assert(!interactionSourceState.source.supportsThumbstick); Debug.Assert(!interactionSourceState.source.supportsTouchpad); selectPressed = interactionSourceState.anyPressed; } #endif // UNITY_EDITOR return selectPressed; } #endregion Update data functions #endif // UNITY_WSA } }