Skip to main content
Describe screenshot
Source Link

What should be the starting normal at each control point? Well you want to decide that for yourself, right? I use the control handles in addition to a normal for each control point. This way, the user can rotate the bezier at each control point to point the way they want.

The correction looks something like this:

Road using the bezier editor

As you can see, this works vertically too now. The only problem is that you have to calculate the entire curve, you can't just sample the normal of the curve at any point along the bezier without calculating the entire array of points. However, since I assume your curve is not updated during run-time, this shouldn't be a problem. I personally recommend updating these points asynchronously while storing the current array in a variable, so that it can update at a slower frequency than the main loop. This is, I think, a good compromise.

The correction looks something like this:

Road using the bezier editor

What should be the starting normal at each control point? Well you want to decide that for yourself, right? I use the control handles in addition to a normal for each control point. This way, the user can rotate the bezier at each control point to point the way they want.

The correction looks something like this:

Road using the bezier editor

As you can see, this works vertically too now. The only problem is that you have to calculate the entire curve, you can't just sample the normal of the curve at any point along the bezier without calculating the entire array of points. However, since I assume your curve is not updated during run-time, this shouldn't be a problem. I personally recommend updating these points asynchronously while storing the current array in a variable, so that it can update at a slower frequency than the main loop. This is, I think, a good compromise.

Add Screenshot
Source Link

Edit: New Solution

Edit: I have found a solution that works for me (as mentioned in the comments):

Use an iterative approach. Start with a reference frame at the start of the bezier. Then, for each new point, use the orientation of the previous frame to orient the next one:

forwardOnCurve = DeriveCubic(p[0], p[1], p[2], p[3], Mathf.Clamp01(t)).normalized;
normalOnCurve = Vector3.Cross(forwardOnCurve, Vector3.Cross(normalOnCurve, forwardOnCurve)).normalized;

As you can see, I calculate the right vector from the new forwards vector and the previous normal. Then I calculate the new normal.

The problem with this approach is that while it is locally continuous, it ignores the orientation of all control points but the first.

To solve this, I rotate all points along the forwards vector between each control point, linearly interpolating the error angle to be correctly oriented at the next control point. Also, I treat each segment individually, always starting correctly oriented at each control point.

The correction looks something like this:

segmentLength += Vector3.Distance(previousPointOnCurve, p[3]);
lineLength += segmentLength;

forwardOnCurve = DeriveCubic(p[0], p[1], p[2], p[3], 1).normalized;
normalOnCurve = Vector3.Cross(forwardOnCurve, Vector3.Cross(normalOnCurve, forwardOnCurve)).normalized;
float angleError = Vector3.SignedAngle(normalOnCurve, _normals[segment + 1], forwardOnCurve);

// Iterate over evenly spaced points in this segment, and gradually correct angle error
float tStep = spacing / segmentLength;
float tStart = Vector3.Distance(esp[startIndex].position, p[0]) / segmentLength;
for (int i = startIndex; i < endIndexExclusive; ++i)
{
    float t_ = (i - startIndex) * tStep + tStart;
    // TODO: make weight non-linear, depending on handle lengths
    float correction = t_ * angleError;
    esp[i].normal = Quaternion.AngleAxis(correction, esp[i].forward) * esp[i].normal;
}

The full code is this: https://pastebin.com/t5LFLhL9

This can also be found in context in my unity package called "MB Road System"

Screenshot

Road using the bezier editor

My Old solution

Edit: New Solution

Edit: I have found a solution that works for me (as mentioned in the comments):

Use an iterative approach. Start with a reference frame at the start of the bezier. Then, for each new point, use the orientation of the previous frame to orient the next one:

forwardOnCurve = DeriveCubic(p[0], p[1], p[2], p[3], Mathf.Clamp01(t)).normalized;
normalOnCurve = Vector3.Cross(forwardOnCurve, Vector3.Cross(normalOnCurve, forwardOnCurve)).normalized;

As you can see, I calculate the right vector from the new forwards vector and the previous normal. Then I calculate the new normal.

The problem with this approach is that while it is locally continuous, it ignores the orientation of all control points but the first.

To solve this, I rotate all points along the forwards vector between each control point, linearly interpolating the error angle to be correctly oriented at the next control point. Also, I treat each segment individually, always starting correctly oriented at each control point.

The correction looks something like this:

segmentLength += Vector3.Distance(previousPointOnCurve, p[3]);
lineLength += segmentLength;

forwardOnCurve = DeriveCubic(p[0], p[1], p[2], p[3], 1).normalized;
normalOnCurve = Vector3.Cross(forwardOnCurve, Vector3.Cross(normalOnCurve, forwardOnCurve)).normalized;
float angleError = Vector3.SignedAngle(normalOnCurve, _normals[segment + 1], forwardOnCurve);

// Iterate over evenly spaced points in this segment, and gradually correct angle error
float tStep = spacing / segmentLength;
float tStart = Vector3.Distance(esp[startIndex].position, p[0]) / segmentLength;
for (int i = startIndex; i < endIndexExclusive; ++i)
{
    float t_ = (i - startIndex) * tStep + tStart;
    // TODO: make weight non-linear, depending on handle lengths
    float correction = t_ * angleError;
    esp[i].normal = Quaternion.AngleAxis(correction, esp[i].forward) * esp[i].normal;
}

The full code is this: https://pastebin.com/t5LFLhL9

This can also be found in context in my unity package called "MB Road System"

Screenshot

Road using the bezier editor

My Old solution

Source Link

I'm looking into this as well. I only have a solution that works for beziers that are somewhat horizontal, the closer they get to vertical, the less it works.

I'm essentially converting the local up vector into an angle, interpolating that, and turning it back into an up vector.

Conversion from normal to angle:

You use the vector along the bezier (p[t + 1] - p[t]) and take the cross product with the worlds up vector. That gives you the right vector relative to the path. Then you take the cross product between that and the local forward vector, and that gives you a vector that points 90° to the bezier, upwards. You can then measure the angle of the transforms up vector to that, and you should get the local roll.

This roll is then interpolated along the path.

Generating a normal from the local angle works similarly.

This is the code that I wrote:

public static float AngleFromNormal(Vector3 forward, Vector3 normal)
{
    Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
    Vector3 up = Vector3.Cross(forward, right).normalized;
    return Vector3.SignedAngle(normal, up, forward);
}

public static Vector3 NormalFromAngle(Vector3 forward, float angle)
{
    Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
    Vector3 up = Vector3.Cross(forward, right).normalized;
    return Quaternion.AngleAxis(-angle, forward) * up;
}

Now, as I said, this only works if the path is somewhat horizontal, as it relies on the cross product of the forward vector and Vector3.up to return a vector that is long enough to avoid floating point error or division by 0 (when the vector is normalized)

I personally used it for a RoadSystem that lets you edit roads, connect them to intersections, and it also generates a graph for pathfinding, and can then generate a line from your position to the goal. In that use case, this approach works well enough, as roads don't really go up vertically.