Could not work out or find any solution for this.

I have a spline which consists of bezier curves (in 3d space) — in fact, any straight line will also have such problem.

I take a point on the curve and need to get a normal to this point at any given angle 0 - 360 (say, to build a mesh around it or to calculate an offset for the movement).

I understand that in 3d space we have to define a plane to build normal, normally — by using Vector3.Cross(A, B); In general, one uses curve's tangent as A and, e.x., Vector3.up or right as B.

But, if curve becomes vertical (it can go in any direction, imagine any track that is ground-independent, like in space), it gets aligned with Vector3.up, and the direction of normals goes crazy, swaps direction and so on, which is quite logical. More, it does so not only in straight vertical position, but start to change gradually around it.

So, the question is: how can I get consistent (all in equally correct direction) normals, using any given curve point position and angle for normal rotation?


The initial approach looked like this:

public Vector3 GetTangent(...) {

    Vector3 q2 = CalculateTangent (...);

    Quaternion lookAt = Quaternion.LookRotation (q2);
    Vector3 normal = lookAt * Vector3.up;
    normal = Quaternion.AngleAxis (angle, q2) * normal;
    return point + normal * size;

Which generated the following normals (blue is up):

enter image description here

Now, if I try to use the previous normal for the direction:

Quaternion lookAt = Quaternion.LookRotation (q2, previousNormal);

first of all, I don't understand what to place here:

Vector3 normal = lookAt * ?

And any combination of tangent, normal and rotation gives me something like this:

enter image description here

Where it can be clearly seen that normals on the vertical are improved (though not correct), but on the horizontal all went wrong, as if I just changed Vector.up to Vector3.right.

In addition, the code for generating the tube in its main part looks like this:

float vStep = (angleTo - angleFrom) / pipeSegmentCount;
if (b == 0 && _tP == 0f) _tP = 0.01f;
if (b == 0 && _t == 0f) _t = 0.01f;
if (b == spline.Nodes.Count - 2 && _tP == 1f) _tP = 0.9f;
if (b == spline.Nodes.Count - 2 && _t == 1f) _t = 0.9f;
Vector3 vertexA = spline.GetTangent (_pP, _tP, _bP, Vector3.up, pipeAngle, pipeRadius);
Vector3 vertexB = spline.GetTangent (_pointsTotal[b] [s], _t, b, Vector3.up, pipeAngle, pipeRadius);
for (int v = 1; v <= pipeSegmentCount; v++, i += 4) {
    vertices [i] = vertexA;
    vertices [i + 1] = vertexA = spline.GetTangent (_pP, _tP, _bP, Vector3.up, v * vStep + pipeAngle, pipeRadius);
    vertices [i + 2] = vertexB;
    vertices [i + 3] = vertexB = spline.GetTangent (_pointsTotal[b] [s], _t, b, Vector3.up, v * vStep + pipeAngle, pipeRadius);

Where 0.01f and 0.99f are because ends of spline do not have control points and LookRotation produces an error about zero rotation. b and _bP are node indexes, and _P suffix is for "previous".

This particular code is used to generate a round rail and move an object beneath it. Thus, it is important that the bottom normal is consistent and always at the bottom (when it is vertical, this bottom is not the world bottom of course, but it protrudes at the side of the tube, object will turn itself accordingly, looking forward the curve).

  • \$\begingroup\$ According to the Hairy Ball Theorem, you can't. ;) At least, you can't make a function that takes a 3D tangent direction in any random order and returns a normal basis without a discontinuity or vanishing point at some input. But! If you're walking along your spline step by step in order, you can align with the previous step's normal instead of global up. This gives the function a bit of memory, so it's locally consistent, although two points on the spline with the same tangent might have different normals, depending on the path taken to get there. \$\endgroup\$
    – DMGregory
    Feb 8, 2017 at 11:52
  • \$\begingroup\$ Thank you @DMGregory for pointing me to the core of the problem. Naturally, one can not comb a hairy ball smoothly, got it. I tried your suggestion, but it gets worse: when tangents are pointing towards the same direction, it is consistent, but when the curve turns, the normals' angle changes. Plus, at certain direction it seems that it catches that corrupted vector and goes wild again. \$\endgroup\$
    – Sammy S
    Feb 8, 2017 at 14:17

1 Answer 1


I'm not sure why you say the suggestion in the comments "gets worse" - I implemented it and it performs as expected:

Three tubes build from cubic splines

In each of these examples we have the same cubic spline being wrapped in a tube mesh, using a calculated normal to the spline at each point to decide the orientation of each tube cross-section.

I've marked the 0, 90, 180, and 270-degree points on the tube with coloured stripes to help show the differences in the twist.

On the left we have our original problem: when we try to select a normal in a consistent/memoryless way (eg. by choosing the vector in the normal plane closest to the "up" direction of our parent, substituting a default when the plane is horizontal) then we get sharp discontinuities: the tube can pinch at vertical segments, or show unnatural-looking twisting in near-vertical segments.

In the middle we apply a simple trick: instead of trying to align with some universal up, we align with whatever normal the previous step on our spline used. This keeps twisting distortion to a minimum, and our uv-mapped texture flows smoothly around each bend. But, the normal is free to wander all around the tube, which might be undesirable if your scenario has some structure to it that shouldn't linger at diagonal angles (eg. that green stripe, lazily hanging close-but-not-exactly-on the top of the tube).

On the right we apply a fixup: we slightly exaggerate the y component of the last step's normal, before using it to construct the normal for this step. This has the effect that, away from vertical sections, the tube will try to gradually untwist itself to point its normal as close as it can to vertical. This version picks the closest vertical direction, so it will seek down if that lets it introduce less twist.

So you can see, where the ends of the spline meet, the grid lines up, but the coloured markers are 180 degrees out (we now have blue on top and yellow on the bottom). I used distinct colours in this example to show it explicitly, but if you simply tile your texture twice around the circumference then this kind of mismatch becomes invisible.

  • \$\begingroup\$ Thank you @DMGregory once more. What I need it for is an object moving beneath the rail. Therefore, I build the tube around the curve and position the object on the normal pointing down and looking to the forward point. \$\endgroup\$
    – Sammy S
    Feb 11, 2017 at 19:48
  • \$\begingroup\$ I guess I tried to implement your solution in a wrong way, because it's easy to mess with normals when I build lots of them in different parts of the code. Which previous normal should I use as a reference? I guess a cross product of exactly the previous normal for each case with a new tangent should give something, that looks in different direction, than a new normal. I am not sure why it got worse also :) \$\endgroup\$
    – Sammy S
    Feb 11, 2017 at 19:55
  • \$\begingroup\$ In Unity, I used newNormal = Quaternion.LookRotation(newTangent, previousNormal) * Vector3.up. You could get a similar result by crossing the tangent of this point with the binormal from the previous point. \$\endgroup\$
    – DMGregory
    Feb 11, 2017 at 20:39
  • \$\begingroup\$ I used LookRotation of just tangent, then multiplied it by Vector.up and then applied AngleAxis around the tangent. If I swap global up with previous normal or add it to LookRotation as a second argument, I get something feather-like. And one more thing: what do you use for the very first point (where there is yet no previous normal)? I guess it should be global up to then rewrite this variable every time to a normal. \$\endgroup\$
    – Sammy S
    Feb 11, 2017 at 21:22
  • \$\begingroup\$ Hard to debug your code without seeing it, or seeing a screenshot of what "feather-like" means in this context. Consider posting a new question about debugging your approach — that way you can show the code and screenshots, without the tight constraints of a comment thread. ;) You can assign your first normal arbitrarily, I just used LookRotation with the first tangent. \$\endgroup\$
    – DMGregory
    Feb 11, 2017 at 21:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .