Catmull-Rom splines in Unity

Catmull-Rom splines in Unity

Blog | December 2014

So here we are, let’s jump right in.

Catmull-Rom what now?

This has to be one of the most problematic piece of coding I had since I started coding again (like 6 months ago). I needed curves that pass through their control point to be able to make a grid of curves in the Unity Editor.

Assuming you didn’t get the previous article about curves in Unity, I tried to implement elegantly Beziers Splines, but the more control points I used the smoother the curve (and the further from the control points it got).

Catmull-Rom are centripedal and mathematically stable - meaning, they pass trough the point and don’t fu#! up in wierd ways in strange conditions. More than that, they are pretty fast to compute.

Acknowledgement

Well, finding documentation on the centripedal curve and on the Catmull-Rom algorithm is not that difficult [1] (wikipedia [2] itself is pretty well furnished in that regard), but actually finding something "programmer readable" and not in "math speak" and crypted with greek symbols is not a small matter. So, credit goes where it is due: the actual curve equation of my code comes from the great Boon Cotter from booniverse.

Actual Code

I had to tweak Boon’s code quite a bit to make it fit my needs, meaning : this shit has to work with an arbitrary number of points and not only for that you have to trick into thinking there are six of them to get an actual proper curve.

This implementations has a constructor that takes a Vector3[] or a List<Vector3> like my Bezier Splines, and also gets a function onCurve(t) that returns a Vector3 on the Catmull-Rom curve at t, t being a float between 0 and 1.

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

[System.Serializable]
/// <summary>
/// Catmull rom spline implementation for Unity
/// by Stéphane Drouot, laei - http://games.laei.org
///
/// Actual translation of math gibberish to C# credit is due to
/// Boon Cotter - http://www.booncotter.com/waypoints-catmull-rom-splines/
///
/// This takes a list of vector3 (or an array) and gives a function called .onCurve(t)
/// returning a value on a Catmull-Rom spline for 0 <= t <= 1
/// </summary>
public struct CatmullRomSpline {
        [SerializeField]
        private List<Vector3> points;

        [SerializeField]
        private bool loopy;
        private List<Vector3> initialPoints;
        private int initialNumberOfPoints;

        public bool Loop{
                get{
                        return loopy;
                }
                set{
                        loopy = value;
                        if(value){
                                points = new List<Vector3>(looping(points.ToArray()));
                        }else{
                                points = new List<Vector3>(unlooping(points.ToArray()));
                        }
                }
        }

        public List<Vector3> Points{
                set{
                        List<Vector3> pointsWithoutEnds = new List<Vector3>();
                        if(loopy){
                                pointsWithoutEnds = new List<Vector3>(looping(value.ToArray()));
                        }else{
                                pointsWithoutEnds = value;
                        }
                        points = formalize (pointsWithoutEnds);
                        initialPoints = value;
                }
                get{
                        return initialPoints;
                }

        }

        public float length{
                get{
                        return lenght(1f);
                }
        }

        public float lenght(float t, float precision = 0.1f){ //Not to be trusted, vastly approximative, just here in case
                precision = Mathf.Clamp(precision, 0.001f, 1f);
                float l = 0f;
                Vector3 previous = points[0];
                for(float tee = precision ; tee <= t; tee+=precision){
                        Vector3 current = onCurve(tee);
                        l += Vector3.Distance(previous, current);
                        previous = current;
                }
                return l;
        }

        public Vector3 onCurve(float t){
                t = Mathf.Clamp01(t);

                int adjustedIndex = Mathf.FloorToInt(t * (points.Count - 4)); //Since the equation works with 4 points, we adjust the starting point depending on t to return a point on the specific segment

                Vector3 result = new Vector3();

                Vector3 p0 = points[adjustedIndex];
                Vector3 p1 = points[adjustedIndex + 1];
                Vector3 p2 = points[adjustedIndex + 2];
                Vector3 p3 = points[adjustedIndex + 3];

                float adjustedT = (t==1f)?1f:Mathf.Repeat(t * (points.Count - 4), 1f); // Then we adjust t to be that value on that new piece of segment... for t == 1f don't use repeat (that would return 0f);

                float t0 = ((-adjustedT + 2f) * adjustedT - 1f) * adjustedT * 0.5f;
                float t1 = (((3f * adjustedT - 5f) * adjustedT) * adjustedT + 2f) * 0.5f;
                float t2 = ((-3f * adjustedT + 4f) * adjustedT + 1f) * adjustedT * 0.5f;
                float t3 = ((adjustedT - 1f) * adjustedT * adjustedT) * 0.5f;

                result.x = p0.x * t0 + p1.x * t1 + p2.x * t2 + p3.x * t3;
                result.y = p0.y * t0 + p1.y * t1 + p2.y * t2 + p3.y * t3;
                result.z = p0.z * t0 + p1.z * t1 + p2.z * t2 + p3.z * t3;
               
                return result;
        }

        public void trace(float TracingStep, Color Couleur, float duration = 0f){
                Vector3 B = points[0];
                Vector3 BminusOne;

                TracingStep = Mathf.Min(1f,TracingStep);
                TracingStep = Mathf.Max(0.0001f,TracingStep);

                for(float t = 0; t <= 1f; t += TracingStep){
                        BminusOne = B;
                        B = onCurve(t);
                        //Debug.Log("lining");
                        Debug.DrawLine(BminusOne, B, new Color(0.7f, 0.6f , t, 1.0f), duration);
                        //Gizmos.DrawLine(BminusOne, B);
                }
        }

        public void append(Vector3 endpoint){
                if(loopy){
                        points.RemoveAt(points.Count - 1);
                        points[points.Count - 1] = endpoint;
                        points = new List<Vector3>(looping(points.ToArray()));
                }else{
                        points.Add(endpoint);
                }
        }

        public void append(Vector3[] appendees){
                if(loopy){
                        points.RemoveAt(points.Count - 1);
                        points.AddRange(appendees);
                        points = new List<Vector3>(looping(points.ToArray()));
                }else{
                        points.AddRange(appendees);
                       
                }
        }

        public void appendAt(Vector3 point, int index){
                if(loopy && index >= points.Count - 2 )
                {
                        points.RemoveAt(points.Count - 1);
                        points[points.Count - 1] = point;
                        points = new List<Vector3>(looping(points.ToArray()));
                }else{
                        points.Insert(index, point);
                }
        }

        public CatmullRomSpline(Vector3 InPt, Vector3 InTgt, Vector3 OutPt, Vector3 OutTgt, bool loop = false){
                this.points = new List<Vector3>(new Vector3[]{InPt, InTgt, OutPt, OutTgt});
                this.initialPoints = points;
                this.initialNumberOfPoints = 4;
                this.loopy = loop;
        }

        public CatmullRomSpline(Vector3 InPt, Vector3 InTgt, bool loop = false){
                this.points = new List<Vector3>(new Vector3[]{InPt, InTgt});
                this.initialPoints = points;
                this.initialNumberOfPoints = 2;
                this.loopy = loop;
        }

        public CatmullRomSpline(Vector3[] pointsV, bool loop = false){
                this.loopy = loop;
                this.initialPoints = new List<Vector3>(pointsV);
                this.initialNumberOfPoints = pointsV.Length;
                this.points = new List<Vector3>(pointsV);
                if(loop){
                        this.points = new List<Vector3>(looping(points.ToArray()));
                }
                this.points = formalize(this.points);
        }

        public CatmullRomSpline(List<Vector3> PointsList, bool loop = false){
                this.loopy = loop;
                this.points = PointsList;
                this.initialNumberOfPoints = PointsList.Count;
                this.initialPoints = PointsList;
                if(loop){
                        this.points = new List<Vector3>(looping(points.ToArray()));
                }
                this.points = formalize(this.points);
        }

        private Vector3[] looping(Vector3[] inV){
                Vector3[] loopingPoints = new Vector3[inV.Length + 2];
                System.Array.Copy(inV, loopingPoints, inV.Length);
                loopingPoints[loopingPoints.Length - 1] = inV[0];
                loopingPoints[loopingPoints.Length - 2] = inV[0] - inV[1];
                return loopingPoints;
        }

        private Vector3[] unlooping(Vector3[] inV){
                System.Array.Resize(ref inV, inV.Length - 2);
                return inV;
        }

        private List<Vector3> formalize(List<Vector3> pts){
                initialNumberOfPoints = pts.Count;
                List<Vector3> formalizedLst = new List<Vector3>();
                formalizedLst.Add(pts[0]);
                formalizedLst.AddRange(pts);
                do{
                        formalizedLst.Add(pts[pts.Count - 1]);
                }while(formalizedLst.Count < initialNumberOfPoints + 3);

                return formalizedLst;
        }

        public int Count{
                get{
                        return initialNumberOfPoints;
                }
        }

        public void delete(int index){
                points.RemoveAt(index);
        }
}

Script .CS is downloadable in zip form

Zip - 2 kb

For full disclosure, I let the functions in for effect, but I’m pretty sure looping function won’t work, I did not even test them.

As usual, comments and stuff are welcome.

Addendum

Well, it turns out, in a year I actually learnt a lot about coding (thank God), and this version is kinda crappy... the looping doesn’t work properly, I have a better one now, contact me if you need it.


Donation

Please, help us keep on doing what we do!
You will get our eternal gratitude!

Downloads

Tags

Networking

Facebook

Youtube

Bandcamp