每日算法 — 使用java实现贝塞尔曲线:德卡斯特里奥算法与N阶曲线绘制

引言

贝塞尔曲线(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 为采样点数。

虽然直接公式法的时间复杂度更低,但德卡斯特里奥算法的数值稳定性在实际工程中更具优势,尤其在高阶曲线上。对于三次及以下的低阶曲线,两种方法的性能差异可以忽略。

应用场景

  1. 矢量图形编辑:Adobe Illustrator、Figma 等工具的钢笔工具底层
  2. 字体渲染:TrueType 字体使用二次贝塞尔曲线,PostScript 使用三次贝塞尔曲线
  3. 动画插值:CSS cubic-bezier() 函数定义动画的缓动曲线
  4. 游戏路径:NPC 移动路径、相机轨迹的平滑过渡
  5. CAD/CAM:工业设计与数控加工中的曲面建模基础

总结

本文深入讲解了贝塞尔曲线的数学原理,重点实现了数值稳定的德卡斯特里奥递推算法。通过Java代码展示了如何根据控制点生成平滑曲线,并提供了分段三次贝塞尔曲线的扩展思路。贝塞尔曲线作为计算机图形学的基石算法,掌握其原理和实现对于从事图形开发、游戏开发和UI设计的工程师至关重要。