| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- using System;
- using UnityEngine;
- using UnityEngine.Events;
- using UnityEngine.XR.Interaction.Toolkit;
- using UnityEngine.XR.Interaction.Toolkit.Interactables;
- using UnityEngine.XR.Interaction.Toolkit.Interactors;
- namespace Unity.VRTemplate
- {
- /// <summary>
- /// An interactable knob that follows the rotation of the interactor
- /// </summary>
- public class XRKnob : XRBaseInteractable
- {
- const float k_ModeSwitchDeadZone = 0.1f; // Prevents rapid switching between the different rotation tracking modes
- /// <summary>
- /// Helper class used to track rotations that can go beyond 180 degrees while minimizing accumulation error
- /// </summary>
- struct TrackedRotation
- {
- /// <summary>
- /// The anchor rotation we calculate an offset from
- /// </summary>
- float m_BaseAngle;
- /// <summary>
- /// The target rotate we calculate the offset to
- /// </summary>
- float m_CurrentOffset;
- /// <summary>
- /// Any previous offsets we've added in
- /// </summary>
- float m_AccumulatedAngle;
- /// <summary>
- /// The total rotation that occurred from when this rotation started being tracked
- /// </summary>
- public float totalOffset => m_AccumulatedAngle + m_CurrentOffset;
- /// <summary>
- /// Resets the tracked rotation so that total offset returns 0
- /// </summary>
- public void Reset()
- {
- m_BaseAngle = 0.0f;
- m_CurrentOffset = 0.0f;
- m_AccumulatedAngle = 0.0f;
- }
- /// <summary>
- /// Sets a new anchor rotation while maintaining any previously accumulated offset
- /// </summary>
- /// <param name="direction">The XZ vector used to calculate a rotation angle</param>
- public void SetBaseFromVector(Vector3 direction)
- {
- // Update any accumulated angle
- m_AccumulatedAngle += m_CurrentOffset;
- // Now set a new base angle
- m_BaseAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
- m_CurrentOffset = 0.0f;
- }
- /// <summary>
- /// Updates current offset and base angle based on target direction.
- /// </summary>
- /// <param name="direction">The XZ vector used to calculate a rotation angle</param>
- public void SetTargetFromVector(Vector3 direction)
- {
- // Set the target angle
- var targetAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
- // Return the offset
- m_CurrentOffset = ShortestAngleDistance(m_BaseAngle, targetAngle, 360.0f);
- // If the offset is greater than 90 degrees, we update the base so we can rotate beyond 180 degrees
- if (Mathf.Abs(m_CurrentOffset) > 90.0f)
- {
- m_BaseAngle = targetAngle;
- m_AccumulatedAngle += m_CurrentOffset;
- m_CurrentOffset = 0.0f;
- }
- }
- }
- [Serializable]
- [Tooltip("Event called when the value of the knob is changed")]
- public class ValueChangeEvent : UnityEvent<float> { }
- [SerializeField]
- [Tooltip("The object that is visually grabbed and manipulated")]
- Transform m_Handle = null;
- [SerializeField]
- [Tooltip("The value of the knob")]
- [Range(0.0f, 1.0f)]
- float m_Value = 0.5f;
- [SerializeField]
- [Tooltip("Whether this knob's rotation should be clamped by the angle limits")]
- bool m_ClampedMotion = true;
- [SerializeField]
- [Tooltip("Rotation of the knob at value '1'")]
- float m_MaxAngle = 90.0f;
- [SerializeField]
- [Tooltip("Rotation of the knob at value '0'")]
- float m_MinAngle = -90.0f;
- [SerializeField]
- [Tooltip("Angle increments to support, if greater than '0'")]
- float m_AngleIncrement = 0.0f;
- [SerializeField]
- [Tooltip("The position of the interactor controls rotation when outside this radius")]
- float m_PositionTrackedRadius = 0.1f;
- [SerializeField]
- [Tooltip("How much controller rotation")]
- float m_TwistSensitivity = 1.5f;
- [SerializeField]
- [Tooltip("Events to trigger when the knob is rotated")]
- ValueChangeEvent m_OnValueChange = new ValueChangeEvent();
- IXRSelectInteractor m_Interactor;
- bool m_PositionDriven = false;
- bool m_UpVectorDriven = false;
- TrackedRotation m_PositionAngles = new TrackedRotation();
- TrackedRotation m_UpVectorAngles = new TrackedRotation();
- TrackedRotation m_ForwardVectorAngles = new TrackedRotation();
- float m_BaseKnobRotation = 0.0f;
- /// <summary>
- /// The object that is visually grabbed and manipulated
- /// </summary>
- public Transform handle
- {
- get => m_Handle;
- set => m_Handle = value;
- }
- /// <summary>
- /// The value of the knob
- /// </summary>
- public float value
- {
- get => m_Value;
- set
- {
- SetValue(value);
- SetKnobRotation(ValueToRotation());
- }
- }
- /// <summary>
- /// Whether this knob's rotation should be clamped by the angle limits
- /// </summary>
- public bool clampedMotion
- {
- get => m_ClampedMotion;
- set => m_ClampedMotion = value;
- }
- /// <summary>
- /// Rotation of the knob at value '1'
- /// </summary>
- public float maxAngle
- {
- get => m_MaxAngle;
- set => m_MaxAngle = value;
- }
- /// <summary>
- /// Rotation of the knob at value '0'
- /// </summary>
- public float minAngle
- {
- get => m_MinAngle;
- set => m_MinAngle = value;
- }
- /// <summary>
- /// The position of the interactor controls rotation when outside this radius
- /// </summary>
- public float positionTrackedRadius
- {
- get => m_PositionTrackedRadius;
- set => m_PositionTrackedRadius = value;
- }
- /// <summary>
- /// Events to trigger when the knob is rotated
- /// </summary>
- public ValueChangeEvent onValueChange => m_OnValueChange;
- void Start()
- {
- SetValue(m_Value);
- SetKnobRotation(ValueToRotation());
- }
- protected override void OnEnable()
- {
- base.OnEnable();
- selectEntered.AddListener(StartGrab);
- selectExited.AddListener(EndGrab);
- }
- protected override void OnDisable()
- {
- selectEntered.RemoveListener(StartGrab);
- selectExited.RemoveListener(EndGrab);
- base.OnDisable();
- }
- void StartGrab(SelectEnterEventArgs args)
- {
- m_Interactor = args.interactorObject;
- m_PositionAngles.Reset();
- m_UpVectorAngles.Reset();
- m_ForwardVectorAngles.Reset();
- UpdateBaseKnobRotation();
- UpdateRotation(true);
- }
- void EndGrab(SelectExitEventArgs args)
- {
- m_Interactor = null;
- }
- /// <inheritdoc />
- public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
- {
- base.ProcessInteractable(updatePhase);
- if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
- {
- if (isSelected)
- {
- UpdateRotation();
- }
- }
- }
- /// <inheritdoc />
- public override Transform GetAttachTransform(IXRInteractor interactor)
- {
- return m_Handle;
- }
- void UpdateRotation(bool freshCheck = false)
- {
- // Are we in position offset or direction rotation mode?
- var interactorTransform = m_Interactor.GetAttachTransform(this);
- // We cache the three potential sources of rotation - the position offset, the forward vector of the controller, and up vector of the controller
- // We store any data used for determining which rotation to use, then flatten the vectors to the local xz plane
- var localOffset = transform.InverseTransformVector(interactorTransform.position - m_Handle.position);
- localOffset.y = 0.0f;
- var radiusOffset = transform.TransformVector(localOffset).magnitude;
- localOffset.Normalize();
- var localForward = transform.InverseTransformDirection(interactorTransform.forward);
- var localY = Math.Abs(localForward.y);
- localForward.y = 0.0f;
- localForward.Normalize();
- var localUp = transform.InverseTransformDirection(interactorTransform.up);
- localUp.y = 0.0f;
- localUp.Normalize();
- if (m_PositionDriven && !freshCheck)
- radiusOffset *= (1.0f + k_ModeSwitchDeadZone);
- // Determine when a certain source of rotation won't contribute - in that case we bake in the offset it has applied
- // and set a new anchor when they can contribute again
- if (radiusOffset >= m_PositionTrackedRadius)
- {
- if (!m_PositionDriven || freshCheck)
- {
- m_PositionAngles.SetBaseFromVector(localOffset);
- m_PositionDriven = true;
- }
- }
- else
- m_PositionDriven = false;
- // If it's not a fresh check, then we weight the local Y up or down to keep it from flickering back and forth at boundaries
- if (!freshCheck)
- {
- if (!m_UpVectorDriven)
- localY *= (1.0f - (k_ModeSwitchDeadZone * 0.5f));
- else
- localY *= (1.0f + (k_ModeSwitchDeadZone * 0.5f));
- }
- if (localY > 0.707f)
- {
- if (!m_UpVectorDriven || freshCheck)
- {
- m_UpVectorAngles.SetBaseFromVector(localUp);
- m_UpVectorDriven = true;
- }
- }
- else
- {
- if (m_UpVectorDriven || freshCheck)
- {
- m_ForwardVectorAngles.SetBaseFromVector(localForward);
- m_UpVectorDriven = false;
- }
- }
- // Get angle from position
- if (m_PositionDriven)
- m_PositionAngles.SetTargetFromVector(localOffset);
- if (m_UpVectorDriven)
- m_UpVectorAngles.SetTargetFromVector(localUp);
- else
- m_ForwardVectorAngles.SetTargetFromVector(localForward);
- // Apply offset to base knob rotation to get new knob rotation
- var knobRotation = m_BaseKnobRotation - ((m_UpVectorAngles.totalOffset + m_ForwardVectorAngles.totalOffset) * m_TwistSensitivity) - m_PositionAngles.totalOffset;
- // Clamp to range
- if (m_ClampedMotion)
- knobRotation = Mathf.Clamp(knobRotation, m_MinAngle, m_MaxAngle);
- SetKnobRotation(knobRotation);
- // Reverse to get value
- var knobValue = (knobRotation - m_MinAngle) / (m_MaxAngle - m_MinAngle);
- SetValue(knobValue);
- }
- void SetKnobRotation(float angle)
- {
- if (m_AngleIncrement > 0)
- {
- var normalizeAngle = angle - m_MinAngle;
- angle = (Mathf.Round(normalizeAngle / m_AngleIncrement) * m_AngleIncrement) + m_MinAngle;
- }
- if (m_Handle != null)
- m_Handle.localEulerAngles = new Vector3(0.0f, angle, 0.0f);
- }
- void SetValue(float newValue)
- {
- if (m_ClampedMotion)
- newValue = Mathf.Clamp01(newValue);
- if (m_AngleIncrement > 0)
- {
- var angleRange = m_MaxAngle - m_MinAngle;
- var angle = Mathf.Lerp(0.0f, angleRange, newValue);
- angle = Mathf.Round(angle / m_AngleIncrement) * m_AngleIncrement;
- newValue = Mathf.InverseLerp(0.0f, angleRange, angle);
- }
- m_Value = newValue;
- m_OnValueChange.Invoke(m_Value);
- }
- float ValueToRotation()
- {
- return m_ClampedMotion ? Mathf.Lerp(m_MinAngle, m_MaxAngle, m_Value) : Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
- }
- void UpdateBaseKnobRotation()
- {
- m_BaseKnobRotation = Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
- }
- static float ShortestAngleDistance(float start, float end, float max)
- {
- var angleDelta = end - start;
- var angleSign = Mathf.Sign(angleDelta);
- angleDelta = Math.Abs(angleDelta) % max;
- if (angleDelta > (max * 0.5f))
- angleDelta = -(max - angleDelta);
- return angleDelta * angleSign;
- }
- void OnDrawGizmosSelected()
- {
- const int k_CircleSegments = 16;
- const float k_SegmentRatio = 1.0f / k_CircleSegments;
- // Nothing to do if position radius is too small
- if (m_PositionTrackedRadius <= Mathf.Epsilon)
- return;
- var knobTransform = transform;
- // Draw a circle from the handle point at size of position tracked radius
- var circleCenter = knobTransform.position;
- if (m_Handle != null)
- circleCenter = m_Handle.position;
- var circleX = knobTransform.right;
- var circleY = knobTransform.forward;
- Gizmos.color = Color.green;
- var segmentCounter = 0;
- while (segmentCounter < k_CircleSegments)
- {
- var startAngle = segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
- segmentCounter++;
- var endAngle = segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
- Gizmos.DrawLine(circleCenter + (Mathf.Cos(startAngle) * circleX + Mathf.Sin(startAngle) * circleY) * m_PositionTrackedRadius,
- circleCenter + (Mathf.Cos(endAngle) * circleX + Mathf.Sin(endAngle) * circleY) * m_PositionTrackedRadius);
- }
- }
- void OnValidate()
- {
- if (m_ClampedMotion)
- m_Value = Mathf.Clamp01(m_Value);
- if (m_MinAngle > m_MaxAngle)
- m_MinAngle = m_MaxAngle;
- SetKnobRotation(ValueToRotation());
- }
- }
- }
|