Curves
提前提醒,这篇笔记偏向本质理解而不是公式推导,对于指导整体理解的公式我会推一遍,但是具体到某个起调节作用的局部公式,无论其复杂与否我都都只说输入输出和公式设计目的。
一、曲线概览
1.1 图形学中的曲线表示方法
从用户的角度来讲,如果模拟物体表面曲线,又或者是光滑运动轨迹等这些方面就是曲线发挥作用的地方。图形学中对于曲线近似的核心思想其实来自微积分:不断分割,具体到每个小区间以简代繁。
但是就实际情况来说,计算机是不可能无限分割的,更不可能以直代曲,这两个互为因果,因为你只有在无限分割的情况下直线才能近似曲线,如果是有限分割,你用直线近似曲线就会变得全是棱角,没有一点光滑性可言。
所以说,图形学一般选用三次曲线(Cubic)去进行每段区间的模拟,因为Cubic是一个在光滑性和复杂度之间很好的Trade-off,同时虎书没有提到的一点就是,如果你用高次曲线,会产龙格现象,也就是曲线过于陡峭,而且对于控制点的变化及其敏感。
总之呢,曲线近似的输入就是一堆控制点,输出就是控制点控制下的一条曲线。
1.2 这种方法引起的问题
对于我们来讲,我们希望去曲线能够满足以下四个特质:
- 每一段都是Cubic
- 整条曲线(主要是段和段之间的连接点)C2连续
- 当其中一个控制点改变时,不会影响到整条曲线,也就是局部性
- 曲线要穿过我们输入的控制点
这四个特性都不难理解,也是我们想要让曲线满足的,但可惜的是,数学上来讲,最多只能满足其中三个,所以不同的应用需求之下,我们需要用不同的曲线近似方法,对这个四个条件进行取舍。
1.3 曲线(Cubic)的表示
一个Cubic的一般公式形式是:
f(x)=a0+a1⋅x+a2⋅x2+a3⋅x3 f(x) = a_0 + a_1\cdot x + a_2\cdot x^2 + a_3\cdot x^3 f(x)=a0+a1⋅x+a2⋅x2+a3⋅x3
但是你是三维空间的点,所以xyz都要一个方程,也就是:
f(x)=ax0+ax1⋅x+ax2⋅x2+ax3⋅x3f(y)=ay0+ay1⋅y+ay2⋅y2+ay3⋅y3f(z)=az0+az1⋅z+az2⋅z2+az3⋅z3 f(x) = a_{x0} + a_{x1}\cdot x + a_{x2}\cdot x^2 + a_{x3}\cdot x^3\\ f(y) = a_{y0} + a_{y1}\cdot y + a_{y2}\cdot y^2 + a_{y3}\cdot y^3\\ f(z) = a_{z0} + a_{z1}\cdot z + a_{z2}\cdot z^2 + a_{z3}\cdot z^3 f(x)=ax0+ax1⋅x+ax2⋅x2+ax3⋅x3f(y)=ay0+ay1⋅y+ay2⋅y2+ay3⋅y3f(z)=az0+az1⋅z+az2⋅z2+az3⋅z3
可以设u⃗T=(x,y,z)\vec u^T = (x,y,z)u T=(x,y,z),a⃗0=(ax0,ay0,az0)T\vec a_0 = (a_{x0},a_{y0},a_{z0})^Ta 0=(ax0,ay0,az0)T,其他a⃗1\vec a_1a 1以及a⃗2\vec a_2a 2同理,注意u⃗T\vec u^Tu T我专门加个转置就是为了提醒你他是行向量,别忘了。上述方程组用矩阵可以表示为:
f(u)=(1uu2u3)⋅(a⃗0a⃗1a⃗2a⃗3)=u⃗T⋅A(1) f(u) = \begin{pmatrix} 1 & u & u^2 & u^3 \end{pmatrix} \cdot \begin{pmatrix} \vec a_0 \\ \vec a_1 \\ \vec a_2 \\ \vec a_3\end{pmatrix} = \vec{u}^T \cdot A \quad \quad (1) f(u)=(1uu2u3)⋅ a 0a 1a 2a 3 =u T⋅A(1)
显而易见的是,四个系数需要四个约束带入才能解得,可是这里有一个问题,就是自变量uuu的取值。
一般来讲,我们希望曲线的方程能够直观的控制和反应我的几个直觉,也就是f(0)f(0)f(0)是曲线的起点,f(1)f(1)f(1)是曲线的终点,自变量uuu是一个关于曲线整体的参数,u∈[0,1]u \in [0,1]u∈[0,1],在零到一的采样会直接对应到曲线上,方便操作。
现在,我们要求出f(u)f(u)f(u)的具体表达式,当然,你可以选择用四个曲线上点作为你的四个约束,但是为了边界的平滑度处理,我们一般会用边界点的导数作为两个约束,实际的图解如下:

也就是:
f(0)=p0f(1)=p3f′(0)=3(p1−p0)f′(1)=3(p3−p2) f(0) = p_0 \\ f(1) = p_3 \\ f'(0) = 3(p_1 - p_0) \\ f'(1) = 3(p_3 - p_2) f(0)=p0f(1)=p3f′(0)=3(p1−p0)f′(1)=3(p3−p2)
关于那个3怎么来的,我不做详细解释,就明白是为了消除几何空间的模长到代数空间的误差即可。上述方程组用矩阵表示为:
C⋅A=P(2) C \cdot A = P \quad \quad (2) C⋅A=P(2)
其中C就是原方程组的系数矩阵,PPP是控制点相关的矩阵。设B=C−1B = C^{-1}B=C−1,可求得A=B⋅PA = B \cdot PA=B⋅P ,也就是fff可以写成:
f(u)=u⃗T⋅B⋅P(3) f(u) = \vec u^T \cdot B\cdot P \quad \quad(3) f(u)=u T⋅B⋅P(3)
上式展开后可转化为:
f(u)=∑i=1nbi(u)⋅pi(4) f(u) = \sum_{i=1}^{n}b_i(u)\cdot p_i \quad\quad(4) f(u)=i=1∑nbi(u)⋅pi(4)
其中bi(u)b_i(u)bi(u)我们叫做基函数,pip_ipi就是控制点,也就是说,插值后的曲线实际上对控制点的加权平均。记住这个思想,这个思想就是图形学中曲线近似的本源。
二、每段曲线的表示
Natural Cubic就是过四个点往出来算曲线。
Hermite Cubic就是1.3节计算曲线的方法。
Cardinal Cubic在Hermite Cubic基础上了加了一个tension控制参数,用来进一步控制曲线的形状,我不展开细讲了。
三、曲线近似
曲线近似和曲线表示的区别归根到底就是是否连接在一起的区别,就和1.1说的一样,每段之内用Cubic表示,然后整体连接近似。
不过,这个思路是出发点,最终的归宿是式4,也就是用控制点加群平均来计算曲线上的点,之后的B-spline就会完全脱离"连接游戏",我们现在先看Bezier Curve。
3.1 Bezier Spline
Bezier Spline其实就是把Hermite Cubic连起来,不过我们一般不说Hermite Cubic而是直接说Bezier Curve,因为前者属于数学原理,后者才是真正使用的方法。
可是,你如果连接Bezier Spline,在连接点处无法同时保证C2连续和局部性,只能保证C1连续且具有局部性。
3.1.1 为什么Bezier Spline只能保证C1连续
先说明为什么C1连续能保住局部性。
从第一段曲线开始说起,其实就是:
f(0)=p0f(1)=p3f′(0)=3(p1−p0)f′(1)=3(p3−p2) f(0) = p_0 \\ f(1) = p_3 \\ f'(0) = 3(p_1 - p_0) \\ f'(1) = 3(p_3 - p_2) f(0)=p0f(1)=p3f′(0)=3(p1−p0)f′(1)=3(p3−p2)
和之前的没啥区别,因为第一段曲线"自由",你可能理解不了为什么说他自由,咱们继续往下看第二段曲线。为了保证端点处C1的连续性,它在连接点p3的斜率其实是不自由的,必须等于第一段曲线在该点处的斜率,如下图所示:

也就是说,p2,p3,p4三点必须共线,这就导致第二段曲线的方程变为:
f(0)=p3f(1)=p6f′(0)=3(p4−p3)=3(p3−p2)f′(1)=3(p6−p5) f(0) = p_3 \\ f(1) = p_6 \\ f'(0) = 3(p_4 - p_3)= 3(p_3 - p_2)\\ f'(1) = 3(p_6 - p_5) f(0)=p3f(1)=p6f′(0)=3(p4−p3)=3(p3−p2)f′(1)=3(p6−p5)
发现了吗?其实p4被牺牲掉了,因为你需要满足p3点的C1连续,那就必须用到p2点而不是p4点来决定斜率,我图上画的p4点属于p4点和p2,p3在输入时就共线的情况,但是如果p4不和p2,p3共线,Bezier也会为了C1连续牺牲p4。为了好理解,我们下文解释为p4依赖p2。
很明显,第二段曲线它的形状居然取决于p2这个第一段曲线的控制点,所以我们说它"不自由",这个说法只是我原创的,不是严格的术语。这也是产生非局部性的根源,不过这个情况在C1连续还好,试着让p2改变位置,看看会发生什么:
- p2改变位置,第一段曲线取决于:p1,p2,p3,p4,所以产生形变
- 因为p4依赖p2,所以p4改变位置,第二段曲线取决于:p3,p4,p5,p6,所以形变
- 第三段曲线取决于:p6,p7,p8,p9,其中p7依赖p5,但是p5并不改变,所以曲线的变化到这里截止,并不会往下传递。
理解了这一段,再看为什么不能保证局部性就好理解多了,我直接贴出C2连续时,第二段曲线的方程(具体二阶导怎么算到那我不解释了,重点在原理上):
f(0)=p3f(1)=p6f′′(0)=3(p3−p2)f′′(0)=6(p3−2p2+p1)=6(p3−2p4+p5) f(0) = p_3 \\ f(1) = p_6 \\ f''(0) = 3(p_3 - p_2) \\ f''(0) = 6(p_3 - 2p_2 + p_1) = 6(p_3 - 2p_4 +p_5) f(0)=p3f(1)=p6f′′(0)=3(p3−p2)f′′(0)=6(p3−2p2+p1)=6(p3−2p4+p5)
现在看如果p2变了会发生什么:
- p2改变位置,第一段曲线取决于:p1,p2,p3,p4,所以产生形变
- 因为p4,p5依赖p2,所以p4,p5改变位置,第二段曲线取决于:p3,p4,p5,p6,所以形变y
- 同理,因为p7,p8依赖p5,所以p7,p8改变位置,第三段曲线取决于:p6,p7,p8,p9,所以形变
如此循环往复,改了一个p2,就像多米诺骨牌一样把整条曲线都改变了。
3.1.2 Bezier曲线的应用场合
如上所述,Bezier曲线在要求高阶连续的场合是不会使用的,因为会直接丧失局部性,这对需要调整细节的艺术家/工程师来说都是一个痛苦。但是,Bezier胜在简单直观,四个点就能拉出一条光滑的曲线,拉点就是拉斜率,所以一些绘图软件等不太要求高阶连续的场合就是第一选择。
3.2 B-Spline
3.2.1 基本思想
B-Spline就是为了解决局部性和C2连续的同时满足而诞生的,但是正如之前所说,1.2节那四个条件满足了三个就不会有第四个,B-Spilne并不保证穿过控制点这个条件。
它的基本思想和数学模型是利用滑动窗口修改对于当前生效的控制点集合,B-Spline里面其实是没有像是Bezier那种第一段曲线,第二段曲线的分别的,他不玩"拼接游戏",因为他没有分段,他是在做淡入淡出。
比如,"第一段曲线"的控制点是p1,p2,p3,p4,在"第一段曲线"向"第二段曲线"过渡的过程中,p1的权重会逐渐降为0,当p1权重降为0之后,p5的权重会开始增加,"第二段曲线"的控制点就是p2,p3,p4,p5,这种数学模型天然具有连续性,如果你感觉抽象的不可信,之后会有具体的连续性的数学推导,现在先理解思想。
关于局部性,B-Spline也能很好保证,因为每个控制点的作用区间都是有限的,当他过了自己的作用区间,权值归零就不再对曲线施加影响。
3.2.2 为什么能保证C2连续
B-Spline的曲线实际上就是:
f(u)=∑i=1nbi(u)⋅pi f(u) = \sum_{i=1}^{n}b_i(u)\cdot p_i \quad\quad f(u)=i=1∑nbi(u)⋅pi
在这里我不会涉及到基函数求解的那个公式,专注于整体思想的理解,对于基函数只需要知道bi(u)b_i(u)bi(u)是关于参数uuu的三次函数即可。我知道很多书籍包括虎书他关于B-Spline的基函数这么写:bi,kb_{i,k}bi,k,这个意思是多项式为k-1阶情况下,第i个控制点的基函数。这里因为我只讨论三次多项式,k=4,所以我就不写第二个下标了。
为什么会有k呢?因为k=d+1,d是多项式的阶数,在Cubic情况下就是3,而k=d+1=4是在说确定一个Cubic需要四个控制点,这个k就是控制点滑动窗口的大小。
那么,关于连续性其实看第一组控制点控制的曲线和第二组控制点控制的曲线"交接"时的微观性态就好,其他就是类似其实。
第一组控制点控制的曲线结束时:
f(u)=b1p1+b2p2+b3p3+b4p4 f(u) = b_1p_1 + b_2p_2 + b_3p_3 + b_4p_4 f(u)=b1p1+b2p2+b3p3+b4p4
这个式子其实就是一个Cubic,因为bi(u)b_i(u)bi(u)是关于参数uuu的三次函数。此时的微观形态来讲,b1→0b_1\to0b1→0,所以这个式子的左极限就是:
f(u)=b2p2+b3p3+b4p4 f(u) = b_2p_2 + b_3p_3 + b_4p_4 f(u)=b2p2+b3p3+b4p4
第二组控制点控制的曲线开始时:
f(u)=b2p2+b3p3+b4p4+b5p5 f(u) = b_2p_2 + b_3p_3 + b_4p_4 +b_5p_5 f(u)=b2p2+b3p3+b4p4+b5p5
此时的微观形态来讲,b5→0b_5\to0b5→0,所以这个式子的右极限就是:
f(u)=b2p2+b3p3+b4p4 f(u) = b_2p_2 + b_3p_3 + b_4p_4 f(u)=b2p2+b3p3+b4p4
看到了吗?两边极限相等,而且在这一点处的函数是关于u的三阶多项式,它一定三阶可导,二阶导相等。
3.2.3 节点控制的滑动窗口
一直都在说B-Spline是基于滑动窗口来设计控制点生效的,那么那个滑动窗口到底怎么设计呢?首先要明确一件事:B-Spline里面节点(knot)和控制点(control point)是两个不同的概念。
控制点就是我们一直理解的Cubic四个约束的来源,但是节点是指负责滑动窗口功能的节点向量里的元素。我知道很绕,但是咱们从设计目的来讲讲为什么要有几点。
一个常识就是你确定一段Cubic是需要四个控制点的,那么,就会出现这种情况:
- 第一组控制点:p1 p2 p3 p4
- 第二组控制点:p2 p3 p4 p5
- 第三组控制点:p3 p4 p5 p6
- 第四组控制点:p4 p5 p6 p7
可以看出,p4的生命周期是完整的,因为他控制了"四段"曲线,那么但是前三个控制点就不是,他们半残了,这个问题严重吗?说不严重也不严重,但是为了曲线光滑度,我们最好还是让他们生命周期完整为好。现在来看节点向量为啥引入节点的概念就很清晰了,就是要保证节点生命周期的完整。
对于节点控制向量v⃗\vec vv 来说,前k个节点(这里k=4)都是0,第k+i个节点保存的值vi+kv_{i+k}vi+k意为:第i个控制点的权重结束参数值,第i个控制点权重区间为[vi,vi+k)[v_i,v_{i+k})[vi,vi+k) 。举个例子来说:v⃗=(0,0,0,0,1,2,3,4,6,8)\vec v = (0,0,0,0,1,2,3,4,6,8)v =(0,0,0,0,1,2,3,4,6,8)
可以看出,p1的权重在参数u=1u=1u=1时会归零,也就是p1的生效区间为[0,1)[0,1)[0,1)。前面添加k个0就是为了之前说的保证控制点生命周期完整的意图,至于为什么能保证其实是在权重函数bi(u)b_i(u)bi(u)的计算里面,那个计算我不分析了,你只需要知道就是为了配合公式权重计算的正确归零才创造出节点这个概念的就行了。
3.2.4 如何过点
前面说过了,B-Spline是无法过点的,但是其实有一些trick能让它过点。
前面说过,第i个控制点权重区间为[vi,vi+k)[v_i,v_{i+k})[vi,vi+k),如果让这k+1个值全部相等呢?这不就意味着:在pi之前的点到这里权重就归零,在pi之后的点在这一点权重才开始,只有pi这一点在这里权重为1吗?这不就是过点吗?
但是注意,你这样做就牺牲了那一点的C2连续,变成了C0,那四个条件还是只能保住3个。