Jekyll2019-02-12T22:40:16+02:00https://qroph.github.io/Mika’s Coding BitsMika RantanenSmooth Paths Using Catmull-Rom Splines2018-07-30T21:30:00+03:002018-07-30T21:30:00+03:00https://qroph.github.io/2018/07/30/smooth-paths-using-catmull-rom-splines<p>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.</p>
<figure class="svg-figure">
<svg height="130" width="452"><polyline points="84.00,116.00 81.93,114.57 76.30,110.70 68.02,104.98 57.95,98.05 47.00,90.50 36.05,82.95 25.98,76.02 17.70,70.30 12.07,66.43 10.00,65.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="10.00,65.00 12.52,63.46 19.36,59.28 29.44,53.12 41.68,45.64 55.00,37.50 68.32,29.36 80.56,21.88 90.64,15.72 97.48,11.54 100.00,10.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="100.00,10.00 102.35,12.56 108.80,19.59 118.44,30.08 130.35,43.06 143.63,57.52 157.37,72.48 170.65,86.94 182.56,99.92 192.20,110.41 198.65,117.44 201.00,120.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="201.00,120.00 203.04,119.30 208.77,117.33 217.64,114.28 229.07,110.35 242.48,105.74 257.32,100.64 273.01,95.25 288.99,89.75 304.68,84.36 319.52,79.26 332.93,74.65 344.36,70.72 353.23,67.67 358.96,65.70 361.00,65.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="361.00,65.00 361.81,65.48 364.10,66.84 367.68,68.96 372.34,71.72 377.90,75.01 384.15,78.72 390.89,82.71 397.93,86.88 405.07,91.12 412.11,95.29 418.85,99.28 425.10,102.99 430.66,106.28 435.32,109.04 438.90,111.16 441.19,112.52 442.00,113.00" class="svg-text" style="fill:none;stroke-width:2" /><circle class="svg-brand" style="stroke:none" cx="84.00" cy="116.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="10.00" cy="65.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="100.00" cy="10.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="201.00" cy="120.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="361.00" cy="65.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="442.00" cy="113.00" r="5" /></svg>
<figcaption>A path using straight lines.</figcaption>
</figure>
<p>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.</p>
<figure class="svg-figure">
<svg height="130" width="452"><polyline points="84.00,116.00 75.92,110.90 66.79,105.80 57.05,100.69 47.16,95.59 37.58,90.49 28.74,85.38 21.12,80.28 15.15,75.19 11.29,70.09 10.00,65.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="10.00,65.00 11.85,58.88 16.75,51.83 24.14,44.24 33.48,36.52 44.23,29.07 55.84,22.29 67.77,16.59 79.47,12.38 90.39,10.05 100.00,10.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="100.00,10.00 109.25,13.43 118.12,20.52 126.73,30.49 135.20,42.55 143.66,55.93 152.23,69.85 161.03,83.51 170.19,96.14 179.82,106.95 190.05,115.17 201.00,120.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="201.00,120.00 210.22,121.20 220.08,120.50 230.48,118.17 241.32,114.49 252.50,109.75 263.93,104.21 275.49,98.17 287.10,91.90 298.64,85.67 310.02,79.78 321.15,74.49 331.90,70.09 342.20,66.85 351.93,65.07 361.00,65.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="361.00,65.00 366.62,65.84 372.03,67.16 377.26,68.92 382.31,71.06 387.23,73.54 392.01,76.32 396.69,79.34 401.28,82.57 405.80,85.95 410.28,89.44 414.73,92.99 419.18,96.56 423.64,100.10 428.13,103.55 432.67,106.89 437.29,110.05 442.00,113.00" class="svg-text" style="fill:none;stroke-width:2" /><circle class="svg-brand" style="stroke:none" cx="84.00" cy="116.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="10.00" cy="65.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="100.00" cy="10.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="201.00" cy="120.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="361.00" cy="65.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="442.00" cy="113.00" r="5" /></svg>
<figcaption>A smooth path.</figcaption>
</figure>
<p>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.</p>
<h2 id="catmull-rom-splines">Catmull-Rom splines</h2>
<p>Catmull-Rom splines are piecewise-defined polynomial functions. A single spline segment is defined by four control points <script type="math/tex">\boldsymbol{p}_0, \dots, \boldsymbol{p}_3</script> but the actual curve is drawn only between points <script type="math/tex">\boldsymbol{p}_1</script> and <script type="math/tex">\boldsymbol{p}_2</script> as is illustrated in Figure 3. However, it is easy to chain these segments together.</p>
<figure class="svg-figure">
<svg height="91" width="273"><polyline points="10.00,30.00 18.30,35.60 26.60,41.99 34.90,48.82 43.20,55.76 51.50,62.48 59.80,68.64 68.10,73.91 76.40,77.95 84.70,80.42 93.00,81.00" class="svg-grey" style="fill:none;stroke-width:2" stroke-dasharray="8,8" /><polyline points="93.00,81.00 102.02,78.59 111.08,72.96 120.15,64.93 129.23,55.32 138.30,44.93 147.34,34.59 156.35,25.11 165.30,17.31 174.19,12.00 183.00,10.00" class="svg-text" style="fill:none;stroke-width:2" /><polyline points="183.00,10.00 189.81,10.76 196.57,13.03 203.29,16.58 209.98,21.20 216.63,26.67 223.26,32.79 229.88,39.34 236.49,46.10 243.10,52.86 249.71,59.41 256.35,65.52 263.00,71.00" class="svg-grey" style="fill:none;stroke-width:2" stroke-dasharray="8,8" /><circle class="svg-brand" style="stroke:none" cx="10.00" cy="30.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="93.00" cy="81.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="183.00" cy="10.00" r="5" /><circle class="svg-brand" style="stroke:none" cx="263.00" cy="71.00" r="5" /></svg>
<figcaption>One segment of Catmull-Rom spline.</figcaption>
</figure>
<p>If we want to draw a curve that goes through <script type="math/tex">k</script> points, we need <script type="math/tex">k+2</script> 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 <script type="math/tex">\boldsymbol{p}_n</script> and <script type="math/tex">\boldsymbol{p}_{n+1}</script> is calculated using points <script type="math/tex">\boldsymbol{p}_{n-1}</script>, <script type="math/tex">\boldsymbol{p}_n</script>, <script type="math/tex">\boldsymbol{p}_{n+1}</script>, and <script type="math/tex">\boldsymbol{p}_{n+2}</script>, where <script type="math/tex">1 \leq n \leq k - 1</script>. When these segments are combined together, they form a continuous curve, which passes through all points between <script type="math/tex">\boldsymbol{p}_1</script> and <script type="math/tex">\boldsymbol{p}_{k}</script>. Figure 2 shows an example of this kind of curve.</p>
<p>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 <a href="http://www.cemyuksel.com/research/catmullrom_param/">here</a>. 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.</p>
<div class="javascript-container" id="catmullrom">
<div class="dat-gui-container" id="catmullrom-gui"></div>
</div>
<script>
var catmullrom_app;
var catmullrom_gui;
function catmullrom_initialize() {
const container = document.getElementById('catmullrom');
const gui_container = document.getElementById('catmullrom-gui');
const width = container.clientWidth;
const height = Math.max(0.6 * width, minHeight);
const app = new PIXI.Application(width, height, { antialias: true, transparent: true });
const gui = new dat.GUI({ autoPlace: false });
container.appendChild(app.view);
gui_container.appendChild(gui.domElement);
catmullrom_app = app;
catmullrom_gui = gui;
window.addEventListener('resize', catmullrom_resize);
catmullrom_resize();
}
function catmullrom_resize() {
const container = document.getElementById('catmullrom');
const width = container.clientWidth;
const height = Math.max(0.6 * width, minHeight);
catmullrom_app.renderer.resize(width, height);
}
</script>
<p>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.</p>
<h2 id="calculating-spline-segments">Calculating spline segments</h2>
<p>Let’s start with following equations that are described in a <a href="https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline">Wikipedia article</a> about centripetal Catmull-Rom splines:</p>
<script type="math/tex; mode=display">\boldsymbol{q}(t) = \frac{t_2 - t}{t_2 - t_1}\boldsymbol{B}_1 + \frac{t - t_1}{t_2 - t_1}\boldsymbol{B}_2,</script>
<p>where</p>
<script type="math/tex; mode=display">% <![CDATA[
\begin{align}
\boldsymbol{B}_1 &= \frac{t_2 - t}{t_2 - t_0}\boldsymbol{A}_1 + \frac{t - t_0}{t_2 - t_0}\boldsymbol{A}_2, \\
\boldsymbol{B}_2 &= \frac{t_3 - t}{t_3 - t_1}\boldsymbol{A}_2 + \frac{t - t_1}{t_3 - t_1}\boldsymbol{A}_3, \\
\boldsymbol{A}_1 &= \frac{t_1 - t}{t_1 - t_0}\boldsymbol{p}_0 + \frac{t - t_0}{t_1 - t_0}\boldsymbol{p}_1, \\
\boldsymbol{A}_2 &= \frac{t_2 - t}{t_2 - t_1}\boldsymbol{p}_1 + \frac{t - t_1}{t_2 - t_1}\boldsymbol{p}_2, \\
\boldsymbol{A}_3 &= \frac{t_3 - t}{t_3 - t_2}\boldsymbol{p}_2 + \frac{t - t_2}{t_3 - t_2}\boldsymbol{p}_3,
\end{align} %]]></script>
<p>and where</p>
<script type="math/tex; mode=display">t_i = t_{i-1} + \|\boldsymbol{p}_i - \boldsymbol{p}_{i-1}\|^\alpha,</script>
<p>and <script type="math/tex">t_0 = 0</script>, <script type="math/tex">i = 1, 2, 3</script>, and <script type="math/tex">\alpha \in [0, 1]</script>.</p>
<p>The actual segment we are interested in is between <script type="math/tex">t_1</script> and <script type="math/tex">t_2</script> i.e. <script type="math/tex">\boldsymbol{q}(t_1) = \boldsymbol{p}_1</script> and <script type="math/tex">\boldsymbol{q}(t_2) = \boldsymbol{p}_2</script>. If <script type="math/tex">\alpha = 0</script>, the resulting curve <script type="math/tex">\boldsymbol{q}</script> is a uniform Catmull-Rom spline. When <script type="math/tex">\alpha = 0.5</script>, the curve is a centripetal variant and when <script type="math/tex">\alpha = 1</script>, the result is a chordal variant.</p>
<p>Calculating the curve with previous equation can be quite inconvenient. Often it would be better to use precalculated constants <script type="math/tex">\boldsymbol{a}</script>, <script type="math/tex">\boldsymbol{b}</script>, <script type="math/tex">\boldsymbol{c}</script>, and <script type="math/tex">\boldsymbol{d}</script>, and represent the curve segment between <script type="math/tex">\boldsymbol{p}_1</script> and <script type="math/tex">\boldsymbol{p}_2</script> as</p>
<script type="math/tex; mode=display">\boldsymbol{p}(t) = \boldsymbol{a}t^3 + \boldsymbol{b}t^2 + \boldsymbol{c}t + \boldsymbol{d},</script>
<p>where <script type="math/tex">t \in [0, 1]</script>, <script type="math/tex">\boldsymbol{p}(0) = \boldsymbol{p}_1</script>, and <script type="math/tex">\boldsymbol{p}(1) = \boldsymbol{p}_2</script>. Other way to define this is</p>
<script type="math/tex; mode=display">\boldsymbol{p}(t) = \boldsymbol{q}(t_1 + t(t_2 - t_1)).</script>
<p>We can get a relationship between the tangents of <script type="math/tex">\boldsymbol{p}</script> and <script type="math/tex">\boldsymbol{q}</script> by differentiating these with respect to <script type="math/tex">t</script> as follows:</p>
<script type="math/tex; mode=display">\boldsymbol{p}'(t) = 3\boldsymbol{a}t^2 + 2\boldsymbol{b}t + \boldsymbol{c} = (t_2 - t_1)\boldsymbol{q}'(t_1 + t(t_2 - t_1)).</script>
<p>Now we have</p>
<script type="math/tex; mode=display">% <![CDATA[
\begin{align}
\boldsymbol{p}(0) &= \boldsymbol{q}(t_1) &&= \boldsymbol{p}_1 = \boldsymbol{d}, \\
\boldsymbol{p}(1) &= \boldsymbol{q}(t_2) &&= \boldsymbol{p}_2 = \boldsymbol{a} + \boldsymbol{b} + \boldsymbol{c} + \boldsymbol{d}, \\
\boldsymbol{p}'(0) &= (t_2 - t_1)\boldsymbol{q}'(t_1) &&= \boldsymbol{m}_1 = \boldsymbol{c}, \\
\boldsymbol{p}'(1) &= (t_2 - t_1)\boldsymbol{q}'(t_2) &&= \boldsymbol{m}_2 = 3\boldsymbol{a} + 2\boldsymbol{b} + c,
\end{align} %]]></script>
<p>where symbols <script type="math/tex">\boldsymbol{m}_1</script>, and <script type="math/tex">\boldsymbol{m}_2</script> are used to represent tangents at the starting point <script type="math/tex">\boldsymbol{p}_1</script> and at the ending point <script type="math/tex">\boldsymbol{p}_2</script> respectively. By solving <script type="math/tex">\boldsymbol{a}</script>, <script type="math/tex">\boldsymbol{b}</script>, <script type="math/tex">\boldsymbol{c}</script>, and <script type="math/tex">\boldsymbol{d}</script> from these four equations we get</p>
<script type="math/tex; mode=display">% <![CDATA[
\begin{align}
\boldsymbol{a} &= 2\boldsymbol{p}_1 - 2\boldsymbol{p}_2 + \boldsymbol{m}_1 + \boldsymbol{m}_2, \\
\boldsymbol{b} &= -3\boldsymbol{p}_1 + 3\boldsymbol{p}_2 - 2\boldsymbol{m}_1 - \boldsymbol{m}_2, \\
\boldsymbol{c} &= \boldsymbol{m}_1, \\
\boldsymbol{d} &= \boldsymbol{p}_1.
\end{align} %]]></script>
<p>Now we have to determine <script type="math/tex">\boldsymbol{m}_1</script> and <script type="math/tex">\boldsymbol{m}_2</script> and for that we need tangents <script type="math/tex">\boldsymbol{q}'(t_1)</script> and <script type="math/tex">\boldsymbol{q}'(t_2)</script>. Calculating the derivative of <script type="math/tex">\boldsymbol{q}</script> is quite cumbersome, but luckily we can use <a href="https://sandbox.open.wolframcloud.com/">Mathematica</a> to do the work for us. We get</p>
<script type="math/tex; mode=display">% <![CDATA[
\begin{align}
\boldsymbol{q}'(t_1) &= \frac{\boldsymbol{p}_1 - \boldsymbol{p}_0}{t_1 - t_0} - \frac{\boldsymbol{p}_2 - \boldsymbol{p}_0}{t_2 - t_0} + \frac{\boldsymbol{p}_2 - \boldsymbol{p}_1}{t_2 - t_1}, \\
\boldsymbol{q}'(t_2) &= \frac{\boldsymbol{p}_2 - \boldsymbol{p}_1}{t_2 - t_1} - \frac{\boldsymbol{p}_3 - \boldsymbol{p}_1}{t_3 - t_1} + \frac{\boldsymbol{p}_3 - \boldsymbol{p}_2}{t_3 - t_2},
\end{align} %]]></script>
<p>and thus</p>
<script type="math/tex; mode=display">\begin{align}
\boldsymbol{m}_1 = (t_2 - t_1)\left(\frac{\boldsymbol{p}_1 - \boldsymbol{p}_0}{t_1 - t_0} - \frac{\boldsymbol{p}_2 - \boldsymbol{p}_0}{t_2 - t_0} + \frac{\boldsymbol{p}_2 - \boldsymbol{p}_1}{t_2 - t_1}\right), \\
\boldsymbol{m}_2 = (t_2 - t_1)\left(\frac{\boldsymbol{p}_2 - \boldsymbol{p}_1}{t_2 - t_1} - \frac{\boldsymbol{p}_3 - \boldsymbol{p}_1}{t_3 - t_1} + \frac{\boldsymbol{p}_3 - \boldsymbol{p}_2}{t_3 - t_2}\right).
\end{align}</script>
<p>If we want to take tension <script type="math/tex">\tau</script> into account, we must also multiply both <script type="math/tex">\boldsymbol{m}_1</script> and <script type="math/tex">\boldsymbol{m}_2</script> with <script type="math/tex">1 - \tau</script>.</p>
<p>It is now easy to precalculate coefficients <script type="math/tex">\boldsymbol{a}</script>, <script type="math/tex">\boldsymbol{b}</script>, <script type="math/tex">\boldsymbol{c}</script>, and <script type="math/tex">\boldsymbol{d}</script> for each segment and use equation <script type="math/tex">\boldsymbol{p}(t) = \boldsymbol{a}t^3 + \boldsymbol{b}t^2 + \boldsymbol{c}t + \boldsymbol{d}</script> to quickly interpolate that segment.</p>
<h2 id="c-implementation">C++ implementation</h2>
<p>The code is written in C++. It uses <a href="https://glm.g-truc.net">GLM library</a> 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.</p>
<p>In its simplest form, we can define a struct of the spline segment as follows:</p>
<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="k">struct</span> <span class="n">Segment</span>
<span class="p">{</span>
<span class="n">vec2</span> <span class="n">a</span><span class="p">;</span>
<span class="n">vec2</span> <span class="n">b</span><span class="p">;</span>
<span class="n">vec2</span> <span class="n">c</span><span class="p">;</span>
<span class="n">vec2</span> <span class="n">d</span><span class="p">;</span>
<span class="p">};</span></code></pre></figure>
<p>Note that we are using two-dimensional splines here. If you want three-dimensional splines, just replace all occurances of <code class="highlighter-rouge">vec2</code> with <code class="highlighter-rouge">vec3</code>.</p>
<p>As said in the previous chapter, we need parameters <script type="math/tex">\alpha</script> and <script type="math/tex">\tau</script> to calculate the spline. In following code we use variable <code class="highlighter-rouge">alpha</code> for <script type="math/tex">\alpha</script> and <code class="highlighter-rouge">tension</code> for <script type="math/tex">\tau</script>. A good value for <code class="highlighter-rouge">alpha</code> is 0.5 which gives us a centripetal Catmull-Rom spline, and for <code class="highlighter-rouge">tension</code> a value 0 is a good choice. These values can range from 0 to 1.</p>
<p>If we now have points <code class="highlighter-rouge">p0</code>, <code class="highlighter-rouge">p1</code>, <code class="highlighter-rouge">p2</code>, and <code class="highlighter-rouge">p3</code> for each segment, we can calculate coefficients for segments with equations from previous chapter:</p>
<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="kt">float</span> <span class="n">t0</span> <span class="o">=</span> <span class="mf">0.0</span><span class="n">f</span><span class="p">;</span>
<span class="kt">float</span> <span class="n">t1</span> <span class="o">=</span> <span class="n">t0</span> <span class="o">+</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p0</span><span class="p">,</span> <span class="n">p1</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">t2</span> <span class="o">=</span> <span class="n">t1</span> <span class="o">+</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">t3</span> <span class="o">=</span> <span class="n">t2</span> <span class="o">+</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p2</span><span class="p">,</span> <span class="n">p3</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="n">vec2</span> <span class="n">m1</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span><span class="n">f</span> <span class="o">-</span> <span class="n">tension</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">t2</span> <span class="o">-</span> <span class="n">t1</span><span class="p">)</span> <span class="o">*</span>
<span class="p">((</span><span class="n">p1</span> <span class="o">-</span> <span class="n">p0</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t1</span> <span class="o">-</span> <span class="n">t0</span><span class="p">)</span> <span class="o">-</span> <span class="p">(</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p0</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t2</span> <span class="o">-</span> <span class="n">t0</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t2</span> <span class="o">-</span> <span class="n">t1</span><span class="p">));</span>
<span class="n">vec2</span> <span class="n">m2</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span><span class="n">f</span> <span class="o">-</span> <span class="n">tension</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="n">t2</span> <span class="o">-</span> <span class="n">t1</span><span class="p">)</span> <span class="o">*</span>
<span class="p">((</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t2</span> <span class="o">-</span> <span class="n">t1</span><span class="p">)</span> <span class="o">-</span> <span class="p">(</span><span class="n">p3</span> <span class="o">-</span> <span class="n">p1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t3</span> <span class="o">-</span> <span class="n">t1</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">p3</span> <span class="o">-</span> <span class="n">p2</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t3</span> <span class="o">-</span> <span class="n">t2</span><span class="p">));</span>
<span class="n">Segment</span> <span class="n">segment</span><span class="p">;</span>
<span class="n">segment</span><span class="p">.</span><span class="n">a</span> <span class="o">=</span> <span class="mf">2.0</span><span class="n">f</span> <span class="o">*</span> <span class="p">(</span><span class="n">p1</span> <span class="o">-</span> <span class="n">p2</span><span class="p">)</span> <span class="o">+</span> <span class="n">m1</span> <span class="o">+</span> <span class="n">m2</span><span class="p">;</span>
<span class="n">segment</span><span class="p">.</span><span class="n">b</span> <span class="o">=</span> <span class="o">-</span><span class="mf">3.0</span><span class="n">f</span> <span class="o">*</span> <span class="p">(</span><span class="n">p1</span> <span class="o">-</span> <span class="n">p2</span><span class="p">)</span> <span class="o">-</span> <span class="n">m1</span> <span class="o">-</span> <span class="n">m1</span> <span class="o">-</span> <span class="n">m2</span><span class="p">;</span>
<span class="n">segment</span><span class="p">.</span><span class="n">c</span> <span class="o">=</span> <span class="n">m1</span><span class="p">;</span>
<span class="n">segment</span><span class="p">.</span><span class="n">d</span> <span class="o">=</span> <span class="n">p1</span><span class="p">;</span></code></pre></figure>
<p>We can get the same result slightly more efficiently by simplifying the equations and using the following code to calculate <code class="highlighter-rouge">m1</code> and <code class="highlighter-rouge">m2</code>:</p>
<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="kt">float</span> <span class="n">t01</span> <span class="o">=</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p0</span><span class="p">,</span> <span class="n">p1</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">t12</span> <span class="o">=</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p1</span><span class="p">,</span> <span class="n">p2</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="kt">float</span> <span class="n">t23</span> <span class="o">=</span> <span class="n">pow</span><span class="p">(</span><span class="n">distance</span><span class="p">(</span><span class="n">p2</span><span class="p">,</span> <span class="n">p3</span><span class="p">),</span> <span class="n">alpha</span><span class="p">);</span>
<span class="n">vec2</span> <span class="n">m1</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span><span class="n">f</span> <span class="o">-</span> <span class="n">tension</span><span class="p">)</span> <span class="o">*</span>
<span class="p">(</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p1</span> <span class="o">+</span> <span class="n">t12</span> <span class="o">*</span> <span class="p">((</span><span class="n">p1</span> <span class="o">-</span> <span class="n">p0</span><span class="p">)</span> <span class="o">/</span> <span class="n">t01</span> <span class="o">-</span> <span class="p">(</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p0</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t01</span> <span class="o">+</span> <span class="n">t12</span><span class="p">)));</span>
<span class="n">vec2</span> <span class="n">m2</span> <span class="o">=</span> <span class="p">(</span><span class="mf">1.0</span><span class="n">f</span> <span class="o">-</span> <span class="n">tension</span><span class="p">)</span> <span class="o">*</span>
<span class="p">(</span><span class="n">p2</span> <span class="o">-</span> <span class="n">p1</span> <span class="o">+</span> <span class="n">t12</span> <span class="o">*</span> <span class="p">((</span><span class="n">p3</span> <span class="o">-</span> <span class="n">p2</span><span class="p">)</span> <span class="o">/</span> <span class="n">t23</span> <span class="o">-</span> <span class="p">(</span><span class="n">p3</span> <span class="o">-</span> <span class="n">p1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">t12</span> <span class="o">+</span> <span class="n">t23</span><span class="p">)));</span></code></pre></figure>
<p>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 <code class="highlighter-rouge">segment</code> by using the precalculated values:</p>
<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="n">vec2</span> <span class="n">point</span> <span class="o">=</span> <span class="n">segment</span><span class="p">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">t</span> <span class="o">*</span> <span class="n">t</span> <span class="o">*</span> <span class="n">t</span> <span class="o">+</span>
<span class="n">segment</span><span class="p">.</span><span class="n">b</span> <span class="o">*</span> <span class="n">t</span> <span class="o">*</span> <span class="n">t</span> <span class="o">+</span>
<span class="n">segment</span><span class="p">.</span><span class="n">c</span> <span class="o">*</span> <span class="n">t</span> <span class="o">+</span>
<span class="n">segment</span><span class="p">.</span><span class="n">d</span><span class="p">;</span></code></pre></figure>
<p>And <code class="highlighter-rouge">t</code> 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.</p>
<!--
p:=((t2-t)/(t2-t1))*B1+((t-t1)/(t2-t1))*B2
B1:=((t2-t)/(t2-t0))*A1+((t-t0)/(t2-t0))*A2
B2:=((t3-t)/(t3-t1))*A2+((t-t1)/(t3-t1))*A3
A1:=((t1-t)/(t1-t0))*P0+((t-t0)/(t1-t0))*P1
A2:=((t2-t)/(t2-t1))*P1+((t-t1)/(t2-t1))*P2
A3:=((t3-t)/(t3-t2))*P2+((t-t2)/(t3-t2))*P3
D[p, t] /. {t->t1} // FullSimplify
D[p, t] /. {t->t2} // FullSimplify
(t2-t1)D[p, t] /. {t->t1, t0->0} // FullSimplify
(t2-t1)D[p, t] /. {t->t2, t0->0} // FullSimplify
-->Mika RantanenLet’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 but the actual curve is drawn only between points and 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 points, we need 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 and is calculated using points , , , and , where . When these segments are combined together, they form a continuous curve, which passes through all points between and . 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: where and where and , , and . The actual segment we are interested in is between and i.e. and . If , the resulting curve is a uniform Catmull-Rom spline. When , the curve is a centripetal variant and when , 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 , , , and , and represent the curve segment between and as where , , and . Other way to define this is We can get a relationship between the tangents of and by differentiating these with respect to as follows: Now we have where symbols , and are used to represent tangents at the starting point and at the ending point respectively. By solving , , , and from these four equations we get Now we have to determine and and for that we need tangents and . Calculating the derivative of is quite cumbersome, but luckily we can use Mathematica to do the work for us. We get and thus If we want to take tension into account, we must also multiply both and with . It is now easy to precalculate coefficients , , , and for each segment and use equation 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.