mixedreality/com.microsoft.mixedreality..../Core/Utilities/Physics/Interpolator.cs

455 lines
16 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Physics
{
/// <summary>
/// A MonoBehaviour that interpolates a transform's position, rotation or scale.
/// </summary>
[AddComponentMenu("Scripts/MRTK/Core/Interpolator")]
public class Interpolator : MonoBehaviour
{
/// <summary>
/// A very small number that is used in determining if the Interpolator needs to run at all.
/// </summary>
private const float Tolerance = 0.0000001f;
/// <summary>
/// The event fired when an Interpolation is started.
/// </summary>
public event Action InterpolationStarted;
/// <summary>
/// The event fired when an Interpolation is completed.
/// </summary>
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;
/// <summary>
/// Lerp the estimated targets towards the object each update, slowing and smoothing movement.
/// </summary>
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;
/// <summary>
/// If animating position, specifies the target position as specified
/// by SetTargetPosition. Otherwise returns the current position of
/// the transform.
/// </summary>
public Vector3 TargetPosition => AnimatingPosition ? targetPosition : transform.position;
private Vector3 targetPosition;
/// <summary>
/// If animating rotation, specifies the target rotation as specified
/// by SetTargetRotation. Otherwise returns the current rotation of
/// the transform.
/// </summary>
public Quaternion TargetRotation => AnimatingRotation ? targetRotation : transform.rotation;
private Quaternion targetRotation;
/// <summary>
/// If animating local rotation, specifies the target local rotation as
/// specified by SetTargetLocalRotation. Otherwise returns the current
/// local rotation of the transform.
/// </summary>
public Quaternion TargetLocalRotation => AnimatingLocalRotation ? targetLocalRotation : transform.localRotation;
private Quaternion targetLocalRotation;
/// <summary>
/// If animating local scale, specifies the target local scale as
/// specified by SetTargetLocalScale. Otherwise returns the current
/// local scale of the transform.
/// </summary>
public Vector3 TargetLocalScale => AnimatingLocalScale ? targetLocalScale : transform.localScale;
private Vector3 targetLocalScale;
/// <summary>
/// True if the transform's position is animating; false otherwise.
/// </summary>
public bool AnimatingPosition { get; private set; }
/// <summary>
/// True if the transform's rotation is animating; false otherwise.
/// </summary>
public bool AnimatingRotation { get; private set; }
/// <summary>
/// True if the transform's local rotation is animating; false otherwise.
/// </summary>
public bool AnimatingLocalRotation { get; private set; }
/// <summary>
/// True if the transform's scale is animating; false otherwise.
/// </summary>
public bool AnimatingLocalScale { get; private set; }
/// <summary>
/// The velocity of a transform whose position is being interpolated.
/// </summary>
public Vector3 PositionVelocity { get; private set; }
private Vector3 oldPosition = Vector3.zero;
/// <summary>
/// True if position, rotation or scale are animating; false otherwise.
/// </summary>
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;
}
}
/// <summary>
/// Stops the transform in place and terminates any animations.<para/>
/// </summary>
/// <remarks>Reset() is usually reserved as a MonoBehaviour API call in editor, but is used in this case as a convenience method.</remarks>
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
/// <summary>
/// Sets the target position for the transform and if position wasn't
/// already animating, fires the InterpolationStarted event.
/// </summary>
/// <param name="target">The new target position to for the transform.</param>
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;
}
}
/// <summary>
/// Sets the target rotation for the transform and if rotation wasn't
/// already animating, fires the InterpolationStarted event.
/// </summary>
/// <param name="target">The new target rotation for the transform.</param>
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;
}
}
/// <summary>
/// Sets the target local rotation for the transform and if rotation
/// wasn't already animating, fires the InterpolationStarted event.
/// </summary>
/// <param name="target">The new target local rotation for the transform.</param>
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;
}
}
/// <summary>
/// Sets the target local scale for the transform and if scale
/// wasn't already animating, fires the InterpolationStarted event.
/// </summary>
/// <param name="target">The new target local rotation for the transform.</param>
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;
}
}
/// <summary>
/// Interpolates smoothly to a target position.
/// </summary>
/// <param name="start">The starting position.</param>
/// <param name="target">The destination position.</param>
/// <param name="deltaTime">Caller-provided Time.deltaTime.</param>
/// <param name="speed">The speed to apply to the interpolation.</param>
/// <returns>New interpolated position closer to target</returns>
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;
}
/// <summary>
/// Snaps to the final target and stops interpolating
/// </summary>
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();
}
}
/// <summary>
/// Stops the interpolation regardless if it has reached the target
/// </summary>
public void StopInterpolating()
{
if (enabled)
{
Reset();
InterpolationDone?.Invoke();
}
}
}
}