// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Utilities.Solvers { /// /// ConstantViewSize solver scales to maintain a constant size relative to the view (currently tied to the Camera) /// [AddComponentMenu("Scripts/MRTK/SDK/ConstantViewSize")] public class ConstantViewSize : Solver { #region ConstantViewSize Parameters [Range(0f, 1f)] [SerializeField] [Tooltip("The object take up this percent vertically in our view (not technically a percent use 0.5 for 50%)")] private float targetViewPercentV = 0.5f; /// /// The object take up this percent vertically in our view (not technically a percent use 0.5 for 50%) /// public float TargetViewPercentV { get { return targetViewPercentV; } set { targetViewPercentV = value; } } [SerializeField] [Tooltip("If the object is closer than MinDistance, the distance used is clamped here")] private float minDistance = 0.5f; /// /// If the object is closer than MinDistance, the distance used is clamped here /// public float MinDistance { get { return minDistance; } set { minDistance = value; } } [SerializeField] [Tooltip("If the object is farther than MaxDistance, the distance used is clamped here")] private float maxDistance = 3.5f; /// /// If the object is farther than MaxDistance, the distance used is clamped here /// public float MaxDistance { get { return maxDistance; } set { maxDistance = value; } } [SerializeField] [Tooltip("Minimum scale value possible (world space scale)")] private float minScale = 0.01f; /// /// Minimum scale value possible (world space scale) /// public float MinScale { get { return minScale; } set { minScale = value; } } [SerializeField] [Tooltip("Maximum scale value possible (world space scale)")] private float maxScale = 100f; /// /// Maximum scale value possible (world space scale) /// public float MaxScale { get { return maxScale; } set { maxScale = value; } } [SerializeField] [Tooltip("Used for dead zone for scaling")] private float scaleBuffer = 0.01f; /// /// Used for dead zone for scaling /// public float ScaleBuffer { get { return scaleBuffer; } set { scaleBuffer = value; } } [SerializeField] [Tooltip("Overrides auto size calculation with provided manual size. If 0, solver calculates size")] private float manualObjectSize = 0; /// /// Overrides auto size calculation with provided manual size. If 0, solver calculates size /// public float ManualObjectSize { get { return manualObjectSize; } set { manualObjectSize = value; RecalculateBounds(); } } public ScaleState ScaleState { get; private set; } = ScaleState.Static; /// /// 0 to 1 between MinScale and MaxScale. If current is less than max, then scaling is being applied. /// This value is subject to inaccuracies due to smoothing/interpolation/momentum. /// public float CurrentScalePercent { get; private set; } = 1f; /// /// 0 to 1 between MinDistance and MaxDistance. If current is less than max, object is potentially on a surface [or some other condition like interpolating] (since it may still be on surface, but scale percent may be clamped at max). /// This value is subject to inaccuracies due to smoothing/interpolation/momentum. /// public float CurrentDistancePercent { get; private set; } = 1f; /// /// Returns the scale to be applied based on the FOV. This scale will be multiplied by distance as part of /// the final scale calculation, so this is the ratio of vertical fov to distance. /// public float FovScale { get { float cameraFovRadians = (CameraCache.Main.aspect * CameraCache.Main.fieldOfView) * Mathf.Deg2Rad; float sinFov = Mathf.Sin(cameraFovRadians * 0.5f); return 2f * targetViewPercentV * sinFov / objectSize; } } #endregion private float fovScalar = 1f; private float objectSize = 1f; protected override void Start() { base.Start(); RecalculateBounds(); } /// public override void SolverUpdate() { float lastScalePct = CurrentScalePercent; if (SolverHandler.TransformTarget != null) { // Get current fov each time instead of trying to cache it. Can never count on init order these days fovScalar = FovScale; // Set the linked alt scale ahead of our work. This is an attempt to minimize jittering by having solvers work with an interpolated scale. SolverHandler.AltScale.SetGoal(transform.localScale); // Calculate scale based on distance from view. Do not interpolate so we can appear at a constant size if possible. Borrowed from greybox. Vector3 targetPosition = SolverHandler.TransformTarget.position; float distance = Mathf.Clamp(Vector3.Distance(transform.position, targetPosition), minDistance, maxDistance); float scale = Mathf.Clamp(fovScalar * distance, minScale, maxScale); GoalScale = Vector3.one * scale; // Save some state information for external use CurrentDistancePercent = Mathf.InverseLerp(minDistance, maxDistance, distance); CurrentScalePercent = Mathf.InverseLerp(minScale, maxScale, scale); } float scaleDifference = (CurrentScalePercent - lastScalePct) / SolverHandler.DeltaTime; if (scaleDifference > scaleBuffer) { ScaleState = ScaleState.Growing; } else if (scaleDifference < -scaleBuffer) { ScaleState = ScaleState.Shrinking; } else { ScaleState = ScaleState.Static; } } /// /// Attempts to calculate the size of the bounds which contains all child renderers for attached GameObject. This information is used in the core solver calculations /// public void RecalculateBounds() { float baseSize; // If user set object size override apply, otherwise compute baseSize if (manualObjectSize > 0) { baseSize = manualObjectSize; } else { Vector3 cachedScale = transform.root.localScale; transform.root.localScale = Vector3.one; var combinedBounds = new Bounds(transform.position, Vector3.zero); var renderers = GetComponentsInChildren(); for (var i = 0; i < renderers.Length; i++) { combinedBounds.Encapsulate(renderers[i].bounds); } baseSize = combinedBounds.extents.magnitude; transform.root.localScale = cachedScale; } if (baseSize > 0) { objectSize = baseSize; } else { Debug.LogWarning("ConstantViewSize: Object base size calculate was 0, defaulting to 1"); objectSize = 1f; } } } }