何为法律?永恒不变的理性,普适万物的规则。

# 曲线、曲面与多项式

对空间中的一条曲线或曲面,我们往往用参数方程来表示:

p(u)=(x(u)y(u)z(u))p(u,v)=(x(u,v)y(u,v)z(u,v))\mathbf{p}(u)=\begin{pmatrix} x(u)\\ y(u)\\ z(u) \end{pmatrix} \quad \mathbf{p}(u,v)=\begin{pmatrix} x(u,v)\\ y(u,v)\\ z(u,v) \end{pmatrix}

我们往往认为u,vu,v 是一个从 0 变化到 1 的实数标量,那么前者p(u)\mathbf{p}(u) 是一条曲线,后者p(u,v)\mathbf{p}(u,v) 是一个曲面。

然而这是一般情况。在实际应用中,相比于一般函数x(u),y(u),z(u)x(u),y(u),z(u),我们更关心它们是多项式时的情况。参数方程改写为:

p(u)=k=0nukckp(u,v)=k=0nl=0mukvlck,l\mathbf{p}(u)=\sum_{k=0}^n u^k\mathbf{c}_k\quad \mathbf{p}(u,v)=\sum_{k=0}^n\sum_{l=0}^m u^k v^l\mathbf{c}_{k,l}

其中ck,ck,l\mathbf{c}_{k},\mathbf{c}_{k,l} 都是三维的点或向量,严格来说c0\mathbf{c}_0 应为点,其余是向量。

# 插值

插值问题就是给出一组点p0,p1,...,pn\mathbf{p}_0,\mathbf{p}_1,...,\mathbf{p}_n,要求你构造一条曲线去经过这些点。实际中往往采用三次函数来插值,因为二次往往有些欠拟合,而更高次又增加了计算量,是一个经验的折中。

譬如给出四个点p0,p1,p2,p3\mathbf{p}_0,\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3,那么考虑先前的参数方程,一个要求是我们希望这四个点均匀地分布在曲线上,即:

{p(0)=p0p(1/3)=p1p(2/3)=p2p(1)=p3\begin{cases} \mathbf{p}(0)=\mathbf{p}_0\\ \mathbf{p}(1/3)=\mathbf{p}_1\\ \mathbf{p}(2/3)=\mathbf{p}_2\\ \mathbf{p}(1)=\mathbf{p}_3 \end{cases}

这已经有四个约束了,写成矩阵形式:

(p0p1p2p3)=(11131321331232322331111)(c0c1c2c3)p=Ac\begin{pmatrix} \mathbf{p}_0\\ \mathbf{p}_1\\ \mathbf{p}_2\\ \mathbf{p}_3\end{pmatrix}=\begin{pmatrix} 1&&&\\ 1&\frac{1}{3}&\frac{1}{3}^2&\frac{1}{3}^3\\ 1&\frac{2}{3}&\frac{2}{3}^2&\frac{2}{3}^3\\ 1&1&1&1 \end{pmatrix} \begin{pmatrix} \mathbf{c}_0\\ \mathbf{c}_1\\ \mathbf{c}_2\\ \mathbf{c}_3 \end{pmatrix}\\ \Rightarrow\mathbf{p}=A\mathbf{c}

然后我们就可以解出c=A1p\mathbf{c}=A^{-1}\mathbf{p},然后就得到了参数化的曲线方程p(u)=uTc\mathbf{p}(u)=\mathbf{u}^T\mathbf{c},其中uT=(1,u,u2,u3)\mathbf{u}^T=(1,u,u^2,u^3)

再简化写一下,我们称b(u)=(A1)Tu\mathbf{b}(u)=(A^{-1})^T\mathbf{u}调和多项式 (blending polynomial),那么就可以直接写出参数化曲线和四个点的关系:

p(u)=uTc=uTA1p=b(u)Tp\begin{aligned} \mathbf{p}(u)&=\mathbf{u}^T\mathbf{c}\\ &=\mathbf{u}^TA^{-1}\mathbf{p}\\ &=\mathbf{b}(u)^T\mathbf{p} \end{aligned}


讨论完曲线可以讨论三次插值曲面。譬如给出了 16 个点:
1
我们希望插值出一个三次曲面平均地通过它们,即:

p(i3,j3)=piji,j=0,1,2,3\mathbf{p}(\frac{i}{3},\frac{j}{3})=\mathbf{p}_{ij}\\ i,j=0,1,2,3

u=(1,u,u2,u3)T,v=(1,v,v2,v3)T\mathbf{u}=(1,u,u^2,u^3)^T,\mathbf{v}=(1,v,v^2,v^3)^T,于是有:

p(u,v)=uTCv\mathbf{p}(u,v)=\mathbf{u}^TC\mathbf{v}

其中,每个CC 的元素CijC_{ij} 都是一个三维向量。然后我们有 16 个约束函数p(i/3,j/3)=pij\mathbf{p}(i/3,j/3)=\mathbf{p}_{ij},写成矩阵就是:

AiCAjT=pij,i,j=0,1,2,3ACAT=pA_iCA_j^T=\mathbf{p}_{ij},i,j=0,1,2,3\\ \Rightarrow ACA^T=\mathbf{p}

于是解得C=A1p(AT)1C=A^{-1}\mathbf{p}(A^T)^{-1}。所以:

p(u,v)=(uTA1)p((AT)1v)=b(u)Tpb(v)\begin{aligned} \mathbf{p}(u,v)&=(\mathbf{u}^TA^{-1})\mathbf{p}((A^T)^{-1}\mathbf{v})\\ &=\mathbf{b}(u)^T \mathbf{p} \mathbf{b}(v)\\ \end{aligned}


然后讨论一下 Hermite 插值,相较于给出四个点(也就是四个约束),Hermite 插值给出了两个端点,以及两个端点的导数,即给出了p0,p1,p0,p1\mathbf{p}_0,\mathbf{p}_1,\mathbf{p}_0',\mathbf{p}_1',同样也是四个约束。三次 Hermite 插值也是一个三次多项式,满足下列四个约束:

{p(0)=c0=p0p(1)=c0+c1+c2+c3=p1p(0)=c1=p0p(1)=c1+2c2+3c3=p1\begin{cases} \mathbf{p}(0)=\mathbf{c}_0&=\mathbf{p}_0\\ \mathbf{p}(1)=\mathbf{c}_0+\mathbf{c}_1+\mathbf{c}_2+\mathbf{c}_3 &=\mathbf{p}_1\\ \mathbf{p}'(0)=\mathbf{c}_1&=\mathbf{p}_0'\\ \mathbf{p}'(1)=\mathbf{c}_1+2\mathbf{c}_2+3\mathbf{c}_3&=\mathbf{p}_1' \end{cases}

这样也可以解方程,反解出c0,...,c3\mathbf{c}_0,...,\mathbf{c}_3 即可。

# 参数连续性和几何连续性

考虑两个参数化的曲线p(u),q(v)\mathbf{p}(u),\mathbf{q}(v),如果我们要它们接个头即p(1)=q(0)\mathbf{p}(1)=\mathbf{q}(0),然后可以考虑接头处的连续性:

  • 参数连续性,记为CnC^n:

C0:{xp(1)=xq(0)yp(1)=yq(0)zp(1)=zq(0)C1:{xp(1)=xq(0)yp(1)=yq(0)zp(1)=zq(0)xp(1)=xq(0)yp(1)=yq(0)zp(1)=zq(0)C2:...C^0:\begin{cases}x_p(1)=x_q(0)\\ y_p(1)=y_q(0)\\ z_p(1)=z_q(0)\end{cases}\quad C^1: \begin{cases}x_p(1)=x_q(0)\\ y_p(1)=y_q(0)\\ z_p(1)=z_q(0)\\x_p'(1)=x_q'(0)\\ y_p'(1)=y_q'(0)\\ z_p'(1)=z_q'(0)\end{cases}\quad C^2:...

看一下规律就是到是要求nn 阶导数对应的参数完全相同。很显然要求的nn 越大,则约束条件越多,需要的多项式次数也就越高。

  • 几何连续性,记为GnG^n:

G0:p(1)=αq(0)G1:{p(1)=αq(0)p(1)=αq(0)G2:...G^0:\mathbf{p}(1)=\alpha \mathbf{q}(0)\quad G^1:\begin{cases} \mathbf{p}(1)=\alpha \mathbf{q}(0)\\ \mathbf{p}'(1)=\alpha \mathbf{q}'(0) \end{cases}\quad G^2:...

看一下实际上会发现几何连续性比参数连续性实际上少了一个约束,即只需要连接处的微矢是共线的就好。特别地,G0G^0C0C^0 是等价的。

# 三次 Bezier 曲线和 Bezier 曲面

Bezier 曲线是一种特殊的 Hermite 曲线,他给出了四个点p0,p1,p2,p3\mathbf{p}_0,\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3 和如下约束:

{p(0)=c0=p0p(1)=c0+c1+c2+c3=p1p(0)=c1=3p13p0p(1)=c1+2c2+3c3=3p33p2\begin{cases} \mathbf{p}(0)=\mathbf{c}_0&=\mathbf{p}_0\\ \mathbf{p}(1)=\mathbf{c}_0+\mathbf{c}_1+\mathbf{c}_2+\mathbf{c}_3 &=\mathbf{p}_1\\ \mathbf{p}'(0)=\mathbf{c}_1&=3\mathbf{p}_1-3\mathbf{p}_0\\ \mathbf{p}'(1)=\mathbf{c}_1+2\mathbf{c}_2+3\mathbf{c}_3&=3\mathbf{p}_3-3\mathbf{p}_2 \end{cases}

我们可以解得调和多项式:

b(u)=((1u)33u(1u)23u2(1u)u3)\mathbf{b}(u)=\begin{pmatrix} (1-u)^3\\ 3u(1-u)^2\\ 3u^2(1-u)\\ u^3 \end{pmatrix}

于是p(u)=b(u)Tp\mathbf{p}(u)=\mathbf{b}(u)^T\mathbf{p}

同理推广到曲面的话,有p(u,v)=b(u)TPb(v)\mathbf{p}(u,v)=\mathbf{b}(u)^T P\mathbf{b}(v),其中Pij=pijP_{ij}=\mathbf{p}_{ij}。对于曲面,一次偏导pu(0,0)=3(p10p00)\frac{\partial\mathbf{p}}{\partial u}(0,0)=3(\mathbf{p}_{10}-\mathbf{p}_{00}) 和 Bezier 曲线含义相同,但是二阶偏导2puv\frac{\partial^2\mathbf{p}}{\partial u\partial v} 的数值其实反映了曲面的曲率,即弯曲程度。

很显然,点p(u,v)\mathbf{p}(u,v) 的法向量为:pu×pv\frac{\partial\mathbf{p}}{\partial u}\times \frac{\partial\mathbf{p}}{\partial v}

# 三次 B 样条曲线

三次 B 样条曲线给出了四个点p0,p1,p2,p3\mathbf{p}_0,\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3 和如下约束:

{p(0)=c0=16(p0+4p1+p2)p(1)=c0+c1+c2+c3=16(p1+4p2+p3)p(0)=c1=12(p2p0)p(1)=c1+2c2+3c3=12(p3p1)\begin{cases} \mathbf{p}(0)=\mathbf{c}_0&=\frac{1}{6}(\mathbf{p}_0+4\mathbf{p}_1+\mathbf{p}_2)\\ \mathbf{p}(1)=\mathbf{c}_0+\mathbf{c}_1+\mathbf{c}_2+\mathbf{c}_3 &=\frac{1}{6}(\mathbf{p}_1+4\mathbf{p}_2+\mathbf{p}_3)\\ \mathbf{p}'(0)=\mathbf{c}_1&=\frac{1}{2}(\mathbf{p}_2-\mathbf{p}_0)\\ \mathbf{p}'(1)=\mathbf{c}_1+2\mathbf{c}_2+3\mathbf{c}_3&=\frac{1}{2}(\mathbf{p}_3-\mathbf{p}_1) \end{cases}

解出来可得:

b(u)=16((1u)346u2+3u31+3u+3u23u3u3),p(u)=b(u)Tp\mathbf{b}(u)=\frac{1}{6}\begin{pmatrix} (1-u)^3\\ 4-6u^2+3u^3\\ 1+3u+3u^2-3u^3\\ u^3 \end{pmatrix},\quad \mathbf{p}(u)=\mathbf{b}(u)^T\mathbf{p}

这样做的话,使得曲线会被包裹在四个控制点的凸包内部。而且如果有一系列控制点p0,p1,...,pn\mathbf{p}_0,\mathbf{p}_1,...,\mathbf{p}_n,我们可以先用(p0,p1,p2,p3)(\mathbf{p}_0,\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3) 生成曲线,然后再用(p1,p2,p3,p4)(\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3,\mathbf{p}_4) 生成曲线,以此类推,这样的曲线具有较好的连续性。

对于一系列连续的控制点p0,p1,...,pn\mathbf{p}_0,\mathbf{p}_1,...,\mathbf{p}_n,我们单独考虑一个控制点pi\mathbf{p}_i,它为整条曲线做出的贡献只有四段:

(pi3,pi2,pi1,pi)(pi2,pi1,pi,pi+1)(pi1,pi,pi+1,pi+2)(pi,pi+1,pi+2,pi+3)(\mathbf{p}_{i-3}, \mathbf{p}_{i-2}, \mathbf{p}_{i-1}, \mathbf{p}_{i})\\ (\mathbf{p}_{i-2}, \mathbf{p}_{i-1}, \mathbf{p}_{i}, \mathbf{p}_{i+1})\\ (\mathbf{p}_{i-1}, \mathbf{p}_{i}, \mathbf{p}_{i+1}, \mathbf{p}_{i+2})\\ (\mathbf{p}_{i}, \mathbf{p}_{i+1}, \mathbf{p}_{i+2}, \mathbf{p}_{i+3})

如果每一段的曲线参数uu 都有一个单位区间,那么n1n-1 段曲线uu 的范围就是[0,n1][0,n-1]pi\mathbf{p}_i 的贡献可以用一个函数来表示:

Bi(u)={0u<i2b0(u+2)i2u<i1b1(u+1)i1u<ib2(u)iu<i+1b3(u1)i+1u<i+20ui+2B_i(u)=\begin{cases} 0 & u<i-2\\ b_0(u+2) & i-2\leq u<i-1\\ b_1(u+1) & i-1\leq u<i\\ b_2(u) & i\leq u<i+1\\ b_3(u-1) & i+1\leq u<i+2\\ 0 & u\geq i+2 \end{cases}

然后有:

p(u)=i=0nBi(u)pi\mathbf{p}(u)=\sum_{i=0}^nB_i(u)\mathbf{p}_i

这样就不用一段一段计算,然后可能一个控制点要在四段中都要出现的麻烦情况了。

定性分析的话,可以看出一个控制点pi\mathbf{p}_i 对整条曲线的贡献函数:
2

显然就是曲线离控制点越近,受控制点影响越大。

当然,也可以推广到 B-Spline 曲面:

p(u,v)=i=03j=03bi(u)bj(v)pi,j\mathbf{p}(u,v)=\sum_{i=0}^3\sum_{j=0}^3b_i(u)b_j(v)\mathbf{p}_{i,j}

# 一般的 B-spline 曲线

现在考虑这样一个问题,给了一系列控制点p0,...,pm\mathbf{p}_0,...,\mathbf{p}_m,我们想寻找一条曲线p(u),u[umin,umax]\mathbf{p}(u),u\in[u_{min},u_{max}]。使得这条曲线 "平滑、并且接近这些控制点"。假设我们有一系列uu 的值,称为结点(knots):

umin=u0u1...un=umaxu_{min}=u_0\leq u_1\leq...\leq u_n=u_{max}

然后我们就把整条曲线分成了nn 段,每段为一个dd 次多项式:

p(uk<u<uk+1)=j=0dcjkuj\mathbf{p}(u_k<u<u_{k+1})=\sum_{j=0}^d\mathbf{c}_{jk}u^j\\

类似先前的想法,与其去考虑求解cjk\mathbf{c}_{jk},我们直接关心每个控制点对曲线的贡献。即写成:

p(u)=i=0mBid(u)pi\mathbf{p}(u)=\sum_{i=0}^m B_{id}(u)\mathbf{p}_i

其中,Bid(u)B_{id}(u) 表示它是一个dd 次多项式。

实际上,B-spline 的含义就是 Basis Splines,而{Bid(u)}\{B_{id}(u)\} 就是一组 basis。尽管这样的 basis 有很多,但是最著名的一个应该是 Cox-deBoor recursion 定义的:

Bk0={1uku<uk+10otherwiseBkd=uukuk+dukBk,d1(u)+uk+duuk+d+1uk+1Bk+1,d1(u)\begin{aligned} & B_{k0}=\begin{cases} 1 & u_k\leq u<u_{k+1}\\ 0 & otherwise \end{cases}\\ & B_{kd}=\frac{u-u_k}{u_{k+d}-u_k}B_{k,d-1}(u)+\frac{u_{k+d}-u}{u_{k+d+1}-u_{k+1}}B_{k+1,d-1}(u) \end{aligned}

d=0d=0 时,每个控制点pi\mathbf{p}_i 只能影响一个区间[uk,uk+1][u_k,u_{k+1}],实际上,每个控制点可以影响d+1d+1 个区间,取决于 basis 的次数。我们可以画出 0 次、1 次、2 次的 basis 函数图像:
3

控制点pi\mathbf{p}_i 会影响区间:[ui,ui+1],[ui+1,ui+2],...,[ui+d,ui+d+1][u_i,u_{i+1}],[u_{i+1},u_{i+2}],...,[u_{i+d},u_{i+d+1}]

这部分结点数n+1n+1 和控制点数m+1m+1 是没什么关系的,而且结点的选择也是自由的,这样的话就比较容易导致混乱。

所以实际中我们常常要求结点是等距选取的 (或重叠),此时形成了 uniform B-spline。对于三次的 B-spline,我们常常取结点为:

{0,0,0,0,1,2,...,n1,n,n,n,n}\{0,0,0,0,1,2,...,n-1,n,n,n,n\}

而且由于一个控制点可以影响d+1d+1 个区间,所以实际上控制点数m+1m+1、区间数nn、basis 多项式次数dd 有以下关系:

n(m+1)=dn-(m+1)=d

至此,构造一个 B-spline 的方法比较完备了:

  1. 给出控制点p0,...,pm\mathbf{p}_0,...,\mathbf{p}_m,以及多项式次数dd
  2. 选取结点{u0,...,ud+m+1}\{u_0,...,u_{d+m+1}\}
  3. 计算出 basis 多项式{Bid(u)}\{B_{id}(u)\}
  4. 计算出曲线p(u)=i=0mBid(u)pi\mathbf{p}(u)=\sum_{i=0}^m B_{id}(u)\mathbf{p}_i

特别地,若结点为{0,0,0,0,1,1,1,1}\{0,0,0,0,1,1,1,1\} 将退化成 Bezier 曲线。

此外,如果我们想特别地增加或减小某个控制点的影响力,也可以增加一个权重函数wiw_i,生成的曲线为 Nonuniform rational B-spline( NURBS ):

p(u)=i=0mBid(u)wipii=0mBid(u)wi\mathbf{p}(u)=\frac{\sum_{i=0}^m B_{id}(u)w_i\mathbf{p}_i}{\sum_{i=0}^m B_{id}(u)w_i}

# Catmull-Rom Splines

如果我们放宽限制,不要求曲线必须在控制点的凸包内的话,Catmull-Rom spline 也是一种简单、受欢迎的方法。它给出的限制条件为:

{p(0)=c0=p1p(1)=c0+c1+c2+c3=p2p(0)=c1=12(p2p0)p(1)=c1+2c2+3c3=12(p3p1)\begin{cases} \mathbf{p}(0)=\mathbf{c}_0&=\mathbf{p}_1\\ \mathbf{p}(1)=\mathbf{c}_0+\mathbf{c}_1+\mathbf{c}_2+\mathbf{c}_3 &=\mathbf{p}_2\\ \mathbf{p}'(0)=\mathbf{c}_1&=\frac{1}{2}(\mathbf{p}_2-\mathbf{p}_0)\\ \mathbf{p}'(1)=\mathbf{c}_1+2\mathbf{c}_2+3\mathbf{c}_3&=\frac{1}{2}(\mathbf{p}_3-\mathbf{p}_1) \end{cases}

# 插值曲线的渲染

相较于直接去计算矩阵和多项式,有一种更为优雅的方式去渲染 Bezier 曲线。
对于四个控制点p0,p1,p2,p3\mathbf{p}_0, \mathbf{p}_1, \mathbf{p}_2,\mathbf{p}_3 和曲线p(u),u[0,1]\mathbf{p}(u),u\in[0,1],我们可以考虑构造两条子曲线,即l(u),u[0,1]\mathbf{l}(u),u\in[0,1]r(u),u[0,1]\mathbf{r}(u),u\in[0,1],分别对应原曲线[0,12],[12,1][0,\frac{1}{2}],[\frac{1}{2},1] 两部分。

构造方法也很简单,一图表示为:
4

如上图取得都是中点,然后l0,l1,l2,l3\mathbf{l}_0,\mathbf{l}_1,\mathbf{l}_2,\mathbf{l}_3r0,r1,r2,r3\mathbf{r}_0,\mathbf{r}_1,\mathbf{r}_2,\mathbf{r}_3 分别对应了左半边曲线的控制点和右半边曲线的控制点。这样我们就可以递归地分治去构造子曲线了,然后把它们首尾相连起来。

其实这样做法得正确性也很好证明,只需要验证 Bezier 曲线的约束,两条子曲线拼成的曲线和原曲线等价即可。注意参数缩减了一般。所以3(l1l0)3(\mathbf{l}_1-\mathbf{l}_0) 才是p(0)\mathbf{p}'(0)