1
0
mirror of https://github.com/Pomax/BezierInfo-2.git synced 2025-09-01 12:23:19 +02:00

chinese localizations

This commit is contained in:
Pomax
2022-09-03 09:57:23 -07:00
parent 737e020eaa
commit 152db3cdf6
126 changed files with 20648 additions and 48349 deletions

View File

@@ -0,0 +1,149 @@
# 导数
利用贝塞尔函数的导数可以对贝塞尔曲线做一些有用的事,而贝塞尔函数较为有趣的一个性质是其导数也为贝塞尔函数。其实贝塞尔函数的求导相对而言比较直接,只是需要一点数学运算。
首先观察贝塞尔函数的求导法则,即:
\[
\textit{Bézier}'(n,t) = n \cdot \sum_{i=0}^{n-1} (b_{i+1}-b_i) \cdot \textit{Bézier}(n-1,t)_i
\]
上式可改写为(注意式中的*b*即权重*w*,且*n*乘以一个和式等于每个求和项乘以*n*再求和):
\[
\textit{Bézier}'(n,t) = \sum_{i=0}^{n-1} \textit{Bézier}(n-1,t)_i \cdot n \cdot (w_{i+1}-w_i)
\]
直白地说,*n*次贝塞尔函数的导数为*n*-1次贝塞尔函数少了一项而且新的权重*w'*<sub>0</sub>、……、*w'*<sub>*n*-1</sub>可用旧的权重通过*n*(*w*<sub>*i*+1</sub> - *w*<sub>*i*</sub>)求得。对于带四个权重的三次函数,其导数的三个新权重为:*w'*<sub>0</sub> = 3(*w*<sub>1</sub>-*w*<sub>0</sub>)*w'*<sub>1</sub> = 3(*w*<sub>2</sub>-*w*<sub>1</sub>)和*w'*<sub>2</sub> = 3(*w*<sub>3</sub>-*w*<sub>2</sub>)。
<div class="note">
### “慢着,为什么这是对的?”
虽然有时候有人告诉说“这是导数”就行,但还是可能想一探究竟。既然如此,就来看看这个导数的证明。首先,因为权重不影响完整的贝塞尔函数的求导,所以求导只涉及多项式基函数的导数。基函数的导数为:
\[
\frac{\mathrm{d}}{\mathrm{d}t} B_{n,k}(t) = \frac{\mathrm{d}}{\mathrm{d}t} \left( \mathrm{C}_n^k t^k (1-t)^{n-k} \right)
\]
使用[乘积求导法则](https://en.wikipedia.org/wiki/Product_rule)和[复合函数求导法则](https://en.wikipedia.org/wiki/Chain_rule)得到:
\[
\cdots = \mathrm{C}_n^k \left (
k \cdot t^{k-1} (1-t)^{n-k} + t^k \cdot (1-t)^{n-k-1} \cdot (n-k) \cdot -1
\right )
\]
上式不易处理,因此打开括号:
\[
\cdots = \frac{kn!}{k!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k}
\]
现在技巧性的一步是将上式再次化为含二项式系数的形式需要得到形如“x!/y!(x-y)!”的项。如果得到关于*n*-1和*k*-1的项那么说明方向是对的。
\[
\begin{array}{l}
\cdots = \frac{n!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)n!}{k!(n-k)!} t^k (1-t)^{n-1-k} \\
\cdots = n \left (
\frac{(n-1)!}{(k-1)!(n-k)!} t^{k-1} (1-t)^{n-k} - \frac{(n-k)(n-1)!}{k!(n-k)!} t^k (1-t)^{n-1-k}
\right ) \\
\cdots = n \left (
\frac{(n-1)!}{(k-1)!((n-1)-(k-1))!} t^{k-1} (1-t)^{(n-1)-(k-1)} - \frac{(n-1)!}{k!((n-1)-k)!} t^k (1-t)^{(n-1)-k}
\right )
\end{array}
\]
这是第一步。上式括号里的两项其实为标准的、低一次的贝塞尔函数:
\[\begin{array}{l}
\cdots = n \left (
\frac{x!}{y!(x-y)!} t^{y} (1-t)^{x-y} - \frac{x!}{k!(x-k)!} t^k (1-t)^{x-k}
\right )
\textit{,其中}\ x=n-1 \textit{} y=k-1
\\
\cdots = n \left ( B_{n-1,k-1}(t) - B_{n-1,k}(t) \right )
\end{array}
\]
现在将上式应用于已有的加权贝塞尔函数。先写出之前所见的平面曲线公式,再逐步求出导数:
\[\begin{array}{lcl}
\textit{Bézier}_{n,k}(t) &=& B_{n,0}(t) \cdot w_0 + B_{n,1}(t) \cdot w_1 + B_{n,2}(t) \cdot w_2 + B_{n,3}(t) \cdot w_3 + \cdots \\
\frac{\mathrm{d}}{\mathrm{d}t} \textit{Bézier}_{n,k}(t) &=& n \cdot (B_{n-1,-1}(t) - B_{n-1,0}(t)) \cdot w_0 + {}\\
& & n \cdot (B_{n-1,0}(t) - B_{n-1,1}(t)) \cdot w_1 + {}\\
& & n \cdot (B_{n-1,1}(t) - B_{n-1,2}(t)) \cdot w_2 + {}\\
& & n \cdot (B_{n-1,2}(t) - B_{n-1,3}(t)) \cdot w_3 + {}\\
& & \cdots
\end{array}\]
如果打开上式的括号(用颜色表示相匹配的项),再按递增的*k*值重排各项,那么有:
\[\begin{array}{lclc}
n \cdot B_{n-1,-1}(t) \cdot w_0 &+& & \\
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 & + \\
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 & + \\
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 & + \\
\cdots &-& n \cdot B_{n-1,3}(t) \cdot w_3 & + \\
\cdots & & &
\end{array}\]
上式中有两项会消失掉:因为任意和式都没有第-1项所以上式第一项消失。既然这一项总是贡献为零那么求导时就可以放心地将其完全无视。消失的另外一项为展开式的最后一项——包含*B*<sub>*n*-1,*n*</sub>的一项。这一项含有二项式系数C<sub>*i*</sub><sup>*i*+1</sup>而这一系数通常约定等于0。因此这一项贡献为零也可被略去。这意味着剩下的项为
\[\begin{array}{lclc}
n \cdot B_{n-1,BLUE[0]}(t) \cdot w_1 &-& n \cdot B_{n-1,BLUE[0]}(t) \cdot w_0 &+ \\
n \cdot B_{n-1,RED[1]}(t) \cdot w_2 &-& n \cdot B_{n-1,RED[1]}(t) \cdot w_1 &+ \\
n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_3 &-& n \cdot B_{n-1,MAGENTA[2]}(t) \cdot w_2 &+ \\
\cdots
\end{array}\]
此即低次函数之和:
\[
\frac{\mathrm{d}}{\mathrm{d}t} \textit{Bézier}_{n,k}(t) = n \cdot B_{n-1,BLUE[0]}(t) \cdot (w_1 - w_0)
+ n \cdot B_{n-1,RED[1]}(t) \cdot (w_2 - w_1)
+ n \cdot B_{n-1,MAGENTA[2]}(t) \cdot (w_3 - w_2)
+ \cdots
\]
将上式改写为正常的和式即可:
\[
\frac{\mathrm{d}}{\mathrm{d}t} \textit{Bézier}_{n,k}(t) = \sum_{k=0}^{n-1} n \cdot B_{n-1,k}(t) \cdot (w_{k+1} - w_k)
= \sum_{k=0}^{n-1} B_{n-1,k}(t) \cdot \underbrace{n \cdot (w_{k+1} - w_k)}_{\textit{导数的权重}}
\]
</div>
将上式改写为与原式相似的形式有助于看出它们的区别。先写出原式,再写出导数:
\[
\textit{Bézier}(n,t) = \sum_{i=0}^{n}
\underbrace{\mathrm{C}_n^i}_{\textit{二项式系数项}}
\cdot\
\underbrace{(1-t)^{n-i} \cdot t^{i}}_{\textit{多项式项}}
\cdot\
\underbrace{w_i}_{\textit{权重}}
\]
\[
\textit{Bézier}'(n,t) = \sum_{i=0}^{k}
\underbrace{\mathrm{C}_k^i}_{\textit{二项式系数项}}
\cdot\
\underbrace{(1-t)^{k-i} \cdot t^{i}}_{\textit{多项式项}}
\cdot\
\underbrace{n \cdot (w_{i+1} - w_i)}_{\textit{导数的权重}}
\textit{,其中}\ k=n-1
\]
有什么区别?对于实际的贝塞尔曲线而言几乎没有区别!虽然次数降低了(从*n*次变为*n*-1次但是贝塞尔函数没有改变。唯一的真正的区别在于推导表示曲线的函数时权重如何变化。如果有A、B、C、D四个点那么导数有三个点二阶导数有两个点三阶导数有一个点
\[ \begin{array}{lllll}
B(n,t) & & w &= \{A,B,C,D\} \\
B'(n,t) & n = 3 & w' &= \{A',B',C'\} &= \{3 \cdot (B-A),\ 3 \cdot (C-B),\ 3 \cdot (D-C)\} \\
B''(n,t) & n = 2 & w'' &= \{A'',B''\} &= \{2 \cdot (B'-A'),\ 2 \cdot (C'-B')\} \\
B'''(n,t) & n = 1 & w''' &= \{A'''\} &= \{1 \cdot (B''-A'')\}
\end{array} \]
只要有多于一个权重即可运用这一方法。只剩一个权重时,下一步会出现*k*=0而贝塞尔函数的和式因为无项可加而化为零。因此二次函数没有二阶导数三次函数没有三阶导数更一般地有*n*次函数有*n*-1阶有意义的导数其更高阶导数为零。

View File

@@ -288,7 +288,7 @@ Excellent! Now we can form our new quadratic curve:
\end{bmatrix}
\]
***Brilliant***: if we want a subcurve from `t = 0` to `t = z`, we can keep the first coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that looks oddly similar to a [Bernstein polynomial](https://en.wikipedia.org/wiki/Bernstein_polynomial) of degree two. These new coordinates are actually really easy to compute directly!
**Brilliant**: if we want a subcurve from `t = 0` to `t = z`, we can keep the first coordinate the same (which makes sense), our control point becomes a z-ratio mixture of the original control point and the start point, and the new end point is a mixture that looks oddly similar to a [Bernstein polynomial](https://en.wikipedia.org/wiki/Bernstein_polynomial) of degree two. These new coordinates are actually really easy to compute directly!
Of course, that's only one of the two curves. Getting the section from `t = z` to `t = 1` requires doing this again. We first observe that in the previous calculation, we actually evaluated the general interval [0,`z`]. We were able to write it down in a more simple form because of the zero, but what we *actually* evaluated, making the zero explicit, was:
@@ -562,6 +562,6 @@ and
\end{bmatrix}
\]
So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix means we implicitly have the other: push the values of each row in the matrix ***Q*** to the right, with zeroes getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated" ***Q'***.
So, looking at our matrices, did we really need to compute the second segment matrix? No, we didn't. Actually having one segment's matrix means we implicitly have the other: push the values of each row in the matrix **Q** to the right, with zeroes getting pushed off the right edge and appearing back on the left, and then flip the matrix vertically. Presto, you just "calculated" **Q'**.
Implementing curve splitting this way requires less recursion, and is just straight arithmetic with cached values, so can be cheaper on systems where recursion is expensive. If you're doing computation with devices that are good at matrix multiplication, chopping up a Bézier curve with this method will be a lot faster than applying de Casteljau.

View File

@@ -562,6 +562,6 @@
\end{bmatrix}
\]
さて、これらの行列を見るに、後半部分の曲線の行列は本当に計算する必要があったのでしょうか?いえ、ありませんでした。片方の行列が得られれば、実はもう一方の行列も暗に得られたことになります。まず、行列 ***Q*** の各行の値を右側に寄せ、右側にあった0を左側に押しのけます。次に行列を上下に反転させます。これでたちまち ***Q'*** が「計算」できるのです。
さて、これらの行列を見るに、後半部分の曲線の行列は本当に計算する必要があったのでしょうか?いえ、ありませんでした。片方の行列が得られれば、実はもう一方の行列も暗に得られたことになります。まず、行列 **Q** の各行の値を右側に寄せ、右側にあった0を左側に押しのけます。次に行列を上下に反転させます。これでたちまち **Q'** が「計算」できるのです。
この方法で曲線の分割を実装すれば、再帰が少なくて済みます。また、数値のキャッシュを利用した単純な演算になるので、再帰の計算コストが大きいシステムにおいては、コストが抑えられるかもしれません。行列の乗算に適したデバイスで計算を行えば、ド・カステリョのアルゴリズムに比べてかなり速くなるでしょう。

View File

@@ -0,0 +1,567 @@
# 用矩阵分割曲线
另一种分割曲线的方法是利用贝塞尔曲线的矩阵表示。<a href="#matrix">矩阵一章</a>已经说明可以用矩阵乘法表示曲线,尤其是以下两种分别表示二次曲线和三次曲线的形式(为提高可读性,贝塞尔曲线的系数向量已被倒转次序):
\[
B(t) = \begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
B(t) = \begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
-3 & 3 & 0 & 0\\
3 & -6 & 3 & 0\\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]
假设要在点`t = z`处分割曲线得到两条新的(且显然更小的)贝塞尔曲线,则可以用矩阵表示和一点线性代数求出这两条贝塞尔曲线的坐标。首先将实际的“线上点”信息分离到新的矩阵乘法中:
\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
以及
\[
B(t) =
\begin{bmatrix}
1 & (z \cdot t) & (z \cdot t)^2 & (z \cdot t)^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2 & t^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0\\
0 & z & 0 & 0\\
0 & 0 & z^2 & 0\\
0 & 0 & 0 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-3 & 3 & 0 & 0 \\
3 & -6 & 3 & 0 \\
-1 & 3 & -3 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
\]
如果可以将这些矩阵组合成**[t值] · [贝塞尔矩阵] · [列矩阵]**的形式且前两项保持不变,那么右侧的列矩阵即为描述从`t = 0``t = z`的第一段新的贝塞尔曲线的坐标。利用线性代数的一些简单的法则可以很轻松地做到这一点。(如果不在乎推导过程,那么可以直接跳到方框底部得到结果!)
<div class="note">
## 推导新的凸包坐标
推导分割曲线后所得两段曲线的坐标要花上几步,而且曲线次数越高,花的工夫越多,因此先看二次曲线:
\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underbrace{\kern 2.25em Z \cdot M \kern 2.25em}_{\textit{将这一项……}}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\underbrace{ M \cdot M^{-1} \cdot Z \cdot M }_{\textit{变为这一项后……}}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\underbrace{ \kern 1.25em \cdot \kern 1.25em Q \kern 1.25em \cdot \kern 1.25em}_{\textit{……得到这一项!}}
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
以上变形可行是因为[*M* · *M*<sup>-1</sup>]为单位矩阵。这有点像在计算中让某项乘以x/x——不改变函数本身却可以将函数改写为更好处理的形式或者得到不同的分解。类似地将上式中的矩阵左乘以[*M* · *M*<sup>-1</sup>]不会影响整个式子,却可以将矩阵序列[某项 · *M*]变为[*M* · 某项],而这至关重要——如果知道了[*M*<sup>-1</sup> · *Z* · *M*]是什么,那就可以将其施加到已有坐标上得到一条二次贝塞尔曲线的标准矩阵表示(即[*T* · *M* · *P*])和表示从*t = 0*到*t = z*的曲线的一系列新坐标。计算如下:
\[
Q = M^{-1} \cdot Z \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\]
很好!现在得出新的二次曲线:
\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]
***非常好***——如果需要从`t = 0``t = z`的子曲线那么只需保持第一个坐标不变很合理控制点变为原有控制点和起点关于z的分比的平均点而且新的终点为平均点其比例与二次[伯恩斯坦多项式](https://en.wikipedia.org/wiki/Bernstein_polynomial)莫名地相似。这些新坐标其实非常容易直接计算得到!
当然这只是两条曲线中的一条,得到从`t = z``t = 1`的一段需要再算一次。首先注意到之前的计算其实是在一般的区间[0,*z*]上进行的。之所以可以写成更简单的形式是因为0为端点但将0显式地写出可知*真正*所计算的是:
\[
B(t) =
\begin{bmatrix}
1 & ( 0 + z \cdot t) & ( 0 + z \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
0 & z & 0 \\
0 & 0 & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
如果需要[*z*,1]区间,那么计算如下:
\[
B(t) =
\begin{bmatrix}
1 & ( z + (1-z) \cdot t) & ( z + (1-z) \cdot t)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\]
用同样的技巧左乘单位矩阵将`[某项 · M]`变为`[M · 某项]`
\[
Q' = M^{-1} \cdot Z' \cdot M =
\begin{bmatrix}
1 & 0 & 0 \\
1 & \frac{1}{2} & 0 \\
1 & 1 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & z & z^2 \\
0 & 1-z & 2 \cdot z \cdot (1-z) \\
0 & 0 & (1-z)^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
=
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\]
那么最终第二条曲线为:
\[
B(t) =
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot M \cdot Q \cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
M
\cdot
\left (
Q'
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\left (
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
\right )
\]
\[
=
\begin{bmatrix}
1 & t & t^2
\end{bmatrix}
\cdot
\begin{bmatrix}
1 & 0 & 0 \\
-2 & 2 & 0 \\
1 & -2 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]
***很好。***出现了与之前相同的情形保持最后一个坐标不变很合理控制点变为原有控制点和终点关于z的分比的平均点而且新的终点为平均点其比例与二次伯恩斯坦多项式莫名地相似只不过这次用的是z-1而不是1-z。这些新坐标*也*非常容易直接计算得到!
</div>
因此,不用德•卡斯特如算法而用线性代数可知在点`t = z`处分割一条二次曲线可得两条子曲线,它们均为用容易求得的坐标所描述的贝塞尔曲线:
\[
\begin{bmatrix}
1 & 0 & 0 \\
-(z-1) & z & 0 \\
(z - 1)^2 & -2 \cdot (z-1) \cdot z & z^2
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z - 1)^2 \cdot P_1
\end{bmatrix}
\]
\[
\begin{bmatrix}
(z-1)^2 & -2 \cdot z \cdot (z-1) & z^2 \\
0 & -(z-1) & z \\
0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3
\end{bmatrix}
=
\begin{bmatrix}
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z \cdot P_3 - (z-1) \cdot P_2 \\
P_3
\end{bmatrix}
\]
虽然三次曲线可以同理推导,但此处省略实际的推导过程(读者可自行写出)并直接展示所得的新坐标集:
\[
\begin{bmatrix}
1 & 0 & 0 & 0 \\
-(z-1) & z & 0 & 0 \\
(z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 & 0 \\
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
P_1 \\
z \cdot P_2 - (z-1) \cdot P_1 \\
z^2 \cdot P_3 - 2 \cdot z \cdot (z-1) \cdot P_2 + (z-1)^2 \cdot P_1 \\
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1
\end{bmatrix}
\]
\[
\begin{bmatrix}
-(z-1)^3 & 3 \cdot (z-1)^2 \cdot z & -3 \cdot (z-1) \cdot z^2 & z^3 \\
0 & (z-1)^2 & -2 \cdot (z-1) \cdot z & z^2 \\
0 & 0 & -(z-1) & z \\
0 & 0 & 0 & 1
\end{bmatrix}
\cdot
\begin{bmatrix}
P_1 \\ P_2 \\ P_3 \\ P_4
\end{bmatrix}
=
\begin{bmatrix}
z^3 \cdot P_4 - 3 \cdot z^2 \cdot (z-1) \cdot P_3 + 3 \cdot z \cdot (z-1)^2 \cdot P_2 - (z-1)^3 \cdot P_1 \\
z^2 \cdot P_4 - 2 \cdot z \cdot (z-1) \cdot P_3 + (z-1)^2 \cdot P_2 \\
z \cdot P_4 - (z-1) \cdot P_3 \\
P_4
\end{bmatrix}
\]
对于以上矩阵而言,是否真的有必要计算第二段的矩阵?其实不然。有了第一段的矩阵就意味着有了第二段的:将矩阵**Q**每行的非零值推到右侧,左侧的空位补零,再将矩阵上下翻转,**Q'**就“计算”出来了。搞定!
如此实现曲线分割需要的迭代更少,且只用缓存值直接进行四则运算,因此对于迭代耗费较大的系统更为划算。如果使用擅长矩阵操作的设备进行计算,那么用这种方法切割贝塞尔曲线会比使用德•卡斯特如算法快得多。

View File

@@ -0,0 +1,77 @@
# 切线与法线
如果要将物体沿曲线移动或者从曲线附近“移向远处”,那么与之最相关的两个向量为曲线的切向量和法向量,而这两者都非常容易求得。切向量用于沿曲线移动或者对准曲线方向,它标志着曲线在指定点的行进方向,而且就是曲线函数的一阶导数:
\[
\begin{matrix}
\textit{tangent}_x(t) = B'_x(t) \\
\\
\textit{tangent}_y(t) = B'_y(t)
\end{matrix}
\]
此即所需的方向向量。可以在每一点将方向向量规范化后得到单位方向向量即长度为1.0),再根据这些方向进行所需的操作:
\[
\begin{matrix}
d = \left \| \textit{tangent}(t) \right \| = \sqrt{B'_x(t)^2 + B'_y(t)^2} \\
\\
\hat{x}(t) = \left \| \textit{tangent}_x(t) \right \|
=\frac{\textit{tangent}_x(t)}{ \left \| \textit{tangent}(t) \right \| }
= \frac{B'_x(t)}{d} \\
\\
\hat{y}(t) = \left \| \textit{tangent}_y(t) \right \|
= \frac{\textit{tangent}_y(t)}{ \left \| \textit{tangent}(t) \right \| }
= \frac{B'_y(t)}{d}
\end{matrix}
\]
切向量对于沿曲线移动很有用,但如果要从曲线附近“移向远处”,而且移动方向与曲线在某点*t*处垂直,那该怎么办?这时需要的是*法*向量。这一向量与曲线的方向保持垂直且长度通常为1.0,因此只需旋转单位方向向量即可:
\[
\begin{array}{l}
\textit{normal}_x(t) = \hat{x}(t) \cdot \cos{\frac{\pi}{2}} - \hat{y}(t) \cdot \sin{\frac{\pi}{2}} = - \hat{y}(t) \\
\\
\textit{normal}_y(t) = \underbrace{ \hat{x}(t) \cdot \sin{\frac{\pi}{2}} + \hat{y}(t) \cdot \cos{\frac{\pi}{2}} }_{90^\circ \textit{旋转}} = \hat{x}(t)
\end{array}
\]
<div class="note">
其实旋转坐标只要知道方法就非常简单——“施加[旋转矩阵](https://en.wikipedia.org/wiki/Rotation_matrix)”以下即采用这种方法。本质上这一做法是先选取用于旋转的圆再将坐标沿着圆“滑动所需的角度”。如果需要转动90度那么将坐标沿着圆滑动90度即可。
为了将点(*x*,*y*)(绕(0,0))旋转*φ*度得到点(*x'*,*y'*),可以使用以下简洁的计算式:
\[\begin{array}{l}
x' = x \cdot \cos(\phi) - y \cdot \sin(\phi) \\
y' = x \cdot \sin(\phi) + y \cdot \cos(\phi)
\end{array}\]
对应“短”版本的矩阵变换为:
\[
\begin{bmatrix}
x' \\ y'
\end{bmatrix}
=
\begin{bmatrix}
\cos(\phi) & -\sin(\phi) \\
\sin(\phi) & \cos(\phi)
\end{bmatrix}
\begin{bmatrix}
x \\ y
\end{bmatrix}
\]
注意对于90度、180度和270度旋转因为这些角度的*正弦*和*余弦*分别为0和1、-1和0、0和-1所以上式可以更简。
但是***为什么***可以这样做?为什么用这一矩阵乘法?这是因为旋转变换可以表示为三个(初等)剪切变换的复合,而将三个变换合成一个变换(因为所有矩阵变换都可以复合)即得上述矩阵表示。[DataGenetics](https://datagenetics.com/blog/august32013/index.html)对此进行了很好的解释,非常推荐读者一读。
</div>
以下两图展示了二次和三次贝塞尔曲线在各点的切线和法线,其中蓝色的为方向向量,红色的为法向量(标记按*t*值的等分区间放置,并非等距放置)。
<div class="figure">
<graphics-element title="二次贝塞尔曲线的切线和法线" src="./pointvectors.js" data-type="quadratic"></graphics-element>
<graphics-element title="三次贝塞尔曲线的切线和法线" src="./pointvectors.js" data-type="cubic"></graphics-element>
</div>

View File

@@ -0,0 +1,110 @@
# 三维法向量
在进入下一章之前需要花点时间探究二维和三维的区别。尽管这一区别影响的范围不大无关且两种情形的做法相同比如求三维切向量与二维情形所做的一样不过所求为x、y、z而不只是x、y但是法向量的情况有点复杂所做的也就更多。尽管不是“极其困难”但是所需的步骤更多需要仔细看看。
三维法向量的求法原则上与二维一样——将规范化的切向量旋转90度。然而这就是情况变得略微复杂的地方因为三维的“法向量”是法平面上的任意一个向量所以可以旋转的方向并不唯一因此需要定义三维情形中“唯一的”法向量是什么。
“朴素”的方法是构造[弗勒内法向量](https://en.wikipedia.org/wiki/Frenet%E2%80%93Serret_formulas)而以下采用的简单做法在很多情况下都可行但在其他情况下会得到极其怪异的结果。思路是虽然有无穷多个向量与切向量垂直即与之成90度角但是切向量本身已差不多位于自带的平面上——因为曲线上的每一点无论间隔多小都有自己的切向量所以可以说每个点都位于此处的切向量和“近旁”的切向量所在的平面上。
即使这两个切向量的差微乎其微只要“有差”就可求出这一平面或者说求出垂直于平面的向量。计算出这一向量之后因为切向量在平面上所以将切向量绕垂直向量旋转即可。计算这一法向量的逻辑与二维情形相同——“直接旋转90度”。
那么开始吧!令人意外的是四行就做完了:
- **a** = normalize(*B'*(*t*))
- **b** = normalize(**a** + *B''*(*t*))
- **r** = normalize(**b** × **a**)
- **normal** = normalize(**r** × **a**)
展开说几句:
- 先将曲线上一点的导数规范化得到[单位向量](https://en.wikipedia.org/wiki/Unit_vector)。规范化可以减少计算量,而计算量越少越好。
- 再计算**b**。假如曲线从这个点开始不再变化,保持导数和二阶导数不变,则**b**表示下一个点处的切向量。
- 得到两个共面向量后(导数、导数与二阶导数的和),用[叉积](https://en.wikipedia.org/wiki/Cross_product)这一基本的向量运算可以求出与这一平面垂直的向量。注意这一运算使用的符号×绝非乘法运算叉积所得向量可以当做“旋转轴”像二维情形一样将切向量旋转90度得到法向量。
- 既然由叉积可得垂直于由两个向量所确定的平面的另一向量,而法向量又与切向量和旋转轴所在平面垂直,那么再用一次叉积即得法向量。
这样就求出了三维曲线“唯一”的法向量。以一条曲线为例看看效果如何?从左往右拖动滚动条,根据鼠标位置所确定的*t*值显示在此处的法向量——最左为0最右为1中间为0.5,等等:
<graphics-element title="一些已知和未知向量" width="350" height="300" src="./frenet.js">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
然而摆弄图像一阵之后可能会察觉到异样——法向量似乎在t=0.65和t=0.75之间“绕着曲线急转弯”……为什么会这样?
其实出现这种现象是因为数学公式就是这样推导的,所以弗勒内法向量的问题就在于此:虽然“从数学上看”是对的,但是“从实际上看”有问题。因此为了让任何图像都不出问题,所真正需要的是只要……看起来不错就好的方法。
还好不只有弗勒内法向量这一种选择。另一种选择是采用稍微偏算法的方式计算一种形式的[旋转最小化标架](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf)(亦称“平行输运标架”或“比舍标架”),此处“标架”是以线上点为原点,由切向量、旋转轴和法向量构成的集合。
因为计算这种类型的标架依赖于“上一个标架”,所以无法像弗勒内标架一样“按需”对单独的点直接计算,而是需要对整条曲线进行计算。好在计算过程相当简单,而且可以与曲线查询表的构建同时进行。
思路是在t=0处取一个由切向量、旋转轴、法向量构成的初始标架再使用一定的规则计算下一标架“应有”的形式。上文链接的旋转最小化标架论文给出的规则为
- 取曲线上已经求出旋转最小化标架的一个点,
- 取曲线上尚未求出旋转最小化标架的下一个点,
- 再以上一个点和下一个点的中垂面为镜面,将已有标架翻转到下一个点上。
- 翻转后的切向量方向与下一个点的切向量方向大致相反,而且法向量也略有歪斜。
- 于是再以翻转后的切向量和下一个点的切向量所确定的平面为镜面,将翻转后的标架再次翻转。
- 切向量和法向量修正完毕,所得即为好用的标架。
来写点代码吧!
<div class="howtocode">
### 实现旋转最小化标架
首先假设已有函数用于计算上文提及的指定点的弗勒内标架,输出的标架具有如下性质:
```
{
o所有向量的起点即线上点
t切向量
r旋转轴向量
n法向量
}
```
再如下写出生成一系列旋转最小化标架的函数:
```
generateRMFrames(steps) -> frames:
step = 1.0/steps
// 从曲线上t=0处标准的切向量/旋转轴/法向量标架开始:
frames.add(getFrenetFrame(0))
// 开始构造旋转最小化标架:
for t0 = 0, t0 < 1.0, t0 += step:
// 从已有的上一标架开始
x0 = frames.last
// 求出下一标架:要保留它的位置和切向量,
// 但要重新计算轴向量和法向量。
t1 = t0 + step
x1 = { o: getPoint(t1), t: getDerivative(t) }
// 首先以x0和x1的中垂面为镜面
// 将x0的切向量和旋转轴翻转到x1处
v1 = x1.o - x0.o
c1 = v1 · v1
riL = x0.r - v1 * 2/c1 * v1 · x0.r
tiL = x0.t - v1 * 2/c1 * v1 · x0.t
// 注意到v1为向量而2/c1和v1 · ……就是数,
// 因此上述代码只是对v1进行一定倍数的缩放。
// 然后再以过x1的平面为镜面再次翻转
// 标架向量的方向即重新与曲线切向量一致:
v2 = x1.t - tiL
c2 = v2 · v2
// 收尾工作:
x1.r = riL - v2 * 2/c2 * v2 · riL
x1.n = x1.r × x1.t
frames.add(x1)
```
即使忽略注释,代码也明显比计算单个弗勒内标架的多,但也没有多得离谱,而且得到了长得更好的法向量。
</div>
提到长得更好,这样的标架到底是什么样子?下面回顾之前的那条曲线,但这次用的不是弗勒内标架而是旋转最小化标架:
<graphics-element title="一些已知和未知向量" width="350" height="300" src="./rotation-minimizing.js">
<input type="range" min="0" max="1" step="0.01" value="0" class="slide-control">
</graphics-element>
看起来好多了!
给看过代码的读者的话严格来说一开始甚至不需要弗勒内标架。比方说可以将z轴当作初始旋转轴于是初始法向量为 **(0,0,1)** × **切向量**,然后再继续下去。不过求出“数学上正确”的初始标架,从而让初始法向量的方向符合曲线在三维空间中的定向,这总归是不错的。

View File

@@ -0,0 +1,138 @@
# 曲线的升次与降次
贝塞尔曲线有一个有意思的性质——*n*阶曲线总可通过给出*n+1*阶曲线对应的控制点而用高一阶的曲线精确表示。
如果有一条二次曲线那么可以如下构造三次曲线精确重现原来的曲线首先选择相同的起点和终点然后两个控制点分别选为“1/3*起点+2/3*终点”和“2/3*起点+1/3*终点”。所得曲线与原来的相同,只不过表示为了三次曲线而不是二次曲线。
将*n*次曲线升为*n+1*次曲线的一般规则如下(注意起点和终点的权重与旧曲线的相同):
\[
\textit{Bézier}(k,t) = \sum_{i=0}^{k}
\underbrace{\mathrm{C}_k^i}_{\textit{二项式系数项}}
\cdot\
\underbrace{(1-t)^{k-i} \cdot t^{i}}_{\textit{多项式项}}
\ \cdot \
\underbrace{\left ( \frac{(k-i) \cdot w_i + i \cdot w_{i-1}}{k} \right )}_{\textit{新权重}}
\textit{,其中}\ k=n+1 \textit{} w_{-1}=0
\]
然而这一规则也直接意味着通常**无法**将*n*次曲线稳妥地降到*n-1*次,这是因为控制点无法被简洁地“拆开”。可以做些尝试,但所得曲线不会与原曲线重合,而且其实还可能看起来完全不同。
不过有一种好得出人意料的方法可以保证低次曲线看起来与原曲线“尽可能地接近”——用仅仅一次操作优化低次曲线与原曲线之间的“最小二乘法距离”([Sirver's Castle](https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/)中亦有解释),但是为了用上这种方法,需要先做些变形再转用线性代数。正如矩阵表示一章所言,有些东西用矩阵去做比用函数方便得多,而这就是一例。那么……开始吧!
先将标准的贝塞尔函数写得紧凑一些:
\[
\textit{Bézier}(n,t)
=
\sum_{i=0}^{n} w_i B^n_i(t)
\textit{,其中}\
B^n_i(t)
=
\mathrm{C}_n^i \cdot (1-t)^{n-i} \cdot t^{i}
\]
然后用一个朴素(其实极其有用)的变形技巧:既然`t`值总在0到1之间含端点`1-t``t`恒等于1那么任何数都可表示为`t``1-t`的和:
\[
x = 1 x = \left ( (1-t) + t \right ) x = (1-t) x + t x = x (1-t) + x t
\]
于是用这一看似平凡的性质可将贝塞尔函数拆分为`1-t``t`两部分之和:
\[
\begin{aligned}
\textit{Bézier}(n,t) &= (1-t) B(n,t) + t B(n,t) \\
&= \sum_{i=0}^{n} w_i (1 - t) B^n_i(t) + \sum_{i=0}^{n} w_i t B^n_i(t)
\end{aligned}
\]
目前一切顺利。现在为了理解为什么这么做,将`1-t``t`两部分具体写出并观察结果。首先是`1-t`
\[
\begin{aligned}
(1 - t) B^n_i(t) &= (1-t) \frac{n!}{(n-i)!i!} (1-t)^{n-i} t^i \\
&= \frac{n+1-i}{n+1} \frac{(n+1)!}{(n+1-i)!i!} (1-t)^{n+1-i} t^i \\
&= \frac{k-i}{k} \frac{k!}{(k-i)!i!} (1-t)^{k-i} t^i\text{,其中}\ k = n + 1\\
&= \frac{k-i}{k} B^k_i(t)
\end{aligned}
\]
用这一看似朴素的技巧瞬间就将n次贝塞尔函数的一个部分用n+1次贝塞尔函数表示出来了这非常像曲线升次当然`t`的部分也要表示出来,但这不是问题:
\[
\begin{aligned}
t B^n_i(t) &= t \frac{n!}{(n-i)!i!} (1-t)^{n-i} t^i \\
&= \frac{i+1}{n+1} \frac{(n+1)!}{((n+1)-(i+1))!(i+1)!} (1-t)^{(n+1)-(i+1)} t^{i+1} \\
&= \frac{i+1}{k} \frac{k!}{(k-(i+1))!(i+1)!} (1-t)^{k-(i+1)} t^{i+1}\text{,其中}\ k = n + 1 \\
&= \frac{i+1}{k} B^k_{i+1}(t)
\end{aligned}
\]
`n`次的表达式变为`n+1`次的之后再将其重新合并。虽然`n`次函数是从0到`n`求和,`n+1`次函数是从0到`n+1`求和,但补上“贡献为零”的项即可。下一章“导数”会论述为什么“没有对应的二项式系数的更高次项”和“低于零次的项”都“贡献为零”,因此需要什么形式的项就可以加上什么项。将这些项包含在和式中没有影响,而所得函数与低次曲线依然相等:
\[
\begin{aligned}
Bézier(n,t) &= \sum_{i=0}^{n+1} w_i (1 - t) B^n_i(t) + \sum_{i=0}^{n+1} w_i t B^n_i(t) \\
&= \sum_{i=0}^{n+1} w_i \frac{k-i}{k} B^k_i(t) + \sum_{i=0}^{n+1} w_i \frac{i+1}{k} B^k_{i+1}(t)\textit{,其中}\ k = n + 1 \\
&= \sum_{i=0}^{n+1} w_i \frac{k-i}{k} B^k_i(t) + \sum_{i=0}^{n+1} p_{i-1} \frac{i}{k} B^k_i(t) \\
&= \sum_{i=0}^{n+1} \left ( w_i \frac{k-i}{k} + p_{i-1} \frac{i}{k} \right ) B^k_i(t) \\
&= \sum_{i=0}^{n+1} \left ( w_i (1-s) + p_{i-1} s \right ) B^k_i(t)\textit{,其中}\ s = \frac{i}{k}
\end{aligned}
\]
接下来从变形转到线性代数矩阵——现在Bézier(n,t)和Bézier(n+1,t)之间的关系可用非常简单的矩阵乘法表示:
\[
M B_n = B_k
\]
其中矩阵**M**为`(n+1)×n`阶的矩阵,其形如:
\[
M =
\left [
\begin{matrix}
1 & 0 & . & . & . & . & . & . \\
\frac{1}{k} & \frac{k-1}{k} & 0 & . & . & . & 0 & . \\
0 & \frac{2}{k} & \frac{k-2}{k} & 0 & . & . & . & . \\
. & 0 & \frac{3}{k} & \frac{k-3}{k} & 0 & . & . & . \\
. & . & 0 & ... & ... & 0 & . & . \\
. & . & . & 0 & ... & ... & 0 & . \\
. & . & . & . & 0 & \frac{n-1}{k} & \frac{k-n+1}{k} & 0 \\
. & 0 & . & . & . & 0 & \frac{n}{k} & \frac{k-n}{k} \\
. & . & . & . & . & . & 0 & 1
\end{matrix}
\right ]
\]
这虽然看似庞杂,但真的只是几乎全为零的矩阵,而且对角线上为很简单的分数,其左侧为更简单的分数。这意味着将一列坐标乘以这一矩阵,再将所得变形之后的坐标代入高一次的函数即可得到与原曲线一模一样的曲线。
还不错!
同样有意思的一点是,在建立这一矩阵操作之后即可利用非常强大又极其简单的方法求出“最优拟合”倒转操作——即[法方程组](https://mathworld.wolfram.com/NormalEquation.html),这种方法将一组数与另一组数的平方差之和最小化。具体而言,对于超定方程组**A x = b**,利用法方程组可以求出使方程两侧之差长度最小的`x`。既然现在面临的问题即为如此,那么:
\[
\begin{aligned}
M B_n &= B_k \\
(M^T M) B_n &= M^T B_k\\
(M^T M)^{-1} (M^T M) B_n &= (M^T M)^{-1} M^T B_k \\
I B_n &= (M^T M)^{-1} M^T B_k \\
B_n &= (M^T M)^{-1} M^T B_k
\end{aligned}
\]
其中的步骤为:
1. 既然有一个具有法方程组可以处理的形式的方程组,那么
2. 使用法方程组!
3. 然后因为左侧只需保留B<sub>n</sub>所以在两侧左乘矩阵使左侧的很多东西化为“因数1”在矩阵语言中即为[单位矩阵](https://en.wikipedia.org/wiki/Identity_matrix))。
4. 具体而言,左乘左侧已有项的逆可以将这个庞大的矩阵约简为单位矩阵**I**。于是将这一大堆替换为**I**,然后
5. 因为矩阵与单位矩阵相乘不会发生变化就像在四则运算中数与1相乘不会发生变化所以略去单位矩阵。
此即用`n`次曲线逼近`n+1`次曲线的表达式。这虽然不是精确拟合,但却是非常好的近似。下图对一条(半)随机的曲线实现了这些升次和降次的规则,图上的控制点可以移动,点击按钮可以升高或降低曲线的次数。
<graphics-element title="可变次数贝塞尔曲线" src="./reorder.js">
<button class="raise">升次</button>
<button class="lower">降次</button>
</graphics-element>

View File

@@ -17,8 +17,8 @@
\right ),~\textit{our new point} = p_1 + \textit{distance} \cdot \textit{ratio}
\]
让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线
让我们来通过实际操作看一下:下面的图形都是可交互的,因此你可以通过上下键来增加或减少插值距离,来观察图形的变化。我们从三个点构成的两条线段开始。通过对各条线段进行线性插值得到两个点,对点之间的线段再进行线性插值,产生一个新的点。最终这些点——所有的点都可以通过选取不同的距离插值产生——构成了贝塞尔曲线
<graphics-element title="Linear Interpolation leading to Bézier curves" width="825" src="./interpolation.js">
<input type="range" min="10" max="90" step="1" value="25" class="slide-control">
</graphics-element>