One of the effects I wanted for Cipher Break was some sort of tracer effect to show when the player had successfully ‘hit’ an enemy by typing the right character.
I could just draw a straight line from the player to the enemy, like a tracer round in the real world, but that wouldn’t be very interesting, and it doesn’t really play into the whole “you’re in a computer” aesthetic. I thought it’d be much more interesting to use Bezier Curves to draw the tracer fire.
Bezier curves are mathematically interpolated lines, they have a start and end point, and then x number of control points in between that decide the shape of the line.
I tend to think about them like a tourist who’s forgotten to do any sightseeing until the last minute and they need to get to the airport on time. They start at the hotel and have to see The Houses of Parliament and the Tower of London and still make their flight… best they can do is drive as close as possible, while still not taking themselves too far from the next sightseeing location. Ok, that’s a terrible analogy.
Not being a mathematician, I didn’t understand what the Wikipedia page was talking about, so I started looking around for code. Fortunately it’s a problem that has been solved a load of times before, so sources weren’t too hard to find, but everything I found was incomplete, or in the wrong language or incompatible in some way. So I set about making my own class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using UnityEngine; [System.Serializable] public class Bezier : System.Object { //vars to store our control points public Vector3 p0; public Vector3 p1; public Vector3 p2; public Vector3 p3; // Init function v0 = 1st point, v1 = handle of the 1st point , v2 = handle of the 2nd point, v3 = 2nd point public Bezier( Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3 ) { this.p0 = v0; this.p1 = v1; this.p2 = v2; this.p3 = v3; } // 0.0 >= t <= 1.0 In here be dragons and magic public Vector3 GetPointAtTime( float t ) { float u = 1f - t; float tt = t * t; float uu = u * u; float uuu = uu * u; float ttt = tt * t; Vector3 p = uuu * p0; //first term p += 3 * uu * t * p1; //second term p += 3 * u * tt * p2; //third term p += ttt * p3; //fourth term return p; } } |
As you can see from one of the comments, I cobbled together some of this code. I think this is my source for the GetPointAtTime() function.
Now to plot a curve, you just decide how smooth you want it, 10 points would be very rough, 100 would be very smooth.
The problem with beziers is that they doesn’t plot these points evenly along the curve. You can see this on the curve set to 10 above. Depending on the distance between each of the 4 control vector3s, the points plotted by GetPointAtTime() will be close together or spaced apart. This isn’t a problem if we’re just drawing lines, but I want to construct a 3D mesh and map a texture to it. If the spaces between each point is uneven, then the texture will be distorted.
Evenly spacing points along a bezier
The solution is to find the length of the curve and then divide it into equal segments. However, because the bezier function just returns a list of points, we have to calculate all those points and find the distance between each of them and then add them up. First add a couple of public variable to the Bezier class;
1 2 3 |
public float length=0; public Vector3[] points; |
Then we add a function to the Bezier class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
//where _num is the desired output of points and _precision is how good we want matching to be public void CalculatePoints(int _num, int _precision=100) { if(_num>_precision) Debug.LogError("_num must be less than _precision"); //calculate the length using _precision to give a rough estimate, save lengths in array length=0; //store the lengths between PointsAtTime in an array float[] arcLengths = new float[_precision]; Vector3 oldPoint = GetPointAtTime(0); for(int p=1;p<arcLengths.Length;p++) { Vector3 newPoint = GetPointAtTime((float) p/_precision); //get next point arcLengths[p] = Vector3.Distance(oldPoint,newPoint); //find distance to old point length += arcLengths[p]; //add it to the bezier's length oldPoint = newPoint; //new is old for next loop } //create our points array points = new Vector3[_num]; //target length for spacing float segmentLength = length/_num; //arc index is where we got up to in the array to avoid the Shlemiel error http://www.joelonsoftware.com/articles/fog0000000319.html int arcIndex = 0; float walkLength=0; //how far along the path we've walked oldPoint = GetPointAtTime(0); //iterate through points and set them for(int i=0;i<points.Length;i++) { float iSegLength = i * segmentLength; //what the total length of the walkLength must equal to be valid //run through the arcLengths until past it while(walkLength<iSegLength) { walkLength+=arcLengths[arcIndex]; //add the next arcLength to the walk arcIndex++; //go to next arcLength } //walkLength has exceeded target, so lets find where between 0 and 1 it is points[i] = GetPointAtTime((float)arcIndex/arcLengths.Length); } } |
This function runs through the bezier, generating _precision points, saves the lengths between each of those points and saves the total. Then, for each _num it counts up the distances in sequence, when it finds the arc length that’s a good match, it saves out a point into an array.
Now our points are evenly spaced out, we can use them to build a mesh that won’t be textured unevenly.
Here’s my complete Bezier class;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
using UnityEngine; [System.Serializable] public class Bezier : System.Object { public Vector3 p0; public Vector3 p1; public Vector3 p2; public Vector3 p3; public float length=0; public Vector3[] points; // Init function v0 = 1st point, v1 = handle of the 1st point , v2 = handle of the 2nd point, v3 = 2nd point // handle1 = v0 + v1 // handle2 = v3 + v2 public Bezier( Vector3 v0, Vector3 v1, Vector3 v2, Vector3 v3, int _calculatePoints=0 ) { this.p0 = v0; this.p1 = v1; this.p2 = v2; this.p3 = v3; if(_calculatePoints>0) CalculatePoints(_calculatePoints); } // 0.0 >= t <= 1.0 her be magic and dragons public Vector3 GetPointAtTime( float t ) { float u = 1f - t; float tt = t * t; float uu = u * u; float uuu = uu * u; float ttt = tt * t; Vector3 p = uuu * p0; //first term p += 3 * uu * t * p1; //second term p += 3 * u * tt * p2; //third term p += ttt * p3; //fourth term return p; } //where _num is the desired output of points and _precision is how good we want matching to be public void CalculatePoints(int _num, int _precision=100) { if(_num>_precision) Debug.LogError("_num must be less than _precision"); //calculate the length using _precision to give a rough estimate, save lengths in array length=0; //store the lengths between PointsAtTime in an array float[] arcLengths = new float[_precision]; Vector3 oldPoint = GetPointAtTime(0); for(int p=1;p<arcLengths.Length;p++) { Vector3 newPoint = GetPointAtTime((float) p/_precision); //get next point arcLengths[p] = Vector3.Distance(oldPoint,newPoint); //find distance to old point length += arcLengths[p]; //add it to the bezier's length oldPoint = newPoint; //new is old for next loop } //create our points array points = new Vector3[_num]; //target length for spacing float segmentLength = length/_num; //arc index is where we got up to in the array to avoid the Shlemiel error http://www.joelonsoftware.com/articles/fog0000000319.html int arcIndex = 0; float walkLength=0; //how far along the path we've walked oldPoint = GetPointAtTime(0); //iterate through points and set them for(int i=0;i<points.Length;i++) { float iSegLength = i * segmentLength; //what the total length of the walkLength must equal to be valid //run through the arcLengths until past it while(walkLength<iSegLength) { walkLength+=arcLengths[arcIndex]; //add the next arcLength to the walk arcIndex++; //go to next arcLength } //walkLength has exceeded target, so lets find where between 0 and 1 it is points[i] = GetPointAtTime((float)arcIndex/arcLengths.Length); } } } |
In the next part I’ll go into creating a mesh based on these points.