VR4Medical/ICI/Library/PackageCache/com.unity.xr.interaction.toolkit@76258c00be3e/Runtime/Utilities/TeleportationMonitor.cs
2025-08-20 11:12:05 +03:00

361 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.Locomotion;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Teleportation;
using UnityEngine.XR.Interaction.Toolkit.Locomotion.Turning;
using UnityEngine.XR.Interaction.Toolkit.Utilities.Pooling;
namespace UnityEngine.XR.Interaction.Toolkit.Utilities
{
/// <summary>
/// Use this class to maintain a list of Interactors that are potentially influenced by discontinuous locomotion
/// of the rig, such as teleportation and snap turn. Uses the events invoked by <see cref="LocomotionProvider"/>
/// components to detect locomotion.
/// </summary>
/// <remarks>
/// Used by the XR Grab Interactable to cancel out the effect of the teleportation from its tracked velocity
/// so it does not release at unintentionally high energy. Snap turning is treated similar to a teleport
/// where the turn velocity should also be canceled out.
/// </remarks>
/// <seealso cref="XRGrabInteractable"/>
class TeleportationMonitor
{
class PoseContainer
{
public Pose beforePose;
public Pose afterPose;
public Pose deltaPose;
// Used to avoid repeated work capturing or calculating the poses
int m_BeforeFrame = -1;
int m_AfterFrame = -1;
int m_DeltaFrame = -1;
public void CaptureBeforePose(XRBodyTransformer bodyTransformer)
{
// If the origin pose has already been captured this frame, we don't need to do it again
// since locomotion application is only done once per frame.
var currentFrame = Time.frameCount;
if (m_BeforeFrame == currentFrame)
return;
if (!LocomotionUtility.TryGetOriginTransform(bodyTransformer, out var originTransform))
return;
m_BeforeFrame = currentFrame;
beforePose = originTransform.GetWorldPose();
}
public void CaptureAfterPose(XRBodyTransformer bodyTransformer)
{
// If the origin pose has already been captured this frame, we don't need to do it again
// since locomotion application is only done once per frame.
var currentFrame = Time.frameCount;
if (m_AfterFrame == currentFrame)
return;
if (!LocomotionUtility.TryGetOriginTransform(bodyTransformer, out var originTransform))
return;
m_AfterFrame = currentFrame;
afterPose = originTransform.GetWorldPose();
}
public void CalculateDeltaPose()
{
var currentFrame = Time.frameCount;
if (m_DeltaFrame == currentFrame)
return;
var translated = afterPose.position - beforePose.position;
var rotated = afterPose.rotation * Quaternion.Inverse(beforePose.rotation);
m_DeltaFrame = currentFrame;
deltaPose = new Pose(translated, rotated);
}
}
abstract class ProviderMonitor
{
public abstract void AddInteractor(IXRInteractor interactor);
public abstract void RemoveInteractor(IXRInteractor interactor);
/// <summary>
/// The <see cref="Pose"/> of the <see cref="XROrigin"/> rig before and after locomotion.
/// Used to calculate the locomotion delta.
/// </summary>
protected static Dictionary<XRBodyTransformer, PoseContainer> s_OriginPoses;
}
class ProviderMonitor<T> : ProviderMonitor
where T : LocomotionProvider
{
public event Action<PoseContainer> providerStepped;
/// <summary>
/// The list of interactors monitored that are influenced by locomotion.
/// Consists of those that are a child GameObject of the <see cref="XROrigin"/> rig.
/// </summary>
/// <remarks>
/// There will typically only ever be one <see cref="T"/> in the scene.
/// </remarks>
Dictionary<T, List<IXRInteractor>> m_ProviderInteractors;
/// <summary>
/// References to provider instances found.
/// </summary>
static List<T> s_Providers;
static readonly LinkedPool<Dictionary<T, List<IXRInteractor>>> s_ProviderInteractorsPool =
new LinkedPool<Dictionary<T, List<IXRInteractor>>>(() => new Dictionary<T, List<IXRInteractor>>());
public static void InitializeProvidersList()
{
if (s_Providers != null)
return;
s_Providers = new List<T>();
foreach (var provider in LocomotionProvider.locomotionProviders)
{
if (provider == null)
continue;
if (provider is T providerT)
s_Providers.Add(providerT);
}
LocomotionProvider.locomotionProvidersChanged += OnLocomotionProvidersChanged;
return;
void OnLocomotionProvidersChanged(LocomotionProvider provider)
{
if (provider is T providerT)
s_Providers.Add(providerT);
// Prune the list as new locomotion providers are added so that it doesn't infinitely grow in size.
// It's likely if a new locomotion provider is added, the old rig with providers may have been destroyed.
s_Providers.RemoveAll(p => p == null);
}
}
public override void AddInteractor(IXRInteractor interactor)
{
if (interactor == null)
throw new ArgumentNullException(nameof(interactor));
var interactorTransform = interactor.transform;
if (interactorTransform == null)
return;
if (s_Providers == null)
{
InitializeProvidersList();
Debug.Assert(s_Providers != null);
}
foreach (var provider in s_Providers)
{
if (provider == null)
continue;
if (!LocomotionUtility.TryGetOriginTransform(provider, out var originTransform))
continue;
if (!interactorTransform.IsChildOf(originTransform))
continue;
m_ProviderInteractors ??= s_ProviderInteractorsPool.Get();
if (!m_ProviderInteractors.TryGetValue(provider, out var interactors))
{
interactors = new List<IXRInteractor>();
m_ProviderInteractors.Add(provider, interactors);
}
Debug.Assert(!interactors.Contains(interactor));
interactors.Add(interactor);
if (interactors.Count == 1)
{
provider.beforeStepLocomotion += OnBeforeStepLocomotion;
provider.afterStepLocomotion += OnAfterStepLocomotion;
}
}
}
public override void RemoveInteractor(IXRInteractor interactor)
{
if (interactor == null)
throw new ArgumentNullException(nameof(interactor));
var totalInteractors = 0;
if (m_ProviderInteractors != null)
{
foreach (var kvp in m_ProviderInteractors)
{
var provider = kvp.Key;
var interactors = kvp.Value;
if (provider == null)
continue;
if (interactors.Remove(interactor) && interactors.Count == 0)
{
provider.beforeStepLocomotion -= OnBeforeStepLocomotion;
provider.afterStepLocomotion -= OnAfterStepLocomotion;
}
totalInteractors += interactors.Count;
}
}
// Release back to the pool
if (totalInteractors == 0 && m_ProviderInteractors != null)
{
s_ProviderInteractorsPool.Release(m_ProviderInteractors);
m_ProviderInteractors = null;
}
}
static void CaptureOriginPoseBefore(XRBodyTransformer bodyTransformer)
{
s_OriginPoses ??= new Dictionary<XRBodyTransformer, PoseContainer>();
if (!s_OriginPoses.TryGetValue(bodyTransformer, out var poseContainer))
{
poseContainer = new PoseContainer();
s_OriginPoses[bodyTransformer] = poseContainer;
}
poseContainer.CaptureBeforePose(bodyTransformer);
}
static PoseContainer CaptureOriginPoseAfter(XRBodyTransformer bodyTransformer)
{
s_OriginPoses ??= new Dictionary<XRBodyTransformer, PoseContainer>();
if (!s_OriginPoses.TryGetValue(bodyTransformer, out var poseContainer))
{
poseContainer = new PoseContainer();
s_OriginPoses[bodyTransformer] = poseContainer;
}
poseContainer.CaptureAfterPose(bodyTransformer);
return poseContainer;
}
static void OnBeforeStepLocomotion(LocomotionProvider provider)
{
if (provider.mediator == null)
return;
CaptureOriginPoseBefore(provider.mediator.bodyTransformer);
}
void OnAfterStepLocomotion(LocomotionProvider provider)
{
if (provider.mediator == null)
return;
var poseContainer = CaptureOriginPoseAfter(provider.mediator.bodyTransformer);
providerStepped?.Invoke(poseContainer);
}
}
/// <summary>
/// Calls the methods in its invocation list when one of the Interactors monitored has been influenced by teleportation.
/// The <see cref="Pose"/> event args represents the amount the <see cref="XROrigin"/> rig was translated and rotated.
/// </summary>
public event Action<Pose, Pose, Pose> teleported;
int m_TeleportedFrame = -1;
ProviderMonitor[] m_Monitors;
void Initialize()
{
var teleportMonitor = new ProviderMonitor<TeleportationProvider>();
teleportMonitor.providerStepped += OnTeleportedAlways;
var snapMonitor = new ProviderMonitor<SnapTurnProvider>();
snapMonitor.providerStepped += OnTeleportedAlways;
var turnMonitor = new ProviderMonitor<ContinuousTurnProvider>();
turnMonitor.providerStepped += OnTeleportedTurnAround;
ProviderMonitor<TeleportationProvider>.InitializeProvidersList();
ProviderMonitor<SnapTurnProvider>.InitializeProvidersList();
ProviderMonitor<ContinuousTurnProvider>.InitializeProvidersList();
m_Monitors = new ProviderMonitor[] { teleportMonitor, snapMonitor, turnMonitor, };
}
/// <summary>
/// Adds <paramref name="interactor"/> to monitor. If it is a child of the XR Origin, <see cref="teleported"/>
/// will be invoked when the player teleports (or snap turns).
/// </summary>
/// <param name="interactor">The Interactor to add.</param>
/// <seealso cref="RemoveInteractor"/>
public void AddInteractor(IXRInteractor interactor)
{
if (m_Monitors == null)
{
Initialize();
Debug.Assert(m_Monitors != null);
}
foreach (var monitor in m_Monitors)
{
monitor.AddInteractor(interactor);
}
}
/// <summary>
/// Removes <paramref name="interactor"/> from monitor.
/// </summary>
/// <param name="interactor">The Interactor to remove.</param>
/// <seealso cref="AddInteractor"/>
public void RemoveInteractor(IXRInteractor interactor)
{
foreach (var monitor in m_Monitors)
{
monitor.RemoveInteractor(interactor);
}
}
void OnTeleportedAlways(PoseContainer poseContainer)
{
// Ensure that the public event is only invoked once across all monitors
var currentFrame = Time.frameCount;
if (m_TeleportedFrame == currentFrame)
return;
m_TeleportedFrame = currentFrame;
poseContainer.CalculateDeltaPose();
teleported?.Invoke(poseContainer.beforePose, poseContainer.afterPose, poseContainer.deltaPose);
}
void OnTeleportedTurnAround(PoseContainer poseContainer)
{
// Ensure that the public event is only invoked once across all monitors
var currentFrame = Time.frameCount;
if (m_TeleportedFrame == currentFrame)
return;
// Only consider a Turn Around as a teleport
if (Vector3.Dot(poseContainer.beforePose.forward, poseContainer.afterPose.forward) >= 0)
return;
m_TeleportedFrame = currentFrame;
poseContainer.CalculateDeltaPose();
teleported?.Invoke(poseContainer.beforePose, poseContainer.afterPose, poseContainer.deltaPose);
}
}
}