// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Physics { /// /// A MonoBehaviour that interpolates a transform's position, rotation or scale. /// [AddComponentMenu("Scripts/MRTK/Core/Interpolator")] public class Interpolator : MonoBehaviour { /// /// A very small number that is used in determining if the Interpolator needs to run at all. /// private const float Tolerance = 0.0000001f; /// /// The event fired when an Interpolation is started. /// public event Action InterpolationStarted; /// /// The event fired when an Interpolation is completed. /// public event Action InterpolationDone; [SerializeField] [Tooltip("When interpolating, use unscaled time. This is useful for games that have a pause mechanism or otherwise adjust the game timescale.")] private bool useUnscaledTime = true; [SerializeField] [Tooltip("The movement speed in meters per second.")] private float positionPerSecond = 30.0f; [SerializeField] [Tooltip("The rotation speed, in degrees per second.")] private float rotationDegreesPerSecond = 720.0f; [SerializeField] [Tooltip("Adjusts rotation speed based on angular distance.")] private float rotationSpeedScaler = 0.0f; [SerializeField] [Tooltip("The amount to scale per second.")] private float scalePerSecond = 5.0f; /// /// Lerp the estimated targets towards the object each update, slowing and smoothing movement. /// public bool SmoothLerpToTarget { get; set; } = false; public float SmoothPositionLerpRatio { get; set; } = 0.5f; public float SmoothRotationLerpRatio { get; set; } = 0.5f; public float SmoothScaleLerpRatio { get; set; } = 0.5f; /// /// If animating position, specifies the target position as specified /// by SetTargetPosition. Otherwise returns the current position of /// the transform. /// public Vector3 TargetPosition => AnimatingPosition ? targetPosition : transform.position; private Vector3 targetPosition; /// /// If animating rotation, specifies the target rotation as specified /// by SetTargetRotation. Otherwise returns the current rotation of /// the transform. /// public Quaternion TargetRotation => AnimatingRotation ? targetRotation : transform.rotation; private Quaternion targetRotation; /// /// If animating local rotation, specifies the target local rotation as /// specified by SetTargetLocalRotation. Otherwise returns the current /// local rotation of the transform. /// public Quaternion TargetLocalRotation => AnimatingLocalRotation ? targetLocalRotation : transform.localRotation; private Quaternion targetLocalRotation; /// /// If animating local scale, specifies the target local scale as /// specified by SetTargetLocalScale. Otherwise returns the current /// local scale of the transform. /// public Vector3 TargetLocalScale => AnimatingLocalScale ? targetLocalScale : transform.localScale; private Vector3 targetLocalScale; /// /// True if the transform's position is animating; false otherwise. /// public bool AnimatingPosition { get; private set; } /// /// True if the transform's rotation is animating; false otherwise. /// public bool AnimatingRotation { get; private set; } /// /// True if the transform's local rotation is animating; false otherwise. /// public bool AnimatingLocalRotation { get; private set; } /// /// True if the transform's scale is animating; false otherwise. /// public bool AnimatingLocalScale { get; private set; } /// /// The velocity of a transform whose position is being interpolated. /// public Vector3 PositionVelocity { get; private set; } private Vector3 oldPosition = Vector3.zero; /// /// True if position, rotation or scale are animating; false otherwise. /// public bool Running => AnimatingPosition || AnimatingRotation || AnimatingLocalRotation || AnimatingLocalScale; #region MonoBehaviour Implementation private void Awake() { targetPosition = transform.position; targetRotation = transform.rotation; targetLocalRotation = transform.localRotation; targetLocalScale = transform.localScale; enabled = false; } private void Update() { float deltaTime = useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime; bool interpOccuredThisFrame = false; if (AnimatingPosition) { Vector3 lerpTargetPosition = targetPosition; if (SmoothLerpToTarget) { lerpTargetPosition = Vector3.Lerp(transform.position, lerpTargetPosition, SmoothPositionLerpRatio); } Vector3 newPosition = NonLinearInterpolateTo(transform.position, lerpTargetPosition, deltaTime, positionPerSecond); if ((targetPosition - newPosition).sqrMagnitude <= Tolerance) { // Snap to final position newPosition = targetPosition; AnimatingPosition = false; } else { interpOccuredThisFrame = true; } transform.position = newPosition; // Calculate interpolatedVelocity and store position for next frame PositionVelocity = oldPosition - newPosition; oldPosition = newPosition; } // Determine how far we need to rotate if (AnimatingRotation) { Quaternion lerpTargetRotation = targetRotation; if (SmoothLerpToTarget) { lerpTargetRotation = Quaternion.Lerp(transform.rotation, lerpTargetRotation, SmoothRotationLerpRatio); } float angleDiff = Quaternion.Angle(transform.rotation, lerpTargetRotation); float speedScale = 1.0f + (Mathf.Pow(angleDiff, rotationSpeedScaler) / 180.0f); float ratio = Mathf.Clamp01((speedScale * rotationDegreesPerSecond * deltaTime) / angleDiff); if (angleDiff < Mathf.Epsilon) { AnimatingRotation = false; transform.rotation = targetRotation; } else { // Only lerp rotation here, as ratio is NaN if angleDiff is 0.0f transform.rotation = Quaternion.Slerp(transform.rotation, lerpTargetRotation, ratio); interpOccuredThisFrame = true; } } // Determine how far we need to rotate if (AnimatingLocalRotation) { Quaternion lerpTargetLocalRotation = targetLocalRotation; if (SmoothLerpToTarget) { lerpTargetLocalRotation = Quaternion.Lerp(transform.localRotation, lerpTargetLocalRotation, SmoothRotationLerpRatio); } float angleDiff = Quaternion.Angle(transform.localRotation, lerpTargetLocalRotation); float speedScale = 1.0f + (Mathf.Pow(angleDiff, rotationSpeedScaler) / 180.0f); float ratio = Mathf.Clamp01((speedScale * rotationDegreesPerSecond * deltaTime) / angleDiff); if (angleDiff < Mathf.Epsilon) { AnimatingLocalRotation = false; transform.localRotation = targetLocalRotation; } else { // Only lerp rotation here, as ratio is NaN if angleDiff is 0.0f transform.localRotation = Quaternion.Slerp(transform.localRotation, lerpTargetLocalRotation, ratio); interpOccuredThisFrame = true; } } if (AnimatingLocalScale) { Vector3 lerpTargetLocalScale = targetLocalScale; if (SmoothLerpToTarget) { lerpTargetLocalScale = Vector3.Lerp(transform.localScale, lerpTargetLocalScale, SmoothScaleLerpRatio); } Vector3 newScale = NonLinearInterpolateTo(transform.localScale, lerpTargetLocalScale, deltaTime, scalePerSecond); if ((targetLocalScale - newScale).sqrMagnitude <= Tolerance) { // Snap to final scale newScale = targetLocalScale; AnimatingLocalScale = false; } else { interpOccuredThisFrame = true; } transform.localScale = newScale; } // If all interpolations have completed, stop updating if (!interpOccuredThisFrame) { InterpolationDone?.Invoke(); enabled = false; } } /// /// Stops the transform in place and terminates any animations. /// /// Reset() is usually reserved as a MonoBehaviour API call in editor, but is used in this case as a convenience method. public void Reset() { targetPosition = transform.position; targetRotation = transform.rotation; targetLocalRotation = transform.localRotation; targetLocalScale = transform.localScale; AnimatingPosition = false; AnimatingRotation = false; AnimatingLocalRotation = false; AnimatingLocalScale = false; enabled = false; } #endregion MonoBehaviour Implementation /// /// Sets the target position for the transform and if position wasn't /// already animating, fires the InterpolationStarted event. /// /// The new target position to for the transform. public void SetTargetPosition(Vector3 target) { bool wasRunning = Running; targetPosition = target; float magsq = (targetPosition - transform.position).sqrMagnitude; if (magsq > Tolerance) { AnimatingPosition = true; enabled = true; if (InterpolationStarted != null && !wasRunning) { InterpolationStarted(); } } else { // Set immediately to prevent accumulation of error. transform.position = target; AnimatingPosition = false; } } /// /// Sets the target rotation for the transform and if rotation wasn't /// already animating, fires the InterpolationStarted event. /// /// The new target rotation for the transform. public void SetTargetRotation(Quaternion target) { bool wasRunning = Running; targetRotation = target; if (Quaternion.Dot(transform.rotation, target) < 1.0f) { AnimatingRotation = true; enabled = true; if (InterpolationStarted != null && !wasRunning) { InterpolationStarted(); } } else { // Set immediately to prevent accumulation of error. transform.rotation = target; AnimatingRotation = false; } } /// /// Sets the target local rotation for the transform and if rotation /// wasn't already animating, fires the InterpolationStarted event. /// /// The new target local rotation for the transform. public void SetTargetLocalRotation(Quaternion target) { bool wasRunning = Running; targetLocalRotation = target; if (Quaternion.Dot(transform.localRotation, target) < 1.0f) { AnimatingLocalRotation = true; enabled = true; if (InterpolationStarted != null && !wasRunning) { InterpolationStarted(); } } else { // Set immediately to prevent accumulation of error. transform.localRotation = target; AnimatingLocalRotation = false; } } /// /// Sets the target local scale for the transform and if scale /// wasn't already animating, fires the InterpolationStarted event. /// /// The new target local rotation for the transform. public void SetTargetLocalScale(Vector3 target) { bool wasRunning = Running; targetLocalScale = target; float magsq = (targetLocalScale - transform.localScale).sqrMagnitude; if (magsq > Mathf.Epsilon) { AnimatingLocalScale = true; enabled = true; if (InterpolationStarted != null && !wasRunning) { InterpolationStarted(); } } else { // set immediately to prevent accumulation of error transform.localScale = target; AnimatingLocalScale = false; } } /// /// Interpolates smoothly to a target position. /// /// The starting position. /// The destination position. /// Caller-provided Time.deltaTime. /// The speed to apply to the interpolation. /// New interpolated position closer to target public static Vector3 NonLinearInterpolateTo(Vector3 start, Vector3 target, float deltaTime, float speed) { // If no interpolation speed, jump to target value. if (speed <= 0.0f) { return target; } Vector3 distance = (target - start); // When close enough, jump to the target if (distance.sqrMagnitude <= Mathf.Epsilon) { return target; } // Apply the delta, then clamp so we don't overshoot the target Vector3 deltaMove = distance * Mathf.Clamp(deltaTime * speed, 0.0f, 1.0f); return start + deltaMove; } /// /// Snaps to the final target and stops interpolating /// public void SnapToTarget() { if (enabled) { transform.position = TargetPosition; transform.rotation = TargetRotation; transform.localRotation = TargetLocalRotation; transform.localScale = TargetLocalScale; AnimatingPosition = false; AnimatingLocalScale = false; AnimatingRotation = false; AnimatingLocalRotation = false; enabled = false; InterpolationDone?.Invoke(); } } /// /// Stops the interpolation regardless if it has reached the target /// public void StopInterpolating() { if (enabled) { Reset(); InterpolationDone?.Invoke(); } } } }