using System; using UnityEngine; namespace Unity.XRTools.Rendering { /// /// An XR-Focused drop-in replacement for the Trail Renderer /// This renderer draws fixed-width lines with simulated volume and glow. /// This has many of the advantages of the traditional Line Renderer, old-school system-level line rendering functions, /// and volumetric (a linked series of capsules or cubes) rendering /// [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(MeshFilter))] [ExecuteInEditMode] public class XRTrailRenderer : MeshChainRenderer { const float k_AbsoluteMinVertexDistance = 0.01f; // Stored Trail Data [SerializeField] [Tooltip("How many points to store for tracing.")] int m_MaxTrailPoints = 20; [SerializeField] [Tooltip("Whether to use the last point or the first point of the trail when more are needed and none are available.")] bool m_StealLastPointWhenEmpty = true; [SerializeField] [Tooltip("How long the tail should be (second) [ 0, infinity ].")] float m_Time = 5.0f; [SerializeField] [Tooltip("The minimum distance to spawn a new point on the trail [ 0, infinity ].")] float m_MinVertexDistance = 0.1f; [SerializeField] [Tooltip("Destroy GameObject when there is no trail?")] bool m_Autodestruct = false; [SerializeField] [Tooltip("With this enabled, the last point will smooth lerp between the last recorded anchor point and the one after it")] bool m_SmoothInterpolation = false; // Circular array support for trail point recording Vector3[] m_Points; float[] m_PointTimes; int m_PointIndexStart = 0; int m_PointIndexEnd = 0; // Cached Data Vector3 m_LastRecordedPoint = Vector3.zero; float m_LastPointTime; float m_EditorDeltaHelper; // This lets us have access to a time data while not in play mode /// /// How long does the trail take to fade out. /// public float time { get { return m_Time; } set { m_Time = Mathf.Max(value, 0); } } /// /// Set the minimum distance the trail can travel before a new vertex is added to it. /// public float minVertexDistance { get { return m_MinVertexDistance; } set { m_MinVertexDistance = Mathf.Max(value, k_AbsoluteMinVertexDistance); } } /// /// Get the number of line segments in the trail /// public int positionCount { get; private set; } /// /// Destroy GameObject when there is no trail? /// public bool autodestruct { get { return m_Autodestruct; } set { m_Autodestruct = value; } } /// /// Set if the last point will smooth lerp between the last recorded anchor point and the one after it /// public bool smoothInterpolation { get { return m_SmoothInterpolation; } set { m_SmoothInterpolation = value; } } /// /// Updates the built-in mesh data for each control point of the trail /// protected override void LateUpdate() { // We do the actual internal mesh updating as late as possible so nothing ends up a frame behind var deltaTime = Time.deltaTime; // We give the editor a little help with handling delta time in edit mode if (Application.isPlaying == false) { deltaTime = Time.realtimeSinceStartup - m_EditorDeltaHelper; m_EditorDeltaHelper = Time.realtimeSinceStartup; } // Get the current position of the renderer var currentPoint = transform.position; var pointDistance = (currentPoint - m_LastRecordedPoint).sqrMagnitude; var shrunkThisFrame = false; // Is it more than minVertexDistance from the last position? if (pointDistance > (m_MinVertexDistance * m_MinVertexDistance)) { // In the situation we have no points, we need to record the start point as well if (m_PointIndexStart == m_PointIndexEnd) { m_Points[m_PointIndexStart] = m_LastRecordedPoint; m_PointTimes[m_PointIndexStart] = m_Time; } // Make space for a new point var newEndIndex = (m_PointIndexEnd + 1) % m_MaxTrailPoints; // In the situation that we are rendering all available vertices // We can either keep using the current point, or take the last point, depending on the user's preference if (newEndIndex != m_PointIndexStart) { m_PointIndexEnd = newEndIndex; m_PointTimes[m_PointIndexEnd] = 0; positionCount++; } else { if (m_StealLastPointWhenEmpty) { m_XRMeshData.SetElementSize(m_PointIndexStart * 2, 0); m_XRMeshData.SetElementSize((m_PointIndexStart * 2) + 1, 0); m_PointIndexStart = (m_PointIndexStart + 1) % m_MaxTrailPoints; m_PointIndexEnd = newEndIndex; m_PointTimes[m_PointIndexEnd] = 0; m_LastPointTime = m_PointTimes[m_PointIndexStart]; } } m_Points[m_PointIndexEnd] = currentPoint; // Update the last recorded point m_LastRecordedPoint = currentPoint; } // Do time processing // The end point counts up to a maximum of 'time' m_PointTimes[m_PointIndexEnd] = Mathf.Min(m_PointTimes[m_PointIndexEnd] + deltaTime, m_Time); if (m_PointIndexStart != m_PointIndexEnd) { // Run down the counter on the start point m_PointTimes[m_PointIndexStart] -= deltaTime; // If we've hit 0, this point is done for if (m_PointTimes[m_PointIndexStart] <= 0.0f) { m_XRMeshData.SetElementSize(m_PointIndexStart * 2, 0); m_XRMeshData.SetElementSize((m_PointIndexStart * 2) + 1, 0); m_PointIndexStart = (m_PointIndexStart + 1) % m_MaxTrailPoints; m_LastPointTime = m_PointTimes[m_PointIndexStart]; positionCount--; shrunkThisFrame = true; } } if (m_PointIndexStart != m_PointIndexEnd) { m_MeshNeedsRefreshing = true; m_MeshRenderer.enabled = true; } else { m_MeshNeedsRefreshing = false; m_MeshRenderer.enabled = false; if (m_Autodestruct && Application.isPlaying && shrunkThisFrame) { Destroy(gameObject); } } if (m_MeshNeedsRefreshing) { m_MeshRenderer.enabled = true; // Update first and last points position-wise var nextIndex = (m_PointIndexStart + 1) % m_MaxTrailPoints; if (m_SmoothInterpolation) { var toNextPoint = 1.0f - (m_PointTimes[m_PointIndexStart] / m_LastPointTime); var lerpPoint = Vector3.Lerp(m_Points[m_PointIndexStart], m_Points[nextIndex], toNextPoint); m_XRMeshData.SetElementPosition((m_PointIndexStart * 2), ref lerpPoint); m_XRMeshData.SetElementPipe((m_PointIndexStart * 2) + 1, ref lerpPoint, ref m_Points[nextIndex]); } else { m_XRMeshData.SetElementPosition((m_PointIndexStart * 2), ref m_Points[m_PointIndexStart]); m_XRMeshData.SetElementPipe((m_PointIndexStart * 2) + 1, ref m_Points[m_PointIndexStart], ref m_Points[nextIndex]); } var prevIndex = m_PointIndexEnd - 1; if (prevIndex < 0) { prevIndex = m_MaxTrailPoints - 1; } m_XRMeshData.SetElementPipe((prevIndex * 2) + 1, ref m_Points[prevIndex], ref m_Points[m_PointIndexEnd]); m_XRMeshData.SetElementPosition((m_PointIndexEnd * 2), ref m_Points[m_PointIndexEnd]); // Go through all points and update size and color var pointUpdateCounter = m_PointIndexStart; var pointCount = 0; m_StepSize = (positionCount > 0) ? (1.0f / positionCount) : 1.0f; var percent = 0.0f; var lastWidth = m_WidthCurve.Evaluate(percent) * m_Width; var lastColor = m_Color.Evaluate(percent); percent += m_StepSize; while (pointUpdateCounter != m_PointIndexEnd) { var nextWidth = m_WidthCurve.Evaluate(percent) * m_Width; m_XRMeshData.SetElementSize(pointUpdateCounter * 2, lastWidth); m_XRMeshData.SetElementSize((pointUpdateCounter * 2) + 1, lastWidth, nextWidth); lastWidth = nextWidth; var nextColor = m_Color.Evaluate(percent); m_XRMeshData.SetElementColor(pointUpdateCounter * 2, ref lastColor); m_XRMeshData.SetElementColor((pointUpdateCounter * 2) + 1, ref lastColor, ref nextColor); lastColor = nextColor; pointUpdateCounter = (pointUpdateCounter + 1) % m_MaxTrailPoints; pointCount++; percent += m_StepSize; } lastWidth = m_WidthCurve.Evaluate(1) * m_Width; m_XRMeshData.SetElementSize((m_PointIndexEnd * 2), lastWidth); lastColor = m_Color.Evaluate(1); m_XRMeshData.SetElementColor((m_PointIndexEnd * 2), ref lastColor); m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.All); m_XRMeshData.RefreshMesh(); } } /// /// Editor helper function to ensure changes are reflected in edit-mode /// public void EditorCheckForUpdate() { // If we did not initialize, refresh all the properties instead Initialize(); } /// /// Removes all points from the TrailRenderer. Useful for restarting a trail from a new position. /// public void Clear() { var zeroVec = Vector3.zero; var zeroColor = Color.clear; var elementCounter = 0; var pointCounter = 0; while (pointCounter < m_Points.Length) { // Start point m_XRMeshData.SetElementSize(elementCounter, 0); m_XRMeshData.SetElementPosition(elementCounter, ref zeroVec); m_XRMeshData.SetElementColor(elementCounter, ref zeroColor); elementCounter++; // Pipe to the next point m_XRMeshData.SetElementSize(elementCounter, 0); m_XRMeshData.SetElementPipe(elementCounter, ref zeroVec, ref zeroVec); m_XRMeshData.SetElementColor(elementCounter, ref zeroColor); // Go onto the next point while retaining previous values we might need to lerp between elementCounter++; pointCounter++; } m_PointIndexStart = 0; m_PointIndexEnd = 0; positionCount = 0; m_LastRecordedPoint = transform.position; } /// /// Creates or updates the underlying mesh data /// protected override void Initialize(bool setMesh = true) { base.Initialize(setMesh); m_MaxTrailPoints = Mathf.Max(m_MaxTrailPoints, 3); // If we already have the right amount of points and mesh, then we can get away with just clearing the curve out if (m_Points != null && m_MaxTrailPoints == m_Points.Length && m_XRMeshData != null) { Clear(); return; } m_Points = new Vector3[m_MaxTrailPoints]; m_PointTimes = new float[m_MaxTrailPoints]; // For a trail renderer we assume one big chain // We need a control point for each billboard and a control point for each pipe connecting them together // We make this a circular trail so the update logic is easier. This gives us (position * 2) var neededPoints = Mathf.Max((m_MaxTrailPoints * 2), 0); if (m_XRMeshData == null) { m_XRMeshData = new XRMeshChain(); } if (m_XRMeshData.reservedElements != neededPoints) { m_XRMeshData.worldSpaceData = true; m_XRMeshData.centerAtRoot = true; m_XRMeshData.GenerateMesh(gameObject, true, neededPoints, setMesh); if (neededPoints == 0) { return; } // Dirty all the VRMeshChain flags so everything gets refreshed m_MeshRenderer.enabled = false; m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.All); m_MeshNeedsRefreshing = true; } Clear(); } /// /// Tests if the mesh data needs to be created or rebuilt /// /// true if the mesh data needs recreation, false if it is already set up properly protected override bool NeedsReinitialize() { // No mesh data means we definitely need to reinitialize if (m_XRMeshData == null) { return true; } // Mismatched point data means we definitely need to reinitialize if (m_Points == null || m_MaxTrailPoints != m_Points.Length) { return true; } m_MaxTrailPoints = Mathf.Max(m_MaxTrailPoints, 3); var neededPoints = Mathf.Max((m_MaxTrailPoints * 2), 0); return (m_XRMeshData.reservedElements != neededPoints); } /// /// Enables the internal mesh representing the line /// protected override void OnEnable() { m_MeshRenderer.enabled = (m_PointIndexStart != m_PointIndexEnd); } } }