贝塞尔曲线

这篇笔记从"曲线"讲起:汽车设计师需要光滑的曲面,数学家早就准备好了多项式逼近的理论,工程师皮埃尔·贝塞尔(Pierre Bézier)把这些理论变成了计算机辅助设计(CAD)的工具。今天,从字体到动画,从矢量图到机器人路径,到处都有贝塞尔曲线的身影。

为什么要"曲线"

人类最早使用的图形工具,是直线。直线用两点确定,圆用圆心和半径确定,都只需要几个数字就能描述。

但现实世界里的形状,几乎都不是直线或圆:汽车的引擎盖、字母的笔画、小山的轮廓、运动轨迹的平滑过渡……这些"自由曲线"用直线和圆是拼不出来的,或者说,拼出来也是棱角分明的。

描述一条自由曲线,最朴素的想法是记录一串点(点列):

  • 点的密度决定精度,点越多曲线越"像",数据量也越大
  • 点与点之间怎么连?直线连起来仍然是折线,用曲线连起来,又回到了"怎么描述曲线"的问题
  • 想微调某个局部,往往牵一发动全身

更好的思路是:用少数几个"控制点"来描述一条曲线。控制点不要求落在曲线上(除非你想要),它更像一个"磁铁",把曲线拉向自己。设计师拖动磁铁,曲线就跟着变,直观又高效。贝塞尔曲线,就是这套思路里最著名的一个方案。

贝塞尔曲线的发现

汽车工业的难题

20 世纪中叶,汽车、飞机的外壳需要设计出光滑、流线型的曲面。当时设计师的工作方式还是油泥模型:先手工捏出泥模,再测量、再制作模具。这种流程有几个问题:

  • 泥模是物理实物,无法直接进入计算和绘图流程
  • 手工塑形难以保证曲面的光滑性(连续可导)
  • 一次次的修改、测量、重做,周期非常长

计算机的出现让"数字化设计"成为可能,但首先要解决一个数学问题:怎么用计算机精确、简洁地描述一条自由曲线? 法国两家汽车公司——雷诺(Renault)和雪铁龙(Citroën)——的工程师,各自独立地给出了答案。

德卡斯特里奥(Paul de Casteljau)的算法

1959 年,雪铁龙的数学家保罗·德·卡斯特里奥提出了一个纯几何的方法:通过反复的线性插值,用任意多个控制点构造出一条光滑曲线。这个方法现在被称为 de Casteljau 算法,是计算贝塞尔曲线最直观、最稳定的方式。

遗憾的是,雪铁龙把这个成果当作公司机密,只以内部资料的形式流传,直到 1963 年才对外发表。这导致他的贡献在很长一段时间内不为人知,甚至名字都常被拼错(现在算法名仍保留了他的名字)。

皮埃尔·贝塞尔(Pierre Bézier)的公式

1962 年,雷诺的工程师皮埃尔·贝塞尔数学公式(伯恩斯坦多项式)来描述同一种曲线,并以此为核心开发了计算机辅助设计系统 UNISURF,从 1968 年起用于雷诺汽车的车身设计,是 CAD 领域最早的成功应用之一。

贝塞尔在 1966 年、1972 年公开发表了自己的工作,随着 UNISURF 系统的声名远播,这种曲线被命名为贝塞尔曲线(Bézier Curve)。多年后人们才发现,贝塞尔与德卡斯特里奥在完全不知道彼此工作的情况下,得到了同一种曲线——一个用公式描述(贝塞尔),一个用几何构造(德卡斯特里奥),殊途同归。

数学根基:伯恩斯坦多项式

贝塞尔曲线所用的数学工具,早在 1912 年就被俄罗斯数学家谢尔盖·伯恩斯坦(Sergei Bernstein)提出了——他在证明著名的维尔斯特拉斯逼近定理(任何连续函数都能用多项式任意逼近)时,构造了一组多项式,即伯恩斯坦多项式(Bernstein polynomial)

伯恩斯坦当时并不知道,几十年后这组多项式会成为整个 CAD 世界的基石。贝塞尔曲线的公式,本质就是"控制点坐标 × 伯恩斯坦基函数"的加权和。数学理论超前于工程应用几十年,在这里体现得淋漓尽致。

贝塞尔曲线的原理

先约定记号:控制点用 P0、P1、P2... 表示,参数 t 从 0 变化到 1。t 可以理解为"进度"——t=0 是曲线起点,t=1 是曲线终点,t=0.5 是中间。每一个 t 值,对应曲线上一个确定的点。

一阶(线性)贝塞尔曲线:两点

两个点 P0、P1,在它们之间的线段上按比例取点,就是最简单的线性插值:

B(t) = (1-t)P0 + tP1

t=0 时是 P0,t=1 时是 P1,t 从 0 到 1 扫过整条线段。一阶贝塞尔曲线就是一条直线,它是一切的基础。

二阶(二次)贝塞尔曲线:三点

三个控制点 P0、P1、P2,构造过程非常直观,分两步:

  1. 在线段 P0P1 上按比例 t 取点 Q0,在线段 P1P2 上按比例 t 取点 Q1
  2. 在线段 Q0Q1 上再按比例 t 取点,这个点就是曲线上的点 B(t)

Q0Q1 是两条"滑动的线段",B(t) 是在它们之间滑动的点。让 t 从 0 扫到 1,就得到一条抛物线。这个"在连线上取点,取完再连再取"的过程,就像套在三个控制点之间的一根橡皮筋。

公式(把两步插值展开):

B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2

其中 (1-t)²2(1-t)t 恰好是二阶伯恩斯坦基函数,三个系数之和恒等于 1。

三阶(三次)贝塞尔曲线:四点

四个控制点 P0、P1、P2、P3,同样用"连线上取点"的套路,只是多做一轮:

  1. P0P1 上取 Q0,P1P2 上取 Q1,P2P3 上取 Q2(三次线性插值,得到 3 个点)
  2. Q0Q1 上取 R0,Q1Q2 上取 R1(二次插值,得到 2 个点)
  3. R0R1 上取点,就是 B(t)(一次插值)

展开后:

B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3

系数 (1-t)³3(1-t)²t3(1-t)t² 是三阶伯恩斯坦基函数,系数之和同样恒为 1。三次贝塞尔曲线是实践中最重要的形式:两个控制点足以让曲线拥有一个"弯折"(拐点),同时又足够简单,下面会看到,字体、CSS、SVG 全部基于它。

一般形式(n 阶)

把上面的规律推广到 n+1 个控制点 P0 ~ Pn

B(t) = Σ C(n,i) · (1-t)^(n-i) · t^i · Pi   (i 从 0 到 n)

其中 C(n,i) 是组合数,B_i(t) = C(n,i)·(1-t)^(n-i)·t^i 就是第 i 个伯恩斯坦基函数。所有基函数在任意 t 处相加都等于 1,所以整条曲线是控制点的"凸组合"——这也是曲线永远待在控制点"内部"的原因(见下文"凸包性质")。

de Casteljau 算法

前面"连线上取点"的过程写成递推式,就是 de Casteljau 算法:第 k 层第 i 个点,由上一层相邻两点插值得到。

P(i,0) = P(i)                          // 第 0 层就是控制点本身
P(i,k) = (1-t)·P(i,k-1) + t·P(i+1,k-1) // 每一层少一个点

对 n 阶曲线,做 n 轮插值,最后一轮剩下的唯一一个点 P(0,n) 就是曲线上的点。这个算法有三个巨大优势:

  1. 数值稳定:只做加法和乘法,不会像直接套高阶公式那样因系数很大而损失精度
  2. 几何直观:每一步都是"在连线上按比例取点",可以用图形界面直观演示
  3. 天然支持细分:算法过程中产生的中间点,本身就是细分后子曲线的控制点(见下文"细分性质")

一个简单的 JavaScript 实现:

// 用 de Casteljau 算法计算贝塞尔曲线上 t 处的点
function bezierPoint(points, t) {
  let p = points.slice();          // 拷贝一份,避免修改原数组
  while (p.length > 1) {
    const next = [];
    for (let i = 0; i < p.length - 1; i++) {
      next.push({
        x: (1 - t) * p[i].x + t * p[i + 1].x,
        y: (1 - t) * p[i].y + t * p[i + 1].y,
      });
    }
    p = next;
  }
  return p[0];
}

贝塞尔曲线的特性

理解了公式,再看它为什么如此流行——它有一系列"好脾气"的性质:

1. 端点插值

曲线一定经过 P0 和 Pn,但不经过中间的控制点(一阶除外)。这让设计师有完全可控的起点和终点,中间的控制点则负责"拉弯"曲线。

2. 端点切线

曲线在起点的切线方向是 P0 → P1,在终点的切线方向是 Pn-1 → Pn。这意味着两条贝塞尔曲线首尾相接时,只要共享端点、且共享端点处的控制点在同一条直线上,拼接处就是光滑的(切线连续)。字体设计正是利用这一点,用一个端点连接前后两段笔画。

3. 凸包性质

曲线上的所有点,都落在控制点的凸包内(凸包:能包住所有控制点的最小凸多边形)。直观地说,曲线永远不会比控制点构成的框架更"夸张",这保证了大批量渲染时曲线的范围是可以预测的,方便做裁剪和碰撞检测。

4. 仿射不变性

平移、旋转、缩放这类仿射变换,"先变换控制点再生成曲线"和"先生成曲线再变换"结果完全一样。所以设计师旋转、缩放图形时,只需变换几个控制点,曲线自动跟着变,计算量极小。

5. 细分性质

在任意 t 处,一条贝塞尔曲线可以分割成两条子曲线,且每条仍是贝塞尔曲线,子曲线的控制点就是 de Casteljau 算法过程中产生的中间点。这个性质是计算机绘制贝塞尔曲线的核心:

  • 反复细分,每段曲线会越来越平直,直到"足够直"就可以直接用线段画出来
  • 两条子曲线的拼接点自然满足切线连续(端点控制点共线),所以渲染结果光滑

6. 升阶

在不改变曲线形状的前提下,可以增加控制点的个数(用更高阶的形式表示同一条曲线)。这为"不同软件间交换曲线"提供了方便:大家可以用统一的阶数表示同一条曲线。

7. 变差减少性

曲线与控制多边形(把控制点依次连起来的折线)的"相交次数"不会超过控制多边形本身——也就是说,曲线不会比控制点连成的折线更弯弯曲曲。这对曲线设计者来说是一种保证:控制多边形的样子,基本就是曲线的大致样子。

局限:全局性

贝塞尔曲线也有一个明显的缺点:每个控制点都影响整条曲线。阶数越高,移动一个控制点对全局的影响越大,而且高阶曲线数值上不稳定。因此工程实践中很少直接用高阶贝塞尔曲线,而是用多条低阶(通常是三次)曲线拼接,或者用它的推广形式 B 样条曲线(B-spline)NURBS(非均匀有理 B 样条,在 CAD 中几乎是行业标准,是贝塞尔曲线加了权重并分段处理的推广)。

计算机里怎么画贝塞尔曲线

计算机只认直线,所以绘制贝塞尔曲线没有秘密,就是把曲线细分成大量线段

  • 方案一:均匀采样。取 t = 0, 0.01, 0.02, ..., 1,计算 101 个点,依次连成折线。简单直接,但为了精度要选足够多的点,效率一般。
  • 方案二:递归细分。利用细分性质,不断把曲线一分为二,直到每段子曲线的控制点几乎共线(足够直),再把子曲线的两个端点连成线段。自适应、效率高,是渲染引擎(如 Chrome 的 Skia、FreeType)的通用做法。

对 2D 场景(字体、SVG、Canvas),曲线都是平面曲线;把控制点换成三维坐标,同样的公式就是 3D 空间中的曲线;再把"点"换成"曲线",在两条曲线之间做插值,还能构造出贝塞尔曲面——汽车车身设计用的就是它。

贝塞尔曲线的用途

从发明到现在半个多世纪,贝塞尔曲线渗透进了几乎所有跟"光滑形状"有关的地方:

汽车与工业设计(CAD/CAM)

这是贝塞尔曲线的"出生地":雷诺的 UNISURF 系统用它设计车身曲面。今天 CAD 软件(CATIA、UG、SolidWorks 等)中的曲面建模,底层几乎都是 NURBS——贝塞尔曲线的直系后裔。从泥模到数字模型的转变,正是从贝塞尔曲线开始的。

字体

电脑里的字体分两大类,轮廓型(outline)字体全部由贝塞尔曲线构成:

  • TrueType 字体(.ttf):使用二次贝塞尔曲线
  • PostScript / OpenType-CFF 字体(.otf、.ps):使用三次贝塞尔曲线

每个字形的轮廓,就是一段段贝塞尔曲线的拼接。渲染引擎(如 FreeType、Skia)用递归细分把曲线画到屏幕上。你在字体编辑器里拖动的每个锚点和手柄,调节的都是贝塞尔曲线的控制点。

矢量绘图

  • SVG<path> 元素的核心命令就是贝塞尔曲线——C(三次曲线)、S(平滑三次)、Q(二次曲线)、T(平滑二次)
  • Canvas 2DquadraticCurveTo()bezierCurveTo() 两个 API 直接绘制贝塞尔曲线
  • 绘图软件:Photoshop、Illustrator 的钢笔工具(Pen Tool),画的就是贝塞尔路径;Windows 画图、PPT 里的"曲线"工具同理
  • 图表库:ECharts、Chart.js 等把折线图渲染成"平滑曲线"时,用的就是贝塞尔插值

一个简单的 SVG 三次贝塞尔曲线示例:

<svg width="200" height="100">
  <!-- M 起点,C 三次曲线:后两个点是终点+控制点,前两个点是控制点 -->
  <path d="M 10 80 C 40 10, 90 10, 120 50 S 180 90, 190 40"
        fill="none" stroke="black"/>
</svg>

CSS 动画缓动(easing)

这是前端开发者最熟悉的贝塞尔曲线。CSS 的过渡和动画可以用 cubic-bezier() 指定缓动函数:

transition-timing-function: cubic-bezier(x1, y1, x2, y2);

这里画的是函数图:横轴是时间进度,纵轴是数值进度,曲线起点固定在 (0,0)、终点固定在 (1,1),(x1,y1)、(x2,y2) 就是曲线的两个控制点(CSS 只接受三次,且 x 轴控制点坐标必须落在 0~1 之间)。曲线"拱得越高"、跌得越深,动画就显得越有弹性。

CSS 内置的缓动关键字,本质都是预设的 cubic-bezier:

关键字 等价函数 效果
linear cubic-bezier(0, 0, 1, 1) 匀速,两点直接连成直线
ease cubic-bezier(0.25, 0.1, 0.25, 1) 默认值,先快后慢
ease-in cubic-bezier(0.42, 0, 1, 1) 加速进入,像小车起步
ease-out cubic-bezier(0, 0, 0.58, 1) 减速出场,像小车滑行停止
ease-in-out cubic-bezier(0.42, 0, 0.58, 1) 先加速再减速

为什么缓动要用贝塞尔曲线?因为它用 4 个数字就描述了一条任意形状的"进度曲线",而曲线的光滑性正好保证了速度变化是平滑的、不会突然跳变。Chrome 开发者工具的"Easing"面板可以可视化地拖拽这些控制点。

动画与游戏

  • 关键帧插值:After Effects 的图表编辑器、Flash/Animate 的补间动画、PowerPoint 的动画曲线,都是拖拽贝塞尔手柄调节"关键帧之间怎么过渡"
  • 游戏开发:角色移动、UI 弹窗、镜头运镜,用贝塞尔曲线控制位移和缩放;iTweenDOTween 等动画库大量使用曲线插值
  • 轨迹与路径:3D 游戏里让物体沿一条光滑路径移动(如过场动画的相机路线),路径正是贝塞尔曲线或样条曲线

机器人路径规划

机器人的运动轨迹要求光滑(速度连续、加速度有界),否则机械臂会突然抖动、冲击。规划出的路径通常经过贝塞尔曲线或 B 样条平滑处理,让机器人从 A 点移动到 B 点的过程流畅、可预测。

数据可视化与其他

  • 折线图的平滑曲线、词云的标签路径、地图上的流向箭头……凡是"两点之间需要一条好看的弧线"的地方,都在用贝塞尔曲线
  • 苹果的 UIBezierPath、Android 的 Path.cubicTo(),移动端绘制自定义形状的底层 API 都是它
  • 从贝塞尔曲线推广而来的 NURBS,统治着工业设计、电影特效(Maya、Houdini)的建模世界

总结

贝塞尔曲线的故事,是一个典型的"理论等待工程"的故事:1912 年伯恩斯坦构造出基函数,1959 年德卡斯特里奥写出算法,1962 年贝塞尔在雷诺把它变成产品。三个互不知晓的人,共同完成了"用少数控制点描述光滑曲线"这件事。

它成功的秘诀在于:用控制点代替点列,让设计师能直观地"拉动"曲线;用低阶曲线拼接代替高阶曲线,让数值计算稳定可靠;配合细分算法,让计算机能高效地把它画出来。

今天,汽车的车身、屏幕上的每一个字、每一次"先快后慢"的动画过渡,背后都是同一条曲线在默默工作。