Bézier Curve Fun! - Pt. 2.5

A little optimization

This is going to be a quick update to the last post I wrote on expanding my Bézier Curve implementation to a recursive one.

Enter the Generator

I decided to first optimize the code organization. So I went ahead and separated the tasks of generating the Bézier curve and drawing it. This is why I created this curve generator class:

class BezierCurveGenerator
{
    private readonly List<Vector3> m_iterationOfPointsBuffer = new();

    private const float k_tValueStepIncrement = 0.025f;

    public void GenerateBezierCurve(
        List<Vector3> controlPoints,
        List<Vector3> resultBezierCurvePoints)
    {
        if (controlPoints.Count <= 1)
        {
            return;
        }

        resultBezierCurvePoints.Clear();

        CalculateBezierCurve(controlPoints, resultBezierCurvePoints);
    }
    private void CalculateBezierCurve(
        List<Vector3> controlPoints,
        List<Vector3> resultBezierCurvePoints)
    {
        for (float t = 0f; t <= 1.001f; t += k_tValueStepIncrement)
        {
            ResetControlPointIterationBuffer(controlPoints);

            CalculateBezierCurvePointRecursively(
                controlPoints.Count,
                t,
                resultBezierCurvePoints);
        }
    }
    private void ResetControlPointIterationBuffer(List<Vector3> controlPoints)
    {
        m_iterationOfPointsBuffer.Clear();
        foreach (var controlPoint in controlPoints)
        {
            m_iterationOfPointsBuffer.Add(controlPoint);
        }
    }

    private void CalculateBezierCurvePointRecursively(
        int numberOfPointsInIteration,
        float t,
        List<Vector3> resultBezierCurvePoints)
    {
        if (numberOfPointsInIteration == 2)
        {
            var resultCurvePoint =
                Vector3.Lerp(
                    m_iterationOfPointsBuffer[0],
                    m_iterationOfPointsBuffer[1],
                    t);

            resultBezierCurvePoints.Add(resultCurvePoint);

            return;
        }

        ProcessNextIterationOfpoints(numberOfPointsInIteration, t);

        CalculateBezierCurvePointRecursively(
            numberOfPointsInIteration - 1,
            t,
            resultBezierCurvePoints);
    }

    private void ProcessNextIterationOfpoints(int numberOfPointsInIteration, float t)
    {
        for (int i = 1; i < numberOfPointsInIteration; i++)
        {
            var lhsValueForLerp = m_iterationOfPointsBuffer[i - 1];

            m_iterationOfPointsBuffer[i - 1] =
                Vector3.Lerp(
                    lhsValueForLerp,
                    m_iterationOfPointsBuffer[i],
                    t);
        }
    }
}

This class has all of the curve creation code discussed in the previous posts, but now I'm using a memory buffer to avoid creating a new list on every iteration of points, when calculating a result curve point.

Why do this?

By isolating this functionality, it's no longer tied to any kind of component that must be owned by a GameObject . I can now use a BezierCurveGenerator anywhere I need. I could even run tests on this curve generation process to see how fast (or not) it may be, find out how much memory it uses, stress test it with large amounts of control points (20, 200, 10,000!), etc.

Quick Note:

C#'s List< T >.Clear does not actually erase the internal memory used by the list. In the example from the documentation, the internal memory remains the same after calling the clear method:

...
dinosaurs.Clear();
Console.WriteLine("\nClear()");
Console.WriteLine("Capacity: {0}", dinosaurs.Capacity);
Console.WriteLine("Count: {0}", dinosaurs.Count);

/*
...
Clear()
Capacity: 5
Count: 0
*/

We can take advantage of this, knowing that the memory will only be re-allocated when more control points get added than the highest amount used, or when you reset the component. When you clear a list and re-add items, you're simply re-writing the memory, and inserting new values into the buffer.

Updated Curve Drawer

Going back to our old code, ourBezierCurveDrawer class now uses an instance of the BezierCurveGenerator:

public class BezierCurveDrawer : MonoBehaviour
{
    public List<Vector3> m_controlPoints = new()
    { 
        new(-10f, 10f, 0f),
        new (-10f, 20f, 0f),
        new (10f, 20f, 0f),
        new (10f, 10f, 0f)
    };

    private readonly List<Vector3> m_resultBezierCurvePoints = new();

    private readonly BezierCurveGenerator m_bezierCurveGenerator = new();

    private const float k_controlPointRadius = 0.2f;

    private const float k_bezierCurvePointRadius = 0.1f;

    private void OnDrawGizmos()
    {
        DrawControlPoints();

        DrawLineSegmentsOfControlPoints();

        DrawBezierCurve();
    }

    private void DrawControlPoints()
    {
        foreach (var controlPoint in m_controlPoints)
        {
            Gizmos.DrawSphere(controlPoint, k_controlPointRadius);
        }
    }

    private void DrawLineSegmentsOfControlPoints()
    {
        DrawLineSegmentsInBetweenPoints(m_controlPoints);
    }

    private void DrawBezierCurve()
    {
        if (m_controlPoints.Count <= 1)
        {
            return;
        }

        Gizmos.color = Color.cyan;
        m_resultBezierCurvePoints.Clear();

        m_bezierCurveGenerator.GenerateBezierCurve(m_controlPoints, m_resultBezierCurvePoints);

        DrawResultBezierCurvePoints();

        DrawLineSegmentsInBetweenPoints(m_resultBezierCurvePoints);
    }

    private void DrawResultBezierCurvePoints()
    {
        foreach (var curvePoint in m_resultBezierCurvePoints)
        {
            Gizmos.DrawSphere(curvePoint, k_bezierCurvePointRadius);
        }
    }

    private void DrawLineSegmentsInBetweenPoints(List<Vector3> points)
    {
        for (int pointListIndex = 1; pointListIndex < points.Count; pointListIndex++)
        {
            Gizmos.DrawLine(
                points[pointListIndex - 1],
                points[pointListIndex]);
        }
    }
}

Note that I added the list of points representing the Bézier Curve as a member variable. This way I don't create a new list every time the OnDrawGizmos() event is called:

public class BezierCurveDrawer : MonoBehaviour
{
    public List<Vector3> m_controlPoints = new() { ... }

    private readonly List<Vector3> m_resultBezierCurvePoints = new();
    ...
}

Current Status

No real changes have been made to the component's behavior. I just organized the code a bit, and also slightly improved the curve creation process: Unity_dXO45g0WZC.gif

Also, this is pretty neat! I found out that the code still works well when I switch the order of the control points in the editor:

Unity_R0ilVlXGzW.gif

Please let me know what you think!

  • Any questions or thoughts about these new updates?
  • Any flaws that you can identify with the new memory buffer for the curve generation process?

🎉🎉🎉Happy coding!!🎉🎉🎉