Skip to content

Instantly share code, notes, and snippets.

@mathiassoeholm
Last active March 12, 2024 08:48
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mathiassoeholm/15f3eeda606e9be543165360615c8bef to your computer and use it in GitHub Desktop.
Save mathiassoeholm/15f3eeda606e9be543165360615c8bef to your computer and use it in GitHub Desktop.
Similar to Unity's LineRenderer, but renders a cylindrical mesh.
// Author: Mathias Soeholm
// Date: 05/10/2016
// No license, do whatever you want with this script
using UnityEngine;
using UnityEngine.Serialization;
[ExecuteInEditMode]
public class TubeRenderer : MonoBehaviour
{
[SerializeField] Vector3[] _positions;
[SerializeField] int _sides;
[SerializeField] float _radiusOne;
[SerializeField] float _radiusTwo;
[SerializeField] bool _useWorldSpace = true;
[SerializeField] bool _useTwoRadii = false;
private Vector3[] _vertices;
private Mesh _mesh;
private MeshFilter _meshFilter;
private MeshRenderer _meshRenderer;
public Material material
{
get { return _meshRenderer.material; }
set { _meshRenderer.material = value; }
}
void Awake()
{
_meshFilter = GetComponent<MeshFilter>();
if (_meshFilter == null)
{
_meshFilter = gameObject.AddComponent<MeshFilter>();
}
_meshRenderer = GetComponent<MeshRenderer>();
if (_meshRenderer == null)
{
_meshRenderer = gameObject.AddComponent<MeshRenderer>();
}
_mesh = new Mesh();
_meshFilter.mesh = _mesh;
}
private void OnEnable()
{
_meshRenderer.enabled = true;
}
private void OnDisable()
{
_meshRenderer.enabled = false;
}
void Update ()
{
GenerateMesh();
}
private void OnValidate()
{
_sides = Mathf.Max(3, _sides);
}
public void SetPositions(Vector3[] positions)
{
_positions = positions;
GenerateMesh();
}
private void GenerateMesh()
{
if (_mesh == null || _positions == null || _positions.Length <= 1)
{
_mesh = new Mesh();
return;
}
var verticesLength = _sides*_positions.Length;
if (_vertices == null || _vertices.Length != verticesLength)
{
_vertices = new Vector3[verticesLength];
var indices = GenerateIndices();
var uvs = GenerateUVs();
if (verticesLength > _mesh.vertexCount)
{
_mesh.vertices = _vertices;
_mesh.triangles = indices;
_mesh.uv = uvs;
}
else
{
_mesh.triangles = indices;
_mesh.vertices = _vertices;
_mesh.uv = uvs;
}
}
var currentVertIndex = 0;
for (int i = 0; i < _positions.Length; i++)
{
var circle = CalculateCircle(i);
foreach (var vertex in circle)
{
_vertices[currentVertIndex++] = _useWorldSpace ? transform.InverseTransformPoint(vertex) : vertex;
}
}
_mesh.vertices = _vertices;
_mesh.RecalculateNormals();
_mesh.RecalculateBounds();
_meshFilter.mesh = _mesh;
}
private Vector2[] GenerateUVs()
{
var uvs = new Vector2[_positions.Length*_sides];
for (int segment = 0; segment < _positions.Length; segment++)
{
for (int side = 0; side < _sides; side++)
{
var vertIndex = (segment * _sides + side);
var u = side/(_sides-1f);
var v = segment/(_positions.Length-1f);
uvs[vertIndex] = new Vector2(u, v);
}
}
return uvs;
}
private int[] GenerateIndices()
{
// Two triangles and 3 vertices
var indices = new int[_positions.Length*_sides*2*3];
var currentIndicesIndex = 0;
for (int segment = 1; segment < _positions.Length; segment++)
{
for (int side = 0; side < _sides; side++)
{
var vertIndex = (segment*_sides + side);
var prevVertIndex = vertIndex - _sides;
// Triangle one
indices[currentIndicesIndex++] = prevVertIndex;
indices[currentIndicesIndex++] = (side == _sides - 1) ? (vertIndex - (_sides - 1)) : (vertIndex + 1);
indices[currentIndicesIndex++] = vertIndex;
// Triangle two
indices[currentIndicesIndex++] = (side == _sides - 1) ? (prevVertIndex - (_sides - 1)) : (prevVertIndex + 1);
indices[currentIndicesIndex++] = (side == _sides - 1) ? (vertIndex - (_sides - 1)) : (vertIndex + 1);
indices[currentIndicesIndex++] = prevVertIndex;
}
}
return indices;
}
private Vector3[] CalculateCircle(int index)
{
var dirCount = 0;
var forward = Vector3.zero;
// If not first index
if (index > 0)
{
forward += (_positions[index] - _positions[index - 1]).normalized;
dirCount++;
}
// If not last index
if (index < _positions.Length-1)
{
forward += (_positions[index + 1] - _positions[index]).normalized;
dirCount++;
}
// Forward is the average of the connecting edges directions
forward = (forward/dirCount).normalized;
var side = Vector3.Cross(forward, forward+new Vector3(.123564f, .34675f, .756892f)).normalized;
var up = Vector3.Cross(forward, side).normalized;
var circle = new Vector3[_sides];
var angle = 0f;
var angleStep = (2*Mathf.PI)/_sides;
var t = index / (_positions.Length-1f);
var radius = _useTwoRadii ? Mathf.Lerp(_radiusOne, _radiusTwo, t) : _radiusOne;
for (int i = 0; i < _sides; i++)
{
var x = Mathf.Cos(angle);
var y = Mathf.Sin(angle);
circle[i] = _positions[index] + side*x* radius + up*y* radius;
angle += angleStep;
}
return circle;
}
}
@FinnPerry
Copy link

FinnPerry commented Nov 21, 2022

Hi, thanks for this script!
I've found it quite useful, however in my project it actually caused a memory leak.

The leak happens because when you run GenerateMesh Unity will not destroy the old mesh even if nothing is referencing it. My fix for this is to periodically run Resources.UnloadUnusedAssets(). It's a bit out of scope to include that in the script as it affects all objects, not just the tube meshes, but I think it's worth mentioning in case anyone else runs into the same issue.

Secondly, removing the Update function improves the situation as the old meshes don't pile up so quickly if you don't require a new mesh every frame. Since SetPositions calls GenerateMesh, I believe running on Update would only be required if your tube is in world space, so I think it should be changed to

private void Update()
{
    if (_useWorldSpace)
    {
        GenerateMesh();
    }
}

@nicogarciasdev
Copy link

Hi mate, thanks for posting this script. It helped me a lot in my project!

@didiersurka
Copy link

Hi, thanks you for this script. I made a small modification that may be useful to other users, feel free to integrate it. It's a width along curve property, controllable with an animation curve.

//this may be an enum with Single, StartEnd and Curve options.
[SerializeField] bool _useCurve = false;
[SerializeField] AnimationCurve _radiusOverLength; 

and then when setting up the radius:

float radius = _radiusOne;
if(_useTwoRadii) {
    radius = Mathf.Lerp(_radiusOne, _radiusTwo, t);
}
else if(_useCurve){
    radius = _radiusOverLength.Evaluate(t);
}

@nicogarciasdev
Copy link

nicogarciasdev commented May 25, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment