Building roads using Bezier curves in Unity

May 12, 2023 · 7 mins read
Building roads using Bezier curves in Unity

Building roads using Bezier curves in Unity

I recently did a small toy project where I implemented a simple road builder tool for Unity, using Bezier curves. It taught me quite a few things about making editor tools in Unity, so I thought I’d share what I learnt.

Full source code can be found here.

Bezier curves

So first of all: What are Bezier curves?

A Bezier curve is a simple type of parametric curve curve, that can be made up of a start point, an end point and one or more “control” points. The curve may not pass through these control points, but they are used to control the shape of the curve. Bezier curves can be useful for animating movement paths (animated camera) and building surfaces.

Bezier curves come in many shapes:

  • Linear (two control points, only start and end - basically just a line)
  • Quadratic (three control points)
  • Cubic (four control points)

Linear Bezier curve

A linear Bezier curve is a flat line, where we interpolate linearly between two points (A and B) at position/time t (0-1).

(illustration from Wikipedia)

Quadratic Bezier curve

We now add a control point (C) that will affect the shape of the curve.

The curve can still be calculated by linear interpolation, but now in three steps. At time t:

  • P0 = lerp(A, C, t)
  • P1 = lerp(C, B, t)
  • P = lerp(P0, P1 ,t)

In other words, we interpolate between the start and the control point, and between the control point and the end. And then we interpolate between the results.

(illustration from Wikipedia)

Qubic Bezier curve

Cubic Bezier curves follow the same logic - we just add another control point. This gives us even more control over the shape of the curve, so this is the type of Bezier curve that we will use for building our road builder tool.

However, we are not going to calculate the curves using multiple steps of linear interpolation. It turns out there is a nice equation that we can use!

Given 4 points (P0, P1, P2, P3), we have: $$B(t) = (1-t)^3P0+3(1-t)^2P1+3(1-t)t^2P2+t^3P3$$

In code, this becomes:

public static class BezierInterpolator
{
    public static Vector3 GetPosition(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
    {
        float t0 = -1.0f * t * t * t + 3.0f * t * t - 3.0f * t + 1.0f;
        float t1 = 3.0f * t * t * t - 6.0f * t * t + 3.0f * t;
        float t2 = -3.0f * t * t * t + 3.0f * t * t;
        float t3 = t * t * t;
        return t0 * a + t1 * b + t2 * c + t3*d;
    }
}

In case you noticed that the equation changed a bit: We do not want to calculate the (1-t)^3 parts every single time, we’ve just expanded it. This allows us to make it a little bit faster, for example by calculating t * t * t and t * t only once.

So why do we want to use this equation instead of the simple linear interpolation method? Because we want to calculate the derivatives as well! The derivative of a function measures the slope of the function - that is, degree of change at a certain point. When dealing with a 3D Bezier curve, the derivative B'(t) will give us a 3D vector pointing in the direcion we’re moving in at time t. The magnitude of this vector is our speed. We will use this to calculate the normals and tangents of our road.

This is how we calculate the derivatives:

public static Vector3 GetDerivative(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float t)
{
    float t0 = -3.0f * t * t + 6.0f * t - 3.0f;
    float t1 = 9.0f * t * t - 12.0f * t + 3.0f;
    float t2 = -9.0f * t * t + 6.0f * t;
    float t3 = 3.0f * t * t;
    return t0 * a + t1 * b + t2 * c + t3*d;
}

Building the road

We now know what a Bezier curve is, and how to create one. We will create a curve for our road by adding 4 control points that the user can move freely around (I’ll get back to how this is done later). We will then iterate through this curve at even steps, starting at position t=0.0 and continuing until position t=1.0, and build a mesh for the road as we do this.

int numSegmentsPerCurve = 16;

for (int i = 0; i < curvePoints.Count; i+=4)
{
    // Calculate the 4 control points of the current Bezier curve
    Vector3 p0 = curvePointA;
    Vector3 p1 = curvePointB;
    Vector3 p2 = curvePointC;
    Vector3 p3 = curvePointD;

    // Dicide curve into N discrete segments, for which we will create geometry
    float dist = 0.0f;
    for (int iSegment = 1; iSegment <= numSegmentsPerCurve; iSegment++)
    {
        // Calculate stard and end points of current segment
        float tStart = (iSegment - 1) / (float)numSegmentsPerCurve;
        float tEnd = iSegment / (float)numSegmentsPerCurve;
        Vector3 start = BezierInterpolator.GetPosition(p0, p1, p2, p3, tStart);
        Vector3 end = BezierInterpolator.GetPosition(p0, p1, p2, p3, tEnd);

        // Calculate derivative (direction)
        Vector3 startDerivative = BezierInterpolator.GetDerivative(p0, p1, p2, p3, tStart);
        Vector3 endDerivative = BezierInterpolator.GetDerivative(p0, p1, p2, p3, tEnd);

        // Calculate tangent (right vector)
        Vector3 startTangent = Vector3.Cross(startDerivative.normalized, Vector3.up);
        Vector3 endTangent = Vector3.Cross(endDerivative.normalized, Vector3.up);

        float halfWidth = roadWidth * 0.5f;

        vertices.Add(start - startTangent * halfWidth);
        vertices.Add(start + startTangent * halfWidth);
        vertices.Add(end - endTangent * halfWidth);
        vertices.Add(end + endTangent * halfWidth);

        float currDist = Vector3.Distance(start, end);
        float newDist = dist + currDist;
        uvs.Add(new Vector2(0.0f, dist));
        uvs.Add(new Vector2(1.0f, dist));
        uvs.Add(new Vector2(0.0f, newDist));
        uvs.Add(new Vector2(1.0f, newDist));
        dist = newDist;
    }
}

So what happens here is that we divide the road into a number of segments (16). Each segment will be drawn as an uneven rectangle (or two triangles). The segments will look like this:

We then step through the curve at each segment and calculate our t value (between 0 and 1). We then pass the 4 control points of the curve to calculate our position on the curve at point t. This gives us the centre point of the current road segment. To build a mesh we will need to calculate 4 vertices: the left and right vertex of the segment start, and the left and right vertex of the segment end. In other word, we need to know which direction is “right” and “left”. Luckily we can calculate this quite easily! The derivative will give us the forward vector, and by taking the cross product of the forward vector and up vector ((0,1,0) for now..) we will get the right vector. This is how we do that:

Vector3 startDerivative = BezierInterpolator.GetDerivative(p0, p1, p2, p3, tStart);
Vector3 startTangent = Vector3.Cross(startDerivative.normalized, Vector3.up);

Splines: Connecting the curves

So far so good! However, we only have a single curve consisting of 4 control points points, which is not nearly enough for building long and curvy roads. To get what we want, we can create multiple Bezier curves and connect them together. The end point of each curve should be the starting point of the next one.

And this is how it looks:

Full source code can be found here.

Sharing is caring!