The last post covered how a spline tool can be created and edited in Unity's scene view. This post covers how we can use that data and create a mesh around it to get a cable.


Full source for this and the previous post can be found at https://github.com/bonahona/CableSpline


Quick recap

We have a CableSpline.cs file containing the monobehaviour that represents and spline (and soon are cable as well) and Editor/CableSplineEditor.cs containing the editor.


We left of with an empty

UpdateMesh()
in the
CableSpline
class method. The goal is now to implement that method.


Build the mesh generation

Out cable will need some new Unity components to work. Add these two attribute above the declaration of the CableSpline class.

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class ConnectionSystem : MonoBehaviour


The next time a CableSpline is created it will automatically add these two components to the GameObject. That being said, if you have an old CableSpline in the scene, remove and create a new one.


We need a class to help out with holding the mesh data while it's being generated. Add this class as a nested class inside CableSpline.

public class MeshData{
  public List<Vector3> Vertices = new List<Vector3>();
  public List<Vector3> Normals = new List<Vector3>();
  public List<Vector2> Uvs = new List<Vector2>();
  public List<int> Triangles = new List<int>();

  public int CurrentIndex = 0;
  public float CurrentUvOffset = 0;
}


The cable will also need to know what material to apply to the cable. Add it to CableSpline.

public Material Material;


Add it to the

CableSplineEditor
's
OnInspectorGUI()
method along with the other properties so it can be edited.

cableSpline.Material = EditorGUILayout.ObjectField("Material", cableSpline.Material, typeof(Material), false) as Material;


Time to implement the mesh creation. We need to get the mesh renderer so we can set it's material and the mesh filter to set the mesh. After that we need to generate some intermediary points in the line to smooth the roundness of the mesh, follow by lots of calculations to create all the vertices needed for the mesh. Lastly all the data is copied over into a new instance of a mesh and set.

public void UpdateMesh(){
   var meshFilter = GetComponent<MeshFilter>();
   var meshRender = GetComponent<MeshRenderer>();

   var roundedControlPoints = GenerateCableControlPoints(ControlPoints, SmoothnessLevel);

   var meshData = GenerateMeshData(roundedControlPoints);
   var mesh = new Mesh {
      vertices = meshData.Vertices.ToArray(),
      normals = meshData.Normals.ToArray(),
      uv = meshData.Uvs.ToArray(),
      triangles = meshData.Triangles.ToArray()
   };

   meshFilter.mesh = mesh;
   meshRender.material = Material;
}

private List<SplineControlPoint> GenerateCableControlPoints(List<SplineControlPoint> controlPoints, int steps){
 // TODO: Implement me
 return null;
}

private MeshData GenerateMeshData(List<SplineControlPoint> controlPoints) {
 // TODO: Implement me
 return null;
}


Creating intermediary points along a cubic bezier curve.

We'll create a new helper file to deal with all the math of the bezier curve calculations named BezierCurve.cs. This post will not cover the math behind how it works.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// https://en.wikipedia.org/wiki/B%C3%A9zier_curve
public class BezierCurve
{
   public static Vector3 CubicCurve(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t){
       t = Mathf.Clamp01(t);
       float oneMinusT = 1f - t;
       return oneMinusT * oneMinusT * oneMinusT * p0 + 3f * oneMinusT * oneMinusT * t * p1 + 3f * oneMinusT * t * t * p2 + t * t * t * p3;
   }

   public static Vector3 CubicCurveDerivative(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t){
       t = Mathf.Clamp01(t);
       float oneMinusT = 1f - t;
       return 3f * oneMinusT * oneMinusT * (p1 - p0) + 6f * oneMinusT * t * (p2 - p1) + 3f * t * t * (p3 - p2);
   }

   public Vector3 Source;
   public Vector3 SourceTangent;
   public Vector3 End;
   public Vector3 EndTangent;

   public float EstimateBezierCurveLength(int segmentCount){
       var result = 0f;

       var increment = 1f / segmentCount;
       for (int i = 0; i < segmentCount; i++) {
           result += Vector3.Distance(GetBezierCurvePosition(increment * i), GetBezierCurvePosition(increment * (i + 1)));
       }

       return result;
   }

   public Vector3 GetBezierCurvePosition(float value){
       return CubicCurve(Source, End, SourceTangent, EndTangent, value);
   }

   public Vector3 GetCubicCurveDerivative(float value){
       return CubicCurveDerivative(Source, SourceTangent, EndTangent, End, value);
   }
}


Now we can use this the

GenerateCableControlPoints()
method to create a new list of points based on the original one but with lots of extra point added in between to give it a smoother form to follow.

private List<ConnectionControlPoint> GenerateCableControlPoints(List<ConnectionControlPoint> controlPoints, int steps)
   {
       // Just add the first point. It will still be first
       var result = new List<ConnectionControlPoint> {
           controlPoints[0]
       };

       // Can't have a mesh with a single point
       if (controlPoints.Count < 2) {
           return result;
       }

	   return result;
}


Now we use the original list, give as a list of sorted pairs and start using vector math to figure out where the additional points should be. Cubic bezier curves require four points. The first two points are the values of the pair. The other points, the "extra points" will be created in the direction from the control point, about 1/2nd the way towards each other.

Visualization of how the extra points are created in relation to the curve.

Put this logic before the final return statement of the method.

foreach (var pair in GetControlPointPairs(controlPoints)) {
   var pairHalfDistance = (pair.Second.Position - pair.First.Position).magnitude / 2;
   var pairStepDistance = 1f / (steps + 1);


   var firstPoint = pair.First.Position;
   var lastPoint = pair.Second.Position;

   // Calculate extra points
   var extraPosition01 = pair.First.Position + pair.First.Direction * Vector3.forward * pairHalfDistance;
   var extraPosition02 = pair.Second.Position + pair.Second.Direction * Vector3.back * pairHalfDistance;

   // Insert N extra control points along the curve.
   for (int i = 0; i < steps; i++) {
      var distanceFactor = (i + 1) * pairStepDistance;
      var position = BezierCurve.CubicCurve(firstPoint, extraPosition01, extraPosition02, lastPoint, distanceFactor);

      var firstRotation = Quaternion.Euler(0, 0, pair.First.Direction.eulerAngles.z);
      var secondRotation = Quaternion.Euler(0, 0, pair.Second.Direction.eulerAngles.z);
      var rotation = Quaternion.Lerp(firstRotation, secondRotation, steps * pairStepDistance);
      var tangent = BezierCurve.CubicCurveDerivative(firstPoint, extraPosition01, extraPosition02, lastPoint, distanceFactor).normalized;

      result.Add(new SplineControlPoint { Position = position, Direction = Quaternion.LookRotation(tangent) * rotation });
   }

   result.Add(pair.Second);
}


Creating a custom mesh

Before we start, here's a quick recap of how a mesh is build including consideration to the winding order of vertices.

Figure A. shows a single quad being created. It consist of four vertices (0, 1, 2, 3) and 6 edges in total. It consists of two tris, the first being v0 -> v2 -> v1 and second v2 -> v3 -> v1. To create that quad as a triangle list need to supply the indices in that order.


Figure B. Shows three quads being lines up, where the last two are the same as the first two, hence it wraps neatly. We can follow the lines in the image and find the index list to be as follow: 0 2 1 2 3 1 2 4 3 4 5 3 4 0 5 0 1 5. (Just follow all the tris clockwise and put them together.


The mesh generation needs to create three points of data for each index: a position, a UV point, and a face normal. It also needs to generate the index list to put it all together.


First we'll create a helper method for UV generation to make sure it loops nicely when when segments are spatially unevenly placed.


private float GetUvOffset(SplineControlPoint controlPoint, SplineControlPoint lastControlPoint){
   if (lastControlPoint == null) {
      return 0;
   }

   return (lastControlPoint.Position - controlPoint.Position).magnitude;
}

Next is a tiny helper method to get where with a specific vertex will end up, based on the position of the control point and the local rotation if it as well as the intended radius of the cable and the local direction of this point.

private Vector3 GetVectorPosition(Vector3 position, Quaternion rotation, Vector3 direction, float width) {
    return position + rotation * direction * width;
}


This method will use the above theory about triangle and winding order to add a rounded mesh segment to the mesh.

private void AddControlPointToMesh(SplineControlPoint controlPoint, SplineControlPoint lastControlPoint, MeshData meshData){
   // Precalculate shared data
   var radius = Diameter / 2;
   var radiansSteps = (Mathf.PI * 2) / RoundSegments;
   var uvSteps = 1f / RoundSegments;

   meshData.CurrentUvOffset += GetUvOffset(controlPoint, lastControlPoint);

   // Generate data for each vertex
   for (int i = 0; i <= RoundSegments; i++) {
      var localDirection = new Vector3(Mathf.Sin(radiansSteps * i), Mathf.Cos(radiansSteps * i), 0);
      meshData.Vertices.Add(GetVectorPosition(controlPoint.Position, controlPoint.Direction, localDirection, radius));

      // Normals always points straight out
      meshData.Normals.Add(Vector3.up);

      // Wraps perfectly X wise and uses the length of the segment as a base for the Y value.
      meshData.Uvs.Add(new Vector2(uvSteps * i, meshData.CurrentUvOffset * 2));
   }

   // Add 1 to account for it to wrap back on its own start
   var extendedRoundSegments = RoundSegments + 1;
   if (lastControlPoint != null) {
      // Create the triangle list. Each quad has six entries in the list
      for (int i = 0; i < extendedRoundSegments; i++) {
         meshData.Triangles.AddRange(new int[] {
            meshData.CurrentIndex - extendedRoundSegments + i,
            meshData.CurrentIndex + i,
            meshData.CurrentIndex + (i + 1) % extendedRoundSegments,
            meshData.CurrentIndex + (i + 1) % extendedRoundSegments,
            meshData.CurrentIndex - extendedRoundSegments + (i + 1) % extendedRoundSegments,
            meshData.CurrentIndex - extendedRoundSegments + i,
         });
      }
   }

   meshData.CurrentIndex += (RoundSegments + 1);
}


With these helper methods in place we can implement

GenerateMeshData()
.

private MeshData GenerateMeshData(List<SplineControlPoint> controlPoints){
   var result = new MeshData();
   AddControlPointToMesh(controlPoints[0], null, result);
   for (int i = 1; i < controlPoints.Count; i++) {
      AddControlPointToMesh(controlPoints[i], controlPoints[i - 1], result);
   }

   return result;
}


Create a spline

Everything has been put together! Create a new material for your spline, then create a new Spline from the GameObject menu. Assign it the material and start editing it and you will see how it follows the control points as you move them. Adjust the Roundness, smoothness and diameter to your liking and there you have it. A nice spline tool using control points and cubic bezier curves.