using UnityEngine;
namespace Passer {
/// Mechanical Joints can be used to limit the movements of a Kinematic Rigidbody in local space.
[HelpURLAttribute("https://passervr.com/documentation/humanoid-control/grabbing-objects/rigidbody-limitations/")]
public class MechanicalJoint : MonoBehaviour {
protected Rigidbody rb;
protected Transform t;
public Transform parent;
/// When true, the local X-position is limited
/// The limits are set by the X member of the minLocalPosition and maxLocalPosition
/// When the bounds are both zero, the local X position is locked.
public bool limitX = true;
/// When true, the local Y-position is limited
/// The limits are set by the Y member of the minLocalPosition and maxLocalPosition
/// When the bounds are both zero, the local Y position is locked.
public bool limitY = true;
/// When true, the local Z-position is limited
/// The limits are set by the Z member of the minLocalPosition and maxLocalPosition
/// When the bounds are both zero, the local Z position is locked.
public bool limitZ = true;
/// The base position for the bounds
/// The minLocalPosition and maxLocalPosition are relative to the basePosition.
public Vector3 basePosition;
/// The minimum local position.
/// This localPosition is relative to the basePosition.
public Vector3 minLocalPosition;
/// The maximum local position.
/// This localPosition is relative to the basePosition.
public Vector3 maxLocalPosition;
/// Linearly interpolates between min and maxLocalPostion.x
/// 0 matches the minLocalPosition on the X axis
/// 1 matches the maxLocalPosition on the X axis
public void LerpX(float f) {
if (!limitX)
return;
Vector3 localPosition = GetLocalPosition();
float positionX = Mathf.Lerp(minLocalPosition.x, maxLocalPosition.x, f);
localPosition = new Vector3(positionX, localPosition.y, localPosition.z);
transform.position = GetWorldPosition(localPosition);
}
/// Linearly interpolates between min and maxLocalPostion.y
/// 0 matches the minLocalPosition on the Y axis
/// 1 matches the maxLocalPosition on the Y axis
public void LerpY(float f) {
if (!limitY)
return;
Vector3 localPosition = GetLocalPosition();
float positionY = Mathf.Lerp(minLocalPosition.y, maxLocalPosition.y, f);
localPosition = new Vector3(localPosition.x, positionY, localPosition.z);
transform.position = GetWorldPosition(localPosition);
}
/// Linearly interpolates between min and maxLocalPostion.z
/// 0 matches the minLocalPosition on the Z axis
/// 1 matches the maxLocalPosition on the Z axis
public void LerpZ(float f) {
if (!limitZ)
return;
Vector3 localPosition = GetLocalPosition();
float positionZ = Mathf.Lerp(minLocalPosition.z, maxLocalPosition.z, f);
localPosition = new Vector3(localPosition.x, localPosition.y, positionZ);
transform.position = GetWorldPosition(localPosition);
}
protected Vector3 GetLocalPosition() {
Vector3 localPosition = transform.localPosition;
Quaternion localRotation = transform.localRotation;
if (parent != null) {
localPosition = parent.InverseTransformPoint(transform.position);
localRotation = Quaternion.Inverse(parent.rotation) * transform.rotation;
}
Vector3 localPos = Quaternion.Inverse(localRotation) * (localPosition - basePosition);
return localPos;
}
protected Vector3 GetWorldPosition(Vector3 localPosition) {
Quaternion localRotation = transform.localRotation;
if (parent != null) {
localRotation = Quaternion.Inverse(parent.rotation) * transform.rotation;
}
Vector3 position = basePosition + localRotation * localPosition;
Vector3 worldPosition = parent != null ? parent.TransformPoint(position) : position;
return worldPosition;
}
/// The base rotation for the bounds
public Quaternion baseRotation = Quaternion.identity;
/// When the the local rotation is limited
/// The limitation is determined by a maxLocalAngle around the limitAnglesAxis.
/// With this limitation, the rigidbody can only rotated around the limmitAngleAxis.
public bool limitAngle = true;
/// The maximum angle around the limitAngleAxis
/// When this is zero, the local rotation is locked.
public float minLocalAngle;
/// The maximum angle around the limitAngleAxis
/// When this is zero, the local rotation is locked.
public float maxLocalAngle;
/// The axis around which the Rigidbody can rotate
public Vector3 limitAngleAxis = Vector3.up;
public enum RotationMethod {
AngleDifference,
PositionDifference
}
public RotationMethod rotationMethod;
#region Init
protected virtual void Awake() {
t = GetComponent();
parent = t.parent;
}
protected virtual void Start() {
gameObjectEvent.value = this.gameObject;
}
#endregion
#region Update
virtual protected void FixedUpdate() {
if (speed.sqrMagnitude != 0) {
float speedX = ((speed.x < 0 && xValue > 0) || (speed.x > 0 && xValue < 1)) ? speed.x : 0;
float speedY = ((speed.y < 0 && yValue > 0) || (speed.y > 0 && yValue < 1)) ? speed.y : 0;
float speedZ = ((speed.z < 0 && zValue > 0) || (speed.z > 0 && zValue < 1)) ? speed.z : 0;
speed = new Vector3(speedX, speedY, speedZ);
transform.position = transform.position + transform.rotation * speed * Time.fixedDeltaTime;
}
if (rotationSpeed != 0) {
if ((rotationSpeed < 0 && angleValue <= 0) || (rotationSpeed > 0 && angleValue >= 1))
rotationSpeed = 0;
transform.rotation *= Quaternion.AngleAxis(rotationSpeed * Time.fixedDeltaTime, limitAngleAxis);
}
if (rb == null) {
rb = GetComponent();
if (rb == null)
return;
}
Vector3 correctionTranslation = GetCorrectionVector();
rb.transform.position = rb.position + correctionTranslation;
Quaternion correctionRotation = GetCorrectionAxisRotation();
rb.transform.rotation = rb.rotation * correctionRotation;
UpdateEvents();
}
public Vector3 GetCorrectionVector() {
Vector3 localPosition = GetLocalPosition();
float x = limitX ? Mathf.Clamp(localPosition.x, minLocalPosition.x, maxLocalPosition.x) : localPosition.x;
float y = limitY ? Mathf.Clamp(localPosition.y, minLocalPosition.y, maxLocalPosition.y) : localPosition.y;
float z = limitZ ? Mathf.Clamp(localPosition.z, minLocalPosition.z, maxLocalPosition.z) : localPosition.z;
localPosition = new Vector3(x, y, z);
Vector3 worldPosition = GetWorldPosition(localPosition);
xValue = (x - minLocalPosition.x) / (maxLocalPosition.x - minLocalPosition.x);
yValue = (y - minLocalPosition.y) / (maxLocalPosition.y - minLocalPosition.y);
zValue = (z - minLocalPosition.z) / (maxLocalPosition.z - minLocalPosition.z);
Vector3 correctionVector = worldPosition - transform.position;
return correctionVector;
}
public Quaternion GetCorrectionRotation() {
Quaternion correctionRotation = Quaternion.identity;
if (parent == null)
correctionRotation = Quaternion.Inverse(transform.rotation) * baseRotation;
else
correctionRotation = Quaternion.Inverse(transform.rotation) * parent.rotation * baseRotation;
return correctionRotation;
}
public Quaternion GetCorrectionAxisRotation() { // V0gue
Quaternion localRotation = Quaternion.Inverse(baseRotation) * t.rotation;
if (parent != null)
localRotation = Quaternion.Inverse(baseRotation) * Quaternion.Inverse(parent.rotation) * t.rotation;
Quaternion twist = GetTwist(localRotation, limitAngleAxis);
Quaternion clampedLocalRotation;
Vector3 twistAxis;
float twistAngle;
twist.ToAngleAxis(out twistAngle, out twistAxis);
if (limitAngle == false || twistAngle != 0) {
float angle = Vector3.Angle(limitAngleAxis, twistAxis);
twistAngle = UnityAngles.Normalize(twistAngle);
if ((angle < 90 && twistAngle > 0) || (angle > 90 && twistAngle < 0))
clampedLocalRotation = Quaternion.RotateTowards(Quaternion.identity, twist, maxLocalAngle);
else
clampedLocalRotation = Quaternion.RotateTowards(Quaternion.identity, twist, -minLocalAngle);
}
else
clampedLocalRotation = localRotation;
//Quaternion clampedLocalRotation = limitAngle ? Quaternion.RotateTowards(Quaternion.identity, twist, maxLocalAngle) : localRotation;
// This check will prevent instability when rotation > 90 degrees
if (Quaternion.Angle(localRotation, clampedLocalRotation) == 0)
clampedLocalRotation = localRotation;
calculateAngleValue(clampedLocalRotation);
Quaternion rbRotation;
if (parent != null)
rbRotation = parent.rotation * baseRotation * clampedLocalRotation;
else
rbRotation = baseRotation * clampedLocalRotation;
Quaternion correctionRotation = Quaternion.Inverse(rb.rotation) * rbRotation;
return correctionRotation;
}
public Quaternion GetCorrectionAxisRotation_old() {
Quaternion localRotation = Quaternion.Inverse(baseRotation) * t.rotation;
if (parent != null)
localRotation = Quaternion.Inverse(parent.rotation) * localRotation;
Quaternion twist = GetTwist(localRotation, limitAngleAxis);
Quaternion clampedRotation;
Vector3 twistAxis;
float twistAngle;
twist.ToAngleAxis(out twistAngle, out twistAxis);
if (limitAngle == false || twistAngle != 0) {
float angle = Vector3.Angle(limitAngleAxis, twistAxis);
twistAngle = UnityAngles.Normalize(twistAngle);
if ((angle < 90 && twistAngle > 0) || (angle > 90 && twistAngle < 0))
clampedRotation = Quaternion.RotateTowards(Quaternion.identity, twist, maxLocalAngle);
else
clampedRotation = Quaternion.RotateTowards(Quaternion.identity, twist, -minLocalAngle);
}
else
clampedRotation = localRotation;
// This check will prevent instability when rotation > 90 degrees
if (Quaternion.Angle(localRotation, clampedRotation) == 0)
clampedRotation = localRotation;
Quaternion transformRotation = t.rotation;
if (parent != null) {
transformRotation = Quaternion.Inverse(parent.rotation) * transformRotation;
}
Quaternion correctionRotation = clampedRotation * Quaternion.Inverse(transformRotation);
calculateAngleValue(clampedRotation);
return baseRotation * correctionRotation;
}
public Quaternion GetCorrectionAxisRotation2() {
Quaternion localRotation = Quaternion.Inverse(baseRotation) * t.rotation;
if (parent != null)
localRotation = Quaternion.Inverse(parent.rotation) * localRotation;
Quaternion twist = GetTwist(localRotation, limitAngleAxis);
Quaternion clampedRotation = limitAngle ? Quaternion.RotateTowards(Quaternion.identity, twist, maxLocalAngle) : localRotation;
// This check will prevent instability when rotation > 90 degrees
if (Quaternion.Angle(localRotation, clampedRotation) == 0)
clampedRotation = localRotation;
Quaternion rbRotation = clampedRotation;
if (parent != null)
rbRotation = parent.rotation * rbRotation;
Quaternion correctionRotation = rbRotation * Quaternion.Inverse(localRotation);
calculateAngleValue(clampedRotation);
return correctionRotation;
}
private void calculateAngleValue(Quaternion clampedRotation) {
float angle;
Vector3 axis;
clampedRotation.ToAngleAxis(out angle, out axis);
if (VectorIsNan(axis)) {
angle = 0;
axis = limitAngleAxis;
}
angle = UnityAngles.Normalize(angle);
if (Vector3.Angle(axis, limitAngleAxis) > 90)
angle = -angle;
if (angle > 0 && maxLocalAngle > 0)
angleValue = angle / maxLocalAngle;
else if (angle < 0 && minLocalAngle < 0)
angleValue = angle / -minLocalAngle;
else
angleValue = 0;
}
private bool VectorIsNan(Vector3 v) {
if (float.IsNaN(v.x) || float.IsNaN(v.y) || float.IsNaN(v.z))
return true;
if (float.IsInfinity(v.x) || float.IsInfinity(v.y) || float.IsInfinity(v.z))
return true;
return false;
}
private static Quaternion GetTwist(Quaternion rotation, Vector3 axis) {
Vector3 ra = new Vector3(rotation.x, rotation.y, rotation.z); // rotation axis
Vector3 p = Vector3.Project(ra, axis); // return projection v1 on to v2 (parallel component)
Quaternion twist = new Quaternion(p.x, p.y, p.z, rotation.w);
Quaternion normalizedTwist = Normalize(twist);
return normalizedTwist;
}
private static Quaternion Normalize(Quaternion q) {
float length = Mathf.Sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
float scale = 1.0f / length;
Quaternion q1 = new Quaternion(q.x * scale, q.y * scale, q.z * scale, q.w * scale);
return q1;
}
#endregion
protected virtual void OnDestroy() {
gameObjectEvent.value = null;
}
protected float xValue;
protected float yValue;
protected float zValue;
protected float angleValue;
#region Animations
protected Vector3 speed;
protected float rotationSpeed;
public void MoveForward(float speedZ) {
speed = new Vector3(speed.x, speed.y, speedZ);
}
public void MoveSideward(float speedX) {
speed = new Vector3(speedX, speed.y, speed.z);
}
public void MoveUpward(float speedY) {
speed = new Vector3(speed.x, speedY, speed.z);
}
public void SetSpeed(Vector3 speed) {
this.speed = speed;
}
public void Rotate(float speed) {
rotationSpeed = speed; // Quaternion.AngleAxis(speed, limitAngleAxis);
}
#endregion
#region Events
public GameObjectEventHandlers gameObjectEvent = new GameObjectEventHandlers() {
label = "GameObject Event",
id = 0,
tooltip = "Call functions based on the GameObject life cycle",
eventTypeLabels = new string[] {
"Never",
"Start",
"On Destroy",
"Update",
" ",
" ",
" "
}
};
protected static string[] sliderEventTypeLabels = new string[] {
"Never",
"On Min",
"On Max",
"While Min",
"While Max",
"On Change",
"Continuous"
};
public FloatEventHandlers xSliderEvents = new FloatEventHandlers() {
label = "X Axis",
id = 1,
eventTypeLabels = sliderEventTypeLabels,
tooltip =
"Call function using the X axis range value\n" +
"Parameter: the range along the X axis (-1..1)"
};
public FloatEventHandlers ySliderEvents = new FloatEventHandlers() {
label = "Y Axis",
id = 2,
eventTypeLabels = sliderEventTypeLabels,
tooltip =
"Call function using the Y axis range value\n" +
"Parameter: the range along the Y axis (-1..1)"
};
public FloatEventHandlers zSliderEvents = new FloatEventHandlers() {
label = "Z Axis",
id = 3,
eventTypeLabels = sliderEventTypeLabels,
tooltip =
"Call function using the Z axis range value\n" +
"Parameter: the range along the Z axis (-1..1)"
};
public FloatEventHandlers angleEvents = new FloatEventHandlers() {
label = "Angle",
id = 4,
eventTypeLabels = sliderEventTypeLabels,
};
protected void UpdateEvents() {
xSliderEvents.value = xValue;
ySliderEvents.value = yValue;
zSliderEvents.value = zValue;
angleEvents.value = angleValue;
}
#endregion
#region Gizmos
protected virtual void OnDrawGizmosSelected() {
if (!isActiveAndEnabled)
return;
if (!Application.isPlaying)
parent = transform.parent;
Vector3 localPosition = transform.localPosition;
Quaternion localRotation = transform.localRotation;
if (parent != null) {
localPosition = parent.InverseTransformPoint(transform.position);
localRotation = Quaternion.Inverse(parent.rotation) * transform.rotation;
}
if (limitX) {
Gizmos.color = Color.red;
Vector3 localRight = localRotation * Vector3.right;
// Project localPosition on basePane
Vector3 basePositionX = ProjectPointOnPlane(localPosition, basePosition, localRight);
Vector3 minPositionX = basePositionX + localRight * minLocalPosition.x;
Vector3 maxPositionX = basePositionX + localRight * maxLocalPosition.x;
DrawRange(minPositionX, maxPositionX);
}
if (limitY) {
Gizmos.color = Color.green;
Vector3 localUp = localRotation * Vector3.up;
// Project localPosition on baseline;
Vector3 basePositionY = ProjectPointOnPlane(localPosition, basePosition, localUp);
Vector3 minPositionY = basePositionY + localUp * minLocalPosition.y;
Vector3 maxPositionY = basePositionY + localUp * maxLocalPosition.y;
DrawRange(minPositionY, maxPositionY);
}
if (limitZ) {
Gizmos.color = Color.blue;
Vector3 localForward = localRotation * Vector3.forward;
// Project localPosition on baseline;
Vector3 basePositionZ = ProjectPointOnPlane(localPosition, basePosition, localForward);
Vector3 minPositionZ = basePositionZ + localForward * minLocalPosition.z;
Vector3 maxPositionZ = basePositionZ + localForward * maxLocalPosition.z;
DrawRange(minPositionZ, maxPositionZ);
}
}
protected Vector3 ProjectPointOnPlane(Vector3 point, Vector3 planeOrigin, Vector3 planeNormal) {
Vector3 v = point - planeOrigin;
Vector3 d = Vector3.Project(v, planeNormal.normalized);
Vector3 projectedPoint = point - d;
return projectedPoint;
}
private void DrawRange(Vector3 minPosition, Vector3 maxPosition) {
Vector3 worldMinPosition = parent != null ? parent.TransformPoint(minPosition) : minPosition;
Vector3 worldMaxPosition = parent != null ? parent.TransformPoint(maxPosition) : maxPosition;
Gizmos.DrawLine(worldMinPosition, worldMaxPosition);
Gizmos.DrawSphere(worldMinPosition, 0.005F);
Gizmos.DrawSphere(worldMaxPosition, 0.005F);
}
#endregion
}
}