XRTrailRenderer.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. using System;
  2. using UnityEngine;
  3. namespace Unity.XRTools.Rendering
  4. {
  5. /// <summary>
  6. /// An XR-Focused drop-in replacement for the Trail Renderer
  7. /// This renderer draws fixed-width lines with simulated volume and glow.
  8. /// This has many of the advantages of the traditional Line Renderer, old-school system-level line rendering functions,
  9. /// and volumetric (a linked series of capsules or cubes) rendering
  10. /// </summary>
  11. [RequireComponent(typeof(MeshRenderer))]
  12. [RequireComponent(typeof(MeshFilter))]
  13. [ExecuteInEditMode]
  14. public class XRTrailRenderer : MeshChainRenderer
  15. {
  16. const float k_AbsoluteMinVertexDistance = 0.01f;
  17. // Stored Trail Data
  18. [SerializeField]
  19. [Tooltip("How many points to store for tracing.")]
  20. int m_MaxTrailPoints = 20;
  21. [SerializeField]
  22. [Tooltip("Whether to use the last point or the first point of the trail when more are needed and none are available.")]
  23. bool m_StealLastPointWhenEmpty = true;
  24. [SerializeField]
  25. [Tooltip("How long the tail should be (second) [ 0, infinity ].")]
  26. float m_Time = 5.0f;
  27. [SerializeField]
  28. [Tooltip("The minimum distance to spawn a new point on the trail [ 0, infinity ].")]
  29. float m_MinVertexDistance = 0.1f;
  30. [SerializeField]
  31. [Tooltip("Destroy GameObject when there is no trail?")]
  32. bool m_Autodestruct = false;
  33. [SerializeField]
  34. [Tooltip("With this enabled, the last point will smooth lerp between the last recorded anchor point and the one after it")]
  35. bool m_SmoothInterpolation = false;
  36. // Circular array support for trail point recording
  37. Vector3[] m_Points;
  38. float[] m_PointTimes;
  39. int m_PointIndexStart = 0;
  40. int m_PointIndexEnd = 0;
  41. // Cached Data
  42. Vector3 m_LastRecordedPoint = Vector3.zero;
  43. float m_LastPointTime;
  44. float m_EditorDeltaHelper; // This lets us have access to a time data while not in play mode
  45. /// <summary>
  46. /// How long does the trail take to fade out.
  47. /// </summary>
  48. public float time
  49. {
  50. get { return m_Time; }
  51. set { m_Time = Mathf.Max(value, 0); }
  52. }
  53. /// <summary>
  54. /// Set the minimum distance the trail can travel before a new vertex is added to it.
  55. /// </summary>
  56. public float minVertexDistance
  57. {
  58. get { return m_MinVertexDistance; }
  59. set { m_MinVertexDistance = Mathf.Max(value, k_AbsoluteMinVertexDistance); }
  60. }
  61. /// <summary>
  62. /// Get the number of line segments in the trail
  63. /// </summary>
  64. public int positionCount { get; private set; }
  65. /// <summary>
  66. /// Destroy GameObject when there is no trail?
  67. /// </summary>
  68. public bool autodestruct
  69. {
  70. get { return m_Autodestruct; }
  71. set { m_Autodestruct = value; }
  72. }
  73. /// <summary>
  74. /// Set if the last point will smooth lerp between the last recorded anchor point and the one after it
  75. /// </summary>
  76. public bool smoothInterpolation
  77. {
  78. get { return m_SmoothInterpolation; }
  79. set { m_SmoothInterpolation = value; }
  80. }
  81. /// <summary>
  82. /// Updates the built-in mesh data for each control point of the trail
  83. /// </summary>
  84. protected override void LateUpdate()
  85. {
  86. // We do the actual internal mesh updating as late as possible so nothing ends up a frame behind
  87. var deltaTime = Time.deltaTime;
  88. // We give the editor a little help with handling delta time in edit mode
  89. if (Application.isPlaying == false)
  90. {
  91. deltaTime = Time.realtimeSinceStartup - m_EditorDeltaHelper;
  92. m_EditorDeltaHelper = Time.realtimeSinceStartup;
  93. }
  94. // Get the current position of the renderer
  95. var currentPoint = transform.position;
  96. var pointDistance = (currentPoint - m_LastRecordedPoint).sqrMagnitude;
  97. var shrunkThisFrame = false;
  98. // Is it more than minVertexDistance from the last position?
  99. if (pointDistance > (m_MinVertexDistance * m_MinVertexDistance))
  100. {
  101. // In the situation we have no points, we need to record the start point as well
  102. if (m_PointIndexStart == m_PointIndexEnd)
  103. {
  104. m_Points[m_PointIndexStart] = m_LastRecordedPoint;
  105. m_PointTimes[m_PointIndexStart] = m_Time;
  106. }
  107. // Make space for a new point
  108. var newEndIndex = (m_PointIndexEnd + 1) % m_MaxTrailPoints;
  109. // In the situation that we are rendering all available vertices
  110. // We can either keep using the current point, or take the last point, depending on the user's preference
  111. if (newEndIndex != m_PointIndexStart)
  112. {
  113. m_PointIndexEnd = newEndIndex;
  114. m_PointTimes[m_PointIndexEnd] = 0;
  115. positionCount++;
  116. }
  117. else
  118. {
  119. if (m_StealLastPointWhenEmpty)
  120. {
  121. m_XRMeshData.SetElementSize(m_PointIndexStart * 2, 0);
  122. m_XRMeshData.SetElementSize((m_PointIndexStart * 2) + 1, 0);
  123. m_PointIndexStart = (m_PointIndexStart + 1) % m_MaxTrailPoints;
  124. m_PointIndexEnd = newEndIndex;
  125. m_PointTimes[m_PointIndexEnd] = 0;
  126. m_LastPointTime = m_PointTimes[m_PointIndexStart];
  127. }
  128. }
  129. m_Points[m_PointIndexEnd] = currentPoint;
  130. // Update the last recorded point
  131. m_LastRecordedPoint = currentPoint;
  132. }
  133. // Do time processing
  134. // The end point counts up to a maximum of 'time'
  135. m_PointTimes[m_PointIndexEnd] = Mathf.Min(m_PointTimes[m_PointIndexEnd] + deltaTime, m_Time);
  136. if (m_PointIndexStart != m_PointIndexEnd)
  137. {
  138. // Run down the counter on the start point
  139. m_PointTimes[m_PointIndexStart] -= deltaTime;
  140. // If we've hit 0, this point is done for
  141. if (m_PointTimes[m_PointIndexStart] <= 0.0f)
  142. {
  143. m_XRMeshData.SetElementSize(m_PointIndexStart * 2, 0);
  144. m_XRMeshData.SetElementSize((m_PointIndexStart * 2) + 1, 0);
  145. m_PointIndexStart = (m_PointIndexStart + 1) % m_MaxTrailPoints;
  146. m_LastPointTime = m_PointTimes[m_PointIndexStart];
  147. positionCount--;
  148. shrunkThisFrame = true;
  149. }
  150. }
  151. if (m_PointIndexStart != m_PointIndexEnd)
  152. {
  153. m_MeshNeedsRefreshing = true;
  154. m_MeshRenderer.enabled = true;
  155. }
  156. else
  157. {
  158. m_MeshNeedsRefreshing = false;
  159. m_MeshRenderer.enabled = false;
  160. if (m_Autodestruct && Application.isPlaying && shrunkThisFrame)
  161. {
  162. Destroy(gameObject);
  163. }
  164. }
  165. if (m_MeshNeedsRefreshing)
  166. {
  167. m_MeshRenderer.enabled = true;
  168. // Update first and last points position-wise
  169. var nextIndex = (m_PointIndexStart + 1) % m_MaxTrailPoints;
  170. if (m_SmoothInterpolation)
  171. {
  172. var toNextPoint = 1.0f - (m_PointTimes[m_PointIndexStart] / m_LastPointTime);
  173. var lerpPoint = Vector3.Lerp(m_Points[m_PointIndexStart], m_Points[nextIndex], toNextPoint);
  174. m_XRMeshData.SetElementPosition((m_PointIndexStart * 2), ref lerpPoint);
  175. m_XRMeshData.SetElementPipe((m_PointIndexStart * 2) + 1, ref lerpPoint, ref m_Points[nextIndex]);
  176. }
  177. else
  178. {
  179. m_XRMeshData.SetElementPosition((m_PointIndexStart * 2), ref m_Points[m_PointIndexStart]);
  180. m_XRMeshData.SetElementPipe((m_PointIndexStart * 2) + 1, ref m_Points[m_PointIndexStart], ref m_Points[nextIndex]);
  181. }
  182. var prevIndex = m_PointIndexEnd - 1;
  183. if (prevIndex < 0)
  184. {
  185. prevIndex = m_MaxTrailPoints - 1;
  186. }
  187. m_XRMeshData.SetElementPipe((prevIndex * 2) + 1, ref m_Points[prevIndex], ref m_Points[m_PointIndexEnd]);
  188. m_XRMeshData.SetElementPosition((m_PointIndexEnd * 2), ref m_Points[m_PointIndexEnd]);
  189. // Go through all points and update size and color
  190. var pointUpdateCounter = m_PointIndexStart;
  191. var pointCount = 0;
  192. m_StepSize = (positionCount > 0) ? (1.0f / positionCount) : 1.0f;
  193. var percent = 0.0f;
  194. var lastWidth = m_WidthCurve.Evaluate(percent) * m_Width;
  195. var lastColor = m_Color.Evaluate(percent);
  196. percent += m_StepSize;
  197. while (pointUpdateCounter != m_PointIndexEnd)
  198. {
  199. var nextWidth = m_WidthCurve.Evaluate(percent) * m_Width;
  200. m_XRMeshData.SetElementSize(pointUpdateCounter * 2, lastWidth);
  201. m_XRMeshData.SetElementSize((pointUpdateCounter * 2) + 1, lastWidth, nextWidth);
  202. lastWidth = nextWidth;
  203. var nextColor = m_Color.Evaluate(percent);
  204. m_XRMeshData.SetElementColor(pointUpdateCounter * 2, ref lastColor);
  205. m_XRMeshData.SetElementColor((pointUpdateCounter * 2) + 1, ref lastColor, ref nextColor);
  206. lastColor = nextColor;
  207. pointUpdateCounter = (pointUpdateCounter + 1) % m_MaxTrailPoints;
  208. pointCount++;
  209. percent += m_StepSize;
  210. }
  211. lastWidth = m_WidthCurve.Evaluate(1) * m_Width;
  212. m_XRMeshData.SetElementSize((m_PointIndexEnd * 2), lastWidth);
  213. lastColor = m_Color.Evaluate(1);
  214. m_XRMeshData.SetElementColor((m_PointIndexEnd * 2), ref lastColor);
  215. m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.All);
  216. m_XRMeshData.RefreshMesh();
  217. }
  218. }
  219. /// <summary>
  220. /// Editor helper function to ensure changes are reflected in edit-mode
  221. /// </summary>
  222. public void EditorCheckForUpdate()
  223. {
  224. // If we did not initialize, refresh all the properties instead
  225. Initialize();
  226. }
  227. /// <summary>
  228. /// Removes all points from the TrailRenderer. Useful for restarting a trail from a new position.
  229. /// </summary>
  230. public void Clear()
  231. {
  232. var zeroVec = Vector3.zero;
  233. var zeroColor = Color.clear;
  234. var elementCounter = 0;
  235. var pointCounter = 0;
  236. while (pointCounter < m_Points.Length)
  237. {
  238. // Start point
  239. m_XRMeshData.SetElementSize(elementCounter, 0);
  240. m_XRMeshData.SetElementPosition(elementCounter, ref zeroVec);
  241. m_XRMeshData.SetElementColor(elementCounter, ref zeroColor);
  242. elementCounter++;
  243. // Pipe to the next point
  244. m_XRMeshData.SetElementSize(elementCounter, 0);
  245. m_XRMeshData.SetElementPipe(elementCounter, ref zeroVec, ref zeroVec);
  246. m_XRMeshData.SetElementColor(elementCounter, ref zeroColor);
  247. // Go onto the next point while retaining previous values we might need to lerp between
  248. elementCounter++;
  249. pointCounter++;
  250. }
  251. m_PointIndexStart = 0;
  252. m_PointIndexEnd = 0;
  253. positionCount = 0;
  254. m_LastRecordedPoint = transform.position;
  255. }
  256. /// <summary>
  257. /// Creates or updates the underlying mesh data
  258. /// </summary>
  259. protected override void Initialize(bool setMesh = true)
  260. {
  261. base.Initialize(setMesh);
  262. m_MaxTrailPoints = Mathf.Max(m_MaxTrailPoints, 3);
  263. // If we already have the right amount of points and mesh, then we can get away with just clearing the curve out
  264. if (m_Points != null && m_MaxTrailPoints == m_Points.Length && m_XRMeshData != null)
  265. {
  266. Clear();
  267. return;
  268. }
  269. m_Points = new Vector3[m_MaxTrailPoints];
  270. m_PointTimes = new float[m_MaxTrailPoints];
  271. // For a trail renderer we assume one big chain
  272. // We need a control point for each billboard and a control point for each pipe connecting them together
  273. // We make this a circular trail so the update logic is easier. This gives us (position * 2)
  274. var neededPoints = Mathf.Max((m_MaxTrailPoints * 2), 0);
  275. if (m_XRMeshData == null)
  276. {
  277. m_XRMeshData = new XRMeshChain();
  278. }
  279. if (m_XRMeshData.reservedElements != neededPoints)
  280. {
  281. m_XRMeshData.worldSpaceData = true;
  282. m_XRMeshData.centerAtRoot = true;
  283. m_XRMeshData.GenerateMesh(gameObject, true, neededPoints, setMesh);
  284. if (neededPoints == 0)
  285. {
  286. return;
  287. }
  288. // Dirty all the VRMeshChain flags so everything gets refreshed
  289. m_MeshRenderer.enabled = false;
  290. m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.All);
  291. m_MeshNeedsRefreshing = true;
  292. }
  293. Clear();
  294. }
  295. /// <summary>
  296. /// Tests if the mesh data needs to be created or rebuilt
  297. /// </summary>
  298. /// <returns>true if the mesh data needs recreation, false if it is already set up properly</returns>
  299. protected override bool NeedsReinitialize()
  300. {
  301. // No mesh data means we definitely need to reinitialize
  302. if (m_XRMeshData == null)
  303. {
  304. return true;
  305. }
  306. // Mismatched point data means we definitely need to reinitialize
  307. if (m_Points == null || m_MaxTrailPoints != m_Points.Length)
  308. {
  309. return true;
  310. }
  311. m_MaxTrailPoints = Mathf.Max(m_MaxTrailPoints, 3);
  312. var neededPoints = Mathf.Max((m_MaxTrailPoints * 2), 0);
  313. return (m_XRMeshData.reservedElements != neededPoints);
  314. }
  315. /// <summary>
  316. /// Enables the internal mesh representing the line
  317. /// </summary>
  318. protected override void OnEnable()
  319. {
  320. m_MeshRenderer.enabled = (m_PointIndexStart != m_PointIndexEnd);
  321. }
  322. }
  323. }