CircularDrive.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. //======= Copyright (c) Valve Corporation, All rights reserved. ===============
  2. //
  3. // Purpose: Interactable that can be used to move in a circular motion
  4. //
  5. //=============================================================================
  6. using UnityEngine;
  7. using UnityEngine.Events;
  8. using System.Collections;
  9. namespace Valve.VR.InteractionSystem
  10. {
  11. //-------------------------------------------------------------------------
  12. [RequireComponent( typeof( Interactable ) )]
  13. public class CircularDrive : MonoBehaviour
  14. {
  15. public enum Axis_t
  16. {
  17. XAxis,
  18. YAxis,
  19. ZAxis
  20. };
  21. [Tooltip( "The axis around which the circular drive will rotate in local space" )]
  22. public Axis_t axisOfRotation = Axis_t.XAxis;
  23. [Tooltip( "Child GameObject which has the Collider component to initiate interaction, only needs to be set if there is more than one Collider child" )]
  24. public Collider childCollider = null;
  25. [Tooltip( "A LinearMapping component to drive, if not specified one will be dynamically added to this GameObject" )]
  26. public LinearMapping linearMapping;
  27. [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" )]
  28. public bool hoverLock = false;
  29. [HeaderAttribute( "Limited Rotation" )]
  30. [Tooltip( "If true, the rotation will be limited to [minAngle, maxAngle], if false, the rotation is unlimited" )]
  31. public bool limited = false;
  32. public Vector2 frozenDistanceMinMaxThreshold = new Vector2( 0.1f, 0.2f );
  33. public UnityEvent onFrozenDistanceThreshold;
  34. [HeaderAttribute( "Limited Rotation Min" )]
  35. [Tooltip( "If limited is true, the specifies the lower limit, otherwise value is unused" )]
  36. public float minAngle = -45.0f;
  37. [Tooltip( "If limited, set whether drive will freeze its angle when the min angle is reached" )]
  38. public bool freezeOnMin = false;
  39. [Tooltip( "If limited, event invoked when minAngle is reached" )]
  40. public UnityEvent onMinAngle;
  41. [HeaderAttribute( "Limited Rotation Max" )]
  42. [Tooltip( "If limited is true, the specifies the upper limit, otherwise value is unused" )]
  43. public float maxAngle = 45.0f;
  44. [Tooltip( "If limited, set whether drive will freeze its angle when the max angle is reached" )]
  45. public bool freezeOnMax = false;
  46. [Tooltip( "If limited, event invoked when maxAngle is reached" )]
  47. public UnityEvent onMaxAngle;
  48. [Tooltip( "If limited is true, this forces the starting angle to be startAngle, clamped to [minAngle, maxAngle]" )]
  49. public bool forceStart = false;
  50. [Tooltip( "If limited is true and forceStart is true, the starting angle will be this, clamped to [minAngle, maxAngle]" )]
  51. public float startAngle = 0.0f;
  52. [Tooltip( "If true, the transform of the GameObject this component is on will be rotated accordingly" )]
  53. public bool rotateGameObject = true;
  54. [Tooltip( "If true, the path of the Hand (red) and the projected value (green) will be drawn" )]
  55. public bool debugPath = false;
  56. [Tooltip( "If debugPath is true, this is the maximum number of GameObjects to create to draw the path" )]
  57. public int dbgPathLimit = 50;
  58. [Tooltip( "If not null, the TextMesh will display the linear value and the angular value of this circular drive" )]
  59. public TextMesh debugText = null;
  60. [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" )]
  61. public float outAngle;
  62. private Quaternion start;
  63. private Vector3 worldPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
  64. private Vector3 localPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
  65. private Vector3 lastHandProjected;
  66. private Color red = new Color( 1.0f, 0.0f, 0.0f );
  67. private Color green = new Color( 0.0f, 1.0f, 0.0f );
  68. private GameObject[] dbgHandObjects;
  69. private GameObject[] dbgProjObjects;
  70. private GameObject dbgObjectsParent;
  71. private int dbgObjectCount = 0;
  72. private int dbgObjectIndex = 0;
  73. private bool driving = false;
  74. // If the drive is limited as is at min/max, angles greater than this are ignored
  75. private float minMaxAngularThreshold = 1.0f;
  76. private bool frozen = false;
  77. private float frozenAngle = 0.0f;
  78. private Vector3 frozenHandWorldPos = new Vector3( 0.0f, 0.0f, 0.0f );
  79. private Vector2 frozenSqDistanceMinMaxThreshold = new Vector2( 0.0f, 0.0f );
  80. private Hand handHoverLocked = null;
  81. private Interactable interactable;
  82. //-------------------------------------------------
  83. private void Freeze( Hand hand )
  84. {
  85. frozen = true;
  86. frozenAngle = outAngle;
  87. frozenHandWorldPos = hand.hoverSphereTransform.position;
  88. frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
  89. frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
  90. }
  91. //-------------------------------------------------
  92. private void UnFreeze()
  93. {
  94. frozen = false;
  95. frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
  96. }
  97. private void Awake()
  98. {
  99. interactable = this.GetComponent<Interactable>();
  100. }
  101. //-------------------------------------------------
  102. private void Start()
  103. {
  104. if ( childCollider == null )
  105. {
  106. childCollider = GetComponentInChildren<Collider>();
  107. }
  108. if ( linearMapping == null )
  109. {
  110. linearMapping = GetComponent<LinearMapping>();
  111. }
  112. if ( linearMapping == null )
  113. {
  114. linearMapping = gameObject.AddComponent<LinearMapping>();
  115. }
  116. worldPlaneNormal = new Vector3( 0.0f, 0.0f, 0.0f );
  117. worldPlaneNormal[(int)axisOfRotation] = 1.0f;
  118. localPlaneNormal = worldPlaneNormal;
  119. if ( transform.parent )
  120. {
  121. worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
  122. }
  123. if ( limited )
  124. {
  125. start = Quaternion.identity;
  126. outAngle = transform.localEulerAngles[(int)axisOfRotation];
  127. if ( forceStart )
  128. {
  129. outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
  130. }
  131. }
  132. else
  133. {
  134. start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
  135. outAngle = 0.0f;
  136. }
  137. if ( debugText )
  138. {
  139. debugText.alignment = TextAlignment.Left;
  140. debugText.anchor = TextAnchor.UpperLeft;
  141. }
  142. UpdateAll();
  143. }
  144. //-------------------------------------------------
  145. void OnDisable()
  146. {
  147. if ( handHoverLocked )
  148. {
  149. handHoverLocked.HideGrabHint();
  150. handHoverLocked.HoverUnlock(interactable);
  151. handHoverLocked = null;
  152. }
  153. }
  154. //-------------------------------------------------
  155. private IEnumerator HapticPulses( Hand hand, float flMagnitude, int nCount )
  156. {
  157. if ( hand != null )
  158. {
  159. int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
  160. nCount = Mathf.Clamp( nCount, 1, 10 );
  161. //float hapticDuration = nRangeMax * nCount;
  162. //hand.TriggerHapticPulse(hapticDuration, nRangeMax, flMagnitude);
  163. for ( ushort i = 0; i < nCount; ++i )
  164. {
  165. ushort duration = (ushort)Random.Range( 100, nRangeMax );
  166. hand.TriggerHapticPulse( duration );
  167. yield return new WaitForSeconds( .01f );
  168. }
  169. }
  170. }
  171. //-------------------------------------------------
  172. private void OnHandHoverBegin( Hand hand )
  173. {
  174. hand.ShowGrabHint();
  175. }
  176. //-------------------------------------------------
  177. private void OnHandHoverEnd( Hand hand )
  178. {
  179. hand.HideGrabHint();
  180. if ( driving && hand )
  181. {
  182. //hand.TriggerHapticPulse() //todo: fix
  183. StartCoroutine( HapticPulses( hand, 1.0f, 10 ) );
  184. }
  185. driving = false;
  186. handHoverLocked = null;
  187. }
  188. private GrabTypes grabbedWithType;
  189. //-------------------------------------------------
  190. private void HandHoverUpdate( Hand hand )
  191. {
  192. GrabTypes startingGrabType = hand.GetGrabStarting();
  193. bool isGrabEnding = hand.IsGrabbingWithType(grabbedWithType) == false;
  194. if (grabbedWithType == GrabTypes.None && startingGrabType != GrabTypes.None)
  195. {
  196. grabbedWithType = startingGrabType;
  197. // Trigger was just pressed
  198. lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
  199. if ( hoverLock )
  200. {
  201. hand.HoverLock(interactable);
  202. handHoverLocked = hand;
  203. }
  204. driving = true;
  205. ComputeAngle( hand );
  206. UpdateAll();
  207. hand.HideGrabHint();
  208. }
  209. else if (grabbedWithType != GrabTypes.None && isGrabEnding)
  210. {
  211. // Trigger was just released
  212. if ( hoverLock )
  213. {
  214. hand.HoverUnlock(interactable);
  215. handHoverLocked = null;
  216. }
  217. driving = false;
  218. grabbedWithType = GrabTypes.None;
  219. }
  220. if ( driving && isGrabEnding == false && hand.hoveringInteractable == this.interactable )
  221. {
  222. ComputeAngle( hand );
  223. UpdateAll();
  224. }
  225. }
  226. //-------------------------------------------------
  227. private Vector3 ComputeToTransformProjected( Transform xForm )
  228. {
  229. Vector3 toTransform = ( xForm.position - transform.position ).normalized;
  230. Vector3 toTransformProjected = new Vector3( 0.0f, 0.0f, 0.0f );
  231. // Need a non-zero distance from the hand to the center of the CircularDrive
  232. if ( toTransform.sqrMagnitude > 0.0f )
  233. {
  234. toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
  235. }
  236. else
  237. {
  238. Debug.LogFormat("<b>[SteamVR Interaction]</b> The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() );
  239. 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() ) );
  240. }
  241. if ( debugPath && dbgPathLimit > 0 )
  242. {
  243. DrawDebugPath( xForm, toTransformProjected );
  244. }
  245. return toTransformProjected;
  246. }
  247. //-------------------------------------------------
  248. private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
  249. {
  250. if ( dbgObjectCount == 0 )
  251. {
  252. dbgObjectsParent = new GameObject( "Circular Drive Debug" );
  253. dbgHandObjects = new GameObject[dbgPathLimit];
  254. dbgProjObjects = new GameObject[dbgPathLimit];
  255. dbgObjectCount = dbgPathLimit;
  256. dbgObjectIndex = 0;
  257. }
  258. //Actual path
  259. GameObject gSphere = null;
  260. if ( dbgHandObjects[dbgObjectIndex] )
  261. {
  262. gSphere = dbgHandObjects[dbgObjectIndex];
  263. }
  264. else
  265. {
  266. gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
  267. gSphere.transform.SetParent( dbgObjectsParent.transform );
  268. dbgHandObjects[dbgObjectIndex] = gSphere;
  269. }
  270. gSphere.name = string.Format( "actual_{0}", (int)( ( 1.0f - red.r ) * 10.0f ) );
  271. gSphere.transform.position = xForm.position;
  272. gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
  273. gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
  274. gSphere.gameObject.GetComponent<Renderer>().material.color = red;
  275. if ( red.r > 0.1f )
  276. {
  277. red.r -= 0.1f;
  278. }
  279. else
  280. {
  281. red.r = 1.0f;
  282. }
  283. //Projected path
  284. gSphere = null;
  285. if ( dbgProjObjects[dbgObjectIndex] )
  286. {
  287. gSphere = dbgProjObjects[dbgObjectIndex];
  288. }
  289. else
  290. {
  291. gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
  292. gSphere.transform.SetParent( dbgObjectsParent.transform );
  293. dbgProjObjects[dbgObjectIndex] = gSphere;
  294. }
  295. gSphere.name = string.Format( "projed_{0}", (int)( ( 1.0f - green.g ) * 10.0f ) );
  296. gSphere.transform.position = transform.position + toTransformProjected * 0.25f;
  297. gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
  298. gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
  299. gSphere.gameObject.GetComponent<Renderer>().material.color = green;
  300. if ( green.g > 0.1f )
  301. {
  302. green.g -= 0.1f;
  303. }
  304. else
  305. {
  306. green.g = 1.0f;
  307. }
  308. dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
  309. }
  310. //-------------------------------------------------
  311. // Updates the LinearMapping value from the angle
  312. //-------------------------------------------------
  313. private void UpdateLinearMapping()
  314. {
  315. if ( limited )
  316. {
  317. // Map it to a [0, 1] value
  318. linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
  319. }
  320. else
  321. {
  322. // Normalize to [0, 1] based on 360 degree windings
  323. float flTmp = outAngle / 360.0f;
  324. linearMapping.value = flTmp - Mathf.Floor( flTmp );
  325. }
  326. UpdateDebugText();
  327. }
  328. //-------------------------------------------------
  329. // Updates the LinearMapping value from the angle
  330. //-------------------------------------------------
  331. private void UpdateGameObject()
  332. {
  333. if ( rotateGameObject )
  334. {
  335. transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
  336. }
  337. }
  338. //-------------------------------------------------
  339. // Updates the Debug TextMesh with the linear mapping value and the angle
  340. //-------------------------------------------------
  341. private void UpdateDebugText()
  342. {
  343. if ( debugText )
  344. {
  345. debugText.text = string.Format( "Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
  346. }
  347. }
  348. //-------------------------------------------------
  349. // Updates the Debug TextMesh with the linear mapping value and the angle
  350. //-------------------------------------------------
  351. private void UpdateAll()
  352. {
  353. UpdateLinearMapping();
  354. UpdateGameObject();
  355. UpdateDebugText();
  356. }
  357. //-------------------------------------------------
  358. // Computes the angle to rotate the game object based on the change in the transform
  359. //-------------------------------------------------
  360. private void ComputeAngle( Hand hand )
  361. {
  362. Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
  363. if ( !toHandProjected.Equals( lastHandProjected ) )
  364. {
  365. float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
  366. if ( absAngleDelta > 0.0f )
  367. {
  368. if ( frozen )
  369. {
  370. float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
  371. if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
  372. {
  373. outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
  374. float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
  375. if ( magnitude > 0 )
  376. {
  377. StartCoroutine( HapticPulses( hand, magnitude, 10 ) );
  378. }
  379. else
  380. {
  381. StartCoroutine( HapticPulses( hand, 0.5f, 10 ) );
  382. }
  383. if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
  384. {
  385. onFrozenDistanceThreshold.Invoke();
  386. }
  387. }
  388. }
  389. else
  390. {
  391. Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
  392. float dot = Vector3.Dot( worldPlaneNormal, cross );
  393. float signedAngleDelta = absAngleDelta;
  394. if ( dot < 0.0f )
  395. {
  396. signedAngleDelta = -signedAngleDelta;
  397. }
  398. if ( limited )
  399. {
  400. float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
  401. if ( outAngle == minAngle )
  402. {
  403. if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
  404. {
  405. outAngle = angleTmp;
  406. lastHandProjected = toHandProjected;
  407. }
  408. }
  409. else if ( outAngle == maxAngle )
  410. {
  411. if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
  412. {
  413. outAngle = angleTmp;
  414. lastHandProjected = toHandProjected;
  415. }
  416. }
  417. else if ( angleTmp == minAngle )
  418. {
  419. outAngle = angleTmp;
  420. lastHandProjected = toHandProjected;
  421. onMinAngle.Invoke();
  422. if ( freezeOnMin )
  423. {
  424. Freeze( hand );
  425. }
  426. }
  427. else if ( angleTmp == maxAngle )
  428. {
  429. outAngle = angleTmp;
  430. lastHandProjected = toHandProjected;
  431. onMaxAngle.Invoke();
  432. if ( freezeOnMax )
  433. {
  434. Freeze( hand );
  435. }
  436. }
  437. else
  438. {
  439. outAngle = angleTmp;
  440. lastHandProjected = toHandProjected;
  441. }
  442. }
  443. else
  444. {
  445. outAngle += signedAngleDelta;
  446. lastHandProjected = toHandProjected;
  447. }
  448. }
  449. }
  450. }
  451. }
  452. }
  453. }