Smooth Paths Using Catmull-Rom Splines

https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines.html

Smooth Paths Using Catmull-Rom Splines

July 30, 2018 • Mika Rantanen

Let’s assume that we have a set of points and we want to draw a path that goes through them. The simplest and easiest way to do this is to connect the points with straight lines as shown in Figure 1.

A path using straight lines.

However, straight lines would cause sharp corners to the path and in many cases a smoother path would be preferred. Figure 2 shows a smoother path that goes through the same points that are in Figure 1.

A smooth path.

In this post I’m going to focus on Catmull-Rom splines which are commonly used in computer graphics to create smooth curves. For example, the path in Figure 2 uses them.

Catmull-Rom splines

Catmull-Rom splines are piecewise-defined polynomial functions. A single spline segment is defined by four control points p0,…,p3

but the actual curve is drawn only between points p1 and p2

as is illustrated in Figure 3. However, it is easy to chain these segments together.

One segment of Catmull-Rom spline.

If we want to draw a curve that goes through k

points, we need k+2 control points because the curve is not drawn through the first and the last ones. These two additional points can be selected arbitrarily, but they affect the shape of the curve. Now a segment between points pn and pn+1 is calculated using points pn−1, pn, pn+1, and pn+2, where 1≤n≤k−1. When these segments are combined together, they form a continuous curve, which passes through all points between p1 and pk

. Figure 2 shows an example of this kind of curve.

There are some parameters that can be used to control the shape of the spline. Catmull-Rom splines have three common variants: uniform, centripetal, and chordal. Differences have been studied for example here. Additionally, it is possible to use a tension parameter that defines how “tight” the spline is. When tension is 0, the result looks like the curve in Figure 2, and when tension is 1, the result is straight lines as in Figure 1. The following interactive example demonstrates Catmull-Rom splines and these parameters.

  •  
  •  
  •  
  •  

  •  
  •  

You can add points by clicking the canvas and move existing points by dragging them. The control panel allows you to change the tension of the curve and select the spline variant that is drawn.

Calculating spline segments

Let’s start with following equations that are described in a Wikipedia article about centripetal Catmull-Rom splines:

q(t)=t2−tt2−t1B1+t−t1t2−t1B2,

where

B1=t2−tt2−t0A1+t−t0t2−t0A2,B2=t3−tt3−t1A2+t−t1t3−t1A3,A1=t1−tt1−t0p0+t−t0t1−t0p1,A2=t2−tt2−t1p1+t−t1t2−t1p2,A3=t3−tt3−t2p2+t−t2t3−t2p3,

and where

ti=ti−1+∥pi−pi−1∥α,

and t0=0

, i=1,2,3, and α∈[0,1]

.

The actual segment we are interested in is between t1

and t2 i.e. q(t1)=p1 and q(t2)=p2. If α=0, the resulting curve q is a uniform Catmull-Rom spline. When α=0.5, the curve is a centripetal variant and when α=1

, the result is a chordal variant.

Calculating the curve with previous equation can be quite inconvenient. Often it would be better to use precalculated constants a

, b, c, and d, and represent the curve segment between p1 and p2

as

p(t)=at3+bt2+ct+d,

where t∈[0,1]

, p(0)=p1, and p(1)=p2

. Other way to define this is

p(t)=q(t1+t(t2−t1)).

We can get a relationship between the tangents of p

and q by differentiating these with respect to t

as follows:

p′(t)=3at2+2bt+c=(t2−t1)q′(t1+t(t2−t1)).

Now we have

p(0)=q(t1)=p1=d,p(1)=q(t2)=p2=a+b+c+d,p′(0)=(t2−t1)q′(t1)=m1=c,p′(1)=(t2−t1)q′(t2)=m2=3a+2b+c,

where symbols m1

, and m2 are used to represent tangents at the starting point p1 and at the ending point p2 respectively. By solving a, b, c, and d

from these four equations we get

a=2p1−2p2+m1+m2,b=−3p1+3p2−2m1−m2,c=m1,d=p1.

Now we have to determine m1

and m2 and for that we need tangents q′(t1) and q′(t2). Calculating the derivative of q

is quite cumbersome, but luckily we can use Mathematica to do the work for us. We get

q′(t1)=p1−p0t1−t0−p2−p0t2−t0+p2−p1t2−t1,q′(t2)=p2−p1t2−t1−p3−p1t3−t1+p3−p2t3−t2,

and thus

m1=(t2−t1)(p1−p0t1−t0−p2−p0t2−t0+p2−p1t2−t1),m2=(t2−t1)(p2−p1t2−t1−p3−p1t3−t1+p3−p2t3−t2).

If we want to take tension τ

into account, we must also multiply both m1 and m2 with 1−τ

.

It is now easy to precalculate coefficients a

, b, c, and d for each segment and use equation p(t)=at3+bt2+ct+d

to quickly interpolate that segment.

C++ implementation

The code is written in C++. It uses GLM library that provides the basic linear algebra functionality. Overall, I’ll try to keep the code syntax in this blog quite simple so that it is easy to understand and to convert to other languages.

In its simplest form, we can define a struct of the spline segment as follows:

struct Segment
{
    vec2 a;
    vec2 b;
    vec2 c;
    vec2 d;
};

Note that we are using two-dimensional splines here. If you want three-dimensional splines, just replace all occurances of vec2 with vec3.

As said in the previous chapter, we need parameters α

and τ to calculate the spline. In following code we use variable alpha for α and tension for τ

. A good value for alpha is 0.5 which gives us a centripetal Catmull-Rom spline, and for tension a value 0 is a good choice. These values can range from 0 to 1.

If we now have points p0, p1, p2, and p3 for each segment, we can calculate coefficients for segments with equations from previous chapter:

float t0 = 0.0f;
float t1 = t0 + pow(distance(p0, p1), alpha);
float t2 = t1 + pow(distance(p1, p2), alpha);
float t3 = t2 + pow(distance(p2, p3), alpha);

vec2 m1 = (1.0f - tension) * (t2 - t1) *
    ((p1 - p0) / (t1 - t0) - (p2 - p0) / (t2 - t0) + (p2 - p1) / (t2 - t1));
vec2 m2 = (1.0f - tension) * (t2 - t1) *
    ((p2 - p1) / (t2 - t1) - (p3 - p1) / (t3 - t1) + (p3 - p2) / (t3 - t2));

Segment segment;
segment.a = 2.0f * (p1 - p2) + m1 + m2;
segment.b = -3.0f * (p1 - p2) - m1 - m1 - m2;
segment.c = m1;
segment.d = p1;

We can get the same result slightly more efficiently by simplifying the equations and using the following code to calculate m1 and m2:

float t01 = pow(distance(p0, p1), alpha);
float t12 = pow(distance(p1, p2), alpha);
float t23 = pow(distance(p2, p3), alpha);

vec2 m1 = (1.0f - tension) *
    (p2 - p1 + t12 * ((p1 - p0) / t01 - (p2 - p0) / (t01 + t12)));
vec2 m2 = (1.0f - tension) *
    (p2 - p1 + t12 * ((p3 - p2) / t23 - (p3 - p1) / (t12 + t23)));

These coefficients can be precaluclated for all spline segments assuming that the parameters do not change afterwards. It is now simple to retrieve a point in a segment segment by using the precalculated values:

vec2 point = segment.a * t * t * t +
             segment.b * t * t +
             segment.c * t +
             segment.d;

And t is of course a value between 0 and 1. With a value 0, the result is the starting point of the spline segment, and with a value 1, the result is the ending point.

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐