devNotes 7-16-16 Line Drawing – Splines the Discreet to the Continuous

gfhjgfhj1

In this tutorial we will focus on the very basics. We will create a smooth path between a series of empty GameObjects. And if we want to create a connected path so it can form a loop, we will also be able to do that. So what you first have to do after you have created a new scene in Unity is to create an empty GameObject. As children to that GameObject, you should create more empty GameObjects. Name these something like p1, p2, p3,… because they will form our path. Remember to create at least 4 of them, because that is the minimum number of so-called control points in a Catmull-Rom spline. Then create a new script called CatmullRomSpline. Add the following code in the beginning.

public class CatmullRomSpline : MonoBehaviour {
	//Has to be at least 4 so-called control points
	public List<Transform> controlPointsList = new List<Transform>();

	public bool isLooping = true;
}

Step 1 is to just display the control points. We will do that in a method called OnDrawGizmos(). That method will update continuously even though we haven’t pressed the play button. Also drag the path you created (p1, p2, p3,…) to your list (click on the GameObject to which you attached the script and drag the points to the “Control Points List”). If you go to the Unity editor, you should now see spheres at the same positions as the control points.

//Display without having to press Play
void OnDrawGizmos() {
	Gizmos.color = Color.white;

	//Draw a sphere at each control point
	for (int i = 0; i < controlPointsList.Count; i++) {
		Gizmos.DrawWireSphere(controlPointsList[i].position, 0.3f);
	}
}

Now we will add the Catmull-Rom spline. If you have 4 control points called p0 p1 p2 p3, you will be able to add a smooth path between the 2 middle points (p1 and p2) and not all 4. To do that, we will use all 4 points, and then move from p1 to p2 with a distance (or resolution) called t. t will always be between 0 and 1, where 0 is exactly at the same coordinate as p1 and 1 is exactly at the same coordinate as p2. It might sound complicated, but I believe this is the best explanation: Introduction to Catmull-Rom Splines.

The coordinate of a vector in 3d space at distance t between p1 and p2 is given by the following method. So add it!

//Returns a position between 4 Vector3 with Catmull-Rom Spline algorithm
//http://www.iquilezles.org/www/articles/minispline/minispline.htm
Vector3 ReturnCatmullRom(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3) {
	Vector3 a = 0.5f * (2f * p1);
	Vector3 b = 0.5f * (p2 - p0);
	Vector3 c = 0.5f * (2f * p0 - 5f * p1 + 4f * p2 - p3);
	Vector3 d = 0.5f * (-p0 + 3f * p1 - 3f * p2 + p3);

	Vector3 pos = a + (b * t) + (c * t * t) + (d * t * t * t);

	return pos;
}

To be able to use ReturnCatmullRom() we have to add the following to OnDrawGizmos(). It will basically iterate through the control points and see if we should generate a Catmull-Rom spline from the current control point to the next, and if we want a path that loops.

//Draw the Catmull-Rom lines between the points
for (int i = 0; i < controlPointsList.Count; i++) {
	//Cant draw between the endpoints
	//Neither do we need to draw from the second to the last endpoint
	//...if we are not making a looping line
	if ((i == 0 || i == controlPointsList.Count - 2 || i == controlPointsList.Count - 1) && !isLooping) {
		continue;
	}

	DisplayCatmullRomSpline(i);
}
//Display a spline between 2 points derived with the Catmull-Rom spline algorithm
void DisplayCatmullRomSpline(int pos) {
	//Clamp to allow looping
	Vector3 p0 = controlPointsList[ClampListPos(pos - 1)].position;
	Vector3 p1 = controlPointsList[pos].position;
	Vector3 p2 = controlPointsList[ClampListPos(pos + 1)].position;
	Vector3 p3 = controlPointsList[ClampListPos(pos + 2)].position;


	//Just assign a tmp value to this
	Vector3 lastPos = Vector3.zero;

	//t is always between 0 and 1 and determines the resolution of the spline
	//0 is always at p1
	for (float t = 0; t < 1; t += 0.1f) {
		//Find the coordinates between the control points with a Catmull-Rom spline
		Vector3 newPos = ReturnCatmullRom(t, p0, p1, p2, p3);

		//Cant display anything the first iteration
		if (t == 0) {
			lastPos = newPos;
			continue;
		}

		Gizmos.DrawLine(lastPos, newPos);
		lastPos = newPos;
	}

	//Also draw the last line since it is always less than 1, so we will always miss it
	Gizmos.DrawLine(lastPos, p2);
}


//Clamp the list positions to allow looping
//start over again when reaching the end or beginning
int ClampListPos(int pos)  {
	if (pos < 0) {
		pos = controlPointsList.Count - 1;
	}

	if (pos > controlPointsList.Count) {
		pos = 1;
	}
	else if (pos > controlPointsList.Count - 1) {
		pos = 0;
	}

	return pos;
}

 

That wasn’t too difficult? If you go to the editor you should be able to do this:

Catmull-Rom splines finished

 

In the specific Catmull-Rom form, the tangent vector at intermediate points is determined by the locations of neighboring control points. Thus, to create a C1 continuous spline through multiple points, it is sufficient to supply the set of control points and the tangent vectors at the first and last control point. I think the standard behavior is to use P1 – P0 for the tangent vector at P0 and PN – PN-1 at PN.

According to the Wikipedia article, to calculate the tangent at control point Pn, you use this equation:

T(n) = (P(n – 1) + P(n + 1)) / 2
This also answers your first question. For a set of 4 control points, P1, P2, P3, P4, interpolating values between P2 and P3 requires information form all 4 control points. P2 and P3 themselves define the endpoints through which the interpolating segment must pass. P1 and P3 determine the tangent vector the interpolating segment will have at point P2. P4 and P2 determine the tangent vector the segment will have at point P3. The tangent vectors at control points P2 and P3 influence the shape of the interpolating segment between them.

hgklhgkl-7

USE Splines

pop

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

namespace ProceduralToolkit
{
    public static class TriQual
    {
        public const int A = 0;
        public const int B = 1;
        public const int C = 2;
        public const int D = 3;
        public const int E = 4;
        public const int F = 5;
        public const int G = 6;
        public const int H = 7;
        public const int I = 8;
        public const int J = 9;
        public const int K = 10;
        public const int L = 11;
        public const int M = 12;
        public const int N = 13;
        public const int O = 14;
        public const int P = 15;
        public const int Q = 16;
        public const int R = 17;
        public const int S = 18;
        public const int UNDEFINED = int.MaxValue;
    }

    public class ICO_Tree_Node_ID
    {
        public int sector;   // 20 sectors to ICO
        public int nodePath; // 2 bits per depth level 
    }

    public class ArgVert
    {
        public Vector3 vPos;
        public ICO_Tree_Node_ID itn = new ICO_Tree_Node_ID();
        public float dist;
        public Vector3 vForce; //constrain position to sphere radius
        public Vector3 vVel;
        public float maxDistToNeighbor = 0;
        public float minDistToNeighbor = float.MaxValue;
        public int idx;

        public class NB
        {
            public ArgVert aV;
            public float distFrom;

            public NB()
            {
                aV = null;
                distFrom = float.MaxValue;
            }
        }

        public List<NB> avNeigbor = new List<NB>();

        public ArgVert()
        {
            for (int i = 0; i < 6; i++)
            {
                avNeigbor.Add(new NB());
                avNeigbor[i].aV = this;
            }
            vPos = new Vector3();
        }

        public float getMaxDistToNeighbor()
        {
            maxDistToNeighbor = 0;
            for (int i = 0; i < 6; i++)
            {
                if (avNeigbor[i].distFrom > maxDistToNeighbor)
                {
                    maxDistToNeighbor = avNeigbor[i].distFrom;
                }
            }
            return maxDistToNeighbor;
        }

        public float getMinDistToNeighbor()
        {
            minDistToNeighbor = float.MaxValue;
            for (int i = 0; i < 6; i++)
            {
                if (avNeigbor[i].distFrom < minDistToNeighbor)
                {
                    minDistToNeighbor = avNeigbor[i].distFrom;
                }
            }
            return minDistToNeighbor;
        }

        public bool isAVinNeighborList(ArgVert aV)
        {
            for (int i = 0; i < 6; i++)
            {
                if (avNeigbor[i].aV == aV) return true;
            }
            return false;
        }

        public int isLessThan(float dist)
        {
            for (int i = 0; i < 6; i++)
            {
                if (dist < avNeigbor[i].distFrom) return i;
            }
            return -1;
        }

        public bool goesHere(int i, float dist, ArgVert aV_Contender)
        {
            if (dist < avNeigbor[i].distFrom)
            {
                NB nb = new NB();
                nb.aV = aV_Contender;
                nb.distFrom = dist;
                nb.aV.idx = aV_Contender.idx;
                avNeigbor.Insert(i, nb);
                avNeigbor.RemoveAt(6);
                return true;
            }
            return false;
        }


        public void insNeighbor_One_Shot(float dist, ArgVert aV_Contender)
        {
            for (int i = 0; i < 6; i++)
            {
                if (goesHere(i, dist, aV_Contender)) return;
            }
        }
    }

    public class ArgosQuad //tracks VB list divided by 6
    {
        public float lifeTime = 0f;
        public float currTime = 0f;
        public float fadeStart = 0f;
        //public Vector3[] vertices = new Vector3[4];
        //public int[] triangles = new int[6];
        //public Vector3[] normals = new Vector3[4];
        //public Vector2[] uv = new Vector2[4];
        //public Color[] colors = new Color[4];
        public MeshDraft mdQuad;
        public bool bActive = false;

        public ArgosQuad()
        {
            mdQuad = new MeshDraft();
        }

        public void initWithStamp(MeshDraft md)
        {
            mdQuad.Add(md);

            //for(int i = 0; i<4; i++)
            //{
            //    mdQuad.uv[i] = md.uv[i];
            //    mdQuad.colors[i] = md.colors[i];
            //    mdQuad.triangles[i] = md.triangles[i];
            //}
            //mdQuad.triangles[4] = md.triangles[4];
            //mdQuad.triangles[5] = md.triangles[5];
        }
    }

    public class ArgosMeshDraft : MeshDraft
    {
        public List<Vector3> vTriCenter = new List<Vector3>();
        public List<int> vQual = new List<int>();
        public List<ArgVert> icoPointCloud = new List<ArgVert>();
        public List<ArgosQuad> quadLst = new List<ArgosQuad>();

        public ArgosQuad[] quadPool;
        public int qpCurrIdx = 0;
        public int qpLen = 0;

        MeshDraft mdScatch = new MeshDraft();
        Mesh meshInternal = new Mesh();

        public ArgosMeshDraft() : base()
        {

        }

        public void initQuadPool(int poolCount)
        {
            quadPool = new ArgosQuad[poolCount];
            Color col = new Color(1, 1, 1, 1);
            mdScatch.Add(Quad(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.zero, col));

            for (int i = 0; i < poolCount; i++)
            {
                quadPool[i] = new ArgosQuad();
                quadPool[i].initWithStamp(mdScatch);
            }
            qpCurrIdx = 0;
            qpLen = poolCount;
        }

        public ArgosQuad getPoolQuad()
        {
            bool scan = true;
            int idx = qpCurrIdx;
            int cnt = 0;

            while (scan)
            {
                cnt++;
                if (quadPool[idx].bActive == false)
                {
                    quadPool[idx].bActive = true;
                    qpCurrIdx = idx;
                    return quadPool[idx];
                }
                if (++idx > qpLen - 1)
                {
                    idx = 0;
                }
                if (cnt > qpLen)
                {
                    scan = false;
                }
            }
            return null;
        }

        public Mesh ToMeshInternal()
        {
            meshInternal.Clear();
            meshInternal.vertices = vertices.ToArray();
            meshInternal.triangles = triangles.ToArray();
            meshInternal.normals = normals.ToArray();
            meshInternal.uv = uv.ToArray();
            meshInternal.colors = colors.ToArray();
            return meshInternal;
        }

        public Mesh ToMeshInternal_From_QuadPool()
        {
            Clear();
            int poolCount = quadPool.Length;
            int v = 0;//verts
            int t = 0;//tris

            for (int i = 0; i < poolCount; i++)
            {
                if (quadPool[i].bActive)
                {
                    Add(quadPool[i].mdQuad);
                }
            }
            return ToMeshInternal();
        }

        public void Quad_Paint(Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, Color col, float lifetime, float fade_start)
        {
            ArgosQuad aq = getPoolQuad();
            if (aq == null) return;//no non active quads

            aq.lifeTime = lifetime;
            aq.fadeStart = fade_start;
            aq.currTime = 0f;
            //quadLst.Add(aq);

            Init_Quad_fromPool(aq, v0, v1, v2, v3, col);
        }

        public void Init_Quad_fromPool(ArgosQuad aq, Vector3 vertex0, Vector3 vertex1, Vector3 vertex2, Vector3 vertex3, Color col)//TODO REOWKK QUADPOOL ONLY
        {
            Vector3 normal = Vector3.Cross((vertex1 - vertex0), (vertex2 - vertex0)).normalized;

            aq.mdQuad.vertices[0] = vertex0;
            aq.mdQuad.vertices[1] = vertex1;
            aq.mdQuad.vertices[2] = vertex2;
            aq.mdQuad.vertices[3] = vertex3;

            aq.mdQuad.normals[0] = normal;
            aq.mdQuad.normals[1] = normal;
            aq.mdQuad.normals[2] = normal;
            aq.mdQuad.normals[3] = normal;
        }

        public void QuadPoolUpdate()//Change to Pool
        {
            int poolCount = quadPool.Length;
            for (int i = 0; i < poolCount; i++)
            {
                quadPool[i].currTime += Time.deltaTime;

                float fadeStart = quadPool[i].fadeStart * quadPool[i].lifeTime;

                if (quadPool[i].currTime > fadeStart && quadPool[i].bActive)
                {
                    float attenAlpha = 1.0f - (quadPool[i].currTime - fadeStart) / (quadPool[i].lifeTime - fadeStart);
                    Color col;

                    for (int j = 0; j < 4; j++)
                    {
                        col = quadPool[i].mdQuad.colors[j];
                        col.a = attenAlpha;
                        quadPool[i].mdQuad.colors[j] = col;
                    }
                }

                if (quadPool[i].currTime > quadPool[i].lifeTime)
                {
                    quadPool[i].bActive = false;
                    //RemoveQuad(i);
                }
            }
        }

        //public void RemoveQuad(int i)
        //{
        //    quadLst.RemoveAt(i);
        //    vertices.RemoveRange(i * 4, 4);
        //    uv.RemoveRange(i * 4, 4);
        //    colors.RemoveRange(i * 4, 4);
        //    normals.RemoveRange(i * 4, 4);
        //    triangles.RemoveRange(i * 6, 6);

        //    for (int j = i * 6; j < triangles.Count; j++)
        //    {
        //        triangles[j] -= 4;
        //    }
        //}

        public void Add_ITN_Node(MeshDraft tri, int sector, int nodePath, int ITG_idx)
        {
            Vector3 vC = (tri.vertices[0] + tri.vertices[1] + tri.vertices[2]) / 3f;

            ArgVert aV = new ArgVert();

            aV.vPos = vC;
            aV.itn.sector = sector;
            aV.itn.nodePath = nodePath;
            aV.idx = ITG_idx;

            icoPointCloud.Add(aV);
        }

        public void SetSortDist(Vector3 Apex)
        {
            foreach (ArgVert aV in icoPointCloud)
            {
                aV.dist = (aV.vPos - Apex).magnitude;
            }
        }

        public void sortPointCloud()
        {
            icoPointCloud.Sort((x, y) => x.dist.CompareTo(y.dist));
        }

        public void AddTriQual(MeshDraft tri)
        {
            Vector3 vC = (tri.vertices[0] + tri.vertices[1] + tri.vertices[2]) / 3f;
            int qual = TriQual.UNDEFINED;

            Add(tri);

            for (int i = 0; i < 3; i++)//Track the quality of the triangle UVs
            {
                vTriCenter.Add(vC);
                vQual.Add(qual);
            }
        }

        public void AddHex(MeshDraft tri)//from triangle see: http://argos.vu/wp-content/uploads/2016/04/HCs-1.png
        {
            Vector3 HC = (tri.vertices[0] + tri.vertices[1] + tri.vertices[2]) / 3f;

            Vector3 H0 = (tri.vertices[1] + tri.vertices[0]) / 2f;

            Vector3 H1 = (tri.vertices[1] + HC) / 2f;

            Vector3 H2 = (tri.vertices[2] + tri.vertices[1]) / 2f;

            Vector3 H3 = (tri.vertices[2] + HC) / 2f;

            Vector3 H4 = (tri.vertices[0] + tri.vertices[2]) / 2f;

            Vector3 H5 = (tri.vertices[0] + HC) / 2f;

            List<Vector3> vL = new List<Vector3>(8) { HC, H0, H1, H2, H3, H4, H5, H0 };

            List<Vector2> uvMapL = new List<Vector2>(8) { new Vector2(0.5f, 0.5f), new Vector2(0.5f, 1f), new Vector2(1, 0.75f), new Vector2(1, 0.25f), new Vector2(0.5f, 0), new Vector2(0, 0.25f), new Vector2(0, 0.75f), new Vector2(0.5f, 1f) };

            Add(HexFan(vL, uvMapL));
        }

        public void AddTriQual(MeshDraft tri, int qual)
        {
            Vector3 vC = (tri.vertices[0] + tri.vertices[1] + tri.vertices[2]) / 3f;

            Add(tri);

            for (int i = 0; i < 3; i++)//Track the quality of the triangle UVs
            {
                vTriCenter.Add(vC);
                vQual.Add(qual);
            }
        }
    }
}

gsngdn