init
This commit is contained in:
441
Assets/VRTemplateAssets/Scripts/XRKnob.cs
Normal file
441
Assets/VRTemplateAssets/Scripts/XRKnob.cs
Normal file
@@ -0,0 +1,441 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user