Beziers, curves and paths

    Unlike more abstract mathematical concepts, Bezier curves were created for industrial design. They are a popular tool in the graphics software industry.

    They rely on interpolation, which we saw in the previous article, combining multiple steps to create smooth curves. To better understand how Bezier curves work, let’s start from its simplest form: Quadratic Bezier.

    Take three points, the minimum required for Quadratic Bezier to work:

    To draw a curve between them, we first interpolate gradually over the two vertices of each of the two segments formed by the three points, using values ranging from 0 to 1. This gives us two points that move along the segments as we change the value of from 0 to 1.

    GDScript   C#

    1. private Vector2 QuadraticBezier(Vector2 p0, Vector2 p1, Vector2 p2, float t)
    2. {
    3. Vector2 q0 = p0.LinearInterpolate(p1, t);
    4. Vector2 q1 = p1.LinearInterpolate(p2, t);
    5. }

    We then interpolate q0 and q1 to obtain a single point r that moves along a curve.

    GDScript   C#

    1. var r = q0.linear_interpolate(q1, t)
    2. return r
    1. Vector2 r = q0.LinearInterpolate(q1, t);
    2. return r;

    This type of curve is called a Quadratic Bezier curve.

    ../../_images/bezier_quadratic_points2.gif

    (Image credit: Wikipedia)

    Cubic Bezier

    Building upon the previous example, we can get more control by interpolating between four points.

    We first use a function with four parameters to take four points as an input, p0, p1, p2 and p3:

    GDScript   C#

    1. func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
    1. public Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
    2. {
    3. }

    We apply a linear interpolation to each couple of points to reduce them to three:

    GDScript   C#

    1. Vector2 q0 = p0.LinearInterpolate(p1, t);
    2. Vector2 q1 = p1.LinearInterpolate(p2, t);

    We then take our three points and reduce them to two:

    1. var r0 = q0.linear_interpolate(q1, t)
    2. var r1 = q1.linear_interpolate(q2, t)
    1. Vector2 r0 = q0.LinearInterpolate(q1, t);
    2. Vector2 r1 = q1.LinearInterpolate(q2, t);

    And to one:

    GDScript   C#

    1. return s
    1. Vector2 s = r0.LinearInterpolate(r1, t);
    2. return s;

    Here is the full function:

    GDScript   C#

    1. private Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
    2. {
    3. Vector2 q0 = p0.LinearInterpolate(p1, t);
    4. Vector2 q1 = p1.LinearInterpolate(p2, t);
    5. Vector2 q2 = p2.LinearInterpolate(p3, t);
    6. Vector2 r0 = q0.LinearInterpolate(q1, t);
    7. Vector2 r1 = q1.LinearInterpolate(q2, t);
    8. Vector2 s = r0.LinearInterpolate(r1, t);
    9. return s;
    10. }

    The result will be a smooth curve interpolating between all four points:

    ../../_images/bezier_cubic_points.gif

    (Image credit: Wikipedia)

    Note

    Cubic Bezier interpolation works the same in 3D, just use Vector3 instead of Vector2.

    Building upon Cubic Bezier, we can change the way two of the points work to control the shape of our curve freely. Instead of having p0, p1, p2 and p3, we will store them as:

    • point0 = p0: Is the first point, the source

    • control0 = p1 - p0: Is a vector relative to the first control point

    • control1 = p3 - p2: Is a vector relative to the second control point

    This way, we have two points and two control points which are relative vectors to the respective points. If you’ve used graphics or animation software before, this might look familiar:

    This is how graphics software presents Bezier curves to the users, and how they work and look in Godot.

    Curve2D, Curve3D, Path and Path2D

    There are two objects that contain curves: and Curve2D (for 3D and 2D respectively).

    ../../_images/bezier_path_2d.png

    Using them, however, may not be completely obvious, so following is a description of the most common use cases for Bezier curves.

    Just evaluating them may be an option, but in most cases it’s not very useful. The big drawback with Bezier curves is that if you traverse them at constant speed, from t = 0 to t = 1, the actual interpolation will not move at constant speed. The speed is also an interpolation between the distances between points p0, , p2 and p3 and there is not a mathematically simple way to traverse the curve at constant speed.

    Let’s do a simple example with the following pseudocode:

    GDScript   C#

    1. var t = 0.0
    2. func _process(delta):
    3. t += delta
    4. position = _cubic_bezier(p0, p1, p2, p3, t)
    1. private float _t = 0.0f;
    2. public override void _Process(float delta)
    3. {
    4. _t += delta;
    5. Position = CubicBezier(p0, p1, p2, p3, _t);
    6. }

    As you can see, the speed (in pixels per second) of the circle varies, even though t is increased at constant speed. This makes beziers difficult to use for anything practical out of the box.

    Drawing

    Drawing beziers (or objects based on the curve) is a very common use case, but it’s also not easy. For pretty much any case, Bezier curves need to be converted to some sort of segments. This is normally difficult, however, without creating a very high amount of them.

    The reason is that some sections of a curve (specifically, corners) may require considerable amounts of points, while other sections may not:

    ../../_images/bezier_point_amount.png

    Additionally, if both control points were 0, 0 (remember they are relative vectors), the Bezier curve would just be a straight line (so drawing a high amount of points would be wasteful).

    Before drawing Bezier curves, tessellation is required. This is often done with a recursive or divide and conquer function that splits the curve until the curvature amount becomes less than a certain threshold.

    The Curve classes provide this via the function (which receives optional stages of recursion and angle tolerance arguments). This way, drawing something based on a curve is easier.

    The last common use case for the curves is to traverse them. Because of what was mentioned before regarding constant speed, this is also difficult.

    To make this easier, the curves need to be baked into equidistant points. This way, they can be approximated with regular interpolation (which can be improved further with a cubic option). To do this, just use the method together with Curve2D.get_baked_length(). The first call to either of them will bake the curve internally.

    Traversal at constant speed, then, can be done with the following pseudo-code:

    GDScript   C#

    1. var t = 0.0
    2. func _process(delta):
    3. t += delta
    4. position = curve.interpolate_baked(t * curve.get_baked_length(), true)
    1. private float _t = 0.0f;
    2. public override void _Process(float delta)
    3. {
    4. _t += delta;

    And the output will, then, move at constant speed: