Stratasys-450mc-VR/Assets/XRI_Examples/Global/Scripts/TransformJoint.cs

489 lines
17 KiB
C#

namespace UnityEngine.XR.Content.Interaction
{
/// <summary>
/// Joins a rigidbody and transform together in a way that optimizes for transform and rigidbody-based motion automatically when appropriate.
/// Exerts an increasing force when the rigidbody is separate from the anchor position, but does not oscillate like a spring.
/// </summary>
public class TransformJoint : MonoBehaviour, ISerializationCallbackReceiver
{
const float k_MinMass = 0.01f;
const float k_MaxForceDistance = 0.01f;
[SerializeField]
[Tooltip("A reference to another transform this joint connects to.")]
Transform m_ConnectedBody;
[SerializeField]
[Tooltip("The Position of the anchor around which the joints motion is constrained.")]
Vector3 m_Anchor;
[SerializeField]
[Tooltip("The Rotation of the anchor around which the joints motion is constrained")]
Vector3 m_AnchorAngle;
[SerializeField]
[Tooltip("Should the connectedAnchor be calculated automatically?")]
bool m_AutoConfigureConnectedAnchor;
[SerializeField]
[Tooltip("Position of the anchor relative to the connected transform.")]
Vector3 m_ConnectedAnchor;
[SerializeField]
[Tooltip("The Rotation of the anchor relative to the connected transform.")]
Vector3 m_ConnectedAnchorAngle;
[SerializeField]
[Tooltip("Enable collision between bodies connected with the joint.")]
bool m_EnableCollision;
[SerializeField]
[Tooltip("Baseline force applied when an obstacle is between the joint and the connected transform.")]
float m_BaseForce = 0.25f;
[SerializeField]
[Tooltip("Additional force applied based on the distance between joint and connected transform")]
float m_SpringForce = 1f;
[SerializeField]
[Tooltip("The distance this joint must be from the anchor before teleporting.")]
float m_BreakDistance = 1.5f;
[SerializeField]
[Tooltip("The angular distance this joint must be from the anchor before teleporting.")]
float m_BreakAngle = 120f;
[SerializeField]
[Tooltip("Should the angle be matched?")]
bool m_MatchRotation = true;
[SerializeField]
[Tooltip("Should the mass of the rigidbody be temporarily adjusted to stabilize very strong motion?")]
bool m_AdjustMass = true;
/// <summary>
/// A reference to another transform this joint connects to.
/// </summary>
public Transform connectedBody
{
get => m_ConnectedBody;
set
{
if (m_ConnectedBody == value)
return;
m_ConnectedBody = value;
SetupConnectedBodies(true);
}
}
/// <summary>
/// The Position of the anchor around which the joints motion is constrained.
/// </summary>
public Vector3 anchor
{
get => m_Anchor;
set => m_Anchor = value;
}
/// <summary>
/// The Rotation of the anchor around which the joints motion is constrained
/// </summary>
public Vector3 anchorAngle
{
get => m_AnchorAngle;
set
{
m_AnchorAngle = value;
m_AnchorRotation.eulerAngles = m_AnchorAngle;
}
}
/// <summary>
/// Should the connectedAnchor be calculated automatically?
/// </summary>
public bool autoConfigureConnectedAnchor
{
get => m_AutoConfigureConnectedAnchor;
set
{
m_AutoConfigureConnectedAnchor = value;
SetupConnectedBodies(true);
}
}
/// <summary>
/// Position of the anchor relative to the connected transform.
/// </summary>
public Vector3 connectedAnchor
{
get => m_ConnectedAnchor;
set => m_ConnectedAnchor = value;
}
/// <summary>
/// The Rotation of the anchor relative to the connected transform.
/// </summary>
public Vector3 connectedAnchorAngle
{
get => m_ConnectedAnchorAngle;
set
{
m_ConnectedAnchorAngle = value;
m_ConnectedAnchorRotation.eulerAngles = m_ConnectedAnchorAngle;
}
}
/// <summary>
/// Enable collision between bodies connected with the joint.
/// </summary>
public bool enableCollision
{
get => m_EnableCollision;
set
{
m_EnableCollision = value;
SetupConnectedBodies();
}
}
/// <summary>
/// Should the mass of the rigidbody be temporarily adjusted to stabilize very strong motion?
/// </summary>
public bool adjustMass
{
get => m_AdjustMass;
set => m_AdjustMass = value;
}
/// <summary>
/// Baseline force applied when an obstacle is between the joint and the connected transform.
/// </summary>
public float baseForce
{
get => m_BaseForce;
set => m_BaseForce = value;
}
/// <summary>
/// Additional force applied based on the distance between joint and connected transform
/// </summary>
public float springForce
{
get => m_SpringForce;
set => m_SpringForce = value;
}
/// <summary>
/// The distance this joint must be from the anchor before teleporting.
/// </summary>
public float breakDistance
{
get => m_BreakDistance;
set => m_BreakDistance = value;
}
/// <summary>
/// The angular distance this joint must be from the anchor before teleporting.
/// </summary>
public float breakAngle
{
get => m_BreakAngle;
set => m_BreakAngle = value;
}
/// <summary>
/// The angular distance this joint must be from the anchor before teleporting.
/// </summary>
public bool matchRotation
{
get => m_MatchRotation;
set => m_MatchRotation = value;
}
Quaternion m_AnchorRotation;
Quaternion m_ConnectedAnchorRotation;
Transform m_Transform;
Rigidbody m_Rigidbody;
bool m_FixedSyncFrame;
bool m_ActiveCollision;
bool m_CollisionFrame;
bool m_LastCollisionFrame;
Vector3 m_LastPosition;
Vector3 m_LastDirection;
Collider m_SourceCollider;
Collider m_ConnectedCollider;
float m_BaseMass = 1f;
float m_AppliedForce;
float m_OldForce;
void Start()
{
m_Rigidbody = GetComponent<Rigidbody>();
m_SourceCollider = GetComponent<Collider>();
m_Transform = transform;
m_AnchorRotation.eulerAngles = m_AnchorAngle;
m_ConnectedAnchorRotation.eulerAngles = m_ConnectedAnchorAngle;
if (m_Rigidbody != null && m_Rigidbody.mass > k_MinMass)
m_BaseMass = m_Rigidbody.mass;
// Set up connected anchor if attached
SetupConnectedBodies(true);
}
void OnDestroy()
{
if (m_Rigidbody != null)
m_Rigidbody.mass = m_BaseMass;
}
void SetupConnectedBodies(bool updateAnchor = false)
{
// Handle undoing old setup
// If any properties are pre-existing and have changed, reset the last saved collision ignore pairing
if (m_SourceCollider != null && m_ConnectedCollider != null)
{
Physics.IgnoreCollision(m_SourceCollider, m_ConnectedCollider, false);
m_ConnectedCollider = null;
}
// Handle current setup
if (m_ConnectedBody != null)
{
if (m_AutoConfigureConnectedAnchor && updateAnchor)
{
// Calculate what offsets are currently, set them as anchor
m_ConnectedAnchor = m_ConnectedBody.InverseTransformPoint(m_Rigidbody.position + Vector3.Scale((m_Rigidbody.rotation * m_Anchor), m_Transform.lossyScale));
m_ConnectedAnchorRotation = (m_Rigidbody.rotation * m_AnchorRotation);
m_ConnectedAnchorAngle = m_ConnectedAnchorRotation.eulerAngles;
}
if (m_EnableCollision)
{
// Get collider on connected body
m_ConnectedCollider = m_ConnectedBody.GetComponent<Collider>();
if (m_SourceCollider != null && m_ConnectedCollider != null)
{
Physics.IgnoreCollision(m_SourceCollider, m_ConnectedCollider, true);
}
}
}
}
void LateUpdate()
{
// Move freely unless collision has occurred - then rely on physics
if ((m_CollisionFrame || m_ActiveCollision) && !m_FixedSyncFrame)
{
m_Transform.position = m_Rigidbody.position;
if (m_MatchRotation)
m_Transform.rotation = m_Rigidbody.rotation;
}
m_FixedSyncFrame = false;
}
void FixedUpdate()
{
m_FixedSyncFrame = true;
m_OldForce = m_AppliedForce;
m_AppliedForce = 0f;
// Zero out any existing velocity, we are going to set force manually if needed
m_Rigidbody.velocity = Vector3.zero;
m_Rigidbody.angularVelocity = Vector3.zero;
UpdateBufferedCollision();
UpdatePosition();
if (m_MatchRotation)
UpdateRotation();
if (m_AdjustMass)
{
var offset = (m_AppliedForce / m_BaseMass) * Time.fixedDeltaTime * Time.fixedDeltaTime * 0.5f;
var massScale = Mathf.Max((offset / k_MaxForceDistance), 1f);
m_Rigidbody.mass = m_BaseMass * massScale;
}
// and acc = f/m
// offset = acc * fixedTimestep * fixedTimestep * .5
// Is offset over certain desirable distance? ie. .1m
// scale offset down by scaling mass up
// offset*scale = acc * ftp^2 * .5
// offset = acc *
//
// Based on total force, scale mass
}
void UpdateBufferedCollision()
{
// We buffer collision over three updates
// Once from the actual collision to the first fixed update (m_ActiveCollision)
// Once for an entire fixedUpdate-to-fixedUpdate cycle (m_CollisionFrame)
// And once when a collision is lost - to correct against potential errors when a moving a parent transform
m_LastCollisionFrame = m_CollisionFrame;
m_CollisionFrame = m_ActiveCollision;
m_ActiveCollision = false;
}
void UpdatePosition()
{
// Assume transform is synced to the rigid body position from late update
// Convert anchors to world space
var worldSourceAnchor = m_Rigidbody.position + Vector3.Scale((m_Rigidbody.rotation * m_Anchor), m_Transform.lossyScale);
var worldDestAnchor = m_ConnectedBody.TransformPoint(m_ConnectedAnchor);
// Get the delta between these two positions
// Use this to calculate the target world position for the rigidbody
var positionDelta = worldDestAnchor - worldSourceAnchor;
var offset = positionDelta.magnitude;
var direction = positionDelta.normalized;
var targetPos = m_Rigidbody.position + positionDelta;
// Convert the target and actual positions to world space
var worldPos = m_Rigidbody.position;
if (offset > Mathf.Epsilon)
{
// Are we past the break distance?
if (offset > m_BreakDistance)
{
// Warp back to the target
m_Rigidbody.position = targetPos;
m_Transform.position = targetPos;
m_LastDirection = direction;
return;
}
// Can we move back unobstructed? Do that
if (!m_CollisionFrame)
{
if (m_Rigidbody.SweepTest(direction, out var hitInfo, offset))
{
targetPos = worldPos + (hitInfo.distance * direction);
m_CollisionFrame = true;
}
else
{
// If there was a collision during the previous update, we let one more update cycle pass at the current location
// This helps prevent teleporting through objects during scenarios where many things are playing into the object's position
if (m_LastCollisionFrame)
{
// Compare last direction to this direction
// If they are facing opposite directions, no worry of collision
if (Vector3.Dot(direction, m_LastDirection) > 0)
{
targetPos = worldPos;
m_AppliedForce = m_OldForce;
}
}
}
m_Rigidbody.position = targetPos;
m_Transform.position = targetPos;
}
if (m_CollisionFrame)
{
// Apply a constant force based on spring logic
//Debug.Log(m_Rigidbody.velocity);
var force = (m_BaseForce + offset * m_SpringForce);
m_AppliedForce = force;
m_Rigidbody.AddForce(direction * force, ForceMode.Impulse);
m_LastPosition = m_Rigidbody.position;
}
m_LastDirection = direction;
}
}
void UpdateRotation()
{
// Assume transform is synced to the rigid body position from late update
// Convert anchor rotations to world space
var worldSourceAnchor = m_Rigidbody.rotation * m_AnchorRotation;
var worldDestAnchor = m_ConnectedBody.rotation * m_ConnectedAnchorRotation;
// Get the delta between these two positions
// Use this to calculate the target world position for the rigidbody
var rotationDelta = worldDestAnchor * Quaternion.Inverse(worldSourceAnchor);
var targetRotation = rotationDelta * m_Rigidbody.rotation;
rotationDelta.ToAngleAxis(out var angleInDegrees, out var rotationAxis);
if (angleInDegrees > 180f)
angleInDegrees -= 360f;
var angleOffset = Mathf.Abs(angleInDegrees);
if (angleOffset > Mathf.Epsilon)
{
// Are we past the break distance?
if (angleOffset > m_BreakAngle)
{
// Warp back to the target
m_Rigidbody.rotation = targetRotation;
m_Transform.rotation = targetRotation;
}
// Can we move back unobstructed? Do that
if (!m_CollisionFrame)
{
m_Rigidbody.rotation = targetRotation;
m_Transform.rotation = targetRotation;
}
else
{
var force = ((angleInDegrees / 360f) * (m_BaseForce + m_SpringForce));
m_Rigidbody.AddTorque(rotationAxis * force, ForceMode.Impulse);
}
}
}
void OnCollisionEnter()
{
// While in a collision state, we change state so that the regular transform/visual updates are locked to the fixed update rate
m_ActiveCollision = true;
m_CollisionFrame = true;
}
void OnCollisionStay()
{
m_ActiveCollision = true;
m_CollisionFrame = true;
}
void OnCollisionExit()
{
if (!enabled)
return;
// When exiting collision, we lock to the last known rigidbody position.
// This is because we can end up putting fairly strong forces on this object
// If a parent or pure transform change invalidates the collision these forces can cause an object to move through things
m_Rigidbody.velocity = Vector3.zero;
m_Rigidbody.position = m_LastPosition;
transform.position = m_LastPosition;
}
public void OnBeforeSerialize()
{
}
public void OnAfterDeserialize()
{
m_AnchorRotation.eulerAngles = m_AnchorAngle;
m_ConnectedAnchorRotation.eulerAngles = m_ConnectedAnchorAngle;
}
}
}