Bezier 贝塞尔曲线部分数学和代码实现
最后编辑于 2024-01-09
- 本文仅用于快速了解贝塞尔曲线的部分应用代码实现和小部分简单的数学, 如果想要更详细地学习相关数学内容, 建议查阅更详细的文档或书籍. 比如我曾经学习时看的就是这部 A Primer on Bézier Curves;
- 在有些算法中可能会有修改某个控制点的曲率等操作, 但是本文中不会出现这种情况, 所有控制点的曲率都为 1 ;
贝塞尔曲线 数据类型
-
贝塞尔曲线 以 点云访问器 ( Points_Iterator ) 保存; Points_Iterator的数据结构说明见 readme.md
-
方案1: 使用各个维度的各次幂系数作为存储格式; 因为可以快速进行采样点/求导等计算, 所以大部分函数都使用该方案
diff+------> d维度 | | v n次幂的系数
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C = ( C 0 , 0 , C 1 , 0 , ... , C d − 1 , 0 C 0 , 1 , C 1 , 1 , ... , C d − 1 , 1 ⋮ C 0 , n , C 1 , n , ... , C d − 1 , n ) C = \begin{pmatrix} C_{0,0}, C_{1,0}, \dots ,C_{d-1,0} \\ C_{0,1}, C_{1,1}, \dots ,C_{d-1,1} \\ \vdots\\ C_{0,n}, C_{1,n}, \dots ,C_{d-1,n} \\ \end{pmatrix} </math>C= C0,0,C1,0,...,Cd−1,0C0,1,C1,1,...,Cd−1,1⋮C0,n,C1,n,...,Cd−1,n
-
方案2: 保存控制点集合; 应仅在编辑/绘制图形时使用该方案, 渲染时应转换为方案1
diff+------> d维度 | | v n个控制点
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> P = ( P 0 , 0 , P 1 , 0 , ... , P d − 1 , 0 P 0 , 1 , P 1 , 1 , ... , P d − 1 , 1 ⋮ P 0 , n , P 1 , n , ... , P d − 1 , n ) P = \begin{pmatrix} P_{0,0}, P_{1,0}, \dots ,P_{d-1,0} \\ P_{0,1}, P_{1,1}, \dots ,P_{d-1,1} \\ \vdots\\ P_{0,n}, P_{1,n}, \dots ,P_{d-1,n} \\ \end{pmatrix} </math>P= P0,0,P1,0,...,Pd−1,0P0,1,P1,1,...,Pd−1,1⋮P0,n,P1,n,...,Pd−1,n
采样点 to 计算系数
- 想要使用采样点得到计算系数, 这个过程需要用到一些矩阵运算和推演; 本文仅作简单介绍, 另外有许多文献中都有更详细的说明 如 贝塞尔曲线的数学原理 和 用矩阵运算来表示贝塞尔曲率;
直接使用控制点与多项式求采样点
-
使用多项式和控制点求采样点的公式稍微复杂一点, 因为在未知控制点个数的情况下难以仅仅使用一个编程函数来实现的, 这需要一点推导的过程
-
对于已知控制点数量的贝塞尔曲线的采样可以用多项式表示
- 2个控制点,一阶贝塞尔(直线): <math xmlns="http://www.w3.org/1998/Math/MathML"> F l i n e a r ( p , t ) = ( 1 − t ) P 0 + t P 1 F_{linear}(p,t)=(1-t)P_0+tP_1 </math>Flinear(p,t)=(1−t)P0+tP1
- 3个控制点,二阶贝塞尔(平方曲线): <math xmlns="http://www.w3.org/1998/Math/MathML"> F s q u a r e ( p , t ) = ( 1 − t ) 2 P 0 + 2 ( 1 − t ) t P 1 + t P 2 F_{square}(p,t)=(1-t)^2P_0+2(1-t)tP_1+tP_2 </math>Fsquare(p,t)=(1−t)2P0+2(1−t)tP1+tP2
- 4个控制点,三阶贝塞尔(立方曲线): <math xmlns="http://www.w3.org/1998/Math/MathML"> F c u b i c ( p , t ) = ( 1 − t ) 3 P 0 + 3 ( 1 − t ) 2 P 1 t + 3 ( 1 − t ) t 2 P 2 + t 3 P 3 F_{cubic}(p,t)=(1-t)^3P_0+3(1-t)^2P_1t+3(1-t)t^2P_2+t^3P_3 </math>Fcubic(p,t)=(1−t)3P0+3(1−t)2P1t+3(1−t)t2P2+t3P3
- <math xmlns="http://www.w3.org/1998/Math/MathML"> ... \dots </math>...
-
此时其实已经可以用这些公式进行化简与合并同类项的的操作将采样点转换为计算系数了, 但是需要更进一步, 因为我希望用一个函数能求得任意阶的计算系数; 所以需要找到上面那些式子的规律
- 将上面几个式子, 忽视掉 控制点 / t / (1-t), 可以发现有一些系数, 这正是帕斯卡三角的数值
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> L P A S C A L S _ T R I A N G L E = [ 1 ] , [ 1 , 1 ] , [ 1 , 2 , 1 ] , [ 1 , 3 , 3 , 1 ] , [ 1 , 4 , 6 , 4 , 1 ] , [ 1 , 5 , 10 , 10 , 5 , 1 ] , . . . . . L_{PASCALS\_TRIANGLE}=\\ [1],\\ [1,1],\\ [1,2,1],\\ [1,3,3,1],\\ [1,4,6,4,1],\\ [1,5,10,10,5,1],\\ ..... </math>LPASCALS_TRIANGLE=[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1],[1,5,10,10,5,1],.....
- 再看 t / (1-t) 的指数, 其实是有一个此消彼长的关系
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> i = 0 n : ( 1 − t ) i t n − i _{i=0}^n:(1-t)^{i}t^{n-i} </math>i=0n:(1−t)itn−i
-
n个控制点, n-1阶贝塞尔曲线: 先取出对应的一行帕斯卡三角 <math xmlns="http://www.w3.org/1998/Math/MathML"> L ′ = L P A S C A L S _ T R I A N G L E ( n ) L'=L_{PASCALS\TRIANGLE}(n) </math>L′=LPASCALS_TRIANGLE(n) 然后代入下面这段公式
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> F b e z i e r ( n , p , t ) = ∑ i = 0 n L i ′ ( 1 − t ) n − i t i p i F{bezier}(n,p,t)=\sum_{i=0}^{n}L'_i(1-t)^{n-i}t^{i}p_i </math>Fbezier(n,p,t)=i=0∑nLi′(1−t)n−itipi -
这个公式想要变成程序代码的话比较复杂而且计算开销较大, 所以如果将这个公式改成简单多项式的表示, 将会更简单而且计算开销更小
贝塞尔曲线计算矩阵
单凭上面那些还不足以用一个函数将任意个控制点转换为对应的计算系数, 还需要一点矩阵的推导
-
将某个函数展开, 此处使用三阶曲线函数举例 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 1 − t ) 3 P 0 + 3 ( 1 − t ) 2 P 1 t + 3 ( 1 − t ) t 2 P 2 + t 3 P 3 (1-t)^3P_0+3(1-t)^2P_1t+3(1-t)t^2P_2+t^3P_3 </math>(1−t)3P0+3(1−t)2P1t+3(1−t)t2P2+t3P3
可以得到
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> ( 1 − 3 t + 3 t 2 − 1 t 3 ) P 0 + ( 3 t − 6 t 2 + 3 t 3 ) P 1 + ( − 3 t 2 + 3 t 3 ) P 2 + ( t 3 ) P 3 ; (1-3t+3t^2-1t^3) P_0 + \\ (3^t-6t^2+3t^3) P_1 + \\ (-3t^2+3t^3) P_2 + \\ (t^3) P_3 ; \\ </math>(1−3t+3t2−1t3)P0+(3t−6t2+3t3)P1+(−3t2+3t3)P2+(t3)P3;
-
分离t和其他数值, 可以将上面的式子记作一个矩阵乘法
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ 1 , 0 , 0 , 0 − 3 , 3 , 0 , 0 3 , − 6 , 3 , 0 − 1 , 3 , − 3 , 1 ] ⋅ [ p 0 p 1 p 2 p 3 ] ⋅ [ t 0 t 1 t 2 t 3 ] \begin{bmatrix} 1,0,0,0 \\ -3,3,0,0 \\ 3,-6,3,0 \\ -1,3,-3,1 \\ \end{bmatrix} · \begin{bmatrix} p_0\\ p_1\\ p_2\\ p_3\\ \end{bmatrix} · \begin{bmatrix} t^0\\ t^1\\ t^2\\ t^3\\ \end{bmatrix} </math> 1,0,0,0−3,3,0,03,−6,3,0−1,3,−3,1 ⋅ p0p1p2p3 ⋅ t0t1t2t3
-
其中有一个没有任何代数的矩阵, 而且它是一个三角矩阵, 它的内容仅根据控制点的个数改变, 但它其实可以由一些简单的公式获得:
- 已有一个取帕斯卡三角某行的函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> L P A S C A L S _ T R I A N G L E ( l ) L_{PASCALS\_TRIANGLE}(l) </math>LPASCALS_TRIANGLE(l) 下文将简称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L ( l ) L(l) </math>L(l)
- 对一个拥有n个控制点的贝塞尔曲线, 即(n-1)阶贝塞尔曲线的计算矩阵, 它是一个下三角矩阵, 它的每个 <math xmlns="http://www.w3.org/1998/Math/MathML"> M u , v M_{u,v} </math>Mu,v 位置的数值:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M u , v = ( − 1 ) u + v ⋅ L ( n ) u ⋅ L ( u ) v M_{u,v} = (-1)^{u+v} · L(n)_u · L(u)_v </math>Mu,v=(−1)u+v⋅L(n)u⋅L(u)v
-
最后只需要将控制点 P 的坐标代入到矩阵乘法中,即可计算出这个一元n次函数 <math xmlns="http://www.w3.org/1998/Math/MathML"> ∑ i = 0 n t i C i \sum_{i=0}^n t^iC_i </math>∑i=0ntiCi 的各次幂系数 <math xmlns="http://www.w3.org/1998/Math/MathML"> C C </math>C , 当要采样点时将 t 代入到上式中即可快速计算采样点;
-
将系数还原成控制点也只需要做相应的逆运算即可;
代码实现比较长, 如有需要请自行查看代码文件, 帕斯卡三角内容请看 NML_Algebra, 贝塞尔曲线计算内容请看 NML_Bezier
贝塞尔曲线采样点
-
方案1使用的系数可以快速进行采样计算, 具体过程如下
- 采样某一维度的坐标
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> C = ( C 0 , C 1 , ... , C n ) F s a m p l e _ B e z i e r _ n ( C , t ) = ∑ i = 0 n t i C i C=(C_0,C_1,\dots,C_n) \\ F_{sample\_Bezier\n}(C,t) = \sum{i=0}^n t^iC_i </math>C=(C0,C1,...,Cn)Fsample_Bezier_n(C,t)=i=0∑ntiCi
采样所有可用维度的坐标就能得到一个采样点
cpp/** * @brief 采样贝塞尔曲线 使用各次幂的系数 * @param out 输出目标, 采样点, 长度为 coefficients.points_length * @param coefficients 贝塞尔曲线各次幂系数 ( 使用 setup_BezierCoefficients 计算 ) * @param t 时间参数t * @return 修改并返回 out (采样点) */ var*& sample_Bezier__Coefficients(var*& out, Points_Iterator& coefficients, var t){ Idx_Algebra &dimensional=coefficients.dimensional; std::fill_n(out, dimensional, 0); var power_t=1; for(Idx i=0; i<coefficients.points_length; ++i){ var *line=coefficients[i]; for(Idx_Algebra d=0; d<dimensional; ++d){ out[d]+=power_t*line[d]; } power_t*=t; } return out; }
-
方案2使用操作点求采样点, 使用 DeCasteljau 算法, 开销较大不推荐使用
- 步骤很简单, 就是在每个相邻的控制点组成的线段上求这个线段的 t 采样位置作为新的控制点, 每次进行这个操作后控制点就会少一个, 重复操作直到只剩一个点, 这个点就是采样点
cpp/** * @brief 采样贝塞尔曲线 使用DeCasteljau算法 (不建议使用) * @param out 输出目标, 采样点 长度为 points.dimensional * @param points 贝塞尔曲线控制点集合 长度为 dimensional*points_length * @param t 时间参数 t * @return 修改并返回 out */ var *&sample_Bezier__DeCasteljau(var*& out, Points_Iterator& points, var t){ if(points.points_length>1){ Points_Iterator__1DList new_points=Points_Iterator__1DList(points.dimensional, points.points_length-1); var td=1-t; for(int i=0; i>=0; --i){ var *temp_point__new =new_points[i]; var *temp_point__low =points[i]; var *temp_point__low1 =points[i+1]; for(int j=0; j<points.dimensional; ++j){ temp_point__new[j]=td*temp_point__low[j]+t*temp_point__low1[j]; } } sample_Bezier__DeCasteljau(out, new_points, t); new_points.free_Data(); return out; }else{ for(int i=0; i<points.dimensional; ++i){ out[i]=points[0][i]; } return out; } }