using UnityEngine; namespace VolumetricLines { /// /// Render a single volumetric line /// /// 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 VolumetricLineBehavior : 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; /// /// The start position relative to the GameObject's origin /// [SerializeField] private Vector3 m_startPos; /// /// The end position relative to the GameObject's origin /// [SerializeField] private Vector3 m_endPos = new Vector3(0f, 0f, 100f); /// /// 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; #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); } } } /// /// Get or set the start position of this volumetric line's mesh /// public Vector3 StartPos { get { return m_startPos; } set { m_startPos = value; SetStartAndEndPoints(m_startPos, m_endPos); } } /// /// Get or set the end position of this volumetric line's mesh /// public Vector3 EndPos { get { return m_endPos; } set { m_endPos = value; SetStartAndEndPoints(m_startPos, m_endPos); } } #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, light saber factor, start-, endpos) /// private void SetAllMaterialProperties() { SetStartAndEndPoints(m_startPos, m_endPos); 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 start and end points, /// 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 min = new Vector3( Mathf.Min(m_startPos.x, m_endPos.x) - scaledLineWidth, Mathf.Min(m_startPos.y, m_endPos.y) - scaledLineWidth, Mathf.Min(m_startPos.z, m_endPos.z) - scaledLineWidth ); var max = new Vector3( Mathf.Max(m_startPos.x, m_endPos.x) + scaledLineWidth, Mathf.Max(m_startPos.y, m_endPos.y) + scaledLineWidth, Mathf.Max(m_startPos.z, m_endPos.z) + scaledLineWidth ); return new Bounds { min = min, max = max }; } /// /// Updates the bounds of this line according to the current properties, /// which there are: start point, end point, 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(); } } } /// /// Sets the start and end points - updates the data of the Mesh. /// public void SetStartAndEndPoints(Vector3 startPoint, Vector3 endPoint) { m_startPos = startPoint; m_endPos = endPoint; Vector3[] vertexPositions = { m_startPos, m_startPos, m_startPos, m_startPos, m_endPos, m_endPos, m_endPos, m_endPos, }; Vector3[] other = { m_endPos, m_endPos, m_endPos, m_endPos, m_startPos, m_startPos, m_startPos, m_startPos, }; if (null != m_meshFilter) { var mesh = m_meshFilter.sharedMesh; Debug.Assert(null != mesh); if (null != mesh) { mesh.vertices = vertexPositions; mesh.normals = other; UpdateBounds(); } } } #endregion #region event functions void Start () { Mesh mesh = new Mesh(); m_meshFilter = GetComponent(); m_meshFilter.mesh = mesh; SetStartAndEndPoints(m_startPos, m_endPos); mesh.uv = VolumetricLineVertexData.TexCoords; mesh.uv2 = VolumetricLineVertexData.VertexOffsets; mesh.SetIndices(VolumetricLineVertexData.Indices, MeshTopology.Triangles, 0); CreateMaterial(); // TODO: Need to set vertices before assigning new Mesh to the MeshFilter's mesh property => Why? } 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; Gizmos.DrawLine(gameObject.transform.TransformPoint(m_startPos), gameObject.transform.TransformPoint(m_endPos)); } #endregion } }