// 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;
}
}
}
}