Last modified on 15 April 2014, at 10:48

Cg Programming/Unity/Bézier Curves

A smooth curve with control points P0, P1, and P2.

This tutorial discusses one way to render quadratic Bézier curves and splines in Unity. No shader programming is required since all the code is implemented in JavaScript.

There are many applications of smooth curves in computer graphics, in particular in animation and modeling. In a 3D game engine, one would usually use a tube-like mesh and deform it with vertex blending as discussed in Section “Nonlinear Deformations” instead of rendering curves; however, rendering curved lines instead of deformed meshes can offer a substantial performance advantage.

A linear Bézier curve from P0 to P1 is equivalent to linear interpolation.

Linear Bézier CurvesEdit

The simplest Bézier curve is a linear Bézier curve B(t) for t from 0 to 1 between two points P_0 and P_1, which happens to be the same as linear interpolation between the two points:

B(t) = (1-t) P_0 + t P_1

You might be fancy and call 1-t and t the Bernstein basis polynomials of degree 1, but it really is just linear interpolation.

Animation of a sampling point on a quadratic Bézier curve with control points P0, P1, and P2.

Quadratic Bézier CurvesEdit

A more interesting Bézier curve is a quadratic Bézier curve B(t) for t from 0 to 1 between two points P_0 and P_2 but influenced by a third point P_1 in the middle. The definition is:

B(t) = (1-t)^2 P_0 + 2(1-t)t P_1 + t^2 P_2

This defines a smooth curve B(t) with t \in [0,1] that starts (for t=0) from position P_0 in the direction to P_1 but then bends to P_2 (and reaches it for t=1).

In practice, one usually samples the interval from 0 to 1 at sufficiently many points, e.g. B(0), B(0.05), B(0.1), B(0.15), B(0.2), ..., B(1) and then renders straight lines between these sample points.

Curve ScriptEdit

To implement such a curve in Unity, we can use the Unity component LineRenderer. Apart from setting some parameters, one should set the number of sample points on the curve with the function SetVertexCount. Then the sample points have to be computed and set with the function SetPosition. This is can be implemented this way:

   var t : float; 
   var position : Vector3;
   for(var i : int = 0; i < numberOfPoints; i++) 
   {
      t = i / (numberOfPoints - 1.0);
      position = (1.0 - t) * (1.0 - t) * p0 
         + 2.0 * (1.0 - t) * t * p1
         + t * t * p2;
      lineRenderer.SetPosition(i, position);
   }

Here we use an index i from 0 to numberOfPoints-1 to count the sample points. From this index i a parameter t from 0 to 1 is computed. The next line computes B(t), which is then set with the function SetPosition.

The rest of the code just sets up the LineRenderer component and defines public variables that can be used to define the control points and some rendering features of the curve.

@script ExecuteInEditMode()
#pragma strict
 
public var start : GameObject;
public var middle : GameObject;
public var end : GameObject;
 
public var color : Color = Color.white;
public var width : float = 0.2;
public var numberOfPoints : int = 20;
 
function Start() 
{
   // initialize line renderer component
   var lineRenderer : LineRenderer = 
      GetComponent(LineRenderer);
   if (null == lineRenderer)
   {
      gameObject.AddComponent(LineRenderer);
   }
   lineRenderer = GetComponent(LineRenderer);
   lineRenderer.useWorldSpace = true;
   lineRenderer.material = new Material(
      Shader.Find("Particles/Additive"));
}
 
function Update() 
{
   // check parameters and components
   var lineRenderer : LineRenderer = 
      GetComponent(LineRenderer);
   if (null == lineRenderer || null == start 
      || null == middle || null == end)
   {
      return; // no points specified
   } 
 
   // update line renderer
   lineRenderer.SetColors(color, color);
   lineRenderer.SetWidth(width, width);
   if (numberOfPoints > 0)
   {
      lineRenderer.SetVertexCount(numberOfPoints);
   }
 
   // set points of quadratic Bezier curve
   var p0 : Vector3 = start.transform.position;
   var p1 : Vector3 = middle.transform.position;
   var p2 : Vector3 = end.transform.position;
   var t : float; 
   var position : Vector3;
   for(var i : int = 0; i < numberOfPoints; i++) 
   {
      t = i / (numberOfPoints - 1.0);
      position = (1.0 - t) * (1.0 - t) * p0 
         + 2.0 * (1.0 - t) * t * p1
         + t * t * p2;
      lineRenderer.SetPosition(i, position);
   }
}

To use this script create a Javascript in the Project view, double-click it, copy & paste the code above, save it, create a new empty game object (in the main menu: GameObject > Create Empty) and attach the script (drag the script from the Project view over the empty game object in the Hierarchy).

Then create three more empty game objects (or any other game objects) with different(!) positions that will serve as control points. Select the game object with the script and drag the other game objects into the slots Start, Middle, and End in the Inspector. This should render a curve from the game object specified as “Start” to the game object specified as “End” bending towards “Middle”.

A quadratic Bézier spline out of 8 quadratic Bézier curves.

Quadratic Bézier SplinesEdit

A quadratic Bézier Spline is just a continuous, smooth curve that consist of segments that are quadratic Bézier curves. If the control points of the curves were chosen arbitrarily, the spline would neither be continuous nor smooth; thus, the control points have to be chosen in particular ways.

One common way is to use a certain set of user-specified control points (green circles in the figure) as the P_1 control points of the segments and to choose the center positions between two adjacent user-specified control points as the P_0 and P_2 control points (black rectangles in the figure). This actually guarantees that the spline is smooth (also in the mathematical sense that the tangent vector is continuous).

Spline ScriptEdit

The following script implements this idea. For the j-th segment, it computes P_0 as the average of the j-th and (j+1)-th user-specified control points, P_1 is set to the (j+1)-th user-specified control point, and P_2 is the average of the (j+1)-th and (j+2)-th user-specified control points:

      p0 = 0.5 * (controlPoints[j].transform.position 
         + controlPoints[j + 1].transform.position);
      p1 = controlPoints[j + 1].transform.position;
      p2 = 0.5 * (controlPoints[j + 1].transform.position 
         + controlPoints[j + 2].transform.position);

Each individual segment is then just computed as a quadratic Bézier curve. The only adjustment is that all but the last segment should not reach P_2. If they did, the first sample position of the next segment would be at the same position which would be visible in the rendering. The complete script is:

@script ExecuteInEditMode()
#pragma strict
 
public var controlPoints : GameObject[] = new GameObject[3];
public var color : Color = Color.white;
public var width : float = 0.2;
public var numberOfPoints : int = 20;
 
function Start() 
{
   // initialize line renderer component
   var lineRenderer : LineRenderer = 
      GetComponent(LineRenderer);
   if (null == lineRenderer)
   {
      gameObject.AddComponent(LineRenderer);
   }
   lineRenderer = GetComponent(LineRenderer);
   lineRenderer.useWorldSpace = true;
   lineRenderer.material = new Material(
      Shader.Find("Particles/Additive"));
}
 
function Update() 
{
   // check parameters and components
   var lineRenderer : LineRenderer = 
      GetComponent(LineRenderer);
   if (null == lineRenderer || controlPoints == null 
      || controlPoints.length < 3)
   {
      return; // not enough points specified
   } 
 
   // update line renderer
   lineRenderer.SetColors(color, color);
   lineRenderer.SetWidth(width, width);
   if (numberOfPoints < 2)
   {
      numberOfPoints = 2;
   }
   lineRenderer.SetVertexCount(numberOfPoints * 
      (controlPoints.length - 2));
 
   // loop over segments of spline
   var p0 : Vector3;
   var p1 : Vector3;
   var p2 : Vector3;
   for (var j : int = 0; j < controlPoints.length - 2; j++)
   {
      // check control points
      if (controlPoints[j] == null || 
         controlPoints[j + 1] == null ||
         controlPoints[j + 2] == null)
      {
         return;  
      }
      // determine control points of segment
      p0 = 0.5 * (controlPoints[j].transform.position 
         + controlPoints[j + 1].transform.position);
      p1 = controlPoints[j + 1].transform.position;
      p2 = 0.5 * (controlPoints[j + 1].transform.position 
         + controlPoints[j + 2].transform.position);
 
      // set points of quadratic Bezier curve
      var position : Vector3;
      var t : float; 
      var pointStep : float = 1.0 / numberOfPoints;
      if (j == controlPoints.length - 3)
      {
         pointStep = 1.0 / (numberOfPoints - 1.0);
         // last point of last segment should reach p2
      }  
      for(var i : int = 0; i < numberOfPoints; i++) 
      {
         t = i * pointStep;
         position = (1.0 - t) * (1.0 - t) * p0 
            + 2.0 * (1.0 - t) * t * p1
            + t * t * p2;
         lineRenderer.SetPosition(i + j * numberOfPoints, 
            position);
      }
   }
}

The script works in the same way as the script for Bézier curves except that the user can specify an arbitrary number of control points. For closed splines, the last two user-specified control points should be the same as the first two control points. For open splines that actually reach the end points, the first and last control point should be specified twice.

SummaryEdit

In this tutorial, we have looked at:

  • the definition of linear and quadratic Bézier curves and quadratic Bézier splines
  • implementations of quadratic Bézier curves and quadratic Bézier splines with Unity's LineRenderer component.

Further ReadingEdit

If you want to know more

  • about Bézier curves (and Bézier splines), the Wikipedia article on “Bézier curve” provides a good starting point.
  • about Unity's LineRenderer, you should read Unity's documentation of the class LineRenderer.


< Cg Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.