// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Physics;
using System.Collections.Generic;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities
{
///
/// Base class that provides data about a line.
///
/// Data to be consumed by other classes like the
[ExecuteAlways]
public abstract class BaseMixedRealityLineDataProvider : MonoBehaviour
{
#region Properties
[Range(MinLineStartClamp, MaxLineEndClamp)]
[SerializeField]
[Tooltip("Clamps the line's normalized start point. This setting will affect line renderers.")]
private float lineStartClamp = MinLineStartClamp;
///
/// Clamps the line's normalized start point. This setting will affect line renderers.
///
public float LineStartClamp
{
get => lineStartClamp;
set => lineStartClamp = Mathf.Clamp(value, MinLineStartClamp, MaxLineEndClamp);
}
[Range(MinLineStartClamp, MaxLineEndClamp)]
[SerializeField]
[Tooltip("Clamps the line's normalized end point. This setting will affect line renderers.")]
private float lineEndClamp = MaxLineEndClamp;
///
/// Clamps the line's normalized end point. This setting will affect line renderers.
///
public float LineEndClamp
{
get => lineEndClamp;
set => lineEndClamp = Mathf.Clamp(value, MinLineStartClamp, MaxLineEndClamp);
}
[SerializeField]
[Tooltip("Transform to use when translating points from local to world space. If null, this object's transform is used.")]
private Transform customLineTransform;
///
/// Transform to use when translating points from local to world space. If null, this object's transform is used.
///
public Transform LineTransform
{
get => customLineTransform != null ? customLineTransform : transform;
set => customLineTransform = value;
}
[SerializeField]
[Tooltip("Controls whether this line loops \nNote: some classes override this setting")]
private bool loops = false;
///
/// Controls whether this line loops
///
/// Some classes override this setting.
public virtual bool Loops
{
get => loops;
set => loops = value;
}
[SerializeField]
[Tooltip("The transform mode used by the line. UseTransform will work when line is disabled, but at a performance cost. UseMatrix requires that the line be active and enabled to return accurate points.")]
private LinePointTransformMode transformMode = LinePointTransformMode.UseTransform;
///
/// Defines how a base line data provider will transform its points
///
public LinePointTransformMode TransformMode
{
get => transformMode;
set => transformMode = value;
}
[SerializeField]
[Tooltip("The rotation mode used in the GetRotation function. You can visualize rotations by checking Draw Rotations under Editor Settings.")]
private LineRotationMode rotationMode = LineRotationMode.Velocity;
///
/// The rotation mode used in the GetRotation function. You can visualize rotations by checking Draw Rotations under Editor Settings.
///
public LineRotationMode RotationMode
{
get => rotationMode;
set => rotationMode = value;
}
[SerializeField]
[Tooltip("Reverses up vector when determining rotation along line")]
private bool flipUpVector = false;
///
/// Reverses up vector when determining rotation along line
///
public bool FlipUpVector
{
get => flipUpVector;
set => flipUpVector = value;
}
[SerializeField]
[Tooltip("Local space offset to transform position. Used to determine rotation along line in RelativeToOrigin rotation mode")]
private Vector3 originOffset = Vector3.zero;
///
/// Local space offset to transform position. Used to determine rotation along line in RelativeToOrigin rotation mode
///
public Vector3 OriginOffset
{
get => originOffset;
set => originOffset = value;
}
[Range(0f, 1f)]
[SerializeField]
[Tooltip("The weight of manual up vectors in Velocity rotation mode")]
private float manualUpVectorBlend = 0f;
///
/// The weight of manual up vectors in Velocity rotation mode
///
public float ManualUpVectorBlend
{
get => manualUpVectorBlend;
set => manualUpVectorBlend = Mathf.Clamp01(value);
}
[SerializeField]
[Tooltip("These vectors are used with ManualUpVectorBlend to determine rotation along the line in Velocity rotation mode. Vectors are distributed along the normalized length of the line.")]
private Vector3[] manualUpVectors = { Vector3.up, Vector3.up, Vector3.up };
///
/// These vectors are used with ManualUpVectorBlend to determine rotation along the line in Velocity rotation mode. Vectors are distributed along the normalized length of the line.
///
public Vector3[] ManualUpVectors
{
get => manualUpVectors;
set => manualUpVectors = value;
}
[SerializeField]
[Range(0.0001f, 0.1f)]
[Tooltip("Used in Velocity rotation mode. Smaller values are more accurate but more expensive")]
private float velocitySearchRange = 0.02f;
///
/// Used in Velocity rotation mode.
///
///
/// Smaller values are more accurate but more expensive
///
public float VelocitySearchRange
{
get => velocitySearchRange;
set => velocitySearchRange = Mathf.Clamp(value, 0.001f, 0.1f);
}
[SerializeField]
private List distorters = new List();
///
/// A list of distorters that apply to this line
///
public IReadOnlyList Distorters
{
get
{
if (distorters.Count == 0)
{
distorters.AddRange(GetComponents());
distorters.Sort();
}
return distorters;
}
}
[SerializeField]
[Tooltip("Enables / disables all distorters used by line")]
private bool distortionEnabled = true;
///
/// Enabled / disables all distorters used by line.
///
public bool DistortionEnabled
{
get => distortionEnabled;
set => distortionEnabled = value;
}
[SerializeField]
[Tooltip("NormalizedLength mode uses the DistortionStrength curve for distortion strength, Uniform uses UniformDistortionStrength along entire line")]
private DistortionMode distortionMode = DistortionMode.NormalizedLength;
///
/// NormalizedLength mode uses the DistortionStrength curve for distortion strength, Uniform uses UniformDistortionStrength along entire line
///
public DistortionMode DistortionMode
{
get => distortionMode;
set => distortionMode = value;
}
[SerializeField]
[Tooltip("Curve that defines distortion strength over distance, only used when DistortionMode = NormalizedLength")]
private AnimationCurve distortionStrength = AnimationCurve.Linear(0f, 1f, 1f, 1f);
///
/// Curve that defines distortion strength over distance, only used when DistortionMode = NormalizedLength
///
public AnimationCurve DistortionStrength
{
get => distortionStrength;
set => distortionStrength = value;
}
[Range(0f, 1f)]
[Tooltip("Float value that defines distortion strength uniformly over distance, only used when DistortionMode = Uniform")]
[SerializeField]
private float uniformDistortionStrength = 1f;
///
/// Float value that defines distortion strength uniformly over distance, only used when DistortionMode = Uniform
///
public float UniformDistortionStrength
{
get => uniformDistortionStrength;
set => uniformDistortionStrength = Mathf.Clamp01(value);
}
///
/// Returns world position of first point along line as defined by this data provider
///
public Vector3 FirstPoint
{
get => GetPoint(0);
set => SetPoint(0, value);
}
///
/// Returns world position of last point along line as defined by this data provider
///
public Vector3 LastPoint
{
get => GetPoint(PointCount - 1);
set => SetPoint(PointCount - 1, value);
}
public float UnClampedWorldLength => GetUnClampedWorldLengthInternal();
#endregion
#region BaseMixedRealityLineDataProvider Abstract Declarations
///
/// The number of points this line has.
///
public abstract int PointCount { get; }
///
/// Sets the point at index.
///
protected abstract void SetPointInternal(int pointIndex, Vector3 point);
///
/// Get a point based on normalized distance along line
/// Normalized distance will be pre-clamped
///
protected abstract Vector3 GetPointInternal(float normalizedLength);
///
/// Get a point based on point index
/// Point index will be pre-clamped
///
protected abstract Vector3 GetPointInternal(int pointIndex);
///
/// Gets the up vector at a normalized length along line (used for rotation)
///
protected virtual Vector3 GetUpVectorInternal(float normalizedLength)
{
return LineTransform.forward;
}
///
/// Get the UnClamped world length of the line
///
protected abstract float GetUnClampedWorldLengthInternal();
private Matrix4x4 localToWorldMatrix;
private Matrix4x4 worldToLocalMatrix;
protected const int UnclampedWorldLengthSearchSteps = 10;
private const float MinRotationMagnitude = 0.0001f;
private const float MinLineStartClamp = 0.0001f;
private const float MaxLineEndClamp = 0.9999f;
#endregion BaseMixedRealityLineDataProvider Abstract Declarations
#region MonoBehaviour Implementation
protected virtual void OnEnable()
{
UpdateMatrix();
}
protected virtual void LateUpdate()
{
UpdateMatrix();
}
#endregion MonoBehaviour Implementation
///
/// Returns a normalized length corresponding to a world length
/// Useful for determining LineStartClamp / LineEndClamp values
///
public float GetNormalizedLengthFromWorldLength(float worldLength, int searchResolution = 10)
{
if (searchResolution < 1)
{
return 0;
}
Vector3 lastPoint = GetUnClampedPoint(0f);
float normalizedLength = 0f;
float distanceSoFar = 0f;
float normalizedSegmentLength = 1f / searchResolution;
for (int i = 1; i <= searchResolution; i++)
{
// Get the normalized length of this position along the line
normalizedLength = normalizedSegmentLength * i;
Vector3 currentPoint = GetUnClampedPoint(normalizedLength);
float segmentLength = Vector3.Distance(lastPoint, currentPoint);
distanceSoFar += segmentLength;
if (distanceSoFar >= worldLength)
{
// We've reached the world length, so subtract the amount we overshot
normalizedLength -= ((distanceSoFar - worldLength) / segmentLength) * normalizedSegmentLength;
break;
}
lastPoint = currentPoint;
}
return Mathf.Clamp01(normalizedLength);
}
///
/// Gets the velocity along the line
///
public Vector3 GetVelocity(float normalizedLength)
{
Vector3 velocity;
if (normalizedLength < velocitySearchRange)
{
Vector3 currentPos = GetPoint(normalizedLength);
Vector3 nextPos = GetPoint(normalizedLength + velocitySearchRange);
velocity = (nextPos - currentPos).normalized;
}
else
{
Vector3 currentPos = GetPoint(normalizedLength);
Vector3 prevPos = GetPoint(normalizedLength - velocitySearchRange);
velocity = (currentPos - prevPos).normalized;
}
return velocity;
}
///
/// Gets the rotation of a point along the line at the specified length
///
public Quaternion GetRotation(float normalizedLength, LineRotationMode lineRotationMode = LineRotationMode.None)
{
lineRotationMode = (lineRotationMode != LineRotationMode.None) ? lineRotationMode : rotationMode;
Vector3 rotationVector = Vector3.zero;
switch (lineRotationMode)
{
case LineRotationMode.Velocity:
rotationVector = GetVelocity(normalizedLength);
break;
case LineRotationMode.RelativeToOrigin:
Vector3 point = GetPoint(normalizedLength);
Vector3 origin = originOffset;
TransformPoint(ref origin);
rotationVector = (point - origin).normalized;
break;
case LineRotationMode.None:
return LineTransform.rotation;
}
if (rotationVector.magnitude < MinRotationMagnitude)
{
return LineTransform.rotation;
}
Vector3 upVector = GetUpVectorInternal(normalizedLength);
if (manualUpVectorBlend > 0f)
{
Vector3 manualUpVector = LineUtility.GetVectorCollectionBlend(manualUpVectors, normalizedLength, Loops);
upVector = Vector3.Lerp(upVector, manualUpVector, manualUpVector.magnitude);
}
if (flipUpVector)
{
upVector = -upVector;
}
return Quaternion.LookRotation(rotationVector, upVector);
}
///
/// Gets the rotation of a point along the line at the specified index
///
public Quaternion GetRotation(int pointIndex, LineRotationMode lineRotationMode = LineRotationMode.None)
{
return GetRotation((float)pointIndex / PointCount, lineRotationMode != LineRotationMode.None ? lineRotationMode : rotationMode);
}
///
/// Gets a point along the line at the specified normalized length.
///
public Vector3 GetPoint(float normalizedLength)
{
normalizedLength = Mathf.Lerp(lineStartClamp, lineEndClamp, Mathf.Clamp01(normalizedLength));
Vector3 point = GetPointInternal(normalizedLength);
TransformPoint(ref point);
DistortPoint(ref point, normalizedLength);
return point;
}
///
/// Gets a point along the line at the specified length without using LineStartClamp or LineEndClamp
///
public Vector3 GetUnClampedPoint(float normalizedLength)
{
normalizedLength = Mathf.Clamp01(normalizedLength);
Vector3 point = GetPointInternal(normalizedLength);
TransformPoint(ref point);
DistortPoint(ref point, normalizedLength);
return point;
}
///
/// Gets a point along the line at the specified index
///
public Vector3 GetPoint(int pointIndex)
{
if (pointIndex < 0 || pointIndex >= PointCount)
{
Debug.LogError("Invalid point index");
return Vector3.zero;
}
Vector3 point = GetPointInternal(pointIndex);
TransformPoint(ref point);
return point;
}
///
/// Sets a point in the line
/// This function is not guaranteed to have an effect
///
public void SetPoint(int pointIndex, Vector3 point)
{
if (pointIndex < 0 || pointIndex >= PointCount)
{
Debug.LogError("Invalid point index");
return;
}
InverseTransformPoint(ref point);
SetPointInternal(pointIndex, point);
}
///
/// Iterates along line until it finds the point closest to worldPosition
///
public Vector3 GetClosestPoint(Vector3 worldPosition, int resolution = 5, int maxIterations = 5)
{
float length = GetNormalizedLengthFromWorldPos(worldPosition, resolution, maxIterations);
return GetPoint(length);
}
///
/// Iterates along line until it finds the length closest to worldposition.
///
public float GetNormalizedLengthFromWorldPos(Vector3 worldPosition, int resolution = 5, int maxIterations = 5)
{
int iteration = 0;
return GetNormalizedLengthFromWorldPosInternal(worldPosition, 0f, ref iteration, resolution, maxIterations, 0f, 1f);
}
private void InverseTransformPoint(ref Vector3 point)
{
switch (transformMode)
{
case LinePointTransformMode.UseTransform:
default:
point = LineTransform.InverseTransformPoint(point);
return;
case LinePointTransformMode.UseMatrix:
point = worldToLocalMatrix.MultiplyPoint3x4(point);
return;
}
}
private void TransformPoint(ref Vector3 point)
{
switch (transformMode)
{
case LinePointTransformMode.UseTransform:
default:
point = LineTransform.TransformPoint(point);
return;
case LinePointTransformMode.UseMatrix:
point = localToWorldMatrix.MultiplyPoint3x4(point);
return;
}
}
public void UpdateMatrix()
{
if (transformMode == LinePointTransformMode.UseMatrix)
{
Transform t = LineTransform;
if (t.hasChanged)
{
t.hasChanged = false;
localToWorldMatrix = LineTransform.localToWorldMatrix;
worldToLocalMatrix = LineTransform.worldToLocalMatrix;
}
}
}
private float GetNormalizedLengthFromWorldPosInternal(Vector3 worldPosition, float currentLength, ref int iteration, int resolution, int maxIterations, float start, float end)
{
iteration++;
// If we've maxed out our iterations, don't go any further
if (iteration > maxIterations)
{
return currentLength;
}
float searchLengthStep = (end - start) / resolution;
float closestDistanceSoFar = Mathf.Infinity;
float currentSearchLength = start;
for (int i = 0; i < resolution; i++)
{
Vector3 currentPoint = GetUnClampedPoint(currentSearchLength);
float distSquared = (currentPoint - worldPosition).sqrMagnitude;
if (distSquared < closestDistanceSoFar)
{
currentLength = currentSearchLength;
closestDistanceSoFar = distSquared;
}
currentSearchLength += searchLengthStep;
}
// Our start and end lengths will now be 1 resolution to the left and right
float newStart = currentLength - searchLengthStep;
float newEnd = currentLength + searchLengthStep;
if (newStart < 0)
{
newEnd -= newStart;
newStart = 0;
}
if (newEnd > 1)
{
newEnd = 1;
}
return GetNormalizedLengthFromWorldPosInternal(worldPosition, currentLength, ref iteration, resolution, maxIterations, newStart, newEnd);
}
private void DistortPoint(ref Vector3 point, float normalizedLength)
{
if (!distortionEnabled || distorters.Count == 0)
{
return;
}
float strength = uniformDistortionStrength;
if (distortionMode == DistortionMode.NormalizedLength)
{
strength = distortionStrength.Evaluate(normalizedLength);
}
for (int i = 0; i < distorters.Count; i++)
{
Distorter distorter = distorters[i];
if (distorter.DistortionEnabled)
{
point = distorter.DistortPoint(point, strength);
}
}
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
// Draw a crude, performant gizmo for lines that are unselected
if (Application.isPlaying || UnityEditor.Selection.activeGameObject == gameObject)
{
return;
}
#endif
DrawUnselectedGizmosPreview();
}
protected virtual void DrawUnselectedGizmosPreview()
{
int linePreviewResolution = Mathf.Max(16, PointCount / 4);
Vector3 firstPosition = FirstPoint;
Vector3 lastPosition = firstPosition;
for (int i = 1; i < linePreviewResolution; i++)
{
Vector3 currentPosition;
if (i == linePreviewResolution - 1)
{
currentPosition = LastPoint;
}
else
{
float normalizedLength = (1f / (linePreviewResolution - 1)) * i;
currentPosition = GetPoint(normalizedLength);
}
Gizmos.color = Color.magenta;
Gizmos.DrawLine(lastPosition, currentPosition);
lastPosition = currentPosition;
}
if (Loops)
{
Gizmos.color = Color.magenta;
Gizmos.DrawLine(lastPosition, firstPosition);
}
}
}
}