I'm writing a BezierPath class that contains a list of BezierPoints. Each BezierPoint has a position, an inTangent and an outTangent:
BezierPath contains functions for getting linear positions and tangents from the path. My next step is to provide functionality for getting the normals from the path.
I'm aware that any given line in 3D is going to have an unlimited number of lines that are perpendicular, so there isn't going to be a set answer.
My aim is for the user to be able to specify normals (or a roll angle?) at each BezierPoint which I will interpolate between to get normals along the path. My problem is that I don't know how to choose a starting tangent (what should the default tangent be?).
My first attempt at getting the starting tangents is using the Unity3D method Quaternion.LookRotation:
Quaternion lookAt = Quaternion.LookRotation(tangent);
Vector3 normal = lookAt * Vector3.up;
Handles.DrawLine(position, position + normal * 10.0f);
This results in the following (green lines are tangents, blue are normals):
For the most part this seems like a good base result, but it looks like there are sudden changes in certain orientations:
So my question is: Is there a good way for getting consistent default normals for lines in 3D?
Thanks, Ves
Getting the normal for a point on a Bezier curve is actually pretty straight forward, as normals are simply perpendicular to a function's tangent (oriented in the plane of the direction of travel for the curve), and the tangent function of a Bezier curve is actually just another Bezier curve, 1 order lower. Let's find the normal for a cubic Bezier curve. The regular function, with (a,b,c,d) being the curve coordinates in a single dimension:
Note that Bezier curves are symmetrical, the only difference between
t
vs1-t
is which end of the curve represents "the start". Usinga * (1-t)³
means the curve starts ata
. Usinga * ³t
would make it start atd
instead.So let's define a quick curve with the following coordinates:
In order to get the normal for this function, we first need the derivative:
Done. Computing the derivative is stupidly simple (a fantastic property of Bezier curves).
Now, in order to get the normal, we need to take the normalised tangent vector at some value
t
, and rotate it by a quarter turn. We can turn it in quite a few directions, so a further restriction is that we want to turn it only in the plane that is defined by the tangent vector, and the tangent vector "right next to it", an infinitesimally small interval apart.The tangent vector for any Bezier curve is formed simply by taking however-many-dimensions you have, and evaluating them separately, so for a 3D curve:
Again, quite simple to compute. To normalise this vector (or in fact any vector), we simply perform a vector division by its length:
So let's draw those in green:
The only trick is now to find the plane in which to rotate the tangent vector, to turn the tangent into a normal. We know we can use another t value arbitrarily close to the one we want, and turn that into a second tangent vector damn near on the same point, for finding the plane with arbitrary correctness, so we can do that:
Given an original point
f(t1)=p
we take a pointf(t2)=q
witht2=t1+e
, where e is some small value like 0.001 -- this pointq
has a derivativeq' = pointDerivative(t2)
, and in order to make things easier for us, we move that tangent vector a tiny bit byp-q
so that the two vectors both "start" atp
. Pretty simple.However, this is equivalent to computing the first and second derivative at
p
, and then forming the second vector by adding those two together, as the second derivative gives us the change of the tangent at a point, so adding the second derivative vector to the first derivative vector gets us two vectors in the plane atp
without having to find an adjacent point. This can be useful in curves where there are discontinuities in the derivative, i.e. curves with cusps.We now have two vectors, departing at the same coordinate: our real tangent, and the "next" point's tangent, which is so close it might as well be the same point. Thankfully, due to how Bezier curves work, this second tangent is never the same, but slightly different, and "slightly different" is all we need: If we have two normalised vectors, starting at the same point but pointing in different directions, we can find the axis over which we need to rotate one to get the other simply by taking the cross product between them, and thus we can find the plane that they both go through.
Order matters: we compute c = tangent₂ × tangent₁, because if we compute c = tangent₁ × tangent₂ we'll be computing the rotation axis and resulting normals in the "wrong" direction. Correcting that is literally just a "take vector, multiply by -1" at the end, but why correct after the fact when we can get it right, here. Let's see those axes of rotation in blue:
Now we have everything we need: in order to turn our normalised tangent vectors into normal vectors, all we have to do is rotate them about the axes we just found by a quarter turn. If we turn them one way, we get normals, if we turn them the other, we get backfacing normals.
For arbitrary rotation about an axis in 3D, that job is perhaps laborious, but not difficult, and the quarter turns are generally special in that they greatly simplify the maths: to rotate a point over our rotation axis c, the rotation matrix turns out to be:
Where the 1, 2 and 3 subscripts are really just the x, y, and z components of our vector. So that's still easy, and all that's left is to matrix-rotate our normalised tangent:
Which is:
And we have the normal vector(s) we need. Perfect!
Except we can do better: since we're not working with arbitrary angles but with right angles, there's a significant shortcut we can use. In the same way that the vector c was perpendicular to both tangents, our normal n is perpendicular to both c and the regular tangent, so we can use the cross product a second time to find the normal:
This will give us exactly the same vector, with less work.
And if we want internal normals, it's the same vector, just multiply by -1:
Pretty easy once you know the tricks! And finally, because code is always useful this gist is the Processing program I used to make sure I was telling the truth.
What if the normals behave really weird?
For example, what if we're using a 3D curve but it's planar (e.g. all
z
coordinates at 0)? Things suddenly do horrible things. For instance, let's look at a curve with coordinates (0,0,0), (-38,260,0), (-25,541,0), and (-15,821,0):Similarly, particularly curvy curves may yield rather twisting normals. Looking at a curve with coordinates (0,0,0), (-38,260,200), (-25,541,-200), and (-15,821,600):
In this case, we want normals that rotate and twist as little as possible, which can be found using a Rotation Minimising Frame algorithm, such as explained in section 4 or "Computation of Rotation Minimizing Frames" (Wenping Wang, Bert Jüttler, Dayue Zheng, and Yang Liu, 2008).
Implementing their 9 line algorithm takes a little more work in a normal programming language, such as Java/Processing:
Still, this works really well. With the note that the Frenet frame is the "standard" tangent/axis/normal frame:
For our planar curve, we now see perfectly behaved normals:
And in the non-planar curve, there is minimal rotation:
And finally, these normals can be uniformly reoriented by rotating all vectors around their associated tangent vectors.