Setting the needle of a gauge in code is easy when the scale is linear but it gets surprisingly complicated when the scale is not linear.

There are a few ways to deal with this problem. The easiest is to simply map different linear ranges to different segments of the gauge. However, this creates a change in needle speed when crossing the boundary. A better way is to create a logarithmic function which best fits the scale. But this can be difficult to maintain and it can be hard to make the needle follow the scale exactly, especially when the scale is not logarithmic to begin with.

The best way to deal with this problem is to make the needle follow a spline. The needle angle vs scale value will be stored in an array which are treated as control points for a Catmull-Rom spline.

The function of a Catmull-Rom spline is defined as:

0.5 * (2*P1 + (-P0 + P2) * t + (2*P0 – 5*P1 + 4*P2 – P3) * t^2 + (-P0 + 3*P1 – 3*P2 + P3) * t^3)

Variables P0 to P3 are the control points. Variable t is the position on the spline, with a range of 0 to 1. This only creates a spline with one section and 4 control points. To create a spline with more control points, the spline segments have to be stitched together.

The points P0 to P3 are vectors where in the case of the gauge, x is the needle angle, and y is the scale value at that angle.

A Catmull-Rom spline with multiple control points placed in zig-zag shape Note that the first and last control point is not shown here:

A Catmull-Rom spline with 6 control points placed in curved shape. Note that the spline does not exist at the first and last segment:

It is also possible to make the spline into a closed loop. For that, the first and last two control points have to be overlapping

Using a spline like this will make the needle follow the sampled points (scale values) exactly using smooth interpolation in between. To get an intermediate position on the spline, a value between 0 and 1 (t) has to be supplied to the spline function. The problem is that t is not known because the needle angle (x) has to be found for a certain scale number (y).

There are two ways to find t. One way is by using a brute force method of calculating many points on the spline and then finding the closest one to the number we are looking for. This works but is not exactly elegant, not to mention the performance and memory overhead involved. A better way is to find t mathematically. This is quite complicated but luckily it has been done before:

http://lifeinacubicleblog.com/2016/10/17/finding-catmull-rom-spline-and-line-intersection-part-2-mathematical-approach/

The blog post explains how to substitute the variables from a standard linear equation with parts of the spline formula. This allows you to solve a Cubic equation which gives you the intersection points of a straight line and a spline. Solving a Cubic equation is not exactly easy either, but luckily it has been implemented in code here:

https://www.cs.rit.edu/~ark/pj/lib/edu/rit/numeric/Cubic.shtml

https://www.codeproject.com/Articles/798474/To-Solve-a-Cubic-Equation

The code below includes the Catmull-Rom spline, create a Cubic function from a line spline intersection and solve it. It supports multiple spline segments.

//Get a point on a Catmull-Rom spline. //The percentage is in range 0 to 1, which starts at the second control point and ends at the second last control point. //The array cPoints should contain all control points. The minimum amount of control points should be 4. //Source: https://forum.unity.com/threads/waypoints-and-constant-variable-speed-problems.32954/#post-213942 public static Vector2 GetPointOnSpline(float percentage, Vector2[] cPoints) { //Minimum size is 4 if (cPoints.Length >= 4) { //Convert the input range (0 to 1) to range (0 to numSections) int numSections = cPoints.Length - 3; int curPoint = Mathf.Min(Mathf.FloorToInt(percentage * (float)numSections), numSections - 1); float t = percentage * (float)numSections - (float)curPoint; //Get the 4 control points around the location to be sampled. Vector2 p0 = cPoints[curPoint]; Vector2 p1 = cPoints[curPoint + 1]; Vector2 p2 = cPoints[curPoint + 2]; Vector2 p3 = cPoints[curPoint + 3]; //The Catmull-Rom spline can be written as: // 0.5 * (2*P1 + (-P0 + P2) * t + (2*P0 - 5*P1 + 4*P2 - P3) * t^2 + (-P0 + 3*P1 - 3*P2 + P3) * t^3) //Variables P0 to P3 are the control points. //Variable t is the position on the spline, with a range of 0 to numSections. //C# way of writing the function. Note that f means float (to force precision). Vector2 result = .5f * (2f * p1 + (-p0 + p2) * t + (2f * p0 - 5f * p1 + 4f * p2 - p3) * (t * t) + (-p0 + 3f * p1 - 3f * p2 + p3) * (t * t * t)); return new Vector2(result.x, result.y); } else { return new Vector2(0, 0); } } //Finds the intersection points between a straight line and a spline. Solves a Cubic polynomial equation //The output is in the form of a percentage along the length of the spline (range 0 to 1). //The linePoints array should contain two points which form a straight line. //The cPoints array should contain all the control points of the spline. //Use case: create a gauge with a non-linear scale by defining an array with needle angles vs the number it should point at. The array creates a spline. //Driving the needle with a float in range 0 to 1 gives an unpredictable result. Instead, use the GetLineSplineIntersections() function to find the angle the //gauge needle should have for a given number it should point at. In this case, cPoints should contain x for angle and y for scale number. //Make a horizontal line at the given scale number (y) you want to find the needle angle for. The returned float is a percentage location on the spline (range 0 to 1). //Plug this value into the GetPointOnSpline() function to get the x coordinate which represents the needle angle. //Source: http://lifeinacubicleblog.com/2016/10/17/finding-catmull-rom-spline-and-line-intersection-part-2-mathematical-approach/ public static float[] GetLineSplineIntersections(Vector2[] linePoints, Vector2[] cPoints) { List<float> list = new List<float>(); float[] crossings; int numSections = cPoints.Length - 3; //The line spline intersection can only be calculated for one segment of a spline, meaning 4 control points, //with a spline segment between the middle two control points. So check all spline segments. for (int i = 0; i < numSections; i++) { //Get the 4 control points around the location to be sampled. Vector2 p0 = cPoints[i]; Vector2 p1 = cPoints[i + 1]; Vector2 p2 = cPoints[i + 2]; Vector2 p3 = cPoints[i + 3]; //The Catmull-Rom spline can be written as: // 0.5 * (2P1 + (-P0 + P2) * t + (2P0 - 5P1 + 4P2 - P3) * t^2 + (-P0 + 3P1 - 3P2 + P3) * t^3) //Variables P0 to P3 are the control points. //Notation: 2P1 means 2*controlPoint1 //Variable t is the position on the spline, converted from a range of 0 to 1. //C# way of writing the function is below. Note that f means float (to force precision). //Vector2 result = .5f * (2f * p1 + (-p0 + p2) * t + (2f * p0 - 5f * p1 + 4f * p2 - p3) * (t * t) + (-p0 + 3f * p1 - 3f * p2 + p3) * (t * t * t)); //The variable t is the only unknown, so the rest can be substituted: //a = 0.5 * (-p0 + 3*p1 - 3*p2 + p3) //b = 0.5 * (2*p0 - 5*p1 + 4*p2 - p3) //c = 0.5 * (-p0 + p2) //d = 0.5 * (2*p1) //This gives rise to the following Cubic equation: //a * t^3 + b * t^2 + c * t + d = 0 //The spline control points (p0-3) consist of two variables: the x and y coordinates. They are independent so we can handle them separately. //Below, a1 is substitution a where the x coordinate of each point is used, like so: a1 = 0.5 * (-p0.x + 3*p1.x - 3*p2.x + p3.x) //Below, a2 is substitution a where the y coordinate of each point is used, like so: a2 = 0.5 * (-p0.y + 3*p1.y - 3*p2.y + p3.y) //The same logic applies for substitutions b, c, and d. float a1 = 0.5f * (-p0.x + 3f * p1.x - 3f * p2.x + p3.x); float a2 = 0.5f * (-p0.y + 3f * p1.y - 3f * p2.y + p3.y); float b1 = 0.5f * (2f * p0.x - 5f * p1.x + 4f * p2.x - p3.x); float b2 = 0.5f * (2f * p0.y - 5f * p1.y + 4f * p2.y - p3.y); float c1 = 0.5f * (-p0.x + p2.x); float c2 = 0.5f * (-p0.y + p2.y); float d1 = 0.5f * (2f * p1.x); float d2 = 0.5f * (2f * p1.y); //We now have two Cubic functions. One for x and one for y. //Note that a, b, c, and d are not vector variables itself but substituted functions. //x = a1 * t^3 + b1 * t^2 + c1 * t + d1 //y = a2 * t^3 + b2 * t^2 + c2 * t + d2 //Line formula, standard form: //Ax + By + C = 0 float A = linePoints[0].y - linePoints[1].y; float B = linePoints[1].x - linePoints[0].x; float C = (linePoints[0].x - linePoints[1].x) * linePoints[0].y + (linePoints[1].y - linePoints[0].y) * linePoints[0].x; //Substituting the values of x and y from the separated Spline formula into the Line formula, we get: //A * (a1 * t^3 + b1 * t^2 + c1 * t + d1) + B * (a2 * t^3 + b2 * t^2 + c2 * t + d2) + C = 0 //Rearranged version: //(A * a1 + B * a2) * t^3 + (A * b1 + B * b2) * t^2 + (A * c1 + B * c2) * t + (A * d1 + B * d2 + C) = 0 //Substituting gives rise to a Cubic function: //a * t^3 + b * t^2 + c * t + d = 0 float a = A * a1 + B * a2; float b = A * b1 + B * b2; float c = A * c1 + B * c2; float d = A * d1 + B * d2 + C; //This is again a Cubic equation, combined from the Line and the Spline equation. If you solve this you can get up to 3 line-spline cross points. //How to solve a Cubic equation is described here: //https://www.cs.rit.edu/~ark/pj/lib/edu/rit/numeric/Cubic.shtml //https://www.codeproject.com/Articles/798474/To-Solve-a-Cubic-Equation int crossAmount; float cross1; float cross2; float cross3; float crossCorrected; //Two different implementations of solving a Cubic equation. // SolveCubic2(out crossAmount, out cross1, out cross2, out cross3, a, b, c, d); SolveCubic(out crossAmount, out cross1, out cross2, out cross3, a, b, c, d); //Get the highest and lowest value (in range 0 to 1) of the current section and calculate the difference. float currentSectionLowest = (float)i / (float)numSections; float currentSectionHighest = ((float)i + 1f) / (float)numSections; float diff = currentSectionHighest - currentSectionLowest; //Only use the result if it is within range 0 to 1. //The range 0 to 1 is within the current segment. It has to be converted to the range of the entire spline, //which still uses a range of 0 to 1. if (cross1 >= 0 && cross1 <= 1) { //Map an intermediate range (0 to 1) to the lowest and highest section values. crossCorrected = (cross1 * diff) + currentSectionLowest; //Add the result to the list. list.Add(crossCorrected); } if (cross2 >= 0 && cross2 <= 1) { //Map an intermediate range (0 to 1) to the lowest and highest section values. crossCorrected = (cross2 * diff) + currentSectionLowest; //Add the result to the list. list.Add(crossCorrected); } if (cross3 >= 0 && cross3 <= 1) { //Map an intermediate range (0 to 1) to the lowest and highest section values. crossCorrected = (cross3 * diff) + currentSectionLowest; //Add the result to the list. list.Add(crossCorrected); } } //Convert the list to an array. crossings = list.ToArray(); return crossings; } //Solve cubic equation according to Cardano. //Source: https://www.cs.rit.edu/~ark/pj/lib/edu/rit/numeric/Cubic.shtml private static void SolveCubic(out int nRoots, out float x1, out float x2, out float x3, float a, float b, float c, float d) { float TWO_PI = 2f * Mathf.PI; float FOUR_PI = 4f * Mathf.PI; // Normalize coefficients. float denom = a; a = b / denom; b = c / denom; c = d / denom; // Commence solution. float a_over_3 = a / 3f; float Q = (3f * b - a * a) / 9f; float Q_CUBE = Q * Q * Q; float R = (9f * a * b - 27f * c - 2f * a * a * a) / 54f; float R_SQR = R * R; float D = Q_CUBE + R_SQR; if (D < 0.0f) { // Three unequal real roots. nRoots = 3; float theta = Mathf.Acos(R / Mathf.Sqrt(-Q_CUBE)); float SQRT_Q = Mathf.Sqrt(-Q); x1 = 2f * SQRT_Q * Mathf.Cos(theta / 3f) - a_over_3; x2 = 2f * SQRT_Q * Mathf.Cos((theta + TWO_PI) / 3f) - a_over_3; x3 = 2f * SQRT_Q * Mathf.Cos((theta + FOUR_PI) / 3f) - a_over_3; } else if (D > 0.0f) { // One real root. nRoots = 1; float SQRT_D = Mathf.Sqrt(D); float S = CubeRoot(R + SQRT_D); float T = CubeRoot(R - SQRT_D); x1 = (S + T) - a_over_3; x2 = float.NaN; x3 = float.NaN; } else { // Three real roots, at least two equal. nRoots = 3; float CBRT_R = CubeRoot(R); x1 = 2 * CBRT_R - a_over_3; x2 = CBRT_R - a_over_3; x3 = x2; } } //Mathf.Pow is used as an alternative for cube root (Math.cbrt) here. private static float CubeRoot(float d) { if (d < 0.0f) { return -Mathf.Pow(-d, 1f / 3f); } else { return Mathf.Pow(d, 1f / 3f); } }

In case of the gauge, we need to make a horizontal line (y) at the location of the scale value we want to find the needle angle for. This will give us the intersection (t). This is not a coordinate yet, but if you simply plug this value (t) in the spline function, it will give a point with values x (needle angle, yay!), and y (scale value). The scale value was already known but it can be used to check the result.

Here is an implementation in Unity which calculates the intersection between a line and a spline:

The Unity project can be found here:

https://drive.google.com/file/d/0Bwk4bDWv3jAceUl0cm5ENHFFQ2M/view?usp=sharing

Note that the Unity project contains both implementations of solving a Cubic function which was used to verify the result.

The red cubes are the control points of the spline. The yellow cubes create a straight line. The magenta cubes are the intersection points between the line and the spline. The green cube can be moved along the spline by moving the slider. To use, press Run, then move the cubes in the Scene window.

Another closely related application is to make a gauge follow a non-linear animation, for example the EGT of a jet engine during startup. A video of a the event would be recorded and used to capture sample points consisting of EGT vs time. The time (x) and EGT (y) values would then be used to create a spline, allowing smooth interpolation between the original sample points. The line-spline intersection function can then be used to get the EGT for any point in time.

So there you have it. A real world use case of finding the intersection points between a line and a spline by solving a Cubic equation. Learning mathematics was not a waste of time after all đŸ˜‰

I added the spline and spline solve functions in the Math3D Unity wiki too:

http://wiki.unity3d.com/index.php/3d_Math_functions

The functions are called GetPointOnSpline() and GetLineSplineIntersections()