1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-08-30 11:40:27 +02:00
This commit is contained in:
Pomax
2020-09-07 13:10:20 -07:00
parent 42b9818441
commit ebe69a732a
24 changed files with 1288 additions and 713 deletions

View File

@@ -11,6 +11,9 @@ setup() {
{x:560,y:170}
];
setMovable(points);
if (this.parameters.showCurves) {
addSlider(`highlight`, `highlight`, 1, 4, 1, 1);
}
}
draw() {
@@ -26,13 +29,19 @@ draw() {
line(p.x, p.y, n.x, n.y);
}
setColor(`black`);
this.drawSplineData();
points.forEach((p,i) => {
circle(p.x, p.y, 3)
if (this.parameters.showCurves) {
if (this.highlight - 1 <= i && i <= this.highlight + 2) {
setColor(`red`);
circle(p.x, p.y, 5);
}
}
setColor(`black`);
circle(p.x, p.y, 3);
text(`${i+1}`, p.x+5, p.y+5);
});
this.drawSplineData();
}
drawSplineData() {
@@ -41,15 +50,19 @@ drawSplineData() {
if (this.parameters.showCurves) {
for(let i=0; i<points.length-3; i++) {
let c = new Bezier(this, points.slice(i,i+4));
c.drawCurve(randomColor());
const c = new Bezier(this, points.slice(i,i+4));
const highlight = this.highlight === i+1;
if (highlight) c.drawSkeleton();
setWidth(highlight ? 3 : 1);
c.drawCurve(randomColor(highlight? 1 : 0.4));
}
setWidth(1);
}
let spline = new BSpline(this, points);
noFill();
setStroke(this.parameters.showCurves ? `#00000040` : `black`);
setStroke(this.parameters.showCurves ? `#000000CC` : `black`);
start();
spline.getLUT((points.length - 3) * 20).forEach(p => vertex(p.x, p.y));
end();
@@ -62,6 +75,9 @@ onMouseDown() {
y: this.cursor.y
});
resetMovable(points);
if (this.parameters.showCurves) {
updateSlider(`highlight`, 1, points.length-3, 1, 1);
}
redraw();
}
}

View File

@@ -2,7 +2,7 @@
No discussion on Bézier curves is complete without also giving mention of that other beast in the curve design space: B-Splines. Easily confused to mean Bézier splines, that's not actually what they are; they are "basis function" splines, which makes a lot of difference, and we'll be looking at those differences in this section. We're not going to dive as deep into B-Splines as we have for Bézier curves (that would be an entire primer on its own) but we'll be looking at how B-Splines work, what kind of maths is involved in computing them, and how to draw them based on a number of parameters that you can pick for individual B-Splines.
First off: B-Splines are [piecewise polynomial interpolation curves](https://en.wikipedia.org/wiki/Piecewise), where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.
First off: B-Splines are [piecewise](https://en.wikipedia.org/wiki/Piecewise), [polynomial interpolation curves](https://en.wikipedia.org/wiki/Spline_(mathematics)), where the "single curve" is built by performing polynomial interpolation over a set of points, using a sliding window of a fixed number of points. For instance, a "cubic" B-Spline defined by twelve points will have its curve built by evaluating the polynomial interpolation of four points, and the curve can be treated as a lot of different sections, each controlled by four points at a time, such that the full curve consists of smoothly connected sections defined by points {1,2,3,4}, {2,3,4,5}, ..., {8,9,10,11}, and finally {9,10,11,12}, for eight sections.
What do they look like? They look like this! Tap on the graphic to add more points, and move points around to see how they map to the spline curve drawn.
@@ -15,9 +15,11 @@ Consider the difference to be this:
- for Bézier curves, the curve is defined as an interpolation of points, but:
- for B-Splines, the curve is defined as an interpolation of *curves*.
In fact, let's look at that again, but this time with the base curves shown, too. Each consecutive four points defined one curve:
In fact, let's look at that again, but this time with the base curves shown, too. Each consecutive four points define one curve:
<graphics-element title="The components of a B-Spline " width="600" height="300" src="./basic.js" data-show-curves="true"></graphics-element>
<graphics-element title="The components of a B-Spline " width="600" height="300" src="./basic.js" data-show-curves="true">
<!-- basis curve highlighter goes here -->
</graphics-element>
In order to make this interpolation of curves work, the maths is necessarily more complex than the maths for Bézier curves, so let's have a look at how things work.
@@ -69,7 +71,9 @@ This is another recursive function, with *k* values decreasing from the curve or
\alpha_{i,k} = \frac{t - knots[i]}{knots[i+1+n-k] - knots[i]}
\]
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers and once we have our alpha value, computing (1-alpha) is literally just "computing one minus alpha". Computing this d() function is thus simply a matter of "computing simple arithmetics but with recursion", which might be computationally expensive because we're doing "a lot of" steps, but is also computationally cheap because each step only involves very simple maths. Of course as before the recursion has to stop:
That looks complicated, but it's not. Computing alpha is just a fraction involving known, plain numbers. And, once we have our alpha value, we also have `(1-alpha)` because it's a trivial subtraction. Computing the `d()` function is thus mostly a matter of computing pretty simple arithmetical statements, with some caching of results so we can refer to them as we recurve. While the recursion might see computationally expensive, the total algorithm is cheap, as each step only involves very simple maths.
Of course, the recursion does need a stop condition:
\[
d^k_0(t) = 0, \ d^0_i(t) = N_{i,1}(t) =
@@ -79,46 +83,69 @@ That looks complicated, but it's not. Computing alpha is just a fraction involvi
\end{matrix}\right.
\]
So, we see two stopping conditions: either `i` becomes 0, in which case d() is zero, or `k` becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
So, we actually see two stopping conditions: either `i` becomes 0, in which case `d()` is zero, or `k` becomes zero, in which case we get the same "either 1 or 0" that we saw in the N() function above.
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily: we just need to compute a triangle of interconnected values. For instance, d() for i=3, k=3 yields the following triangle:
Thanks to Cox and de Boor, we can compute points on a B-Spline pretty easily using the same kind of linear interpolation we saw in de Casteljau's algorithm. For instance, if we write out `d()` for `i=3` and `k=3`, we get the following recursion diagram:
\[
\begin{array}{ccccccc}
d^3_3 && d^2_3 && d^1_3 && d^0_3 (= 0 \text{ or } 1) \\
&+^{α^3_3 \times …}_{(1-{α^3_3}) \times …}& &+^{α^2_3 \times …}_{(1-{α^2_3}) \times …}& &+^{α^1_3 \times …}_{(1-{α^1_3}) \times …}&\\
&& && && \\
& & d^2_2 && d^1_2 && d^0_2 (= 0 \text{ or } 1) \\
& & &+^{α^2_2 \times …}_{(1-{α^2_2}) \times …}& &+^{α^1_2 \times …}_{(1-{α^1_2}) \times …}&\\
& & && && \\
& & & & d^1_1 && d^0_1 (= 0 \text{ or } 1) \\
& & & & &+^{α^1_1 \times …}_{(1-{α^1_1}) \times …}&\\
& & & & && \\
& & & & & & d^0_0 (= 0)
\end{array}
d^3_3 = \left \{
\begin{aligned}
\alpha^3_3 \times d^2_3, & \ \textit{ with } d^2_3 = \left \{
\begin{aligned}
\alpha^2_3 \times d^1_3, & \ \textit{ with } d^1_3 =
\left \{
\begin{aligned}
\alpha^1_3 \times d^0_3, & \ \textit{ with } d^0_3 \textit{ either 0 or 1} \\
+ & \\
\left ( 1 - \alpha^1_3 \right ) \times d^0_2, & \ \textit{ with } d^0_2 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
+ & \\
\left ( 1 - \alpha^2_3 \right ) \times d^1_2, & \ \textit{ with } d^1_2 =
\left \{
\begin{aligned}
\alpha^1_2 \times d^0_2 & \\
+ & \\
\left ( 1 - \alpha^1_2 \right ) \times d^0_1, & \ \textit{ with } d^0_1 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
\end{aligned}
\right . \\
+ & \\
\left ( 1 - \alpha^3_3 \right ) \times d^2_2, & \ \textit{ with } d^2_2 = \left \{
\begin{aligned}
\alpha^2_2 \times d^1_2 & \\
& \\
+ & \\
\left ( 1 - \alpha^2_2 \right ) \times d^1_1, & \ \textit{ with } d^1_1 =
\left \{
\begin{aligned}
\alpha^1_1 \times d^0_1 \\
+ & \\
\left ( 1 - \alpha^1_1 \right ) \times d^0_0, & \ \textit{ with } d^0_0 \textit{ either 0 or 1} \\
\end{aligned}
\right . \\
\end{aligned}
\right .
\end{aligned}
\right .
\]
That is, we compute d(3,3) as a mixture of d(2,3) and d(2,2): d(3,3) = a(3,3) x d(2,3) + (1-a(3,3)) x d(2,2)... and we simply keep expanding our triangle until we reach the terminating function parameters. Done deal!
One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the `d(..., k)` values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point: that's pretty essential!
If we run this computation "down", starting at d(3,3), then without special code in place we would be computing quite a few terms multiple times at each step. On the other hand, we can also start with that last "column", we can generate the terminating d() values first, then compute the a() constants, perform our multiplications, generate the previous step's d() values, compute their a() constants, do the multiplications, etc. until we end up all the way back at the top. If we run our computation this way, we don't need any explicit caching, we can just "recycle" the list of numbers we start with and simply update them as we move up the triangle. So, let's implement that!
That is, we compute `d(3,3)` as a mixture of `d(2,3)` and `d(2,2)`, where those two are themselves a mixture of `d(1,3)` and `d(1,2)`, and `d(1,2)` and `d(1,1)`, respectively, which are themselves a mixture of etc. etc. We simply keep expanding our terms until we reach the stop conditions, and then sum everything back up. It's really quite elegant.
One thing we need to keep in mind is that we're working with a spline that is constrained by its control points, so even though the `d(..., k)` values are zero or one at the lowest level, they are really "zero or one, times their respective control point", so in the next section you'll see the algorithm for running through the computation in a way that starts with a copy of the control point vector and then works its way up to that single point, rather than first starting "on the left", working our way "to the right" and then summing back up "to the left". We can just start on the right and work our way left immediately.
<!--
## Cool, cool... but I don't know what to do with that information
I know, this is pretty mathy, so let's have a look at what happens when we change parameters here. We can't change the maths for the interpolation functions, so that gives us only one way to control what happens here: the knot vector itself. As such, let's look at the graph that shows the interpolation functions for a cubic B-Spline with seven points with a uniform knot vector (so we see seven identical functions), representing how much each point (represented by one function each) influences the total curvature, given our knot values. And, because exploration is the key to discovery, let's make the knot vector a thing we can actually manipulate (you will notice that knots are constrained in their value: any knot must strictly be equal to, or greater than, the previous value).
<!-- THIS GRAPH IS EXTREMELY NOT-USEFUL, BUT WE'RE PORTING IT FIRST, AND REWRITING IT LATER -->
<graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./interpolation.js">
<!-- weight factors go here, similar to curve fitting sliders -->
</graphics-element>
<graphics-element title="Visualising relative interpolation strengths" width="600" height="300" src="./interpolation.js"></graphics-element>
Changing the values in the knot vector changes how much each point influences the total curvature (with some clever knot value manipulation, we can even make the influence of certain points disappear entirely!), so we can see that while the control points define the hull inside of which we're going to be drawing a curve, it is actually the knot vector that determines the actual *shape* of the curve inside that hull.
After reading the rest of this section you may want to come back here to try some specific knot vectors, and see if the resulting interpolation landscape makes sense given what you will now think should happen!
-->
## Running the computation
@@ -172,7 +199,7 @@ The most important thing to understand when it comes to B-Splines is that they w
The most straightforward type of B-Spline is the uniform spline. In a uniform spline, the knots are distributed uniformly over the entire curve interval. For instance, if we have a knot vector of length twelve, then a uniform knot vector would be [0,1,2,3,...,9,10,11]. Or [4,5,6,...,13,14,15], which defines *the same intervals*, or even [0,2,3,...,18,20,22], which also defines *the same intervals*, just scaled by a constant factor, which becomes normalised during interpolation and so does not contribute to the curvature.
<graphics-element title="A uniform B-Spline" width="400" height="400" src="./uniform.js">
<!-- knot sliders go here, similar to the curve fitter section -->
<!-- knot sliders go here -->
</graphics-element>
This is an important point: the intervals that the knot vector defines are *relative* intervals, so it doesn't matter if every interval is size 1, or size 100 - the relative differences between the intervals is what shapes any particular curve.
@@ -185,7 +212,7 @@ The problem with uniform knot vectors is that, as we need `order` control points
Collapsing knot intervals, by making two or more consecutive knots have the same value, allows us to reduce the curve complexity in the sections that are affected by the knots involved. This can have drastic effects: for every interval collapse, the curve order goes down, and curve continuity goes down, to the point where collapsing `order` knots creates a situation where all continuity is lost and the curve "kinks".
<graphics-element title="A reduced uniform B-Spline" width="400" height="400" src="./reduced.js">
<!-- knot sliders go here, similar to the curve fitter section -->
<!-- knot sliders go here -->
</graphics-element>
@@ -196,7 +223,7 @@ By combining knot interval collapsing at the start and end of the curve, with un
For any curve of degree `D` with control points `N`, we can define a knot vector of length `N+D+1` in which the values `0 ... D+1` are the same, the values `D+1 ... N+1` follow the "uniform" pattern, and the values `N+1 ... N+D+1` are the same again. For example, a cubic B-Spline with 7 control points can have a knot vector [0,0,0,0,1,2,3,4,4,4,4], or it might have the "identical" knot vector [0,0,0,0,2,4,6,8,8,8,8], etc. Again, it is the relative differences that determine the curve shape.
<graphics-element title="An open, uniform B-Spline" width="400" height="400" src="./uniform.js" data-open="true">
<!-- knot sliders go here, similar to the curve fitter section -->
<!-- knot sliders go here -->
</graphics-element>
@@ -209,7 +236,7 @@ This is essentially the "free form" version of a B-Spline, and also the least in
While it is true that this section on B-Splines is running quite long already, there is one more thing we need to talk about, and that's "Rational" splines, where the rationality applies to the "ratio", or relative weights, of the control points themselves. By introducing a ratio vector with weights to apply to each control point, we greatly increase our influence over the final curve shape: the more weight a control point carries, the closer to that point the spline curve will lie, a bit like turning up the gravity of a control pointl, just like for rational Bézier curves.
<graphics-element title="A (closed) rational, uniform B-Spline" width="400" height="400" src="rational-uniform.js">
<!-- knot sliders go here, similar to the curve fitter section -->
<!-- knot sliders go here -->
</graphics-element>
Of course this brings us to the final topic that any text on B-Splines must touch on before calling it a day: the [NURBS](https://en.wikipedia.org/wiki/Non-uniform_rational_B-spline), or Non-Uniform Rational B-Spline (NURBS is not a plural, the capital S actually just stands for "spline", but a lot of people mistakenly treat it as if it is, so now you know better). NURBS is an important type of curve in computer-facilitated design, used a lot in 3D modelling (typically as NURBS surfaces) as well as in arbitrary-precision 2D design due to the level of control a NURBS curve offers designers.

View File

@@ -1,22 +1,10 @@
let cache = { N: [] },
points,
degree = 3,
spline,
pad=20,
knots,
colors = [
'#C00',
'#CC0',
'#0C0',
'#0CC',
'#00C',
'#C0C',
'#600',
'#660',
'#060',
'#066',
'#006',
'#606'
];
colors = ['#C00','#CC0','#0C0','#0CC','#00C','#C0C','#600','#660','#060','#066','#006','#606'];
setup() {
points = [
@@ -44,6 +32,7 @@ setKnotValue(i, v) {
draw() {
clear();
setWidth(1);
setStroke(`lightgrey`);
drawGrid(pad);
@@ -52,7 +41,6 @@ draw() {
var y = this.height - pad;
this.line(0,y,this.width,y);
var degree = 3;
var n = points.length || 4;
for (let i=0, e=n+degree+1; i<e; i++) {
@@ -67,7 +55,8 @@ drawN(i, k, pad, w, h) {
start()
for (let start=i-1, t=start, step=0.1, end=i+k+1; t<end; t+=step) {
let x = pad + i*w + t*w;
let y = this.height - pad - this.N(i, k, t) * h;
let v = this.N(i, k, t);
let y = this.height - pad - h * v;
vertex(x, y);
}
end();
@@ -79,10 +68,20 @@ N(i, k, t) {
let t_ik1 = knots[i+k-1];
let t_ik = knots[i+k];
// terminal condition: if the degree is one, then
// the return value is either 1 ("contributes to
// the interpolation at this point"), or 0 ("does
// not contribute").
if (k===1) {
return (t_i <= t && t <= t_i1) ? 1 : 0;
}
// If we're not at k=1 yet, we can still determine
// whether we need to "do work", or whether the
// index and degree such that we know this knot
// contributes nothing:
let n1 = t - t_i;
let d1 = t_ik1 - t_i;
let a1 = d1===0? 0: n1/d1;
@@ -93,30 +92,34 @@ N(i, k, t) {
let N1 = 0;
if (a1 !== 0) {
// iteration: get the current index's (k-1)th value
let n1v = this.ensureN(i,k-1,t);
N1 = n1v === undefined ? this.N(i,k-1,t) : n1v;
}
let v1 = a1 * N1;
let N2 = 0;
if (a2 !== 0) {
// iteration: get the next index's (k-1)th value
let n2v = this.ensureN(i+1,k-1,t);
N2 = n2v === undefined ? this.N(i+1,k-1,t) : n2v;
}
let v2 = a2 * N2;
this.cacheN(i,k,t, a1 * N1 + a2 * N2);
// store their interpolation
this.cacheN(i,k,t, v1 + v2);
return cache.N[i][k][t];
}
ensureN(i,k,t) {
if (!cache.N) { cache.N = []; }
let N = cache.N;
if (!N[i]) { N[i] = []; }
if (!N[i][k]) { N[i][k] = []; }
return N[i][k][t];
}
cacheN(i,k,t,value) {
this.ensureN(i,k,t);
cache.N[i][k][t] = value;
}
ensureN(i,k,t) {
const N = cache.N ?? [];
N[i] = N[i] ?? [];
N[i][k] = N[i][k] ?? [];
cache.N = N;
return N[i][k][t];
}

View File

@@ -12,7 +12,7 @@ setup() {
weights = new BSpline(this, points, !!this.parameters.open).formWeights();
points.forEach((_,i) => {
addSlider(`slide-control`, `!weight ${i+1}`, 0, 10, 0.1, i%2===1? 2 : 6, v => this.setWeight(i, v));
addSlider(`slide-control`, `!weight ${i+1}`, 0, 10, 0.1, i%2===1? 2 : 8, v => this.setWeight(i, v));
});
points = points.concat(points.slice(0,3));