// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities.Solvers
{
///
/// RadialViewPoser solver locks a tag-along type object within a view cone
///
[AddComponentMenu("Scripts/MRTK/SDK/RadialView")]
public class RadialView : Solver
{
[SerializeField]
[Tooltip("Which direction to position the element relative to: HeadOriented rolls with the head, HeadFacingWorldUp view direction but ignores head roll, and HeadMoveDirection uses the direction the head last moved without roll")]
private RadialViewReferenceDirection referenceDirection = RadialViewReferenceDirection.FacingWorldUp;
///
/// Which direction to position the element relative to:
/// HeadOriented rolls with the head,
/// HeadFacingWorldUp view direction but ignores head roll,
/// and HeadMoveDirection uses the direction the head last moved without roll.
///
public RadialViewReferenceDirection ReferenceDirection
{
get => referenceDirection;
set => referenceDirection = value;
}
public Vector3 headOffset = Vector3.up * 0.2f;
[SerializeField]
[Tooltip("Min distance from eye to position element around, i.e. the sphere radius")]
private float minDistance = 1f;
///
/// Min distance from eye to position element around, i.e. the sphere radius.
///
public float MinDistance
{
get => minDistance;
set => minDistance = value;
}
[SerializeField]
[Tooltip("Max distance from eye to element")]
private float maxDistance = 2f;
///
/// Max distance from eye to element.
///
public float MaxDistance
{
get => maxDistance;
set => maxDistance = value;
}
[SerializeField]
[Tooltip("The element will stay at least this far away from the center of view")]
private float minViewDegrees = 0f;
///
/// The element will stay at least this far away from the center of view.
///
public float MinViewDegrees
{
get => minViewDegrees;
set => minViewDegrees = value;
}
[SerializeField]
[Tooltip("The element will stay at least this close to the center of view")]
private float maxViewDegrees = 30f;
///
/// The element will stay at least this close to the center of view.
///
public float MaxViewDegrees
{
get => maxViewDegrees;
set => maxViewDegrees = value;
}
[SerializeField]
[Tooltip("Apply a different clamp to vertical FOV than horizontal. Vertical = Horizontal * aspectV")]
private float aspectV = 1f;
///
/// Apply a different clamp to vertical FOV than horizontal. Vertical = Horizontal * AspectV.
///
public float AspectV
{
get => aspectV;
set => aspectV = value;
}
[SerializeField]
[Tooltip("Option to ignore angle clamping")]
private bool ignoreAngleClamp = false;
///
/// Option to ignore angle clamping.
///
public bool IgnoreAngleClamp
{
get => ignoreAngleClamp;
set => ignoreAngleClamp = value;
}
[SerializeField]
[Tooltip("Option to ignore distance clamping")]
private bool ignoreDistanceClamp = false;
///
/// Option to ignore distance clamping.
///
public bool IgnoreDistanceClamp
{
get => ignoreDistanceClamp;
set => ignoreDistanceClamp = value;
}
[SerializeField]
[Tooltip("Ignore vertical movement and lock the Y position of the object")]
private bool useFixedVerticalPosition = false;
///
/// Ignore vertical movement and lock the Y position of the object.
///
public bool UseFixedVerticalPosition
{
get => useFixedVerticalPosition;
set => useFixedVerticalPosition = value;
}
[SerializeField]
[Tooltip("Offset amount of the vertical position")]
private float fixedVerticalPosition = -0.4f;
///
/// Offset amount of the vertical position.
///
public float FixedVerticalPosition
{
get => fixedVerticalPosition;
set => fixedVerticalPosition = value;
}
[SerializeField]
[Tooltip("If true, element will orient to ReferenceDirection, otherwise it will orient to ref position.")]
private bool orientToReferenceDirection = false;
///
/// If true, element will orient to ReferenceDirection, otherwise it will orient to ref position.
///
public bool OrientToReferenceDirection
{
get => orientToReferenceDirection;
set => orientToReferenceDirection = value;
}
///
/// Position to the view direction, or the movement direction, or the direction of the view cone.
///
private Vector3 SolverReferenceDirection => SolverHandler.TransformTarget != null ? SolverHandler.TransformTarget.forward : Vector3.forward;
///
/// The up direction to use for orientation.
///
/// Cone may roll with head, or not.
private Vector3 UpReference
{
get
{
Vector3 upReference = Vector3.up;
if (referenceDirection == RadialViewReferenceDirection.ObjectOriented)
{
upReference = SolverHandler.TransformTarget != null ? SolverHandler.TransformTarget.up : Vector3.up;
}
return upReference;
}
}
private Vector3 ReferencePoint => SolverHandler.TransformTarget != null ? SolverHandler.TransformTarget.position : Vector3.zero;
///
public override void SolverUpdate()
{
Vector3 goalPosition = WorkingPosition;
if (ignoreAngleClamp)
{
if (ignoreDistanceClamp)
{
goalPosition = transform.position;
}
else
{
GetDesiredOrientation_DistanceOnly(ref goalPosition);
}
}
else
{
GetDesiredOrientation(ref goalPosition);
}
// Element orientation
Vector3 refDirUp = UpReference;
Quaternion goalRotation;
if (orientToReferenceDirection)
{
goalRotation = Quaternion.LookRotation(SolverReferenceDirection, refDirUp);
}
else
{
goalRotation = Quaternion.LookRotation(goalPosition - ReferencePoint, refDirUp);
}
// If gravity aligned then zero out the x and z axes on the rotation
if (referenceDirection == RadialViewReferenceDirection.GravityAligned)
{
goalRotation.x = goalRotation.z = 0f;
}
if (UseFixedVerticalPosition)
{
goalPosition.y = ReferencePoint.y + FixedVerticalPosition;
}
if (goalPosition.y > ReferencePoint.y - headOffset.y)
{
goalPosition.y = ReferencePoint.y - headOffset.y;
}
GoalPosition = goalPosition;
GoalRotation = goalRotation;
}
///
/// Optimized version of GetDesiredOrientation.
///
private void GetDesiredOrientation_DistanceOnly(ref Vector3 desiredPos)
{
// TODO: There should be a different solver for distance constraint.
// Determine reference locations and directions
Vector3 refPoint = ReferencePoint;
Vector3 elementPoint = transform.position;
Vector3 elementDelta = elementPoint - refPoint;
float elementDist = elementDelta.magnitude;
Vector3 elementDir = elementDist > 0 ? elementDelta / elementDist : Vector3.one;
// Clamp distance too
float clampedDistance = Mathf.Clamp(elementDist, minDistance, maxDistance);
if (!clampedDistance.Equals(elementDist))
{
desiredPos = refPoint + clampedDistance * elementDir;
}
}
private void GetDesiredOrientation(ref Vector3 desiredPos)
{
// Determine reference locations and directions
Vector3 direction = SolverReferenceDirection;
Vector3 upDirection = UpReference;
Vector3 referencePoint = ReferencePoint;
Vector3 elementPoint = transform.position;
Vector3 elementDelta = elementPoint - referencePoint;
float elementDist = elementDelta.magnitude;
Vector3 elementDir = elementDist > 0 ? elementDelta / elementDist : Vector3.one;
// Generate basis: First get axis perpendicular to reference direction pointing toward element
Vector3 perpendicularDirection = (elementDir - direction);
perpendicularDirection -= direction * Vector3.Dot(perpendicularDirection, direction);
perpendicularDirection.Normalize();
// Calculate the clamping angles, accounting for aspect (need the angle relative to view plane)
float heightToViewAngle = Vector3.Angle(perpendicularDirection, upDirection);
float verticalAspectScale = Mathf.Lerp(aspectV, 1f, Mathf.Abs(Mathf.Sin(heightToViewAngle * Mathf.Deg2Rad)));
// Calculate the current angle
float currentAngle = Vector3.Angle(elementDir, direction);
float currentAngleClamped = Mathf.Clamp(currentAngle, minViewDegrees * verticalAspectScale, maxViewDegrees * verticalAspectScale);
// Clamp distance too, if desired
float clampedDistance = ignoreDistanceClamp ? elementDist : Mathf.Clamp(elementDist, minDistance, maxDistance);
// If the angle was clamped, do some special update stuff
if (currentAngle != currentAngleClamped)
{
float angRad = currentAngleClamped * Mathf.Deg2Rad;
// Calculate new position
desiredPos = referencePoint + clampedDistance * (direction * Mathf.Cos(angRad) + perpendicularDirection * Mathf.Sin(angRad));
}
else if (!clampedDistance.Equals(elementDist))
{
// Only need to apply distance
desiredPos = referencePoint + clampedDistance * elementDir;
}
}
}
}