贝塞尔曲线
这篇笔记从"曲线"讲起:汽车设计师需要光滑的曲面,数学家早就准备好了多项式逼近的理论,工程师皮埃尔·贝塞尔(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,构造过程非常直观,分两步:
- 在线段 P0P1 上按比例 t 取点 Q0,在线段 P1P2 上按比例 t 取点 Q1
- 在线段 Q0Q1 上再按比例 t 取点,这个点就是曲线上的点 B(t)
Q0 和 Q1 是两条"滑动的线段",B(t) 是在它们之间滑动的点。让 t 从 0 扫到 1,就得到一条抛物线。这个"在连线上取点,取完再连再取"的过程,就像套在三个控制点之间的一根橡皮筋。
公式(把两步插值展开):
B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2
其中 (1-t)²、2(1-t)t、t² 恰好是二阶伯恩斯坦基函数,三个系数之和恒等于 1。
三阶(三次)贝塞尔曲线:四点
四个控制点 P0、P1、P2、P3,同样用"连线上取点"的套路,只是多做一轮:
- P0P1 上取 Q0,P1P2 上取 Q1,P2P3 上取 Q2(三次线性插值,得到 3 个点)
- Q0Q1 上取 R0,Q1Q2 上取 R1(二次插值,得到 2 个点)
- R0R1 上取点,就是 B(t)(一次插值)
展开后:
B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
系数 (1-t)³、3(1-t)²t、3(1-t)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) 就是曲线上的点。这个算法有三个巨大优势:
- 数值稳定:只做加法和乘法,不会像直接套高阶公式那样因系数很大而损失精度
- 几何直观:每一步都是"在连线上按比例取点",可以用图形界面直观演示
- 天然支持细分:算法过程中产生的中间点,本身就是细分后子曲线的控制点(见下文"细分性质")
一个简单的 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 2D:
quadraticCurveTo()、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 弹窗、镜头运镜,用贝塞尔曲线控制位移和缩放;
iTween、DOTween等动画库大量使用曲线插值 - 轨迹与路径:3D 游戏里让物体沿一条光滑路径移动(如过场动画的相机路线),路径正是贝塞尔曲线或样条曲线
机器人路径规划
机器人的运动轨迹要求光滑(速度连续、加速度有界),否则机械臂会突然抖动、冲击。规划出的路径通常经过贝塞尔曲线或 B 样条平滑处理,让机器人从 A 点移动到 B 点的过程流畅、可预测。
数据可视化与其他
- 折线图的平滑曲线、词云的标签路径、地图上的流向箭头……凡是"两点之间需要一条好看的弧线"的地方,都在用贝塞尔曲线
- 苹果的
UIBezierPath、Android 的Path.cubicTo(),移动端绘制自定义形状的底层 API 都是它 - 从贝塞尔曲线推广而来的 NURBS,统治着工业设计、电影特效(Maya、Houdini)的建模世界
总结
贝塞尔曲线的故事,是一个典型的"理论等待工程"的故事:1912 年伯恩斯坦构造出基函数,1959 年德卡斯特里奥写出算法,1962 年贝塞尔在雷诺把它变成产品。三个互不知晓的人,共同完成了"用少数控制点描述光滑曲线"这件事。
它成功的秘诀在于:用控制点代替点列,让设计师能直观地"拉动"曲线;用低阶曲线拼接代替高阶曲线,让数值计算稳定可靠;配合细分算法,让计算机能高效地把它画出来。
今天,汽车的车身、屏幕上的每一个字、每一次"先快后慢"的动画过渡,背后都是同一条曲线在默默工作。