123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548 |
- //======= Copyright (c) Valve Corporation, All rights reserved. ===============
- //
- // Purpose: Interactable that can be used to move in a circular motion
- //
- //=============================================================================
- using UnityEngine;
- using UnityEngine.Events;
- using System.Collections;
- namespace Valve.VR.InteractionSystem
- {
- //-------------------------------------------------------------------------
- [RequireComponent( typeof( Interactable ) )]
- public class CircularDrive : MonoBehaviour
- {
- public enum Axis_t
- {
- XAxis,
- YAxis,
- ZAxis
- };
- [Tooltip( "The axis around which the circular drive will rotate in local space" )]
- public Axis_t axisOfRotation = Axis_t.XAxis;
- [Tooltip( "Child GameObject which has the Collider component to initiate interaction, only needs to be set if there is more than one Collider child" )]
- public Collider childCollider = null;
- [Tooltip( "A LinearMapping component to drive, if not specified one will be dynamically added to this GameObject" )]
- public LinearMapping linearMapping;
- [Tooltip( "If true, the drive will stay manipulating as long as the button is held down, if false, it will stop if the controller moves out of the collider" )]
- public bool hoverLock = false;
- [HeaderAttribute( "Limited Rotation" )]
- [Tooltip( "If true, the rotation will be limited to [minAngle, maxAngle], if false, the rotation is unlimited" )]
- public bool limited = false;
- public Vector2 frozenDistanceMinMaxThreshold = new Vector2( 0.1f, 0.2f );
- public UnityEvent onFrozenDistanceThreshold;
- [HeaderAttribute( "Limited Rotation Min" )]
- [Tooltip( "If limited is true, the specifies the lower limit, otherwise value is unused" )]
- public float minAngle = -45.0f;
- [Tooltip( "If limited, set whether drive will freeze its angle when the min angle is reached" )]
- public bool freezeOnMin = false;
- [Tooltip( "If limited, event invoked when minAngle is reached" )]
- public UnityEvent onMinAngle;
- [HeaderAttribute( "Limited Rotation Max" )]
- [Tooltip( "If limited is true, the specifies the upper limit, otherwise value is unused" )]
- public float maxAngle = 45.0f;
- [Tooltip( "If limited, set whether drive will freeze its angle when the max angle is reached" )]
- public bool freezeOnMax = false;
- [Tooltip( "If limited, event invoked when maxAngle is reached" )]
- public UnityEvent onMaxAngle;
- [Tooltip( "If limited is true, this forces the starting angle to be startAngle, clamped to [minAngle, maxAngle]" )]
- public bool forceStart = false;
- [Tooltip( "If limited is true and forceStart is true, the starting angle will be this, clamped to [minAngle, maxAngle]" )]
- public float startAngle = 0.0f;
- [Tooltip( "If true, the transform of the GameObject this component is on will be rotated accordingly" )]
- public bool rotateGameObject = true;
- [Tooltip( "If true, the path of the Hand (red) and the projected value (green) will be drawn" )]
- public bool debugPath = false;
- [Tooltip( "If debugPath is true, this is the maximum number of GameObjects to create to draw the path" )]
- public int dbgPathLimit = 50;
- [Tooltip( "If not null, the TextMesh will display the linear value and the angular value of this circular drive" )]
- public TextMesh debugText = null;
- [Tooltip( "The output angle value of the drive in degrees, unlimited will increase or decrease without bound, take the 360 modulus to find number of rotations" )]
- public float outAngle;
- private Quaternion start;
- private Vector3 worldPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
- private Vector3 localPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
- private Vector3 lastHandProjected;
- private Color red = new Color( 1.0f, 0.0f, 0.0f );
- private Color green = new Color( 0.0f, 1.0f, 0.0f );
- private GameObject[] dbgHandObjects;
- private GameObject[] dbgProjObjects;
- private GameObject dbgObjectsParent;
- private int dbgObjectCount = 0;
- private int dbgObjectIndex = 0;
- private bool driving = false;
- // If the drive is limited as is at min/max, angles greater than this are ignored
- private float minMaxAngularThreshold = 1.0f;
- private bool frozen = false;
- private float frozenAngle = 0.0f;
- private Vector3 frozenHandWorldPos = new Vector3( 0.0f, 0.0f, 0.0f );
- private Vector2 frozenSqDistanceMinMaxThreshold = new Vector2( 0.0f, 0.0f );
- private Hand handHoverLocked = null;
- private Interactable interactable;
- //-------------------------------------------------
- private void Freeze( Hand hand )
- {
- frozen = true;
- frozenAngle = outAngle;
- frozenHandWorldPos = hand.hoverSphereTransform.position;
- frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
- frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
- }
- //-------------------------------------------------
- private void UnFreeze()
- {
- frozen = false;
- frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
- }
- private void Awake()
- {
- interactable = this.GetComponent<Interactable>();
- }
- //-------------------------------------------------
- private void Start()
- {
- if ( childCollider == null )
- {
- childCollider = GetComponentInChildren<Collider>();
- }
- if ( linearMapping == null )
- {
- linearMapping = GetComponent<LinearMapping>();
- }
- if ( linearMapping == null )
- {
- linearMapping = gameObject.AddComponent<LinearMapping>();
- }
- worldPlaneNormal = new Vector3( 0.0f, 0.0f, 0.0f );
- worldPlaneNormal[(int)axisOfRotation] = 1.0f;
- localPlaneNormal = worldPlaneNormal;
- if ( transform.parent )
- {
- worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
- }
- if ( limited )
- {
- start = Quaternion.identity;
- outAngle = transform.localEulerAngles[(int)axisOfRotation];
- if ( forceStart )
- {
- outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
- }
- }
- else
- {
- start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
- outAngle = 0.0f;
- }
- if ( debugText )
- {
- debugText.alignment = TextAlignment.Left;
- debugText.anchor = TextAnchor.UpperLeft;
- }
- UpdateAll();
- }
- //-------------------------------------------------
- void OnDisable()
- {
- if ( handHoverLocked )
- {
- handHoverLocked.HideGrabHint();
- handHoverLocked.HoverUnlock(interactable);
- handHoverLocked = null;
- }
- }
- //-------------------------------------------------
- private IEnumerator HapticPulses( Hand hand, float flMagnitude, int nCount )
- {
- if ( hand != null )
- {
- int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
- nCount = Mathf.Clamp( nCount, 1, 10 );
- //float hapticDuration = nRangeMax * nCount;
- //hand.TriggerHapticPulse(hapticDuration, nRangeMax, flMagnitude);
- for ( ushort i = 0; i < nCount; ++i )
- {
- ushort duration = (ushort)Random.Range( 100, nRangeMax );
- hand.TriggerHapticPulse( duration );
- yield return new WaitForSeconds( .01f );
- }
- }
- }
- //-------------------------------------------------
- private void OnHandHoverBegin( Hand hand )
- {
- hand.ShowGrabHint();
- }
- //-------------------------------------------------
- private void OnHandHoverEnd( Hand hand )
- {
- hand.HideGrabHint();
- if ( driving && hand )
- {
- //hand.TriggerHapticPulse() //todo: fix
- StartCoroutine( HapticPulses( hand, 1.0f, 10 ) );
- }
- driving = false;
- handHoverLocked = null;
- }
- private GrabTypes grabbedWithType;
- //-------------------------------------------------
- private void HandHoverUpdate( Hand hand )
- {
- GrabTypes startingGrabType = hand.GetGrabStarting();
- bool isGrabEnding = hand.IsGrabbingWithType(grabbedWithType) == false;
- if (grabbedWithType == GrabTypes.None && startingGrabType != GrabTypes.None)
- {
- grabbedWithType = startingGrabType;
- // Trigger was just pressed
- lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
- if ( hoverLock )
- {
- hand.HoverLock(interactable);
- handHoverLocked = hand;
- }
- driving = true;
- ComputeAngle( hand );
- UpdateAll();
- hand.HideGrabHint();
- }
- else if (grabbedWithType != GrabTypes.None && isGrabEnding)
- {
- // Trigger was just released
- if ( hoverLock )
- {
- hand.HoverUnlock(interactable);
- handHoverLocked = null;
- }
- driving = false;
- grabbedWithType = GrabTypes.None;
- }
- if ( driving && isGrabEnding == false && hand.hoveringInteractable == this.interactable )
- {
- ComputeAngle( hand );
- UpdateAll();
- }
- }
- //-------------------------------------------------
- private Vector3 ComputeToTransformProjected( Transform xForm )
- {
- Vector3 toTransform = ( xForm.position - transform.position ).normalized;
- Vector3 toTransformProjected = new Vector3( 0.0f, 0.0f, 0.0f );
- // Need a non-zero distance from the hand to the center of the CircularDrive
- if ( toTransform.sqrMagnitude > 0.0f )
- {
- toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
- }
- else
- {
- Debug.LogFormat("<b>[SteamVR Interaction]</b> The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() );
- Debug.Assert( false, string.Format("<b>[SteamVR Interaction]</b> The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() ) );
- }
- if ( debugPath && dbgPathLimit > 0 )
- {
- DrawDebugPath( xForm, toTransformProjected );
- }
- return toTransformProjected;
- }
- //-------------------------------------------------
- private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
- {
- if ( dbgObjectCount == 0 )
- {
- dbgObjectsParent = new GameObject( "Circular Drive Debug" );
- dbgHandObjects = new GameObject[dbgPathLimit];
- dbgProjObjects = new GameObject[dbgPathLimit];
- dbgObjectCount = dbgPathLimit;
- dbgObjectIndex = 0;
- }
- //Actual path
- GameObject gSphere = null;
- if ( dbgHandObjects[dbgObjectIndex] )
- {
- gSphere = dbgHandObjects[dbgObjectIndex];
- }
- else
- {
- gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
- gSphere.transform.SetParent( dbgObjectsParent.transform );
- dbgHandObjects[dbgObjectIndex] = gSphere;
- }
- gSphere.name = string.Format( "actual_{0}", (int)( ( 1.0f - red.r ) * 10.0f ) );
- gSphere.transform.position = xForm.position;
- gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
- gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
- gSphere.gameObject.GetComponent<Renderer>().material.color = red;
- if ( red.r > 0.1f )
- {
- red.r -= 0.1f;
- }
- else
- {
- red.r = 1.0f;
- }
- //Projected path
- gSphere = null;
- if ( dbgProjObjects[dbgObjectIndex] )
- {
- gSphere = dbgProjObjects[dbgObjectIndex];
- }
- else
- {
- gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
- gSphere.transform.SetParent( dbgObjectsParent.transform );
- dbgProjObjects[dbgObjectIndex] = gSphere;
- }
- gSphere.name = string.Format( "projed_{0}", (int)( ( 1.0f - green.g ) * 10.0f ) );
- gSphere.transform.position = transform.position + toTransformProjected * 0.25f;
- gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
- gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
- gSphere.gameObject.GetComponent<Renderer>().material.color = green;
- if ( green.g > 0.1f )
- {
- green.g -= 0.1f;
- }
- else
- {
- green.g = 1.0f;
- }
- dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
- }
- //-------------------------------------------------
- // Updates the LinearMapping value from the angle
- //-------------------------------------------------
- private void UpdateLinearMapping()
- {
- if ( limited )
- {
- // Map it to a [0, 1] value
- linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
- }
- else
- {
- // Normalize to [0, 1] based on 360 degree windings
- float flTmp = outAngle / 360.0f;
- linearMapping.value = flTmp - Mathf.Floor( flTmp );
- }
- UpdateDebugText();
- }
- //-------------------------------------------------
- // Updates the LinearMapping value from the angle
- //-------------------------------------------------
- private void UpdateGameObject()
- {
- if ( rotateGameObject )
- {
- transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
- }
- }
- //-------------------------------------------------
- // Updates the Debug TextMesh with the linear mapping value and the angle
- //-------------------------------------------------
- private void UpdateDebugText()
- {
- if ( debugText )
- {
- debugText.text = string.Format( "Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
- }
- }
- //-------------------------------------------------
- // Updates the Debug TextMesh with the linear mapping value and the angle
- //-------------------------------------------------
- private void UpdateAll()
- {
- UpdateLinearMapping();
- UpdateGameObject();
- UpdateDebugText();
- }
- //-------------------------------------------------
- // Computes the angle to rotate the game object based on the change in the transform
- //-------------------------------------------------
- private void ComputeAngle( Hand hand )
- {
- Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
- if ( !toHandProjected.Equals( lastHandProjected ) )
- {
- float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
- if ( absAngleDelta > 0.0f )
- {
- if ( frozen )
- {
- float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
- if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
- {
- outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
- float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
- if ( magnitude > 0 )
- {
- StartCoroutine( HapticPulses( hand, magnitude, 10 ) );
- }
- else
- {
- StartCoroutine( HapticPulses( hand, 0.5f, 10 ) );
- }
- if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
- {
- onFrozenDistanceThreshold.Invoke();
- }
- }
- }
- else
- {
- Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
- float dot = Vector3.Dot( worldPlaneNormal, cross );
- float signedAngleDelta = absAngleDelta;
- if ( dot < 0.0f )
- {
- signedAngleDelta = -signedAngleDelta;
- }
- if ( limited )
- {
- float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
- if ( outAngle == minAngle )
- {
- if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
- {
- outAngle = angleTmp;
- lastHandProjected = toHandProjected;
- }
- }
- else if ( outAngle == maxAngle )
- {
- if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
- {
- outAngle = angleTmp;
- lastHandProjected = toHandProjected;
- }
- }
- else if ( angleTmp == minAngle )
- {
- outAngle = angleTmp;
- lastHandProjected = toHandProjected;
- onMinAngle.Invoke();
- if ( freezeOnMin )
- {
- Freeze( hand );
- }
- }
- else if ( angleTmp == maxAngle )
- {
- outAngle = angleTmp;
- lastHandProjected = toHandProjected;
- onMaxAngle.Invoke();
- if ( freezeOnMax )
- {
- Freeze( hand );
- }
- }
- else
- {
- outAngle = angleTmp;
- lastHandProjected = toHandProjected;
- }
- }
- else
- {
- outAngle += signedAngleDelta;
- lastHandProjected = toHandProjected;
- }
- }
- }
- }
- }
- }
- }
|