The Swartz-Manning’s first exhibit will provide a detailed history of Aaron Swartz Day. https://www.aaronswartzday.org/vr
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

CircularDrive.cs 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  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. Hand handHoverLocked = null;
  81. //-------------------------------------------------
  82. private void Freeze( Hand hand )
  83. {
  84. frozen = true;
  85. frozenAngle = outAngle;
  86. frozenHandWorldPos = hand.hoverSphereTransform.position;
  87. frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
  88. frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
  89. }
  90. //-------------------------------------------------
  91. private void UnFreeze()
  92. {
  93. frozen = false;
  94. frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
  95. }
  96. //-------------------------------------------------
  97. void Start()
  98. {
  99. if ( childCollider == null )
  100. {
  101. childCollider = GetComponentInChildren<Collider>();
  102. }
  103. if ( linearMapping == null )
  104. {
  105. linearMapping = GetComponent<LinearMapping>();
  106. }
  107. if ( linearMapping == null )
  108. {
  109. linearMapping = gameObject.AddComponent<LinearMapping>();
  110. }
  111. worldPlaneNormal = new Vector3( 0.0f, 0.0f, 0.0f );
  112. worldPlaneNormal[(int)axisOfRotation] = 1.0f;
  113. localPlaneNormal = worldPlaneNormal;
  114. if ( transform.parent )
  115. {
  116. worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
  117. }
  118. if ( limited )
  119. {
  120. start = Quaternion.identity;
  121. outAngle = transform.localEulerAngles[(int)axisOfRotation];
  122. if ( forceStart )
  123. {
  124. outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
  125. }
  126. }
  127. else
  128. {
  129. start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
  130. outAngle = 0.0f;
  131. }
  132. if ( debugText )
  133. {
  134. debugText.alignment = TextAlignment.Left;
  135. debugText.anchor = TextAnchor.UpperLeft;
  136. }
  137. UpdateAll();
  138. }
  139. //-------------------------------------------------
  140. void OnDisable()
  141. {
  142. if ( handHoverLocked )
  143. {
  144. ControllerButtonHints.HideButtonHint( handHoverLocked, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
  145. handHoverLocked.HoverUnlock( GetComponent<Interactable>() );
  146. handHoverLocked = null;
  147. }
  148. }
  149. //-------------------------------------------------
  150. private IEnumerator HapticPulses( SteamVR_Controller.Device controller, float flMagnitude, int nCount )
  151. {
  152. if ( controller != null )
  153. {
  154. int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
  155. nCount = Mathf.Clamp( nCount, 1, 10 );
  156. for ( ushort i = 0; i < nCount; ++i )
  157. {
  158. ushort duration = (ushort)Random.Range( 100, nRangeMax );
  159. controller.TriggerHapticPulse( duration );
  160. yield return new WaitForSeconds( .01f );
  161. }
  162. }
  163. }
  164. //-------------------------------------------------
  165. private void OnHandHoverBegin( Hand hand )
  166. {
  167. ControllerButtonHints.ShowButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
  168. }
  169. //-------------------------------------------------
  170. private void OnHandHoverEnd( Hand hand )
  171. {
  172. ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
  173. if ( driving && hand.GetStandardInteractionButton() )
  174. {
  175. StartCoroutine( HapticPulses( hand.controller, 1.0f, 10 ) );
  176. }
  177. driving = false;
  178. handHoverLocked = null;
  179. }
  180. //-------------------------------------------------
  181. private void HandHoverUpdate( Hand hand )
  182. {
  183. if ( hand.GetStandardInteractionButtonDown() )
  184. {
  185. // Trigger was just pressed
  186. lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
  187. if ( hoverLock )
  188. {
  189. hand.HoverLock( GetComponent<Interactable>() );
  190. handHoverLocked = hand;
  191. }
  192. driving = true;
  193. ComputeAngle( hand );
  194. UpdateAll();
  195. ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
  196. }
  197. else if ( hand.GetStandardInteractionButtonUp() )
  198. {
  199. // Trigger was just released
  200. if ( hoverLock )
  201. {
  202. hand.HoverUnlock( GetComponent<Interactable>() );
  203. handHoverLocked = null;
  204. }
  205. }
  206. else if ( driving && hand.GetStandardInteractionButton() && hand.hoveringInteractable == GetComponent<Interactable>() )
  207. {
  208. ComputeAngle( hand );
  209. UpdateAll();
  210. }
  211. }
  212. //-------------------------------------------------
  213. private Vector3 ComputeToTransformProjected( Transform xForm )
  214. {
  215. Vector3 toTransform = ( xForm.position - transform.position ).normalized;
  216. Vector3 toTransformProjected = new Vector3( 0.0f, 0.0f, 0.0f );
  217. // Need a non-zero distance from the hand to the center of the CircularDrive
  218. if ( toTransform.sqrMagnitude > 0.0f )
  219. {
  220. toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
  221. }
  222. else
  223. {
  224. Debug.LogFormat( "The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() );
  225. Debug.Assert( false, string.Format( "The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() ) );
  226. }
  227. if ( debugPath && dbgPathLimit > 0 )
  228. {
  229. DrawDebugPath( xForm, toTransformProjected );
  230. }
  231. return toTransformProjected;
  232. }
  233. //-------------------------------------------------
  234. private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
  235. {
  236. if ( dbgObjectCount == 0 )
  237. {
  238. dbgObjectsParent = new GameObject( "Circular Drive Debug" );
  239. dbgHandObjects = new GameObject[dbgPathLimit];
  240. dbgProjObjects = new GameObject[dbgPathLimit];
  241. dbgObjectCount = dbgPathLimit;
  242. dbgObjectIndex = 0;
  243. }
  244. //Actual path
  245. GameObject gSphere = null;
  246. if ( dbgHandObjects[dbgObjectIndex] )
  247. {
  248. gSphere = dbgHandObjects[dbgObjectIndex];
  249. }
  250. else
  251. {
  252. gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
  253. gSphere.transform.SetParent( dbgObjectsParent.transform );
  254. dbgHandObjects[dbgObjectIndex] = gSphere;
  255. }
  256. gSphere.name = string.Format( "actual_{0}", (int)( ( 1.0f - red.r ) * 10.0f ) );
  257. gSphere.transform.position = xForm.position;
  258. gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
  259. gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
  260. gSphere.gameObject.GetComponent<Renderer>().material.color = red;
  261. if ( red.r > 0.1f )
  262. {
  263. red.r -= 0.1f;
  264. }
  265. else
  266. {
  267. red.r = 1.0f;
  268. }
  269. //Projected path
  270. gSphere = null;
  271. if ( dbgProjObjects[dbgObjectIndex] )
  272. {
  273. gSphere = dbgProjObjects[dbgObjectIndex];
  274. }
  275. else
  276. {
  277. gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
  278. gSphere.transform.SetParent( dbgObjectsParent.transform );
  279. dbgProjObjects[dbgObjectIndex] = gSphere;
  280. }
  281. gSphere.name = string.Format( "projed_{0}", (int)( ( 1.0f - green.g ) * 10.0f ) );
  282. gSphere.transform.position = transform.position + toTransformProjected * 0.25f;
  283. gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
  284. gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
  285. gSphere.gameObject.GetComponent<Renderer>().material.color = green;
  286. if ( green.g > 0.1f )
  287. {
  288. green.g -= 0.1f;
  289. }
  290. else
  291. {
  292. green.g = 1.0f;
  293. }
  294. dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
  295. }
  296. //-------------------------------------------------
  297. // Updates the LinearMapping value from the angle
  298. //-------------------------------------------------
  299. private void UpdateLinearMapping()
  300. {
  301. if ( limited )
  302. {
  303. // Map it to a [0, 1] value
  304. linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
  305. }
  306. else
  307. {
  308. // Normalize to [0, 1] based on 360 degree windings
  309. float flTmp = outAngle / 360.0f;
  310. linearMapping.value = flTmp - Mathf.Floor( flTmp );
  311. }
  312. UpdateDebugText();
  313. }
  314. //-------------------------------------------------
  315. // Updates the LinearMapping value from the angle
  316. //-------------------------------------------------
  317. private void UpdateGameObject()
  318. {
  319. if ( rotateGameObject )
  320. {
  321. transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
  322. }
  323. }
  324. //-------------------------------------------------
  325. // Updates the Debug TextMesh with the linear mapping value and the angle
  326. //-------------------------------------------------
  327. private void UpdateDebugText()
  328. {
  329. if ( debugText )
  330. {
  331. debugText.text = string.Format( "Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
  332. }
  333. }
  334. //-------------------------------------------------
  335. // Updates the Debug TextMesh with the linear mapping value and the angle
  336. //-------------------------------------------------
  337. private void UpdateAll()
  338. {
  339. UpdateLinearMapping();
  340. UpdateGameObject();
  341. UpdateDebugText();
  342. }
  343. //-------------------------------------------------
  344. // Computes the angle to rotate the game object based on the change in the transform
  345. //-------------------------------------------------
  346. private void ComputeAngle( Hand hand )
  347. {
  348. Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
  349. if ( !toHandProjected.Equals( lastHandProjected ) )
  350. {
  351. float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
  352. if ( absAngleDelta > 0.0f )
  353. {
  354. if ( frozen )
  355. {
  356. float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
  357. if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
  358. {
  359. outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
  360. float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
  361. if ( magnitude > 0 )
  362. {
  363. StartCoroutine( HapticPulses( hand.controller, magnitude, 10 ) );
  364. }
  365. else
  366. {
  367. StartCoroutine( HapticPulses( hand.controller, 0.5f, 10 ) );
  368. }
  369. if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
  370. {
  371. onFrozenDistanceThreshold.Invoke();
  372. }
  373. }
  374. }
  375. else
  376. {
  377. Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
  378. float dot = Vector3.Dot( worldPlaneNormal, cross );
  379. float signedAngleDelta = absAngleDelta;
  380. if ( dot < 0.0f )
  381. {
  382. signedAngleDelta = -signedAngleDelta;
  383. }
  384. if ( limited )
  385. {
  386. float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
  387. if ( outAngle == minAngle )
  388. {
  389. if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
  390. {
  391. outAngle = angleTmp;
  392. lastHandProjected = toHandProjected;
  393. }
  394. }
  395. else if ( outAngle == maxAngle )
  396. {
  397. if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
  398. {
  399. outAngle = angleTmp;
  400. lastHandProjected = toHandProjected;
  401. }
  402. }
  403. else if ( angleTmp == minAngle )
  404. {
  405. outAngle = angleTmp;
  406. lastHandProjected = toHandProjected;
  407. onMinAngle.Invoke();
  408. if ( freezeOnMin )
  409. {
  410. Freeze( hand );
  411. }
  412. }
  413. else if ( angleTmp == maxAngle )
  414. {
  415. outAngle = angleTmp;
  416. lastHandProjected = toHandProjected;
  417. onMaxAngle.Invoke();
  418. if ( freezeOnMax )
  419. {
  420. Freeze( hand );
  421. }
  422. }
  423. else
  424. {
  425. outAngle = angleTmp;
  426. lastHandProjected = toHandProjected;
  427. }
  428. }
  429. else
  430. {
  431. outAngle += signedAngleDelta;
  432. lastHandProjected = toHandProjected;
  433. }
  434. }
  435. }
  436. }
  437. }
  438. }
  439. }