// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using System; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Utilities.Solvers { /// /// The base abstract class for all Solvers to derive from. It provides state tracking, smoothing parameters /// and implementation, automatic solver system integration, and update order. Solvers may be used without a link, /// as long as updateLinkedTransform is false. /// [RequireComponent(typeof(SolverHandler))] [HelpURL("https://microsoft.github.io/MixedRealityToolkit-Unity/Documentation/README_Solver.html")] public abstract class Solver : MonoBehaviour { [SerializeField] [Tooltip("If true, the position and orientation will be calculated, but not applied, for other components to use")] private bool updateLinkedTransform = false; /// /// If true, the position and orientation will be calculated, but not applied, for other components to use /// public bool UpdateLinkedTransform { get => updateLinkedTransform; set => updateLinkedTransform = value; } [SerializeField] [Tooltip("If 0, the position will update immediately. Otherwise, the greater this attribute the slower the position updates")] private float moveLerpTime = 0.1f; /// /// If 0, the position will update immediately. Otherwise, the greater this attribute the slower the position updates /// public float MoveLerpTime { get => moveLerpTime; set => moveLerpTime = value; } [SerializeField] [Tooltip("If 0, the rotation will update immediately. Otherwise, the greater this attribute the slower the rotation updates")] private float rotateLerpTime = 0.1f; /// /// If 0, the rotation will update immediately. Otherwise, the greater this attribute the slower the rotation updates")] /// public float RotateLerpTime { get => rotateLerpTime; set => rotateLerpTime = value; } [SerializeField] [Tooltip("If 0, the scale will update immediately. Otherwise, the greater this attribute the slower the scale updates")] private float scaleLerpTime = 0; /// /// If 0, the scale will update immediately. Otherwise, the greater this attribute the slower the scale updates /// public float ScaleLerpTime { get => scaleLerpTime; set => scaleLerpTime = value; } [SerializeField] [Tooltip("If true, the Solver will respect the object's original scale values")] private bool maintainScale = true; [SerializeField] [Tooltip("If true, updates are smoothed to the target. Otherwise, they are snapped to the target")] private bool smoothing = true; /// /// If true, updates are smoothed to the target. Otherwise, they are snapped to the target /// public bool Smoothing { get => smoothing; set => smoothing = value; } [SerializeField] [Tooltip("If > 0, this solver will deactivate after this much time, even if the state is still active")] private float lifetime = 0; private float currentLifetime; /// /// The handler reference for this solver that's attached to this GameObject /// [HideInInspector] protected SolverHandler SolverHandler; /// /// The final position to be attained /// protected Vector3 GoalPosition { get { return SolverHandler.GoalPosition; } set { SolverHandler.GoalPosition = value; } } /// /// The final rotation to be attained /// protected Quaternion GoalRotation { get { return SolverHandler.GoalRotation; } set { SolverHandler.GoalRotation = value; } } /// /// The final scale to be attained /// protected Vector3 GoalScale { get { return SolverHandler.GoalScale; } set { SolverHandler.GoalScale = value; } } /// /// Automatically uses the shared position if the solver is set to use the 'linked transform'. /// UpdateLinkedTransform may be set to false, and a solver will automatically update the object directly, /// and not inherit work done by other solvers to the shared position /// public Vector3 WorkingPosition { get { return updateLinkedTransform ? GoalPosition : transform.position; } protected set { if (updateLinkedTransform) { GoalPosition = value; } else { transform.position = value; } } } /// /// Rotation version of WorkingPosition /// public Quaternion WorkingRotation { get { return updateLinkedTransform ? GoalRotation : transform.rotation; } protected set { if (updateLinkedTransform) { GoalRotation = value; } else { transform.rotation = value; } } } /// /// Scale version of WorkingPosition /// public Vector3 WorkingScale { get { return updateLinkedTransform ? GoalScale : transform.localScale; } protected set { if (updateLinkedTransform) { GoalScale = value; } else { transform.localScale = value; } } } #region MonoBehaviour Implementation protected virtual void Awake() { if (SolverHandler == null) { SolverHandler = GetComponent(); } if (updateLinkedTransform && SolverHandler == null) { Debug.LogError("No SolverHandler component found on " + name + " when UpdateLinkedTransform was set to true! Disabling UpdateLinkedTransform."); updateLinkedTransform = false; } GoalScale = maintainScale ? transform.localScale : Vector3.one; } /// /// Typically when a solver becomes enabled, it should update its internal state to the system, in case it was disabled far away /// protected virtual void OnEnable() { if (SolverHandler != null) { SnapGoalTo(GoalPosition, GoalRotation, GoalScale); } currentLifetime = 0; } protected virtual void Start() { if (SolverHandler != null) { SolverHandler.RegisterSolver(this); } } protected virtual void OnDestroy() { if (SolverHandler != null) { SolverHandler.UnregisterSolver(this); } } #endregion MonoBehaviour Implementation /// /// Should be implemented in derived classes, but Solver can be used to flush shared transform to real transform /// public abstract void SolverUpdate(); /// /// Tracks lifetime of the solver, disabling it when expired, and finally runs the orientation update logic /// public void SolverUpdateEntry() { currentLifetime += SolverHandler.DeltaTime; if (lifetime > 0 && currentLifetime >= lifetime) { enabled = false; return; } SolverUpdate(); UpdateWorkingToGoal(); } /// /// Snaps the solver to the desired pose. /// /// /// SnapTo may be used to bypass smoothing to a certain position if the object is teleported or spawned. /// public virtual void SnapTo(Vector3 position, Quaternion rotation, Vector3 scale) { SnapGoalTo(position, rotation, scale); WorkingPosition = position; WorkingRotation = rotation; WorkingScale = scale; } /// /// SnapGoalTo only sets the goal orientation. Not really useful. /// public virtual void SnapGoalTo(Vector3 position, Quaternion rotation, Vector3 scale) { GoalPosition = position; GoalRotation = rotation; GoalScale = scale; } /// /// Snaps the solver to the desired pose. /// /// /// SnapTo may be used to bypass smoothing to a certain position if the object is teleported or spawned. /// [Obsolete("Use SnapTo(Vector3, Quaternion, Vector3) instead.")] public virtual void SnapTo(Vector3 position, Quaternion rotation) { SnapGoalTo(position, rotation); WorkingPosition = position; WorkingRotation = rotation; } /// /// SnapGoalTo only sets the goal orientation. Not really useful. /// [Obsolete("Use SnapGoalTo(Vector3, Quaternion, Vector3) instead.")] public virtual void SnapGoalTo(Vector3 position, Quaternion rotation) { GoalPosition = position; GoalRotation = rotation; } /// /// Add an offset position to the target goal position. /// public virtual void AddOffset(Vector3 offset) { GoalPosition += offset; } /// /// Lerps Vector3 source to goal. /// /// /// Handles lerpTime of 0. /// public static Vector3 SmoothTo(Vector3 source, Vector3 goal, float deltaTime, float lerpTime) { return Vector3.Lerp(source, goal, lerpTime.Equals(0.0f) ? 1f : deltaTime / lerpTime); } /// /// Slerps Quaternion source to goal, handles lerpTime of 0 /// public static Quaternion SmoothTo(Quaternion source, Quaternion goal, float deltaTime, float lerpTime) { return Quaternion.Slerp(source, goal, lerpTime.Equals(0.0f) ? 1f : deltaTime / lerpTime); } /// /// Updates all object orientations to the goal orientation for this solver, with smoothing accounted for (smoothing may be off) /// protected void UpdateTransformToGoal() { if (smoothing) { Vector3 pos = transform.position; Quaternion rot = transform.rotation; Vector3 scale = transform.localScale; pos = SmoothTo(pos, GoalPosition, SolverHandler.DeltaTime, moveLerpTime); rot = SmoothTo(rot, GoalRotation, SolverHandler.DeltaTime, rotateLerpTime); scale = SmoothTo(scale, GoalScale, SolverHandler.DeltaTime, scaleLerpTime); transform.position = pos; transform.rotation = rot; transform.localScale = scale; } else { transform.position = GoalPosition; transform.rotation = GoalRotation; transform.localScale = GoalScale; } } /// /// Updates the Working orientation (which may be the object, or the shared orientation) to the goal with smoothing, if enabled /// public void UpdateWorkingToGoal() { UpdateWorkingPositionToGoal(); UpdateWorkingRotationToGoal(); UpdateWorkingScaleToGoal(); } /// /// Updates only the working position to goal with smoothing, if enabled /// public void UpdateWorkingPositionToGoal() { WorkingPosition = smoothing ? SmoothTo(WorkingPosition, GoalPosition, SolverHandler.DeltaTime, moveLerpTime) : GoalPosition; } /// /// Updates only the working rotation to goal with smoothing, if enabled /// public void UpdateWorkingRotationToGoal() { WorkingRotation = smoothing ? SmoothTo(WorkingRotation, GoalRotation, SolverHandler.DeltaTime, rotateLerpTime) : GoalRotation; } /// /// Updates only the working scale to goal with smoothing, if enabled /// public void UpdateWorkingScaleToGoal() { WorkingScale = smoothing ? SmoothTo(WorkingScale, GoalScale, SolverHandler.DeltaTime, scaleLerpTime) : GoalScale; } } }