Reputation: 141
I am trying to implement the formula for bezier curves of Nth order in my program. It looks to me that I have done everything right but the visual result is not correct.
Here it is:
The red cube is P0 and the blue is P8. The white cubes are the actual set of points that make the curve. The orange cubes are the control points.
What I see is that there is a loop before the end of the curve where the curve attaches to the last (blue cube) point. Looks like there is an invisible point. And another thing is that between P0 and P1 is also something weird going on...
Can anyone help me to resolve it?
Here is the code I use:
private void Update()
{
controlPointsCoords = ControlPoints.Select(p => p.transform.position).ToArray();
for (int p = 0; p < PointsSet.Count; p++)
{
PointsSet[p].transform.position = CurveDegreeN
(
controlPointsCoords,
Rt(p, PointsSet.Count)
);
}
}
private Vector3 CurveDegreeN(Vector3[] pointsCoords, float u)
{
float X = 0, Y = 0, Z = 0;
float n = pointsCoords.Length - 1;
for (int i = 0; i < pointsCoords.Length; i++)
{
var coef = (Factorial(n) / (Factorial((float)i) * Factorial(n - i))) * Mathf.Pow(u, i) * Mathf.Pow(1 - u, n - i);
X += coef * pointsCoords[i].x;
Y += coef * pointsCoords[i].y;
Z += coef * pointsCoords[i].z;
}
return new Vector3(X, Y, Z);
}
private float Factorial(float n)
{
if (n == 0) return 1;
float res = 0.0f;
for (int i = 1; i < n; i++) res += (float)Math.Log(i);
return (float)Math.Exp(res);
}
private float Rt(int current, int count)
{
return ((float)current - 0) / ((float)count - 0) * (1 - 0) + 0;
}
I hope this will be clear for someone! Thank you in advance!
UPDATE: I reduced amount of points to 3. Here is the result: 3 Points curve. It is clearly visible here that something is wrong with the computations... Any more suggestions?
Upvotes: 2
Views: 2215
Reputation: 53735
Start by simplifying that code, because this is going to be unreliable to debug. Step one: let's not use calculus unless there is an actual benefit to doing so. Using the full binomial calculation and powers-of-t is typically just as fast (or slow) as interpolation (Bezier curves are trivially expressed as list reductions), but interpolation is dead-easily implemented with simple addition and multiplication, while binomial computation and powers are more work. So let's evaluate geometrically instead of using calculus:
function drawCurve(coords[]):
points = []
// the higher you make "steps", the more curve points you generate:
for (s=0, steps=10; s<=steps; s++):
t = s/steps
nt = 1 - t
list[] = coords.shallowCopy()
// We now run our list reduction to get our on-curve
// point at t, using de Casteljau's algorithm:
while(list.length > 1)
for(i = 0, e = list.length; i < e; i++):
list[i] = nt * list[i] + t * list[i+1]
list.pop()
// And what's left is our on-curve point at t.
// Beauty; push and move on to the next point.
points.push(list[0])
return points
Done. By ruling out binomials and powers, and implementing curve evaluation purely based on the iterative interpolation (i.e. using de Casteljau's algorithm) there is literally nothing that can be "done wrong" in this code: a great quality for code to have!
You can make this code even more efficient by being explicit about your coordinates, using array[3] instead of 3d vector classes so that you don't have to rely on operator overloading, or function call slowldowns, during the interpolation steps, so you get something like:
function drawCurve(coords[]):
coords = flatten(coords) // one-time convert Vector3 to flat [x,y,z] arrays
...
while(list.length > 1)
for(i = 0, e = list.length; i < e; i++):
v1 = list[i]
v2 = list[i+1]
list[i][0] = nt * v1[0] + t * v2[0] // x
list[i][1] = nt * v1[1] + t * v2[1] // y
list[i][2] = nt * v1[2] + t * v2[2] // z
list.pop()
points.push(new Vector3(list[0]))
return points
(and a final optimization, though typically not worth it, is to unroll the while
as well, to effect a single for
loop based on the initial L=list.length
and counter i
, where L
is decremented by one and i
resets to 0 when i==L
, and which terminates when L==1
)
And if you absolutely need calculus (which is honestly not the case here) at the very least generate your binomial coefficients "efficiently": they are super simple to generate based on Pascal's triangle so for the love of your math coprocessor do not use factorials to evaluate them, they can literally be generated by just adding up some integers:
lut = [ [1], // n=0
[1,1], // n=1
[1,2,1], // n=2
[1,3,3,1], // n=3
[1,4,6,4,1], // n=4
[1,5,10,10,5,1], // n=5
[1,6,15,20,15,6,1]] // n=6
binomial(n,k):
while(n >= lut.length):
s = lut.length
nextRow = []
nextRow[0] = 1
for(i=1, prev=s-1; i<prev; i++):
nextRow[i] = lut[prev][i-1] + lut[prev][i]
nextRow[s] = 1
lut.push(nextRow)
return lut[n][k]
(If you do this, either make sure you remember that you're programming and array offsets start at 0, or add dummy values at row/column positions [0] so that you can "intuitively" call binomial(4,2)
to get 4 choose 2 rather than 5 choose 3)
Upvotes: 9
Reputation: 26040
Apart from the fact that this is a most inefficient computation of the binomials, that you should pre-compute the binomial sequence and not recompute them for every point you plot, and that you could avoid most of the calls to the power function by employing a Horner like method, ....
your code seems to be correct and also the visual is consistent with the control points forcing the middle part into a straight line. High order polynomial interpolation can have some complicated wiggles at segments where the sample or control points have (relatively) large variations in value.
Update: I did not at first look too closely at the helper functions as I would not compute binomials using separate computation of factorials, but your factorial function does not contain the factor n
for the call with argument n
, i.e., it computes (n-1)!
.
Upvotes: 0