using UnityEngine;
using UnityEngine.Serialization;
namespace Unity.XRTools.Rendering
{
///
/// An XR-Focused drop-in replacement for the Line 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 XRLineRenderer : MeshChainRenderer
{
// Stored Line Data
[SerializeField]
[Tooltip("All of the connected points to render as a line.")]
Vector3[] m_Positions;
[SerializeField]
[FormerlySerializedAs("m_WorldSpaceData")]
[Tooltip("Draw lines in worldspace (or local space) - driven via shader.")]
bool m_UseWorldSpace;
[SerializeField]
[Tooltip("Connect the first and last vertices, to create a loop.")]
bool m_Loop;
///
/// Connect the first and last vertices, to create a loop.
///
public bool loop
{
get { return m_Loop; }
set
{
m_Loop = value;
if (NeedsReinitialize())
Initialize();
}
}
///
/// Draw lines in worldspace (or local space)
///
public bool useWorldSpace
{
get { return m_UseWorldSpace; }
set { m_UseWorldSpace = value; }
}
///
/// Returns the first instantiated Material assigned to the renderer.
///
public override Material material
{
get { return m_MeshRenderer.material; }
set
{
m_MeshRenderer.material = value;
CopyWorldSpaceDataFromMaterial();
}
}
///
/// Returns all the instantiated materials of this object.
///
public override Material[] materials
{
get { return m_MeshRenderer.materials; }
set
{
m_MeshRenderer.materials = value;
CopyWorldSpaceDataFromMaterial();
}
}
///
/// Returns the shared material of this object.
///
public override Material sharedMaterial
{
get { return m_MeshRenderer.sharedMaterial; }
set
{
m_MeshRenderer.sharedMaterial = value;
CopyWorldSpaceDataFromMaterial();
}
}
///
/// Returns all shared materials of this object.
///
public override Material[] SharedMaterials
{
get { return m_MeshRenderer.materials; }
set
{
m_MeshRenderer.sharedMaterials = value;
CopyWorldSpaceDataFromMaterial();
}
}
///
/// Makes sure that the internal world space flag of the line renderer
/// matches the world space flag of the first material on the object
///
void CopyWorldSpaceDataFromMaterial()
{
var firstMaterial = m_MeshRenderer.sharedMaterial;
if (firstMaterial == null)
{
return;
}
if (firstMaterial.HasProperty("_WorldData"))
{
m_UseWorldSpace = !Mathf.Approximately(firstMaterial.GetFloat("_WorldData"), 0.0f);
}
else
{
m_UseWorldSpace = false;
}
}
///
/// Gets the position of the vertex in the line.
///
/// The index of the position to retrieve
/// The position at the specified index of the array
public Vector3 GetPosition(int index)
{
return m_Positions[index];
}
///
/// Sets the position of the vertex in the line.
///
/// Which vertex to set
/// The new location in space of this vertex
public void SetPosition(int index, Vector3 position)
{
// Update internal data
m_Positions[index] = position;
// See if the data needs initializing
if (NeedsReinitialize())
{
Initialize();
return;
}
// Otherwise, do fast setting
var prevIndex = (index - 1 + m_Positions.Length) % m_Positions.Length;
var endIndex = (index + 1) % m_Positions.Length;
if (index > 0 || m_Loop)
{
m_XRMeshData.SetElementPipe((index * 2) - 1, ref m_Positions[prevIndex], ref m_Positions[index]);
}
m_XRMeshData.SetElementPosition(index * 2, ref m_Positions[index]);
if (index < (m_Positions.Length - 1) || m_Loop)
{
m_XRMeshData.SetElementPipe((index * 2) + 1, ref m_Positions[index], ref m_Positions[endIndex]);
}
m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.Positions);
m_MeshNeedsRefreshing = true;
}
///
/// Get the position of all vertices in the line.
///
/// The array of positions to retrieve. The array passed should be of at least numPositions in size.
/// How many positions were actually stored in the output array.
public int GetPositions(Vector3[] positions)
{
if (m_Positions != null)
{
m_Positions.CopyTo(positions, 0);
return m_Positions.Length;
}
return 0;
}
///
/// Sets all positions in the line. Cheaper than calling SetPosition repeatedly
///
/// All of the new endpoints of the line
/// Turn on to run a safety check to make sure the number of endpoints does not change (bad for garbage collection)
public void SetPositions(Vector3[] newPositions, bool knownSizeChange = false)
{
// Update internal data
m_Positions = newPositions;
if (NeedsReinitialize())
{
if (knownSizeChange == false)
{
Debug.LogWarning("New positions does not match size of existing array. Adjusting vertex count as well");
}
Initialize();
return;
}
if (m_Positions.Length <= 0)
{
return;
}
// Otherwise, do fast setting
var pointCounter = 0;
var elementCounter = 0;
m_XRMeshData.SetElementPosition(elementCounter, ref m_Positions[pointCounter]);
elementCounter++;
pointCounter++;
while (pointCounter < m_Positions.Length)
{
m_XRMeshData.SetElementPipe(elementCounter, ref m_Positions[pointCounter - 1], ref m_Positions[pointCounter]);
elementCounter++;
m_XRMeshData.SetElementPosition(elementCounter, ref m_Positions[pointCounter]);
elementCounter++;
pointCounter++;
}
if (m_Loop)
{
m_XRMeshData.SetElementPipe(elementCounter, ref m_Positions[pointCounter - 1], ref m_Positions[0]);
}
// Dirty all the VRMeshChain flags so everything gets refreshed
m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.Positions);
m_MeshNeedsRefreshing = true;
}
///
/// Sets the number of billboard-line chains. This function regenerates the point list if the
/// number of vertex points changes, so use it sparingly.
///
/// The new number of vertices in the line
public void SetVertexCount(int count)
{
// See if anything needs updating
if (m_Positions.Length == count)
{
return;
}
// Adjust this array
var newPositions = new Vector3[count];
var copyCount = Mathf.Min(m_Positions.Length, count);
var copyIndex = 0;
while (copyIndex < copyCount)
{
newPositions[copyIndex] = m_Positions[copyIndex];
copyIndex++;
}
m_Positions = newPositions;
// Do an initialization, this changes everything
Initialize();
}
///
/// Get the number of billboard-line chains.
///
/// The number of chains
public int GetVertexCount()
{
return m_Positions.Length;
}
///
/// Updates any internal variables to represent the new color that has been applied
///
protected override void UpdateColors()
{
// See if the data needs initializing
if (NeedsReinitialize())
{
Initialize();
return;
}
if (m_Positions.Length <= 0)
{
return;
}
// If it doesn't, go through each point and set the data
var pointCounter = 0;
var elementCounter = 0;
var stepPercent = 0.0f;
var lastColor = m_Color.Evaluate(stepPercent);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor);
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
while (pointCounter < m_Positions.Length)
{
var currentColor = m_Color.Evaluate(stepPercent);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor, ref currentColor);
elementCounter++;
m_XRMeshData.SetElementColor(elementCounter, ref currentColor);
lastColor = currentColor;
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
}
if (m_Loop)
{
lastColor = m_Color.Evaluate(stepPercent);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor);
}
// Dirty the color meshChain flags so the mesh gets new data
m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.Colors);
m_MeshNeedsRefreshing = true;
}
///
/// Updates any internal variables to represent the new width that has been applied
///
protected override void UpdateWidth()
{
// See if the data needs initializing
if (NeedsReinitialize())
{
Initialize();
return;
}
if (m_Positions.Length <= 0)
{
return;
}
// Otherwise, do fast setting
var pointCounter = 0;
var elementCounter = 0;
var stepPercent = 0.0f;
// We go through the element list, much like initialization, but only update the width part of the variables
var lastWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
m_XRMeshData.SetElementSize(elementCounter, lastWidth);
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
while (pointCounter < m_Positions.Length)
{
var currentWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
m_XRMeshData.SetElementSize(elementCounter, lastWidth, currentWidth);
elementCounter++;
m_XRMeshData.SetElementSize(elementCounter, currentWidth);
lastWidth = currentWidth;
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
}
if (m_Loop)
{
var currentWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
m_XRMeshData.SetElementSize(elementCounter, lastWidth, currentWidth);
}
// Dirty all the VRMeshChain flags so everything gets refreshed
m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.Sizes);
m_MeshNeedsRefreshing = true;
}
///
/// Creates or updates the underlying mesh data
///
protected override void Initialize(bool setMesh = true)
{
base.Initialize();
CopyWorldSpaceDataFromMaterial();
if (m_Positions == null)
m_Positions = new Vector3[0];
// For a line renderer we assume one big chain
// We need a control point for each billboard and a control point for each pipe connecting them together
// Except for the end, which must be capped with another billboard. This gives us (positions * 2) - 1
// If we're looping, then we do need one more pipe
var neededPoints = m_Loop ? 1 : 0;
neededPoints = Mathf.Max(neededPoints + (m_Positions.Length * 2) - 1, 0);
if (m_XRMeshData == null)
{
m_XRMeshData = new XRMeshChain();
}
if (m_XRMeshData.reservedElements != neededPoints)
{
m_XRMeshData.worldSpaceData = useWorldSpace;
m_XRMeshData.GenerateMesh(gameObject, true, neededPoints, setMesh);
}
// If we have no points, then just assume that stepping through a single point would take us through the whole line
if (neededPoints == 0)
{
m_StepSize = 1.0f;
return;
}
m_StepSize = 1.0f / Mathf.Max(m_Loop ? m_Positions.Length : m_Positions.Length - 1, 1.0f);
var pointCounter = 0;
var elementCounter = 0;
var stepPercent = 0.0f;
var lastColor = m_Color.Evaluate(stepPercent);
var lastWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
// Initialize the single starting point
m_XRMeshData.SetElementSize(elementCounter, lastWidth);
m_XRMeshData.SetElementPosition(elementCounter, ref m_Positions[pointCounter]);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor);
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
// Now do the chain
while (pointCounter < m_Positions.Length)
{
var currentWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
var currentColor = m_Color.Evaluate(stepPercent);
// Create a pipe from the previous point to here
m_XRMeshData.SetElementSize(elementCounter, lastWidth, currentWidth);
m_XRMeshData.SetElementPipe(elementCounter, ref m_Positions[pointCounter - 1], ref m_Positions[pointCounter]);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor, ref currentColor);
elementCounter++;
// Now record our own point data
m_XRMeshData.SetElementSize(elementCounter, currentWidth);
m_XRMeshData.SetElementPosition(elementCounter, ref m_Positions[pointCounter]);
m_XRMeshData.SetElementColor(elementCounter, ref currentColor);
// Go onto the next point while retaining previous values we might need to lerp between
lastWidth = currentWidth;
lastColor = currentColor;
elementCounter++;
pointCounter++;
stepPercent += m_StepSize;
}
if (m_Loop)
{
var currentWidth = m_WidthCurve.Evaluate(stepPercent) * m_Width;
var currentColor = m_Color.Evaluate(stepPercent);
m_XRMeshData.SetElementSize(elementCounter, lastWidth, currentWidth);
m_XRMeshData.SetElementPipe(elementCounter, ref m_Positions[pointCounter - 1], ref m_Positions[0]);
m_XRMeshData.SetElementColor(elementCounter, ref lastColor, ref currentColor);
}
// Dirty all the VRMeshChain flags so everything gets refreshed
m_XRMeshData.SetMeshDataDirty(XRMeshChain.MeshRefreshFlag.All);
m_MeshNeedsRefreshing = true;
}
///
/// 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;
}
// If we have any positions, figure out how many points we need to render a line along it
var neededPoints = 0;
if (m_Positions != null)
{
neededPoints = Mathf.Max((m_Positions.Length * 2) - 1, 0);
if (m_Loop)
{
neededPoints++;
}
}
return (m_XRMeshData.reservedElements != neededPoints);
}
}
}