Advertisement
DugganSC

Untitled

Oct 1st, 2023
1,121
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 14.59 KB | None | 0 0
  1. using System;
  2. using UnityEngine.Events;
  3. using UnityEngine.XR.Interaction.Toolkit;
  4.  
  5. namespace UnityEngine.XR.Content.Interaction
  6. {
  7.     /// <summary>
  8.     /// An interactable knob that follows the rotation of the interactor
  9.     /// </summary>
  10.     public class XRKnob : XRBaseInteractable
  11.     {
  12.         const float k_ModeSwitchDeadZone = 0.1f; // Prevents rapid switching between the different rotation tracking modes
  13.  
  14.         /// <summary>
  15.         /// Helper class used to track rotations that can go beyond 180 degrees while minimizing accumulation error
  16.         /// </summary>
  17.         struct TrackedRotation
  18.         {
  19.             /// <summary>
  20.             /// The anchor rotation we calculate an offset from
  21.             /// </summary>
  22.             float m_BaseAngle;
  23.  
  24.             /// <summary>
  25.             /// The target rotate we calculate the offset to
  26.             /// </summary>
  27.             float m_CurrentOffset;
  28.  
  29.             /// <summary>
  30.             /// Any previous offsets we've added in
  31.             /// </summary>
  32.             float m_AccumulatedAngle;
  33.  
  34.             /// <summary>
  35.             /// The total rotation that occurred from when this rotation started being tracked
  36.             /// </summary>
  37.             public float totalOffset => m_AccumulatedAngle + m_CurrentOffset;
  38.  
  39.             /// <summary>
  40.             /// Resets the tracked rotation so that total offset returns 0
  41.             /// </summary>
  42.             public void Reset()
  43.             {
  44.                 m_BaseAngle = 0.0f;
  45.                 m_CurrentOffset = 0.0f;
  46.                 m_AccumulatedAngle = 0.0f;
  47.             }
  48.  
  49.             /// <summary>
  50.             /// Sets a new anchor rotation while maintaining any previously accumulated offset
  51.             /// </summary>
  52.             /// <param name="direction">The XZ vector used to calculate a rotation angle</param>
  53.             public void SetBaseFromVector(Vector3 direction)
  54.             {
  55.                 // Update any accumulated angle
  56.                 m_AccumulatedAngle += m_CurrentOffset;
  57.  
  58.                 // Now set a new base angle
  59.                 m_BaseAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
  60.                 m_CurrentOffset = 0.0f;
  61.             }
  62.  
  63.             public void SetTargetFromVector(Vector3 direction)
  64.             {
  65.                 // Set the target angle
  66.                 var targetAngle = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
  67.  
  68.                 // Return the offset
  69.                 m_CurrentOffset = ShortestAngleDistance(m_BaseAngle, targetAngle, 360.0f);
  70.  
  71.                 // If the offset is greater than 90 degrees, we update the base so we can rotate beyond 180 degrees
  72.                 if (Mathf.Abs(m_CurrentOffset) > 90.0f)
  73.                 {
  74.                     m_BaseAngle = targetAngle;
  75.                     m_AccumulatedAngle += m_CurrentOffset;
  76.                     m_CurrentOffset = 0.0f;
  77.                 }
  78.             }
  79.         }
  80.  
  81.         // Test
  82.         // Tes 2
  83.         /* More Test
  84.          *
  85.          *
  86.          *
  87.          *
  88.          */
  89.  
  90.         [Serializable]
  91.         public class ValueChangeEvent : UnityEvent<float> { }
  92.  
  93.         [SerializeField]
  94.         [Tooltip("The object that is visually grabbed and manipulated")]
  95.         Transform m_Handle = null;
  96.  
  97.         [SerializeField]
  98.         [Tooltip("The value of the knob")]
  99.         [Range(0.0f, 1.0f)]
  100.         float m_Value = 0.5f;
  101.  
  102.         [SerializeField]
  103.         [Tooltip("Whether this knob's rotation should be clamped by the angle limits")]
  104.         bool m_ClampedMotion = true;
  105.  
  106.         [SerializeField]
  107.         [Tooltip("Rotation of the knob at value '1'")]
  108.         float m_MaxAngle = 90.0f;
  109.  
  110.         [SerializeField]
  111.         [Tooltip("Rotation of the knob at value '0'")]
  112.         float m_MinAngle = -90.0f;
  113.  
  114.         [SerializeField]
  115.         [Tooltip("Angle increments to support, if greater than '0'")]
  116.         float m_AngleIncrement = 0.0f;
  117.  
  118.         [SerializeField]
  119.         [Tooltip("The position of the interactor controls rotation when outside this radius")]
  120.         float m_PositionTrackedRadius = 0.1f;
  121.  
  122.         [SerializeField]
  123.         [Tooltip("How much controller rotation ")]
  124.         float m_TwistSensitivity = 1.5f;
  125.  
  126.         [SerializeField]
  127.         [Tooltip("Events to trigger when the knob is rotated")]
  128.         ValueChangeEvent m_OnValueChange = new ValueChangeEvent();
  129.  
  130.         IXRSelectInteractor m_Interactor;
  131.  
  132.         bool m_PositionDriven = false;
  133.         bool m_UpVectorDriven = false;
  134.  
  135.         TrackedRotation m_PositionAngles = new TrackedRotation();
  136.         TrackedRotation m_UpVectorAngles = new TrackedRotation();
  137.         TrackedRotation m_ForwardVectorAngles = new TrackedRotation();
  138.  
  139.         float m_BaseKnobRotation = 0.0f;
  140.  
  141.         /// <summary>
  142.         /// The object that is visually grabbed and manipulated
  143.         /// </summary>
  144.         public Transform handle
  145.         {
  146.             get => m_Handle;
  147.             set => m_Handle = value;
  148.         }
  149.  
  150.         /// <summary>
  151.         /// The value of the knob
  152.         /// </summary>
  153.         public float value
  154.         {
  155.             get => m_Value;
  156.             set
  157.             {
  158.                 SetValue(value);
  159.                 SetKnobRotation(ValueToRotation());
  160.             }
  161.         }
  162.  
  163.         /// <summary>
  164.         /// Whether this knob's rotation should be clamped by the angle limits
  165.         /// </summary>
  166.         public bool clampedMotion
  167.         {
  168.             get => m_ClampedMotion;
  169.             set => m_ClampedMotion = value;
  170.         }
  171.  
  172.         /// <summary>
  173.         /// Rotation of the knob at value '1'
  174.         /// </summary>
  175.         public float maxAngle
  176.         {
  177.             get => m_MaxAngle;
  178.             set => m_MaxAngle = value;
  179.         }
  180.  
  181.         /// <summary>
  182.         /// Rotation of the knob at value '0'
  183.         /// </summary>
  184.         public float minAngle
  185.         {
  186.             get => m_MinAngle;
  187.             set => m_MinAngle = value;
  188.         }
  189.  
  190.         /// <summary>
  191.         /// The position of the interactor controls rotation when outside this radius
  192.         /// </summary>
  193.         public float positionTrackedRadius
  194.         {
  195.             get => m_PositionTrackedRadius;
  196.             set => m_PositionTrackedRadius = value;
  197.         }
  198.  
  199.         /// <summary>
  200.         /// Events to trigger when the knob is rotated
  201.         /// </summary>
  202.         public ValueChangeEvent onValueChange => m_OnValueChange;
  203.  
  204.         void Start()
  205.         {
  206.             SetValue(m_Value);
  207.             SetKnobRotation(ValueToRotation());
  208.         }
  209.  
  210.         protected override void OnEnable()
  211.         {
  212.             base.OnEnable();
  213.             selectEntered.AddListener(StartGrab);
  214.             selectExited.AddListener(EndGrab);
  215.         }
  216.  
  217.         protected override void OnDisable()
  218.         {
  219.             selectEntered.RemoveListener(StartGrab);
  220.             selectExited.RemoveListener(EndGrab);
  221.             base.OnDisable();
  222.         }
  223.  
  224.         void StartGrab(SelectEnterEventArgs args)
  225.         {
  226.             m_Interactor = args.interactorObject;
  227.  
  228.             m_PositionAngles.Reset();
  229.             m_UpVectorAngles.Reset();
  230.             m_ForwardVectorAngles.Reset();
  231.  
  232.             UpdateBaseKnobRotation();
  233.             UpdateRotation(true);
  234.         }
  235.  
  236.         void EndGrab(SelectExitEventArgs args)
  237.         {
  238.             m_Interactor = null;
  239.         }
  240.  
  241.         public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
  242.         {
  243.             base.ProcessInteractable(updatePhase);
  244.  
  245.             if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic)
  246.             {
  247.                 if (isSelected)
  248.                 {
  249.                     UpdateRotation();
  250.                 }
  251.             }
  252.         }
  253.  
  254.         void UpdateRotation(bool freshCheck = false)
  255.         {
  256.             // Are we in position offset or direction rotation mode?
  257.             var interactorTransform = m_Interactor.GetAttachTransform(this);
  258.  
  259.             // We cache the three potential sources of rotation - the position offset, the forward vector of the controller, and up vector of the controller
  260.             // We store any data used for determining which rotation to use, then flatten the vectors to the local xz plane
  261.             var localOffset = transform.InverseTransformVector(interactorTransform.position - m_Handle.position);
  262.             localOffset.y = 0.0f;
  263.             var radiusOffset = transform.TransformVector(localOffset).magnitude;
  264.             localOffset.Normalize();
  265.  
  266.             var localForward = transform.InverseTransformDirection(interactorTransform.forward);
  267.             var localY = Math.Abs(localForward.y);
  268.             localForward.y = 0.0f;
  269.             localForward.Normalize();
  270.  
  271.             var localUp = transform.InverseTransformDirection(interactorTransform.up);
  272.             localUp.y = 0.0f;
  273.             localUp.Normalize();
  274.  
  275.  
  276.             if (m_PositionDriven && !freshCheck)
  277.                 radiusOffset *= (1.0f + k_ModeSwitchDeadZone);
  278.  
  279.             // Determine when a certain source of rotation won't contribute - in that case we bake in the offset it has applied
  280.             // and set a new anchor when they can contribute again
  281.             if (radiusOffset >= m_PositionTrackedRadius)
  282.             {
  283.                 if (!m_PositionDriven || freshCheck)
  284.                 {
  285.                     m_PositionAngles.SetBaseFromVector(localOffset);
  286.                     m_PositionDriven = true;
  287.                 }
  288.             }
  289.             else
  290.                 m_PositionDriven = false;
  291.  
  292.             // 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
  293.             if (!freshCheck)
  294.             {
  295.                 if (!m_UpVectorDriven)
  296.                     localY *= (1.0f - (k_ModeSwitchDeadZone * 0.5f));
  297.                 else
  298.                     localY *= (1.0f + (k_ModeSwitchDeadZone * 0.5f));
  299.             }
  300.  
  301.             if (localY > 0.707f)
  302.             {
  303.                 if (!m_UpVectorDriven || freshCheck)
  304.                 {
  305.                     m_UpVectorAngles.SetBaseFromVector(localUp);
  306.                     m_UpVectorDriven = true;
  307.                 }
  308.             }
  309.             else
  310.             {
  311.                 if (m_UpVectorDriven || freshCheck)
  312.                 {
  313.                     m_ForwardVectorAngles.SetBaseFromVector(localForward);
  314.                     m_UpVectorDriven = false;
  315.                 }
  316.             }
  317.  
  318.             // Get angle from position
  319.             if (m_PositionDriven)
  320.                 m_PositionAngles.SetTargetFromVector(localOffset);
  321.  
  322.             if (m_UpVectorDriven)
  323.                 m_UpVectorAngles.SetTargetFromVector(localUp);
  324.             else
  325.                 m_ForwardVectorAngles.SetTargetFromVector(localForward);
  326.  
  327.             // Apply offset to base knob rotation to get new knob rotation
  328.             var knobRotation = m_BaseKnobRotation - ((m_UpVectorAngles.totalOffset + m_ForwardVectorAngles.totalOffset) * m_TwistSensitivity) - m_PositionAngles.totalOffset;
  329.  
  330.             // Clamp to range
  331.             if (m_ClampedMotion)
  332.                 knobRotation = Mathf.Clamp(knobRotation, m_MinAngle, m_MaxAngle);
  333.  
  334.             SetKnobRotation(knobRotation);
  335.  
  336.             // Reverse to get value
  337.             var knobValue = (knobRotation - m_MinAngle) / (m_MaxAngle - m_MinAngle);
  338.             SetValue(knobValue);
  339.         }
  340.  
  341.         void SetKnobRotation(float angle)
  342.         {
  343.             if (m_AngleIncrement > 0)
  344.             {
  345.                 var normalizeAngle = angle - m_MinAngle;
  346.                 angle = (Mathf.Round(normalizeAngle / m_AngleIncrement) * m_AngleIncrement) + m_MinAngle;
  347.             }
  348.  
  349.             if (m_Handle != null)
  350.                 m_Handle.localEulerAngles = new Vector3(0.0f, angle, 0.0f);
  351.         }
  352.  
  353.         void SetValue(float value)
  354.         {
  355.             if (m_ClampedMotion)
  356.                 value = Mathf.Clamp01(value);
  357.  
  358.             if (m_AngleIncrement > 0)
  359.             {
  360.                 var angleRange = m_MaxAngle - m_MinAngle;
  361.                 var angle = Mathf.Lerp(0.0f, angleRange, value);
  362.                 angle = Mathf.Round(angle / m_AngleIncrement) * m_AngleIncrement;
  363.                 value = Mathf.InverseLerp(0.0f, angleRange, angle);
  364.             }
  365.  
  366.             m_Value = value;
  367.             m_OnValueChange.Invoke(m_Value);
  368.         }
  369.  
  370.         float ValueToRotation()
  371.         {
  372.             return m_ClampedMotion ? Mathf.Lerp(m_MinAngle, m_MaxAngle, m_Value) : Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
  373.         }
  374.  
  375.         void UpdateBaseKnobRotation()
  376.         {
  377.             m_BaseKnobRotation = Mathf.LerpUnclamped(m_MinAngle, m_MaxAngle, m_Value);
  378.         }
  379.  
  380.         static float ShortestAngleDistance(float start, float end, float max)
  381.         {
  382.             var angleDelta = end - start;
  383.             var angleSign = Mathf.Sign(angleDelta);
  384.  
  385.             angleDelta = Math.Abs(angleDelta) % max;
  386.             if (angleDelta > (max * 0.5f))
  387.                 angleDelta = -(max - angleDelta);
  388.  
  389.             return angleDelta * angleSign;
  390.         }
  391.  
  392.         void OnDrawGizmosSelected()
  393.         {
  394.             const int k_CircleSegments = 16;
  395.             const float k_SegmentRatio = 1.0f / k_CircleSegments;
  396.  
  397.             // Nothing to do if position radius is too small
  398.             if (m_PositionTrackedRadius <= Mathf.Epsilon)
  399.                 return;
  400.  
  401.             // Draw a circle from the handle point at size of position tracked radius
  402.             var circleCenter = transform.position;
  403.  
  404.             if (m_Handle != null)
  405.                 circleCenter = m_Handle.position;
  406.  
  407.             var circleX = transform.right;
  408.             var circleY = transform.forward;
  409.  
  410.             Gizmos.color = Color.green;
  411.             var segmentCounter = 0;
  412.             while (segmentCounter < k_CircleSegments)
  413.             {
  414.                 var startAngle = (float)segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
  415.                 segmentCounter++;
  416.                 var endAngle = (float)segmentCounter * k_SegmentRatio * 2.0f * Mathf.PI;
  417.  
  418.                 Gizmos.DrawLine(circleCenter + (Mathf.Cos(startAngle) * circleX + Mathf.Sin(startAngle) * circleY) * m_PositionTrackedRadius,
  419.                     circleCenter + (Mathf.Cos(endAngle) * circleX + Mathf.Sin(endAngle) * circleY) * m_PositionTrackedRadius);
  420.             }
  421.         }
  422.  
  423.         void OnValidate()
  424.         {
  425.             if (m_ClampedMotion)
  426.                 m_Value = Mathf.Clamp01(m_Value);
  427.  
  428.             if (m_MinAngle > m_MaxAngle)
  429.                 m_MinAngle = m_MaxAngle;
  430.  
  431.             SetKnobRotation(ValueToRotation());
  432.         }
  433.     }
  434. }
  435.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement