引言
贝塞尔曲线(Bézier Curve)是计算机图形学中最基础、最核心的曲线表示方法之一,广泛应用于矢量图形、字体设计、动画路径规划、CAD建模等领域。它以法国工程师皮埃尔·贝塞尔(Pierre Bézier)的名字命名,其数学基础最早由保罗·德·卡斯特里奥(Paul de Casteljau)在1959年提出。本文将深入讲解贝塞尔曲线的数学原理,重点实现德卡斯特里奥(de Casteljau)递推算法,并提供完整的Java实现,帮助读者理解如何通过控制点生成平滑曲线。
贝塞尔曲线的数学原理
定义
给定 n+1 个控制点 P₀, P₁, …, Pₙ,n 阶贝塞尔曲线 B(t) 的参数方程定义为:
$$B(t) = \sum_{i=0}^{n} \binom{n}{i} (1-t)^{n-i} t^i P_i, \quad t \in [0,1]$$
其中 $\binom{n}{i}$ 是二项式系数,t 是参数。当 t 从 0 变化到 1 时,曲线从起点 P₀ 平滑过渡到终点 Pₙ。
德卡斯特里奥算法
德卡斯特里奥算法是一种几何构造方法,通过反复线性插值来计算曲线上的点,避免了直接计算高次多项式,数值稳定性更好。
算法步骤如下:
1. 给定 n+1 个控制点,对相邻两点进行线性插值,得到 n 个新点
2. 对这 n 个新点再次进行线性插值,得到 n-1 个点
3. 重复此过程,直到只剩一个点,该点即为曲线在参数 t 处的位置
线性插值公式:
$$Q_i = (1-t) \cdot P_i + t \cdot P_{i+1}$$
常见阶次
- 一阶(线性):两个控制点,曲线为直线段
- 二阶(二次):三个控制点,曲线为抛物线
- 三阶(三次):四个控制点,最常用,可表达复杂曲线
- 高阶:更多控制点,但高阶曲线计算量大,通常用分段低阶曲线拼接
Java实现
以下实现包含控制点类、德卡斯特里奥算法核心逻辑、以及可视化绘制工具。
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* 贝塞尔曲线实现
* 包含德卡斯特里奥算法核心与可视化绘制
*/
public class BezierCurveDemo extends JPanel {
// 控制点列表
private List<Point> controlPoints;
// 曲线采样精度(点数越多越平滑)
private int sampleCount = 200;
public BezierCurveDemo() {
this.controlPoints = new ArrayList<>();
// 示例:构造一个三次贝塞尔曲线的控制点
controlPoints.add(new Point(50, 350));
controlPoints.add(new Point(150, 50));
controlPoints.add(new Point(350, 50));
controlPoints.add(new Point(450, 350));
}
/**
* 德卡斯特里奥算法核心:计算参数t对应的曲线点
* @param points 控制点列表
* @param t 参数,范围[0,1]
* @return 曲线上的点坐标
*/
public Point deCasteljau(List<Point> points, double t) {
// 复制控制点,避免修改原列表
List<Point> temp = new ArrayList<>();
for (Point p : points) {
temp.add(new Point(p.x, p.y));
}
// 逐层插值,从n个点递减到1个点
int n = temp.size();
for (int layer = 1; layer < n; layer++) {
for (int i = 0; i < n - layer; i++) {
// 对相邻两点进行线性插值
int x = (int) Math.round(
(1 - t) * temp.get(i).x + t * temp.get(i + 1).x
);
int y = (int) Math.round(
(1 - t) * temp.get(i).y + t * temp.get(i + 1).y
);
temp.set(i, new Point(x, y));
}
}
return temp.get(0);
}
/**
* 使用直接公式法计算贝塞尔曲线点(用于验证)
* @param points 控制点
* @param t 参数
* @return 曲线上的点
*/
public Point bernsteinFormula(List<Point> points, double t) {
int n = points.size() - 1;
double x = 0, y = 0;
for (int i = 0; i <= n; i++) {
double coeff = binomial(n, i) * Math.pow(1 - t, n - i) * Math.pow(t, i);
x += coeff * points.get(i).x;
y += coeff * points.get(i).y;
}
return new Point((int) Math.round(x), (int) Math.round(y));
}
/**
* 计算二项式系数 C(n,k)
*/
private long binomial(int n, int k) {
if (k < 0 || k > n) return 0;
if (k == 0 || k == n) return 1;
long res = 1;
for (int i = 1; i <= k; i++) {
res = res * (n - k + i) / i;
}
return res;
}
/**
* 生成曲线上的所有采样点
*/
public List<Point> generateCurvePoints(List<Point> ctrlPoints, int samples) {
List<Point> curve = new ArrayList<>();
for (int i = 0; i <= samples; i++) {
double t = (double) i / samples;
curve.add(deCasteljau(ctrlPoints, t));
}
return curve;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制控制多边形(虚线)
g2d.setColor(Color.LIGHT_GRAY);
g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL, 0, new float[]{5}, 0));
for (int i = 0; i < controlPoints.size() - 1; i++) {
g2d.drawLine(controlPoints.get(i).x, controlPoints.get(i).y,
controlPoints.get(i + 1).x, controlPoints.get(i + 1).y);
}
// 绘制控制点
g2d.setColor(Color.RED);
for (Point p : controlPoints) {
g2d.fillOval(p.x - 5, p.y - 5, 10, 10);
}
// 绘制贝塞尔曲线
List<Point> curve = generateCurvePoints(controlPoints, sampleCount);
g2d.setColor(Color.BLUE);
g2d.setStroke(new BasicStroke(2));
for (int i = 0; i < curve.size() - 1; i++) {
g2d.drawLine(curve.get(i).x, curve.get(i).y,
curve.get(i + 1).x, curve.get(i + 1).y);
}
// 标注信息
g2d.setColor(Color.BLACK);
g2d.drawString("阶数: " + (controlPoints.size() - 1) + "次贝塞尔曲线", 20, 20);
g2d.drawString("控制点: " + controlPoints.size() + "个", 20, 40);
}
public static void main(String[] args) {
JFrame frame = new JFrame("贝塞尔曲线 - de Casteljau算法");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(550, 450);
frame.add(new BezierCurveDemo());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
/**
* 辅助类:二维点
*/
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
}
扩展:分段贝塞尔曲线
实际工程中很少使用超过三次的贝塞尔曲线,因为高阶曲线对单个控制点的变化过于敏感。更常见的做法是使用分段三次贝塞尔曲线(Cubic Bézier Spline),通过连续拼接多个三次曲线段来保证整体平滑。
/**
* 分段三次贝塞尔曲线(Cubic Spline)
* 保证相邻段在连接点处具有一阶连续性(C1连续)
*/
public class CubicBezierSpline {
/**
* 根据型值点(曲线经过的点)计算控制点
* 使用三对角矩阵求解C1连续条件
*/
public static List<Point[]> computeControlPoints(List<Point> dataPoints) {
int n = dataPoints.size() - 1;
List<Point[]> segments = new ArrayList<>();
// 简化的弦长参数化控制点计算
for (int i = 0; i < n; i++) {
Point p0 = dataPoints.get(i);
Point p3 = dataPoints.get(i + 1);
// 控制点取弦长的1/3处
Point p1 = new Point(
p0.x + (p3.x - p0.x) / 3,
p0.y + (p3.y - p0.y) / 3
);
Point p2 = new Point(
p0.x + 2 * (p3.x - p0.x) / 3,
p0.y + 2 * (p3.y - p0.y) / 3
);
segments.add(new Point[]{p0, p1, p2, p3});
}
return segments;
}
}
复杂度分析
| 指标 | 德卡斯特里奥算法 | 直接公式法 |
|---|---|---|
| 单点计算 | O(n²) | O(n) |
| 生成整条曲线(m个采样点) | O(m·n²) | O(m·n) |
| 数值稳定性 | 优秀(仅线性插值) | 一般(需计算高次幂) |
其中 n 为曲线阶数(控制点数-1),m 为采样点数。
虽然直接公式法的时间复杂度更低,但德卡斯特里奥算法的数值稳定性在实际工程中更具优势,尤其在高阶曲线上。对于三次及以下的低阶曲线,两种方法的性能差异可以忽略。
应用场景
- 矢量图形编辑:Adobe Illustrator、Figma 等工具的钢笔工具底层
- 字体渲染:TrueType 字体使用二次贝塞尔曲线,PostScript 使用三次贝塞尔曲线
- 动画插值:CSS
cubic-bezier()函数定义动画的缓动曲线 - 游戏路径:NPC 移动路径、相机轨迹的平滑过渡
- CAD/CAM:工业设计与数控加工中的曲面建模基础
总结
本文深入讲解了贝塞尔曲线的数学原理,重点实现了数值稳定的德卡斯特里奥递推算法。通过Java代码展示了如何根据控制点生成平滑曲线,并提供了分段三次贝塞尔曲线的扩展思路。贝塞尔曲线作为计算机图形学的基石算法,掌握其原理和实现对于从事图形开发、游戏开发和UI设计的工程师至关重要。