'Tube mesh generation in unity

I've created a procedural mesh script while watching one of Freya Holmér's improvised live course vods, and re purposed the code to create a procedural tube mesh with subdivision and plenty of other niche features.

But, after looking over the code and the lesson, I still cannot for the life of me figure out why sometimes I will get an:

argument out of range exception

...and sometimes I won't depending on the level of subdivision; also, entire faces wont be generated by the script.

TL;DR Problems list:

  • Argument out of range exception (Depending on level of subdivision).
  • Entire sides/faces of the tube mesh will not be generated, even when no errors are provided.

These problems are stemming from line 154 in the UpdateTris() method.

Code

using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
using OpenNexus.ExtraGizmos;
#endif

using OpenNexus.BaseMath;
using OpenNexus.MeshFormat;

namespace OpenNexus.Procedurals
{
    [RequireComponent(typeof(MeshFilter))]
    public class TubeMesh : MonoBehaviour
    {
        private MeshFilter filter;
        private Mesh mesh;

        public List<Vector3> Points = new List<Vector3>()
        {
            new Vector3(0, 0, 0),
            new Vector3(0, 1, 1),
            new Vector3(0, 0, 2)
        };

        [Header("Bezier Data")]
        public Precision BezierPrecision = Precision.Fast;

        [Header("Tube Data")]
        [Range(0.01f, 1f)]public float Radius = 0.01f;
        [Range(3, 32)] public int Segments = 8;
        [Range(3, 6)] public int Subdivisions = 3;

        [Header("Mesh Data")]
        public ProceduralMesh2D BaseProceduralMesh = new ProceduralMesh2D();
        public List<Vector3> Vertices;
        public List<int> Tris;

        private void OnEnable()
        {
            if (!GetComponent<MeshRenderer>()) // Check for and add missing mesh renderer.
                gameObject.AddComponent<MeshRenderer>();

            if (filter == null) // Check for and assign mesh filter variable.
                filter = GetComponent<MeshFilter>();

            if (mesh == null) // Check for and instantiate a new mesh object.
                mesh = new Mesh();

            mesh.name = "TubeMesh"; // Set name to mesh object.
            filter.sharedMesh = mesh; // Set MeshFilter's shared mesh variable to new mesh object.
        }

        private void Update()
        {
            /*
                Data reset
            ------------------------------
             */

            // Reset base mesh data.
            BaseProceduralMesh.Vertices = new List<Vertex>();
            BaseProceduralMesh.LineIndex = new List<int>();
            
            // Reset mesh data.
            Vertices = new List<Vector3>();
            Tris = new List<int>();

            /*
                Data update
            ------------------------------
             */

            // Update base mesh.
            UpdateBaseMesh();

            // Update mesh data.
            UpdateVertices();
            UpdateTris();
        }

        private void LateUpdate() => UpdateMesh();

        private BezierPoint GetBezierPoint(int index)
        {
            float _t = index / (Segments - 1f);
            BezierPoint _bp = BezierMath.QuadraticBezier(Points, BezierPrecision, _t);
            
            return _bp;
        }

        private void UpdateBaseMesh()
        {
            // Generate base vertices.
            for (int i = 0; i < Subdivisions; i++)
            {
                float _point = i / (float)Subdivisions;
                float _radius = _point * Floats.TAU;
                Vertex _vertex = new Vertex(VectorThrees.UnitVectorByAngle(_radius) * Radius);

                BaseProceduralMesh.Vertices.Add(_vertex);
            }

            // Generate base LineIndexes.
            for (int i = 0; i < BaseProceduralMesh.VertexCount; i++)
            {
                BaseProceduralMesh.LineIndex.Add(i);
            }
            BaseProceduralMesh.LineIndex.Add(0);
        }

        private void UpdateVertices()
        {
            for (int i = 0; i < Segments; i++)
            {
                BezierPoint _point = GetBezierPoint(i);
                for (int j = 0; j < BaseProceduralMesh.VertexCount; j++)
                {
                    Vertices.Add(_point.LocalToWorldPosition(BaseProceduralMesh.Vertices[j].Point));
                }              
            }
        }

        private void UpdateTris()
        {
            for (int s = 0; s < Segments - 1; s++)
            {
                int _root = s * BaseProceduralMesh.VertexCount;
                int _rootNext = (s + 1) * BaseProceduralMesh.VertexCount;

                for (int i = 0; i < BaseProceduralMesh.EdgeCount; i+=2)
                {
                    int _lineA = BaseProceduralMesh.LineIndex[i];
                    int _lineB = BaseProceduralMesh.LineIndex[i + 1];

                    int _currentA = _root + _lineA;
                    int _currentB = _root + _lineB;

                    int _nextA = _rootNext + _lineA;
                    int _nextB = _rootNext + _lineB;

                    Tris.Add(_currentA);
                    Tris.Add(_nextA);
                    Tris.Add(_nextB);

                    Tris.Add(_currentA);
                    Tris.Add(_nextB);
                    Tris.Add(_currentB);
                }
            }
        }

        private void UpdateMesh()
        {
            mesh.Clear();
            mesh.SetVertices(Vertices);
            mesh.SetTriangles(Tris, 0);
            mesh.RecalculateNormals();
        }

#if UNITY_EDITOR

        private void OnDrawGizmos()
        {
            // Draw psudo mesh with gizmos.


            /*
                Draw segment/edge loops
            -------------------------------------
             */

            for (int i = 0; i < Segments; i++) // Debug each segment, and what their 2D mesh segment should look like.
            {
                BezierPoint _point = GetBezierPoint(i);
                WireGizmos.DrawWireCircle(_point.Position, _point.Rotation, Radius, Subdivisions);
            }

            Gizmos.color = Color.red;
            for (int i = 0; i < Vertices.Count; i++) // Debug each vertex.
            {
                Gizmos.DrawSphere(Vertices[i], 0.01f);
                Handles.Label(Vertices[i], "\n\nVertex: " + i + "\n\nVertex position:\n" + Vertices[i].ToString());
            }

            for (int i = 0; i < Tris.Count; i++)
            {
                Gizmos.DrawLine(Vertices[Tris[i]], Vertices[Tris[(i + 1) % Tris.Count]]);
            }
        }

#endif
    }
}

I've already looked over my code compared to the code in the video, it's fundamentally the same, with the main differences being, how I'm creating the 2D mesh format for the script to work with, and the structure of the code.

After looking back at what they had done compared to mine, I just don't see how I'm running into this issue.

Things I've tried

  • Change the loop iteration i+=2 -to- i++ (This spits out the exception, but generates the first section of the tube before getting stuck between the second vertex in the next segment, and vert 0).

Every suggestion I try from this question, I will update the "Things I've tried..." list.

Please let me know if I need to clarify anything in this post, and if you wish to see the other classes/structs referenced in this script.



Solution 1:[1]

After a day of resting, and the a couple of hours of testing different values and questioning the code. I've found the problem.

The real issue

The nested for loop in the UpdateTris() method was going up by twos assuming I was making a hard edge mesh with pared vertices. This caused the loop to skip over an entire face of the mesh, and causing the

ArgumentOutOfRangeException

...when the Subdivision value was an even number.

My solution

I had to bring it back to the default single iteration (since I was trying to make a smooth lighting mesh), the second issue with the for loop was the iteration limit BaseProceduralMesh.LineCount needing to be subtracted by one since that was causing another

ArgumentOutOfRangeException

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 OneSmolCookie