using UnityEngine; namespace VolumetricLines { /// /// Render a line strip of volumetric lines /// /// Based on the Volumetric lines algorithm by Sebastien Hillaire /// http://sebastien.hillaire.free.fr/index.php?option=com_content&view=article&id=57&Itemid=74 /// /// Thread in the Unity3D Forum: /// http://forum.unity3d.com/threads/181618-Volumetric-lines /// /// Unity3D port by Johannes Unterguggenberger /// johannes.unterguggenberger@gmail.com /// /// Thanks to Michael Probst for support during development. /// /// Thanks for bugfixes and improvements to Unity Forum User "Mistale" /// http://forum.unity3d.com/members/102350-Mistale /// /// /// Shader code optimization and cleanup by Lex Darlog (aka DRL) /// http://forum.unity3d.com/members/lex-drl.67487/ /// /// [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] [ExecuteInEditMode] public class VolumetricLineStripBehavior : MonoBehaviour { // Used to compute the average value of all the Vector3's components: static readonly Vector3 Average = new Vector3(1f/3f, 1f/3f, 1f/3f); #region private variables /// /// Template material to be used /// [SerializeField] public Material m_templateMaterial; /// /// Set to false in order to change the material's properties as specified in this script. /// Set to true in order to *initially* leave the material's properties as they are in the template material. /// [SerializeField] private bool m_doNotOverwriteTemplateMaterialProperties; /// /// Line Color /// [SerializeField] private Color m_lineColor; /// /// The width of the line /// [SerializeField] private float m_lineWidth; /// /// Light saber factor /// [SerializeField] [Range(0.0f, 1.0f)] private float m_lightSaberFactor; /// /// This GameObject's specific material /// private Material m_material; /// /// This GameObject's mesh filter /// private MeshFilter m_meshFilter; /// /// The vertices of the line /// [SerializeField] private Vector3[] m_lineVertices; #endregion #region properties /// /// Gets or sets the tmplate material. /// Setting this will only have an impact once. /// Subsequent changes will be ignored. /// public Material TemplateMaterial { get { return m_templateMaterial; } set { m_templateMaterial = value; } } /// /// Gets or sets whether or not the template material properties /// should be used (false) or if the properties of this MonoBehavior /// instance should be used (true, default). /// Setting this will only have an impact once, and then only if it /// is set before TemplateMaterial has been assigned. /// public bool DoNotOverwriteTemplateMaterialProperties { get { return m_doNotOverwriteTemplateMaterialProperties; } set { m_doNotOverwriteTemplateMaterialProperties = value; } } /// /// Get or set the line color of this volumetric line's material /// public Color LineColor { get { return m_lineColor; } set { CreateMaterial(); if (null != m_material) { m_lineColor = value; m_material.color = m_lineColor; } } } /// /// Get or set the line width of this volumetric line's material /// public float LineWidth { get { return m_lineWidth; } set { CreateMaterial(); if (null != m_material) { m_lineWidth = value; m_material.SetFloat("_LineWidth", m_lineWidth); } UpdateBounds(); } } /// /// Get or set the light saber factor of this volumetric line's material /// public float LightSaberFactor { get { return m_lightSaberFactor; } set { CreateMaterial(); if (null != m_material) { m_lightSaberFactor = value; m_material.SetFloat("_LightSaberFactor", m_lightSaberFactor); } } } /// /// Gets the vertices of this line strip /// public Vector3[] LineVertices { get { return m_lineVertices; } } #endregion #region methods /// /// Creates a copy of the template material for this instance /// private void CreateMaterial() { if (null == m_material || null == GetComponent().sharedMaterial) { if (null != m_templateMaterial) { m_material = Material.Instantiate(m_templateMaterial); GetComponent().sharedMaterial = m_material; SetAllMaterialProperties(); } else { m_material = GetComponent().sharedMaterial; } } } /// /// Destroys the copy of the template material which was used for this instance /// private void DestroyMaterial() { if (null != m_material) { DestroyImmediate(m_material); m_material = null; } } /// /// Calculates the (approximated) _LineScale factor based on the object's scale. /// private float CalculateLineScale() { return Vector3.Dot(transform.lossyScale, Average); } /// /// Updates the line scaling of this volumetric line based on the current object scaling. /// public void UpdateLineScale() { if (null != m_material) { m_material.SetFloat("_LineScale", CalculateLineScale()); } } /// /// Sets all material properties (color, width, start-, endpos) /// private void SetAllMaterialProperties() { UpdateLineVertices(m_lineVertices); if (null != m_material) { if (!m_doNotOverwriteTemplateMaterialProperties) { m_material.color = m_lineColor; m_material.SetFloat("_LineWidth", m_lineWidth); m_material.SetFloat("_LightSaberFactor", m_lightSaberFactor); } UpdateLineScale(); } } /// /// Calculate the bounds of this line based on the coordinates of the line vertices, /// the line width, and the scaling of the object. /// private Bounds CalculateBounds() { var maxWidth = Mathf.Max(transform.lossyScale.x, transform.lossyScale.y, transform.lossyScale.z); var scaledLineWidth = maxWidth * LineWidth * 0.5f; var scaledLineWidthVec = new Vector3(scaledLineWidth, scaledLineWidth, scaledLineWidth); Debug.Assert(m_lineVertices.Length > 0); if (m_lineVertices.Length == 0) { return new Bounds(); } var min = m_lineVertices[0]; var max = m_lineVertices[0]; for (int i = 1; i < m_lineVertices.Length; ++i) { min = new Vector3( Mathf.Min(min.x, m_lineVertices[i].x), Mathf.Min(min.y, m_lineVertices[i].y), Mathf.Min(min.z, m_lineVertices[i].z) ); max = new Vector3( Mathf.Max(max.x, m_lineVertices[i].x), Mathf.Max(max.y, m_lineVertices[i].y), Mathf.Max(max.z, m_lineVertices[i].z) ); } return new Bounds { min = min - scaledLineWidthVec, max = max + scaledLineWidthVec }; } /// /// Updates the bounds of this line according to the current properties, /// which there are: coordinates of the line vertices, line width, scaling of the object. /// public void UpdateBounds() { if (null != m_meshFilter) { var mesh = m_meshFilter.sharedMesh; Debug.Assert(null != mesh); if (null != mesh) { mesh.bounds = CalculateBounds(); } } } /// /// Updates the vertices of this VolumetricLineStrip. /// This is an expensive operation. /// /// new set of vertices for the line strip. public void UpdateLineVertices(Vector3[] newSetOfVertices) { if (null == newSetOfVertices) { return; } if (newSetOfVertices.Length < 3) { Debug.LogError("Add at least 3 vertices to the VolumetricLineStrip"); return; } m_lineVertices = newSetOfVertices; // fill vertex positions, and indices // 2 for each position, + 2 for the start, + 2 for the end Vector3[] vertexPositions = new Vector3[m_lineVertices.Length * 2 + 4]; // there are #vertices - 2 faces, and 3 indices each int[] indices = new int[(m_lineVertices.Length * 2 + 2) * 3]; int v = 0; int x = 0; vertexPositions[v++] = m_lineVertices[0]; vertexPositions[v++] = m_lineVertices[0]; for (int i = 0; i < m_lineVertices.Length; ++i) { vertexPositions[v++] = m_lineVertices[i]; vertexPositions[v++] = m_lineVertices[i]; indices[x++] = v - 2; indices[x++] = v - 3; indices[x++] = v - 4; indices[x++] = v - 1; indices[x++] = v - 2; indices[x++] = v - 3; } vertexPositions[v++] = m_lineVertices[m_lineVertices.Length - 1]; vertexPositions[v++] = m_lineVertices[m_lineVertices.Length - 1]; indices[x++] = v - 2; indices[x++] = v - 3; indices[x++] = v - 4; indices[x++] = v - 1; indices[x++] = v - 2; indices[x++] = v - 3; // fill texture coordinates and vertex offsets Vector2[] texCoords = new Vector2[vertexPositions.Length]; Vector2[] vertexOffsets = new Vector2[vertexPositions.Length]; int t = 0; int o = 0; texCoords[t++] = new Vector2(1.0f, 0.0f); texCoords[t++] = new Vector2(1.0f, 1.0f); texCoords[t++] = new Vector2(0.5f, 0.0f); texCoords[t++] = new Vector2(0.5f, 1.0f); vertexOffsets[o++] = new Vector2(1.0f, -1.0f); vertexOffsets[o++] = new Vector2(1.0f, 1.0f); vertexOffsets[o++] = new Vector2(0.0f, -1.0f); vertexOffsets[o++] = new Vector2(0.0f, 1.0f); for (int i = 1; i < m_lineVertices.Length - 1; ++i) { if ((i & 0x1) == 0x1) { texCoords[t++] = new Vector2(0.5f, 0.0f); texCoords[t++] = new Vector2(0.5f, 1.0f); } else { texCoords[t++] = new Vector2(0.5f, 0.0f); texCoords[t++] = new Vector2(0.5f, 1.0f); } vertexOffsets[o++] = new Vector2(0.0f, 1.0f); vertexOffsets[o++] = new Vector2(0.0f, -1.0f); } texCoords[t++] = new Vector2(0.5f, 0.0f); texCoords[t++] = new Vector2(0.5f, 1.0f); texCoords[t++] = new Vector2(0.0f, 0.0f); texCoords[t++] = new Vector2(0.0f, 1.0f); vertexOffsets[o++] = new Vector2(0.0f, 1.0f); vertexOffsets[o++] = new Vector2(0.0f, -1.0f); vertexOffsets[o++] = new Vector2(1.0f, 1.0f); vertexOffsets[o++] = new Vector2(1.0f, -1.0f); // fill previous and next positions Vector3[] prevPositions = new Vector3[vertexPositions.Length]; Vector4[] nextPositions = new Vector4[vertexPositions.Length]; int p = 0; int n = 0; prevPositions[p++] = m_lineVertices[1]; prevPositions[p++] = m_lineVertices[1]; prevPositions[p++] = m_lineVertices[1]; prevPositions[p++] = m_lineVertices[1]; nextPositions[n++] = m_lineVertices[1]; nextPositions[n++] = m_lineVertices[1]; nextPositions[n++] = m_lineVertices[1]; nextPositions[n++] = m_lineVertices[1]; for (int i = 1; i < m_lineVertices.Length - 1; ++i) { prevPositions[p++] = m_lineVertices[i - 1]; prevPositions[p++] = m_lineVertices[i - 1]; nextPositions[n++] = m_lineVertices[i + 1]; nextPositions[n++] = m_lineVertices[i + 1]; } prevPositions[p++] = m_lineVertices[m_lineVertices.Length - 2]; prevPositions[p++] = m_lineVertices[m_lineVertices.Length - 2]; prevPositions[p++] = m_lineVertices[m_lineVertices.Length - 2]; prevPositions[p++] = m_lineVertices[m_lineVertices.Length - 2]; nextPositions[n++] = m_lineVertices[m_lineVertices.Length - 2]; nextPositions[n++] = m_lineVertices[m_lineVertices.Length - 2]; nextPositions[n++] = m_lineVertices[m_lineVertices.Length - 2]; nextPositions[n++] = m_lineVertices[m_lineVertices.Length - 2]; if (null != m_meshFilter) { var mesh = m_meshFilter.sharedMesh; Debug.Assert(null != mesh); if (null != mesh) { mesh.SetIndices(null, MeshTopology.Triangles, 0); // Reset before setting again to prevent a unity error message. mesh.vertices = vertexPositions; mesh.normals = prevPositions; mesh.tangents = nextPositions; mesh.uv = texCoords; mesh.uv2 = vertexOffsets; mesh.SetIndices(indices, MeshTopology.Triangles, 0); UpdateBounds(); } } } #endregion #region event functions void Start () { Mesh mesh = new Mesh(); m_meshFilter = GetComponent(); m_meshFilter.mesh = mesh; UpdateLineVertices(m_lineVertices); CreateMaterial(); } void OnDestroy() { if (null != m_meshFilter) { if (Application.isPlaying) { Mesh.Destroy(m_meshFilter.sharedMesh); } else // avoid "may not be called from edit mode" error { Mesh.DestroyImmediate(m_meshFilter.sharedMesh); } m_meshFilter.sharedMesh = null; } DestroyMaterial(); } void Update() { if (transform.hasChanged) { UpdateLineScale(); UpdateBounds(); } } void OnValidate() { // This function is called when the script is loaded or a value is changed in the inspector (Called in the editor only). // => make sure, everything stays up-to-date if(string.IsNullOrEmpty(gameObject.scene.name) || string.IsNullOrEmpty(gameObject.scene.path)) { return; // ...but not if a Prefab is selected! (Only if we're using it within a scene.) } CreateMaterial(); SetAllMaterialProperties(); UpdateBounds(); } void OnDrawGizmos() { Gizmos.color = Color.green; if (null == m_lineVertices) { return; } for (int i=0; i < m_lineVertices.Length - 1; ++i) { Gizmos.DrawLine(gameObject.transform.TransformPoint(m_lineVertices[i]), gameObject.transform.TransformPoint(m_lineVertices[i+1])); } } #endregion } }