// Knob|Controls3D|100060
|
|
namespace VRTK
|
|
{
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Attaching the script to a game object will allow the user to interact with it as if it were a radial knob. The direction can be freely set.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The script will instantiate the required Rigidbody and Interactable components automatically in case they do not exist yet.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// `VRTK/Examples/025_Controls_Overview` has a couple of rotator knobs that can be rotated by grabbing with the controller and then rotating the controller in the desired direction.
|
|
/// </example>
|
|
[AddComponentMenu("VRTK/Scripts/Controls/3D/VRTK_Knob")]
|
|
[System.Obsolete("`VRTK.VRTK_Knob` has been deprecated and can be recreated with `VRTK.Controllables.PhysicsBased.VRTK_PhysicsRotator`. This script will be removed in a future version of VRTK.")]
|
|
public class VRTK_Knob : VRTK_Control
|
|
{
|
|
/// <summary>
|
|
/// The direction of the knob.
|
|
/// </summary>
|
|
public enum KnobDirection
|
|
{
|
|
/// <summary>
|
|
/// The world x direction.
|
|
/// </summary>
|
|
x,
|
|
/// <summary>
|
|
/// The world y direction.
|
|
/// </summary>
|
|
y,
|
|
/// <summary>
|
|
/// The world z direction.
|
|
/// </summary>
|
|
z
|
|
}
|
|
|
|
[Tooltip("An optional game object to which the knob will be connected. If the game object moves the knob will follow along.")]
|
|
public GameObject connectedTo;
|
|
[Tooltip("The axis on which the knob should rotate. All other axis will be frozen.")]
|
|
public KnobDirection direction = KnobDirection.x;
|
|
[Tooltip("The minimum value of the knob.")]
|
|
public float min = 0f;
|
|
[Tooltip("The maximum value of the knob.")]
|
|
public float max = 100f;
|
|
[Tooltip("The increments in which knob values can change.")]
|
|
public float stepSize = 1f;
|
|
|
|
protected const float MAX_AUTODETECT_KNOB_WIDTH = 3; // multiple of the knob width
|
|
protected KnobDirection finalDirection;
|
|
protected KnobDirection subDirection;
|
|
protected bool subDirectionFound = false;
|
|
protected Quaternion initialRotation;
|
|
protected Vector3 initialLocalRotation;
|
|
protected ConfigurableJoint knobJoint;
|
|
protected bool knobJointCreated = false;
|
|
|
|
protected override void InitRequiredComponents()
|
|
{
|
|
initialRotation = transform.rotation;
|
|
initialLocalRotation = transform.localRotation.eulerAngles;
|
|
InitKnob();
|
|
}
|
|
|
|
protected override bool DetectSetup()
|
|
{
|
|
finalDirection = direction;
|
|
|
|
if (knobJointCreated)
|
|
{
|
|
knobJoint.angularXMotion = ConfigurableJointMotion.Locked;
|
|
knobJoint.angularYMotion = ConfigurableJointMotion.Locked;
|
|
knobJoint.angularZMotion = ConfigurableJointMotion.Locked;
|
|
|
|
switch (finalDirection)
|
|
{
|
|
case KnobDirection.x:
|
|
knobJoint.angularXMotion = ConfigurableJointMotion.Free;
|
|
break;
|
|
case KnobDirection.y:
|
|
knobJoint.angularYMotion = ConfigurableJointMotion.Free;
|
|
break;
|
|
case KnobDirection.z:
|
|
knobJoint.angularZMotion = ConfigurableJointMotion.Free;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (knobJoint)
|
|
{
|
|
knobJoint.xMotion = ConfigurableJointMotion.Locked;
|
|
knobJoint.yMotion = ConfigurableJointMotion.Locked;
|
|
knobJoint.zMotion = ConfigurableJointMotion.Locked;
|
|
|
|
if (connectedTo)
|
|
{
|
|
knobJoint.connectedBody = connectedTo.GetComponent<Rigidbody>();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override ControlValueRange RegisterValueRange()
|
|
{
|
|
return new ControlValueRange()
|
|
{
|
|
controlMin = min,
|
|
controlMax = max
|
|
};
|
|
}
|
|
|
|
protected override void HandleUpdate()
|
|
{
|
|
value = CalculateValue();
|
|
}
|
|
|
|
protected virtual void InitKnob()
|
|
{
|
|
Rigidbody knobRigidbody = GetComponent<Rigidbody>();
|
|
if (knobRigidbody == null)
|
|
{
|
|
knobRigidbody = gameObject.AddComponent<Rigidbody>();
|
|
knobRigidbody.angularDrag = 10; // otherwise knob will continue to move too far on its own
|
|
}
|
|
knobRigidbody.isKinematic = false;
|
|
knobRigidbody.useGravity = false;
|
|
|
|
VRTK_InteractableObject knobInteractableObject = GetComponent<VRTK_InteractableObject>();
|
|
if (knobInteractableObject == null)
|
|
{
|
|
knobInteractableObject = gameObject.AddComponent<VRTK_InteractableObject>();
|
|
}
|
|
knobInteractableObject.isGrabbable = true;
|
|
knobInteractableObject.grabAttachMechanicScript = gameObject.AddComponent<GrabAttachMechanics.VRTK_TrackObjectGrabAttach>();
|
|
knobInteractableObject.grabAttachMechanicScript.precisionGrab = true;
|
|
knobInteractableObject.secondaryGrabActionScript = gameObject.AddComponent<SecondaryControllerGrabActions.VRTK_SwapControllerGrabAction>();
|
|
knobInteractableObject.stayGrabbedOnTeleport = false;
|
|
|
|
knobJoint = GetComponent<ConfigurableJoint>();
|
|
if (knobJoint == null)
|
|
{
|
|
knobJoint = gameObject.AddComponent<ConfigurableJoint>();
|
|
knobJoint.configuredInWorldSpace = false;
|
|
knobJointCreated = true;
|
|
}
|
|
|
|
if (connectedTo)
|
|
{
|
|
Rigidbody knobConnectedToRigidbody = connectedTo.GetComponent<Rigidbody>();
|
|
if (knobConnectedToRigidbody == null)
|
|
{
|
|
knobConnectedToRigidbody = connectedTo.AddComponent<Rigidbody>();
|
|
knobConnectedToRigidbody.useGravity = false;
|
|
knobConnectedToRigidbody.isKinematic = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual KnobDirection DetectDirection()
|
|
{
|
|
KnobDirection returnDirection = KnobDirection.x;
|
|
Bounds bounds = VRTK_SharedMethods.GetBounds(transform);
|
|
|
|
// shoot rays in all directions to learn about surroundings
|
|
RaycastHit hitForward;
|
|
RaycastHit hitBack;
|
|
RaycastHit hitLeft;
|
|
RaycastHit hitRight;
|
|
RaycastHit hitUp;
|
|
RaycastHit hitDown;
|
|
Physics.Raycast(bounds.center, Vector3.forward, out hitForward, bounds.extents.z * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.back, out hitBack, bounds.extents.z * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.left, out hitLeft, bounds.extents.x * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.right, out hitRight, bounds.extents.x * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.up, out hitUp, bounds.extents.y * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.down, out hitDown, bounds.extents.y * MAX_AUTODETECT_KNOB_WIDTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
|
|
// shortest valid ray wins
|
|
float lengthX = (hitRight.collider != null) ? hitRight.distance : float.MaxValue;
|
|
float lengthY = (hitDown.collider != null) ? hitDown.distance : float.MaxValue;
|
|
float lengthZ = (hitBack.collider != null) ? hitBack.distance : float.MaxValue;
|
|
float lengthNegX = (hitLeft.collider != null) ? hitLeft.distance : float.MaxValue;
|
|
float lengthNegY = (hitUp.collider != null) ? hitUp.distance : float.MaxValue;
|
|
float lengthNegZ = (hitForward.collider != null) ? hitForward.distance : float.MaxValue;
|
|
|
|
// TODO: not yet the right decision strategy, works only partially
|
|
if (VRTK_SharedMethods.IsLowest(lengthX, new float[] { lengthY, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = KnobDirection.z;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthY, new float[] { lengthX, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = KnobDirection.y;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthZ, new float[] { lengthX, lengthY, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = KnobDirection.x;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegX, new float[] { lengthX, lengthY, lengthZ, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = KnobDirection.z;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegY, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegZ }))
|
|
{
|
|
returnDirection = KnobDirection.y;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegZ, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegY }))
|
|
{
|
|
returnDirection = KnobDirection.x;
|
|
}
|
|
|
|
return returnDirection;
|
|
}
|
|
|
|
protected virtual float CalculateValue()
|
|
{
|
|
if (!subDirectionFound)
|
|
{
|
|
float angleX = Mathf.Abs(transform.localRotation.eulerAngles.x - initialLocalRotation.x) % 90;
|
|
float angleY = Mathf.Abs(transform.localRotation.eulerAngles.y - initialLocalRotation.y) % 90;
|
|
float angleZ = Mathf.Abs(transform.localRotation.eulerAngles.z - initialLocalRotation.z) % 90;
|
|
angleX = (Mathf.RoundToInt(angleX) >= 89) ? 0 : angleX;
|
|
angleY = (Mathf.RoundToInt(angleY) >= 89) ? 0 : angleY;
|
|
angleZ = (Mathf.RoundToInt(angleZ) >= 89) ? 0 : angleZ;
|
|
|
|
if (Mathf.RoundToInt(angleX) != 0 || Mathf.RoundToInt(angleY) != 0 || Mathf.RoundToInt(angleZ) != 0)
|
|
{
|
|
subDirection = angleX < angleY ? (angleY < angleZ ? KnobDirection.z : KnobDirection.y) : (angleX < angleZ ? KnobDirection.z : KnobDirection.x);
|
|
subDirectionFound = true;
|
|
}
|
|
}
|
|
|
|
float angle = 0;
|
|
switch (subDirection)
|
|
{
|
|
case KnobDirection.x:
|
|
angle = transform.localRotation.eulerAngles.x - initialLocalRotation.x;
|
|
break;
|
|
case KnobDirection.y:
|
|
angle = transform.localRotation.eulerAngles.y - initialLocalRotation.y;
|
|
break;
|
|
case KnobDirection.z:
|
|
angle = transform.localRotation.eulerAngles.z - initialLocalRotation.z;
|
|
break;
|
|
}
|
|
angle = Mathf.Round(angle * 1000f) / 1000f; // not rounding will produce slight offsets in 4th digit that mess up initial value
|
|
|
|
// Quaternion.angle will calculate shortest route and only go to 180
|
|
float calculatedValue = 0;
|
|
if (angle > 0 && angle <= 180)
|
|
{
|
|
calculatedValue = 360 - Quaternion.Angle(initialRotation, transform.rotation);
|
|
}
|
|
else
|
|
{
|
|
calculatedValue = Quaternion.Angle(initialRotation, transform.rotation);
|
|
}
|
|
|
|
// adjust to value scale
|
|
calculatedValue = Mathf.Round((min + Mathf.Clamp01(calculatedValue / 360f) * (max - min)) / stepSize) * stepSize;
|
|
if (min > max && angle != 0)
|
|
{
|
|
calculatedValue = (max + min) - calculatedValue;
|
|
}
|
|
|
|
return calculatedValue;
|
|
}
|
|
}
|
|
}
|