mixedreality/com.microsoft.mixedreality..../Core/Utilities/MathUtilities.cs

551 lines
23 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace Microsoft.MixedReality.Toolkit.Utilities
{
/// <summary>
/// Math Utilities class.
/// </summary>
public static class MathUtilities
{
/// <summary>
/// Takes a point in the coordinate space specified by the "from" transform and transforms it to be the correct
/// point in the coordinate space specified by the "to" transform applies rotation, scale and translation.
/// </summary>
/// <returns>Point to.</returns>
public static Vector3 TransformPointFromTo(Transform from, Transform to, Vector3 fromPoint)
{
Vector3 worldPoint = (from == null) ? fromPoint : from.TransformPoint(fromPoint);
return (to == null) ? worldPoint : to.InverseTransformPoint(worldPoint);
}
/// <summary>
/// Takes a direction in the coordinate space specified by the "from" transform and transforms it to be the correct direction in the coordinate space specified by the "to" transform
/// applies rotation only, no translation or scale
/// </summary>
/// <returns>Direction to.</returns>
public static Vector3 TransformDirectionFromTo(Transform from, Transform to, Vector3 fromDirection)
{
Vector3 worldDirection = (from == null) ? fromDirection : from.TransformDirection(fromDirection);
return (to == null) ? worldDirection : to.InverseTransformDirection(worldDirection);
}
/// <summary>
/// Takes a vector in the coordinate space specified by the "from" transform and transforms it to be the correct direction in the coordinate space specified by the "to" transform
/// applies rotation and scale, no translation
/// </summary>
public static Vector3 TransformVectorFromTo(Transform from, Transform to, Vector3 vecInFrom)
{
Vector3 vecInWorld = (from == null) ? vecInFrom : from.TransformVector(vecInFrom);
Vector3 vecInTo = (to == null) ? vecInWorld : to.InverseTransformVector(vecInWorld);
return vecInTo;
}
/// <summary>
/// Retrieve angular measurement describing how large a sphere or circle appears from a given point of view.
/// Takes an angle (at given point of view) and a distance and returns the actual diameter of the object.
/// </summary>
public static float ScaleFromAngularSizeAndDistance(float angle, float distance)
{
float scale = 2.0f * distance * Mathf.Tan(angle * Mathf.Deg2Rad * 0.5f);
return scale;
}
[System.Obsolete("Method obsolete. Use ScaleFromAngularSizeAndDistance instead")]
public static float AngularScaleFromDistance(float angle, float distance)
{
return ScaleFromAngularSizeAndDistance(angle, distance);
}
/// <summary>
/// Takes a ray in the coordinate space specified by the "from" transform and transforms it to be the correct ray in the coordinate space specified by the "to" transform
/// </summary>
public static Ray TransformRayFromTo(Transform from, Transform to, Ray rayToConvert)
{
Ray outputRay = new Ray
{
origin = TransformPointFromTo(from, to, rayToConvert.origin),
direction = TransformDirectionFromTo(from, to, rayToConvert.direction)
};
return outputRay;
}
/// <summary>
/// Creates a quaternion containing the rotation from the input matrix.
/// </summary>
/// <param name="m">Input matrix to convert to quaternion</param>
public static Quaternion QuaternionFromMatrix(Matrix4x4 m)
{
// TODO: test and replace with this simpler, more unity-friendly code
// Quaternion q = Quaternion.LookRotation(m.GetColumn(2),m.GetColumn(1));
Quaternion q = new Quaternion
{
w = Mathf.Sqrt(Mathf.Max(0, 1 + m[0, 0] + m[1, 1] + m[2, 2])) / 2,
x = Mathf.Sqrt(Mathf.Max(0, 1 + m[0, 0] - m[1, 1] - m[2, 2])) / 2,
y = Mathf.Sqrt(Mathf.Max(0, 1 - m[0, 0] + m[1, 1] - m[2, 2])) / 2,
z = Mathf.Sqrt(Mathf.Max(0, 1 - m[0, 0] - m[1, 1] + m[2, 2])) / 2
};
q.x *= Mathf.Sign(q.x * (m[2, 1] - m[1, 2]));
q.y *= Mathf.Sign(q.y * (m[0, 2] - m[2, 0]));
q.z *= Mathf.Sign(q.z * (m[1, 0] - m[0, 1]));
return q;
}
/// <summary>
/// Extract the translation and rotation components of a Unity matrix
/// </summary>
public static void ToTranslationRotation(Matrix4x4 unityMtx, out Vector3 translation, out Quaternion rotation)
{
Vector3 upwards = new Vector3(unityMtx.m01, unityMtx.m11, unityMtx.m21);
Vector3 forward = new Vector3(unityMtx.m02, unityMtx.m12, unityMtx.m22);
translation = new Vector3(unityMtx.m03, unityMtx.m13, unityMtx.m23);
rotation = Quaternion.LookRotation(forward, upwards);
}
/// <summary>
/// Project vector onto XZ plane
/// </summary>
/// <returns>result of projecting v onto XZ plane</returns>
public static Vector3 XZProject(Vector3 v)
{
return new Vector3(v.x, 0.0f, v.z);
}
/// <summary>
/// Project vector onto YZ plane
/// </summary>
/// <returns>result of projecting v onto YZ plane</returns>
public static Vector3 YZProject(Vector3 v)
{
return new Vector3(0.0f, v.y, v.z);
}
/// <summary>
/// Project vector onto XY plane
/// </summary>
/// <returns>result of projecting v onto XY plane</returns>
public static Vector3 XYProject(Vector3 v)
{
return new Vector3(v.x, v.y, 0.0f);
}
/// <summary>
/// Returns the distance between a point and an infinite line defined by two points; linePointA and linePointB
/// </summary>
public static float DistanceOfPointToLine(Vector3 point, Vector3 linePointA, Vector3 linePointB)
{
Vector3 closestPoint = ClosestPointOnLineToPoint(point, linePointA, linePointB);
return (point - closestPoint).magnitude;
}
public static Vector3 ClosestPointOnLineToPoint(Vector3 point, Vector3 linePointA, Vector3 linePointB)
{
Vector3 v = linePointB - linePointA;
Vector3 w = point - linePointA;
float c1 = Vector3.Dot(w, v);
float c2 = Vector3.Dot(v, v);
float b = c1 / c2;
Vector3 pointB = linePointA + (v * b);
return pointB;
}
public static float DistanceOfPointToLineSegment(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
Vector3 closestPoint = ClosestPointOnLineSegmentToPoint(point, lineStart, lineEnd);
return (point - closestPoint).magnitude;
}
public static Vector3 ClosestPointOnLineSegmentToPoint(Vector3 point, Vector3 lineStart, Vector3 lineEnd)
{
Vector3 v = lineEnd - lineStart;
Vector3 w = point - lineStart;
float c1 = Vector3.Dot(w, v);
if (c1 <= 0)
{
return lineStart;
}
float c2 = Vector3.Dot(v, v);
if (c2 <= c1)
{
return lineEnd;
}
float b = c1 / c2;
Vector3 pointB = lineStart + (v * b);
return pointB;
}
public static bool TestPlanesAABB(Plane[] planes, int planeMask, Bounds bounds, out bool entirelyInside)
{
int planeIndex = 0;
int entirelyInsideCount = 0;
Vector3 boundsCenter = bounds.center; // center of bounds
Vector3 boundsExtent = bounds.extents; // half diagonal
// do intersection test for each active frame
int mask = 1;
// while active frames
while (mask <= planeMask)
{
// if active
if ((uint)(planeMask & mask) != 0)
{
Plane p = planes[planeIndex];
Vector3 n = p.normal;
n.x = Mathf.Abs(n.x);
n.y = Mathf.Abs(n.y);
n.z = Mathf.Abs(n.z);
float distance = p.GetDistanceToPoint(boundsCenter);
float radius = Vector3.Dot(boundsExtent, n);
if (distance + radius < 0)
{
// behind clip plane
entirelyInside = false;
return false;
}
if (distance > radius)
{
entirelyInsideCount++;
}
}
mask += mask;
planeIndex++;
}
entirelyInside = entirelyInsideCount == planes.Length;
return true;
}
/// <summary>
/// Tests component-wise if a Vector2 is in a given range
/// </summary>
/// <param name="vec">The vector to test</param>
/// <param name="lower">The lower bounds</param>
/// <param name="upper">The upper bounds</param>
/// <returns>true if in range, otherwise false</returns>
public static bool InRange(Vector2 vec, Vector2 lower, Vector2 upper)
{
return vec.x >= lower.x && vec.x <= upper.x && vec.y >= lower.y && vec.y <= upper.y;
}
/// <summary>
/// Tests component-wise if a Vector3 is in a given range
/// </summary>
/// <param name="vec">The vector to test</param>
/// <param name="lower">The lower bounds</param>
/// <param name="upper">The upper bounds</param>
/// <returns>true if in range, otherwise false</returns>
public static bool InRange(Vector3 vec, Vector3 lower, Vector3 upper)
{
return vec.x >= lower.x && vec.x <= upper.x && vec.y >= lower.y && vec.y <= upper.y && vec.z >= lower.z && vec.z <= upper.z;
}
/// <summary>
/// Element-wise addition of two Matrix4x4s - extension method
/// </summary>
/// <param name="a">matrix</param>
/// <param name="b">matrix</param>
/// <returns>element-wise (a+b)</returns>
public static Matrix4x4 Add(Matrix4x4 a, Matrix4x4 b)
{
Matrix4x4 result = new Matrix4x4();
result.SetColumn(0, a.GetColumn(0) + b.GetColumn(0));
result.SetColumn(1, a.GetColumn(1) + b.GetColumn(1));
result.SetColumn(2, a.GetColumn(2) + b.GetColumn(2));
result.SetColumn(3, a.GetColumn(3) + b.GetColumn(3));
return result;
}
/// <summary>
/// Element-wise subtraction of two Matrix4x4s - extension method
/// </summary>
/// <param name="a">matrix</param>
/// <param name="b">matrix</param>
/// <returns>element-wise (a-b)</returns>
public static Matrix4x4 Subtract(Matrix4x4 a, Matrix4x4 b)
{
Matrix4x4 result = new Matrix4x4();
result.SetColumn(0, a.GetColumn(0) - b.GetColumn(0));
result.SetColumn(1, a.GetColumn(1) - b.GetColumn(1));
result.SetColumn(2, a.GetColumn(2) - b.GetColumn(2));
result.SetColumn(3, a.GetColumn(3) - b.GetColumn(3));
return result;
}
/// <summary>
/// find unsigned distance of 3D point to an infinite line
/// </summary>
/// <param name="ray">ray that specifies an infinite line</param>
/// <param name="point">3D point</param>
/// <returns>unsigned perpendicular distance from point to line</returns>
public static float DistanceOfPointToLine(Ray ray, Vector3 point)
{
return Vector3.Cross(ray.direction, point - ray.origin).magnitude;
}
/// <summary>
/// Find 3D point that minimizes distance to 2 lines, midpoint of the shortest perpendicular line segment between them
/// </summary>
/// <param name="p">ray that specifies a line</param>
/// <param name="q">ray that specifies a line</param>
/// <returns>point nearest to the lines</returns>
public static Vector3 NearestPointToLines(Ray p, Ray q)
{
float a = Vector3.Dot(p.direction, p.direction);
float b = Vector3.Dot(p.direction, q.direction);
float c = Vector3.Dot(q.direction, q.direction);
Vector3 w0 = p.origin - q.origin;
float den = a * c - b * b;
float epsilon = 0.00001f;
if (den < epsilon)
{
// parallel, so just average origins
return 0.5f * (p.origin + q.origin);
}
float d = Vector3.Dot(p.direction, w0);
float e = Vector3.Dot(q.direction, w0);
float sc = (b * e - c * d) / den;
float tc = (a * e - b * d) / den;
Vector3 point = 0.5f * (p.origin + sc * p.direction + q.origin + tc * q.direction);
return point;
}
/// <summary>
/// Find 3D point that minimizes distance to a set of 2 or more lines, ignoring outliers
/// </summary>
/// <param name="rays">list of rays, each specifying a line, must have at least 1</param>
/// <param name="ransac_iterations">number of iterations: log(1-p)/log(1-(1-E)^s)
/// where p is probability of at least one sample containing s points is all inliers
/// E is proportion of outliers (1-ransac_ratio)
/// e.g. p=0.999, ransac_ratio=0.54, s=2 ==> log(0.001)/(log(1-0.54^2) = 20
/// </param>
/// <param name="ransac_threshold">minimum distance from point to line for a line to be considered an inlier</param>
/// <param name="numActualInliers">return number of inliers: lines that are within ransac_threshold of nearest point</param>
/// <returns>point nearest to the set of lines, ignoring outliers</returns>
public static Vector3 NearestPointToLinesRANSAC(List<Ray> rays, int ransac_iterations, float ransac_threshold, out int numActualInliers)
{
// start with something, just in case no inliers - this works for case of 1 or 2 rays
Vector3 nearestPoint = NearestPointToLines(rays[0], rays[rays.Count - 1]);
numActualInliers = 0;
if (rays.Count > 2)
{
for (int it = 0; it < ransac_iterations; it++)
{
Vector3 testPoint = NearestPointToLines(rays[Random.Range(0, rays.Count)], rays[Random.Range(0, rays.Count)]);
// count inliers
int numInliersForIteration = 0;
for (int ind = 0; ind < rays.Count; ++ind)
{
if (DistanceOfPointToLine(rays[ind], testPoint) < ransac_threshold)
++numInliersForIteration;
}
// remember best
if (numInliersForIteration > numActualInliers)
{
numActualInliers = numInliersForIteration;
nearestPoint = testPoint;
}
}
}
// now find and count actual inliers and do least-squares to find best fit
IEnumerable<Ray> inlierList = rays.Where(r => DistanceOfPointToLine(r, nearestPoint) < ransac_threshold);
numActualInliers = inlierList.Count();
if (numActualInliers >= 2)
{
nearestPoint = NearestPointToLinesLeastSquares(inlierList);
}
return nearestPoint;
}
/// <summary>
/// Find 3D point that minimizes distance to a set of 2 or more lines
/// </summary>
/// <param name="rays">each ray specifies an infinite line</param>
/// <returns>point nearest to the set of lines</returns>
public static Vector3 NearestPointToLinesLeastSquares(IEnumerable<Ray> rays)
{
// finding the point nearest to the set of lines specified by rays
// Use the following formula, where u_i are normalized direction
// vectors along each ray and p_i is a point along each ray.
// -1
// / ===== \ =====
// | \ / T\ | \ / T\
// | > |I - u u | | > |I - u u | p
// | / \ i i/ | / \ i i/ i
// | ===== | =====
// \ i / i
Matrix4x4 sumOfProduct = Matrix4x4.zero;
Vector4 sumOfProductTimesDirection = Vector4.zero;
foreach (Ray r in rays)
{
Vector4 point = r.origin;
Matrix4x4 directionColumnMatrix = new Matrix4x4();
Vector3 rNormal = r.direction.normalized;
directionColumnMatrix.SetColumn(0, rNormal);
Matrix4x4 directionRowMatrix = directionColumnMatrix.transpose;
Matrix4x4 product = directionColumnMatrix * directionRowMatrix;
Matrix4x4 identityMinusDirectionProduct = Subtract(Matrix4x4.identity, product);
sumOfProduct = Add(sumOfProduct, identityMinusDirectionProduct);
Vector4 vectorProduct = identityMinusDirectionProduct * point;
sumOfProductTimesDirection += vectorProduct;
}
Matrix4x4 sumOfProductInverse = sumOfProduct.inverse;
Vector3 nearestPoint = sumOfProductInverse * sumOfProductTimesDirection;
return nearestPoint;
}
/// <summary>
/// Convert degrees to radians.
/// </summary>
/// <param name="degrees">Angle, in degrees.</param>
/// <returns>Angle, in radians.</returns>
public static float DegreesToRadians(double degrees)
{
return (float)(degrees * Mathf.Deg2Rad);
}
/// <summary>
/// Convert radians to degrees.
/// </summary>
/// <param name="radians">Angle, in radians.</param>
/// <returns>Angle, in degrees.</returns>
public static float RadiansToDegrees(float radians)
{
return (radians * Mathf.Rad2Deg);
}
/// <summary>
/// Calculates the angle (at pointA) between two, two-dimensional points.
/// </summary>
/// <param name="pointA">The first point.</param>
/// <param name="pointB">The second point.</param>
/// <returns>
/// The angle between the two points.
/// </returns>
public static float GetAngleBetween(Vector2 pointA, Vector2 pointB)
{
Vector2 diff = pointA - pointB;
return MathUtilities.RadiansToDegrees(Mathf.Atan2(diff.y, diff.x));
}
/// <summary>
/// Clamps via a lerp for a "soft" clamp effect
/// </summary>
/// <param name="pos">number to clamp</param>
/// <param name="min">if pos is less than min, then lerp clamps to this value</param>
/// <param name="max">if pos is more than max, lerp clamps to this value</param>
/// <param name="clampFactor"> Range from 0.0f to 1.0f of how close to snap to min and max </param>
/// <returns>A soft clamped value</returns>
public static float CLampLerp(float pos, float min, float max, float clampFactor)
{
clampFactor = Mathf.Clamp(clampFactor, 0.0f, 1.0f);
if (pos < min)
{
return Mathf.Lerp(pos, min, clampFactor);
}
else if (pos > max)
{
return Mathf.Lerp(pos, max, clampFactor);
}
return pos;
}
/// <summary>
/// Calculates the direction vector from a rotation.
/// </summary>
/// <param name="rotation">Quaternion representing the rotation of the object.</param>
/// <returns>
/// Normalized Vector3 representing the direction vector.
/// </returns>
public static Vector3 GetDirection(Quaternion rotation)
{
return (rotation * Vector3.forward).normalized;
}
/// <summary>
/// Returns if a point lies within a frame of reference view as defined by arguments
/// </summary>
/// <remarks>
/// Field of view parameters are in degrees and plane distances are in meters
/// </remarks>
public static bool IsInFOV(Vector3 testPosition, Transform frameOfReference,
float verticalFOV, float horizontalFOV,
float minPlaneDistance, float maxPlaneDistance)
{
Vector3 deltaPos = testPosition - frameOfReference.position;
Vector3 referenceDeltaPos = TransformDirectionFromTo(null, frameOfReference, deltaPos);
if (referenceDeltaPos.z < minPlaneDistance || referenceDeltaPos.z > maxPlaneDistance)
{
return false;
}
float verticalFovHalf = verticalFOV * 0.5f;
float horizontalFovHalf = horizontalFOV * 0.5f;
referenceDeltaPos = referenceDeltaPos.normalized;
float yaw = Mathf.Asin(referenceDeltaPos.x) * Mathf.Rad2Deg;
float pitch = Mathf.Asin(referenceDeltaPos.y) * Mathf.Rad2Deg;
return Mathf.Abs(yaw) < horizontalFovHalf && Mathf.Abs(pitch) < verticalFovHalf;
}
/// <summary>
/// Returns true if a point lies inside the cone described with given parameters, false otherwise.
/// The cone is inscribed to a radius equal to the vertical height of the provided FOV.
/// The test also ensures the distance from the point to the cone lies within the given range.
/// </summary>
/// <param name="cone">The transform that defines the orientation and position of the cone</param>
/// <param name="point">The point to test if it lies within the cone FOV</param>
/// <param name="fieldOfView">Field of view for the cone which calculates its radius</param>
/// <param name="minDist">Point must be at least this far away (along direction forward) from the cone </param>
/// <param name="maxDist">Point must be at most this far away (along direction forward) from the cone. </param>
/// <remarks>
/// Field of view parameter is in degrees and distances are in meters.
/// </remarks>
public static bool IsInFOVCone(Transform cone,
Vector3 point,
float fieldOfView,
float minDist = 0.05f,
float maxDist = 100f)
{
var dirToPoint = point - cone.position;
var pointDist = Vector3.Dot(cone.forward, dirToPoint);
if (pointDist < minDist || pointDist > maxDist)
{
return false;
}
var degrees = Mathf.Acos(pointDist / dirToPoint.magnitude) * Mathf.Rad2Deg;
return degrees < fieldOfView * 0.5f;
}
}
}