/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using UnityEngine;
namespace Oculus.Interaction
{
///
/// Contains a variety of static methods for specialized utility purposes, along with related child structures and classes.
/// The static methods constrain a 3D data in a variety of ways, or encapsulate highly specific arithmetic operations.
///
///
/// These functions are not general; they are used by various implementations, and the degree to
/// which each function is tightly coupled to the conventions and assumptions of its usage varies. For that reason, you
/// should avoid leveraging these directly; only add new usage if copy-pasting from existing usages, and use the existing
/// implementation as a guide to understand how to use each of these correctly.
///
public class TransformerUtils
{
///
/// Indicates an inclusive range of permissible values for a given floating point datum.
///
[Serializable]
public struct FloatRange
{
public float Min;
public float Max;
}
///
/// Struct describing a set of constraints for a one-dimensional floating point datum. This is used to describe,
/// store, and apply constraints to values like spatial axes (X axis, Y axis, etc.).
///
[Serializable]
public struct ConstrainedAxis
{
///
/// Indicates whether the constraints described in this ConstrainedAxis should be applied.
///
///
/// If false, the datum to which this ConstrainedAxis pertains should be left unconstrained by this instance.
///
public bool ConstrainAxis;
///
/// Indicates the of permissible values for the datum to which this ConstrainedAxis pertains.
///
public FloatRange AxisRange;
///
/// A default ConstrainedAxis instance which applies no constraints.
///
public static ConstrainedAxis Unconstrained => new ConstrainedAxis()
{
ConstrainAxis = false,
AxisRange = new FloatRange() { Min = 1, Max = 1 }
};
}
///
/// A collection of constraints specifically constraining the position values of a
/// 3D datum (typically a Transform).
///
[Serializable]
public class PositionConstraints
{
///
/// Indicates whether the constraints should be considered relative (i.e., applying to a Transform's local position
/// relative to its parent Transform) or absolute (i.e., applying to a Transform's position in world space).
///
public bool ConstraintsAreRelative;
public ConstrainedAxis XAxis;
public ConstrainedAxis YAxis;
public ConstrainedAxis ZAxis;
}
///
/// A collection of constraints specifically constraining the rotation values of a
/// 3D datum (typically a Transform). These constraints are Euler angles expressed in degrees.
///
[Serializable]
public class RotationConstraints
{
public ConstrainedAxis XAxis;
public ConstrainedAxis YAxis;
public ConstrainedAxis ZAxis;
}
///
/// A collection of constraints specifically constraining the scale values of a
/// 3D datum (typically a Transform).
///
[Serializable]
public class ScaleConstraints
{
///
/// Indicates whether the constraints should be considered relative (i.e., applying to a Transform's local scale
/// relative to its parent Transform) or absolute (i.e., applying to a Transform's scale relative to world space).
///
public bool ConstraintsAreRelative;
public ConstrainedAxis XAxis;
public ConstrainedAxis YAxis;
public ConstrainedAxis ZAxis;
}
///
/// Generates a new set of based on an .
///
/// The upon which the new constraints should be based.
/// The initial position from which relative constraints should be relative.
/// New constraints which take into account.
public static PositionConstraints GenerateParentConstraints(PositionConstraints constraints, Vector3 initialPosition)
{
PositionConstraints parentConstraints;
if (!constraints.ConstraintsAreRelative)
{
parentConstraints = constraints;
}
else
{
parentConstraints = new PositionConstraints();
parentConstraints.XAxis = new ConstrainedAxis();
parentConstraints.YAxis = new ConstrainedAxis();
parentConstraints.ZAxis = new ConstrainedAxis();
if (constraints.XAxis.ConstrainAxis)
{
parentConstraints.XAxis.ConstrainAxis = true;
parentConstraints.XAxis.AxisRange.Min = constraints.XAxis.AxisRange.Min + initialPosition.x;
parentConstraints.XAxis.AxisRange.Max = constraints.XAxis.AxisRange.Max + initialPosition.x;
}
if (constraints.YAxis.ConstrainAxis)
{
parentConstraints.YAxis.ConstrainAxis = true;
parentConstraints.YAxis.AxisRange.Min = constraints.YAxis.AxisRange.Min + initialPosition.y;
parentConstraints.YAxis.AxisRange.Max = constraints.YAxis.AxisRange.Max + initialPosition.y;
}
if (constraints.ZAxis.ConstrainAxis)
{
parentConstraints.ZAxis.ConstrainAxis = true;
parentConstraints.ZAxis.AxisRange.Min = constraints.ZAxis.AxisRange.Min + initialPosition.z;
parentConstraints.ZAxis.AxisRange.Max = constraints.ZAxis.AxisRange.Max + initialPosition.z;
}
}
return parentConstraints;
}
///
/// Generates a new set of based on an .
///
/// The upon which the new constraints should be based.
/// The initial scale from which relative constraints should be relative.
/// New constraints which take into account.
public static ScaleConstraints GenerateParentConstraints(ScaleConstraints constraints, Vector3 initialScale)
{
ScaleConstraints parentConstraints;
if (!constraints.ConstraintsAreRelative)
{
parentConstraints = constraints;
}
else
{
parentConstraints = new ScaleConstraints();
parentConstraints.XAxis = new ConstrainedAxis();
parentConstraints.YAxis = new ConstrainedAxis();
parentConstraints.ZAxis = new ConstrainedAxis();
if (constraints.XAxis.ConstrainAxis)
{
parentConstraints.XAxis.ConstrainAxis = true;
parentConstraints.XAxis.AxisRange.Min = constraints.XAxis.AxisRange.Min * initialScale.x;
parentConstraints.XAxis.AxisRange.Max = constraints.XAxis.AxisRange.Max * initialScale.x;
}
if (constraints.YAxis.ConstrainAxis)
{
parentConstraints.YAxis.ConstrainAxis = true;
parentConstraints.YAxis.AxisRange.Min = constraints.YAxis.AxisRange.Min * initialScale.y;
parentConstraints.YAxis.AxisRange.Max = constraints.YAxis.AxisRange.Max * initialScale.y;
}
if (constraints.ZAxis.ConstrainAxis)
{
parentConstraints.ZAxis.ConstrainAxis = true;
parentConstraints.ZAxis.AxisRange.Min = constraints.ZAxis.AxisRange.Min * initialScale.z;
parentConstraints.ZAxis.AxisRange.Max = constraints.ZAxis.AxisRange.Max * initialScale.z;
}
}
return parentConstraints;
}
///
/// Applies a set of to a vector representing the position of a Transform.
///
/// The position of the Transform before constraining.
/// The constraints to be applied to .
///
/// The transform to which constraint should be considered relative; if omitted, constraining will be applied relative to
/// world space.
///
///
/// A position which is as similar as possible to but allowed by
/// .
///
public static Vector3 GetConstrainedTransformPosition(Vector3 unconstrainedPosition, PositionConstraints positionConstraints, Transform relativeTransform = null)
{
Vector3 constrainedPosition = unconstrainedPosition;
// the translation constraints occur in parent space
if (relativeTransform != null)
{
constrainedPosition = relativeTransform.InverseTransformPoint(constrainedPosition);
}
if (positionConstraints.XAxis.ConstrainAxis)
{
constrainedPosition.x = Mathf.Clamp(constrainedPosition.x, positionConstraints.XAxis.AxisRange.Min, positionConstraints.XAxis.AxisRange.Max);
}
if (positionConstraints.YAxis.ConstrainAxis)
{
constrainedPosition.y = Mathf.Clamp(constrainedPosition.y, positionConstraints.YAxis.AxisRange.Min, positionConstraints.YAxis.AxisRange.Max);
}
if (positionConstraints.ZAxis.ConstrainAxis)
{
constrainedPosition.z = Mathf.Clamp(constrainedPosition.z, positionConstraints.ZAxis.AxisRange.Min, positionConstraints.ZAxis.AxisRange.Max);
}
// Convert the constrained position back to world space
if (relativeTransform != null)
{
constrainedPosition = relativeTransform.TransformPoint(constrainedPosition);
}
return constrainedPosition;
}
///
/// Applies a set of to a Quaternion representing the rotation of a Transform.
///
/// The rotation of the Transform before constraining.
/// The constraints to be applied to .
///
/// The transform to which constraint should be considered relative; if omitted, constraining will be applied relative to
/// world space.
///
///
/// A rotation which is as similar as possible to but allowed by
/// .
///
public static Quaternion GetConstrainedTransformRotation(Quaternion unconstrainedRotation, RotationConstraints rotationConstraints, Transform relativeTransform = null)
{
if (relativeTransform != null)
{
unconstrainedRotation = Quaternion.Inverse(relativeTransform.rotation) * unconstrainedRotation;
}
Vector3 euler = unconstrainedRotation.eulerAngles;
float xAngle = euler.x;
float yAngle = euler.y;
float zAngle = euler.z;
if (rotationConstraints.XAxis.ConstrainAxis)
{
xAngle = ClampAngle(xAngle, rotationConstraints.XAxis.AxisRange.Min, rotationConstraints.XAxis.AxisRange.Max);
}
if (rotationConstraints.YAxis.ConstrainAxis)
{
yAngle = ClampAngle(yAngle, rotationConstraints.YAxis.AxisRange.Min, rotationConstraints.YAxis.AxisRange.Max);
}
if (rotationConstraints.ZAxis.ConstrainAxis)
{
zAngle = ClampAngle(zAngle, rotationConstraints.ZAxis.AxisRange.Min, rotationConstraints.ZAxis.AxisRange.Max);
}
Quaternion constrainedRotation = Quaternion.Euler(xAngle, yAngle, zAngle);
// Convert the constrained position back to world space
if (relativeTransform != null)
{
constrainedRotation = relativeTransform.rotation * constrainedRotation;
}
return constrainedRotation.normalized;
float ClampAngle(float angle, float min, float max)
{
if (min == max)
{
return min;
}
if (min <= max)
{
if (angle >= min && angle <= max)
{
return angle;
}
}
else
{
if (angle >= min || angle <= max)
{
return angle;
}
}
if (Mathf.Abs(Mathf.DeltaAngle(angle, min)) <= Mathf.Abs(Mathf.DeltaAngle(max, angle)))
{
return min;
}
return max;
}
}
///
/// Applies a set of to a vector representing the scale of a Transform.
///
/// The scale of the Transform before constraining.
/// The constraints to be applied to .
///
/// A scale which is as similar as possible to but allowed by
/// .
///
public static Vector3 GetConstrainedTransformScale(Vector3 unconstrainedScale, ScaleConstraints scaleConstraints)
{
Vector3 constrainedScale = unconstrainedScale;
if (scaleConstraints.XAxis.ConstrainAxis)
{
constrainedScale.x = Mathf.Clamp(constrainedScale.x, scaleConstraints.XAxis.AxisRange.Min, scaleConstraints.XAxis.AxisRange.Max);
}
if (scaleConstraints.YAxis.ConstrainAxis)
{
constrainedScale.y = Mathf.Clamp(constrainedScale.y, scaleConstraints.YAxis.AxisRange.Min, scaleConstraints.YAxis.AxisRange.Max);
}
if (scaleConstraints.ZAxis.ConstrainAxis)
{
constrainedScale.z = Mathf.Clamp(constrainedScale.z, scaleConstraints.ZAxis.AxisRange.Min, scaleConstraints.ZAxis.AxisRange.Max);
}
return constrainedScale;
}
///
/// Convenience method for taking a Pose in world space (or whatever space is the domain of the transform represented
/// by ) and returning its representation in local space (or whatever space is the
/// range of ).
///
/// The Pose to be transformed.
/// The transformation to be applied.
/// The image of in 's range.
public static Pose WorldToLocalPose(Pose worldPose, Matrix4x4 worldToLocal)
{
return new Pose(worldToLocal.MultiplyPoint3x4(worldPose.position),
worldToLocal.rotation * worldPose.rotation);
}
///
/// This is an old utility method, new uses of which should be avoided. For homogeneous transform arithmetic, just use
/// Unity's built-in Transform and matrix math support.
///
///
/// The naming and use of Pose representation in this implementation can be confusing, so this explanation
/// will avoid them and map back at the end. Conceptually, there are three things: a scaled transform T in world space,
/// an unscaled transform A in T space, and an unscaled transform B in world space. The goal is to find a scaled
/// transform T' (sharing the scale of T) such that an unmodified A in T' equals B when mapped to world space;
/// colloquially, "find where we need to move T to so that A and B are on top of one another." Mathematically, this
/// means the relationship between T' and B is the same as the relationship between T and A; thus, if we find T in
/// the space of A (or, more precisely, A as a scaled transform in world space), we can use that same relationship with
/// B to find T'. This gives us the following arithmetic (treating each transform as a "local to world" matrix):
///
/// given T, A, and B
/// A_inWorldSpace := T * A
/// T_inASpace := inverse(A_inWorldSpace) * T
/// T' := b * T_inASpace
///
/// Mapping this back to the variables and assumptions of this implementation, is T,
/// is A, and is B. The fact that the result T' is a scaled transform
/// is not reflected in the return value of this extension and is instead implicit in its usage. The position and
/// rotation of the returned Pose must be assigned directly to the corresponding fields of the Transform which provided
/// localToWorld, without modifying the scale of that Transform; any other usage may not yield the desired alignment.
///
/// T (see remarks for details).
/// A (see remarks for details).
/// B (see remarks for details).
/// T', excluding scale (see remarks for details).
public static Pose AlignLocalToWorldPose(Matrix4x4 localToWorld, Pose local, Pose world)
{
var basePose = new Pose(localToWorld.MultiplyPoint3x4(local.position),
localToWorld.rotation * local.rotation);
Pose baseInverse = new Pose();
PoseUtils.Inverse(basePose, ref baseInverse);
Pose poseInBase = PoseUtils.Multiply(baseInverse, new Pose(localToWorld.GetPosition(), localToWorld.rotation));
Pose poseInWorld = PoseUtils.Multiply(world, poseInBase);
return poseInWorld;
}
///
/// Calculates how large a certain magnitude in world space is in local space.
///
///
/// This method specifically works by approximating the scale change from the world-space Z axis. If the transform
/// hierarchy involves skew (rotated nonuniform scales), the result may not fully describe the scale relationship between
/// local and world space. For more information, refer to Unity's documentation for Transform.lossyScale.
///
/// The magnitude to be converted to local space.
/// The homogeneous transform from world to local space.
/// The magnitude in local space.
public static float WorldToLocalMagnitude(float magnitude, Matrix4x4 worldToLocal)
{
return worldToLocal.MultiplyVector(magnitude * Vector3.forward).magnitude;
}
///
/// Calculates how large a certain magnitude in local space is in world space.
///
///
/// This method specifically works by approximating the scale change from the local-space Z axis. If the transform
/// hierarchy involves skew (rotated nonuniform scales), the result may not fully describe the scale relationship between
/// local and world space. For more information, refer to Unity's documentation for Transform.lossyScale.
///
/// The magnitude to be converted to world space.
/// The homogeneous transform from local to world space.
/// The magnitude in world space.
public static float LocalToWorldMagnitude(float magnitude, Matrix4x4 localToWorld)
{
return localToWorld.MultiplyVector(magnitude * Vector3.forward).magnitude;
}
///
/// Constrains a to a certain range of positions along a line defined by
/// and minimally distant from a range of points
/// from plus scaled by to
/// plus scaled by .
///
///
/// This is an extremely specialized utility used only by .
///
/// The position to be constrained.
/// The origin of the line to which should be projected for constraint.
/// The direction along which should be constrained.
///
/// The minimum signed distance along the line defined by and to
/// which the closest projection of should be constrained.
///
///
/// The maximum signed distance along the line defined by and to
/// which the closest projection of should be constrained.
///
/// The constrained position.
public static Vector3 ConstrainAlongDirection(
Vector3 position, Vector3 origin, Vector3 direction,
FloatConstraint min, FloatConstraint max)
{
if (!min.Constrain && !max.Constrain) return position;
float distanceAlongDirection = Vector3.Dot(position - origin, direction);
float distanceConstrained = distanceAlongDirection;
if (min.Constrain)
{
distanceConstrained = Mathf.Max(distanceConstrained, min.Value);
}
if (max.Constrain)
{
distanceConstrained = Mathf.Min(distanceConstrained, max.Value);
}
float distanceDelta = distanceConstrained - distanceAlongDirection;
return position + direction * distanceDelta;
}
}
}