日期:2021年12月2日标签:Computer Graphics

Bézier 曲线的导数:计算方法与几何意义 #

在学习 Bézier 曲线时,除了知道如何计算曲线上的点 B(t),还需要理解曲线的导数 B'(t)

如果说:

B(t)

表示曲线在参数 t 处的点,那么:

B'(t)

表示曲线在参数 t 处的切向量

也就是说:

B(t)  -> 曲线上的点
B'(t) -> 曲线在该点处的方向

导数在曲线绘制、路径动画、曲线连接、平滑性判断、法线计算和曲率计算中都非常重要。


1. 导数的直观含义 #

普通函数的导数表示变化率。

对于参数曲线:

B(t) = (x(t), y(t))

它的导数是:

B'(t) = (x'(t), y'(t))

这表示:当参数 t 稍微增加时,曲线点会往哪个方向移动,以及移动得有多快。

所以,Bézier 曲线的导数可以理解为:

曲线在某个参数位置的运动方向

如果只关心方向,可以把 B'(t) 归一化,得到单位切向量。


2. 一次 Bézier 曲线的导数 #

一次 Bézier 曲线有两个控制点:

P0, P1

公式是:

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

展开:

B(t) = P0 + t(P1 - P0)

t 求导:

B'(t) = P1 - P0

这说明一次 Bézier 曲线,也就是线段,它在任意位置的方向都相同。

它的导数是一个常量向量:

P0 -> P1

3. 二次 Bézier 曲线的导数 #

二次 Bézier 曲线有三个控制点:

P0, P1, P2

公式是:

B(t) = (1 - t)^2 P0 + 2(1 - t)t P1 + t^2 P2

它的一阶导数是:

B'(t) = 2(1 - t)(P1 - P0) + 2t(P2 - P1)

这个公式可以看成一条一次 Bézier 曲线。

令:

D0 = 2(P1 - P0)
D1 = 2(P2 - P1)

那么:

B'(t) = (1 - t)D0 + tD1

也就是说:

二次 Bézier 曲线的导数是一条一次 Bézier 曲线。

这里的 D0D1 不是普通点,而是由原控制点相邻差值构成的向量。


4. 三次 Bézier 曲线的导数 #

三次 Bézier 曲线有四个控制点:

P0, P1, P2, P3

三次 Bézier 曲线公式是:

B(t) =
(1 - t)^3 P0
+ 3(1 - t)^2t P1
+ 3(1 - t)t^2 P2
+ t^3 P3

它的一阶导数是:

B'(t) =
3(1 - t)^2(P1 - P0)
+ 6(1 - t)t(P2 - P1)
+ 3t^2(P3 - P2)

这个公式也可以看成一条二次 Bézier 曲线。

令:

D0 = 3(P1 - P0)
D1 = 3(P2 - P1)
D2 = 3(P3 - P2)

则:

B'(t) =
(1 - t)^2D0
+ 2(1 - t)tD1
+ t^2D2

所以:

三次 Bézier 曲线的导数是一条二次 Bézier 曲线。

5. 通用 Bézier 曲线的导数 #

假设原 Bézier 曲线是 n 次曲线,控制点为:

P0, P1, P2, ..., Pn

那么它的一阶导数仍然是一条 Bézier 曲线,但次数降低一阶。

也就是说:

n 次 Bézier 曲线的一阶导数 = n-1 次 Bézier 曲线

导数曲线的新控制点由原控制点的相邻差值构成:

D0 = n(P1 - P0)
D1 = n(P2 - P1)
D2 = n(P3 - P2)
...
D(n-1) = n(Pn - P(n-1))

通用公式是:

Di = n(P(i+1) - Pi)

其中:

i = 0, 1, ..., n - 1

因此:

原曲线:n 次,有 n + 1 个控制点
导数曲线:n - 1 次,有 n 个控制点

这是 Bézier 曲线导数最重要的结论。


6. 导数的几何意义 #

Bézier 曲线的导数表示曲线在某个位置的切向量。

对于三次 Bézier 曲线:

P0, P1, P2, P3

有:

B'(0) = 3(P1 - P0)
B'(1) = 3(P3 - P2)

这说明:

曲线起点处的切线方向由 P0 -> P1 决定
曲线终点处的切线方向由 P2 -> P3 决定

所以在三次 Bézier 曲线中:

P1 控制起点附近的方向
P2 控制终点附近的方向

更准确地说,控制的是起点和终点附近的切向量方向。


7. 导数有什么用? #

7.1 求切线方向 #

在参数 t 处,B'(t) 就是曲线的切向量。

如果只需要方向,可以计算:

T(t) = normalize(B'(t))

其中 T(t) 是单位切向量。

这在路径动画中很常见。例如一个物体沿曲线运动时,可以用切向量决定物体朝向。


7.2 判断曲线连接是否平滑 #

如果两条 Bézier 曲线连接在一起:

第一条曲线的终点 = 第二条曲线的起点

那么它们至少是位置连续的。

如果连接处的切线方向也一致,那么视觉上会更平滑。

对于两条三次 Bézier 曲线:

第一条:

P0, P1, P2, P3

第二条:

Q0, Q1, Q2, Q3

如果:

P3 = Q0

表示位置连续。

如果:

P3 - P2

和:

Q1 - Q0

方向一致,则连接处切线方向一致。

如果导数向量完全相等:

3(P3 - P2) = 3(Q1 - Q0)

也就是:

P3 - P2 = Q1 - Q0

则连接处一阶导数连续。

这类连续性在曲线拼接、字体轮廓、CAD 建模中都很重要。


7.3 求极值点 #

如果想知道曲线在 x 或 y 方向上的最大值或最小值,需要分析导数。

对于二维曲线:

B(t) = (x(t), y(t))

如果想找 x 方向的极值,需要解:

x'(t) = 0

如果想找 y 方向的极值,需要解:

y'(t) = 0

例如曲线的最高点、最低点、最左点、最右点,都和导数有关。


7.4 计算法线和曲率 #

有了切向量,就可以进一步计算法向量。

在二维中,如果切向量是:

T = (x, y)

那么一个法向量可以写成:

N = (-y, x)

法向量可以用于曲线偏移、描边、轮廓生成等操作。

曲率则用于描述曲线在某一点弯曲得有多厉害。曲率需要一阶导数和二阶导数共同计算。

简单理解:

一阶导数 B'(t):表示曲线方向
二阶导数 B''(t):表示方向变化趋势
曲率:表示曲线弯曲程度

8. 通用代码实现 #

学习阶段可以先用同一个 Vec3 类型表示点和向量:

struct Vec3 {
    double x, y, z;

    Vec3 operator+(const Vec3& other) const {
        return {x + other.x, y + other.y, z + other.z};
    }

    Vec3 operator-(const Vec3& other) const {
        return {x - other.x, y - other.y, z - other.z};
    }

    Vec3 operator*(double s) const {
        return {x * s, y * s, z * s};
    }
};

线性插值:

Vec3 lerp(const Vec3& a, const Vec3& b, double t) {
    return a * (1.0 - t) + b * t;
}

通用 De Casteljau 求值:

Vec3 deCasteljau(std::vector<Vec3> points, double t) {
    while (points.size() > 1) {
        std::vector<Vec3> next;
        next.reserve(points.size() - 1);

        for (size_t i = 0; i + 1 < points.size(); ++i) {
            next.push_back(lerp(points[i], points[i + 1], t));
        }

        points = std::move(next);
    }

    return points.front();
}

通用 Bézier 导数求值:

Vec3 bezierDerivative(
    const std::vector<Vec3>& controlPoints,
    double t
) {
    int n = static_cast<int>(controlPoints.size()) - 1;

    if (n <= 0) {
        return {0.0, 0.0, 0.0};
    }

    std::vector<Vec3> derivativePoints;
    derivativePoints.reserve(n);

    for (int i = 0; i < n; ++i) {
        derivativePoints.push_back(
            (controlPoints[i + 1] - controlPoints[i]) * n
        );
    }

    return deCasteljau(derivativePoints, t);
}

使用示例:

std::vector<Vec3> points = {
    {0, 0, 0},
    {3, 8, 0},
    {7, -8, 0},
    {10, 0, 0}
};

double t = 0.5;

Vec3 p = deCasteljau(points, t);
Vec3 tangent = bezierDerivative(points, t);

其中:

p       是曲线在 t 处的点 B(t)
tangent 是曲线在 t 处的切向量 B'(t)

如果只需要方向,可以归一化:

double length(const Vec3& v) {
    return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}

Vec3 normalize(const Vec3& v) {
    double len = length(v);

    if (len == 0.0) {
        return {0.0, 0.0, 0.0};
    }

    return {v.x / len, v.y / len, v.z / len};
}

使用:

Vec3 unitTangent = normalize(tangent);

9. 总结 #

Bézier 曲线导数的核心规则是:

Di = n(P(i+1) - Pi)

也就是说:

导数曲线的控制点 = 原曲线相邻控制点差值 × 原曲线次数

因此:

n 次 Bézier 曲线的一阶导数是 n-1 次 Bézier 曲线

导数的几何意义是:

B'(t) 表示曲线在参数 t 处的切向量

它可以用于:

1. 求曲线方向
2. 判断曲线连接是否平滑
3. 求极值点
4. 计算法线
5. 计算曲率
6. 做路径动画和几何建模分析

一句话记住:

Bézier 曲线的导数,就是由相邻控制点差向量组成的一条低一阶 Bézier 曲线。
目录