貝茲曲線


只要有適當的數學公式,就可以算出每個點的位置然後畫出圖形,除了在〈3D 線段〉 中看過的螺紋或球形曲線之外,之後也會看到利用數學公式來畫〈阿基米德螺線〉。

然而有時候,我們數學不夠好,難以找出數學公式來表現想要的曲線,怎麼辦呢?這時,可以使用〈貝 茲曲線〉來近似出我們想要的曲線,例如,我的作品 Customizable Bezier vase,就使用了三次貝茲曲線,你可以自訂四個點,客製你想要的花瓶曲線。

Customizable Bezier vase

用直線來描述曲線

如果對微積分還有些印象,大概會記得曲線的微分,可以用來求某點的切線斜率,如果忘了,可以這麼想,如果每次只看曲線中的一小段,那麼這一小段 曲線只要夠短,小到趨近於 0 的極限,那麼幾乎就可以看成是一段直線:

貝茲曲線

更具體地說,如果曲線的函數是 f(x),而曲線上有某點 (x, y),若於 X 方向前進 Δx 距離,會得到 Δy = f(x + Δx) - f(x),剛才說 到一小段曲線只要夠短的意思是,若 Δx 趨近於 0,這時這一小段曲線變化就可以看成是直線,而直線的斜率是 Δx / Δy(這段描述其實就是微分的定義,而這一小段直線,就是曲線在點 (x, y) 處的切線)。

從這個觀點來看,一段曲線,可以看成是無數的直線構成(這就是積分的概念),只不過,上一個點跟下一個點的直線斜率也許不相同:

貝茲曲線

如果有曲線的函數 f(x),其微分 f'(x) 代入每個點的 (x, y) 就可以得到通過這個點的直線斜率,從而得到通過這個點的直線方程式,問題就在於剛剛談到的,有時候,我們數學不夠好,難以找出數學公式 f(x) 來表現想要的曲線,怎麼辦呢?

貝茲曲線的概念就是,直接用直線來描述曲線,這是什麼意思?以二次貝茲曲線舉例來說,可以使用三個點 P0、P1、P2,在 P0 與 P1 間直線的四分之一處找個點 Q0,在 P1 與 P2 間直線的四分之一處找個點 Q1,這時 Q0 與 Q1 會構成一條直線,這時也在 Q0 與 Q1 之間直線的四分之一處找個點 B0。

貝茲曲線

接著類似的作法,只是將四分之一變成二分之一、四分之三,分別找出 B1、B2 好了,接著將 P0、B0、B1、B2、P2 連起來會是什麼呢?

貝茲曲線

有點像是曲線了,上面的例子其實就是將直線分為四等分,每次前進四分之一求 B 點,如果四變成了八、十六等更大的數,數字越大就越接近真正曲線了(這也是積分的概念)。

從線性到 n 次貝茲曲線

那麼要怎麼得到 Q0 與 Q1 這條直線?這得先從線性貝茲曲線開始談,也就是…直線!如果 P0 與 P1 之間的距離為 LEN, 從 P0 到 P1 的中間若前進了長度為 len,若 t = len / LEN, 那麼 B(t) 會是:

貝茲曲線

其中 P0、P1、B(t) 都是向量的意思,以 OpenSCAD 具體來說,如果以 [x, y, z] 這個向量來表示座標點的話,那麼點 P0 的座標 (x0, y0, z0) 可以用 [x0, y0, z0] 向量來表示,也就是若用矩陣來表示的話:

貝茲曲線

自行導證出這個公式很簡單,不想導證的話也沒關係,日後有興趣再導就好,總之接著來看,如果有三個點 P0、P1、P2 呢?若 P0 與 P1 間的點有個點 X0,P1 與 P2 間有個點 X1,如果 X0 與 X1 之間的距離為 LEN,從 X0 到 X1 的中間若前進了長度為 len,若 t = len / LEN,那 麼在上頭的某點 B 就會是:

B(t) = (1 - t) X0 + t X1, t ∈ [0, 1]

如果 X0 實際上是從 P0 前進到 P1 得到,那麼 X0 的位置可以表示為 X0(t):

X0(t) = (1 - t) P0 + t P1, t ∈ [0, 1]

如果 X1 實際上是從 P1 前進到 P2 得到,那麼 X1 的位置可以表示為 X1(t):

X1(t) = (1 - t) P1 + t P2, t ∈ [0, 1]

將 X0(t)、X1(t) 代入方才的 B(t),整理一下就會得到:

B(t) = (1 - t) * (1 - t) * P0 + 2 * t * (1 - t) * P1 + t * t * P2, t ∈ [0, 1]

這就是二次貝茲曲線的公式了,依同樣的邏輯,你可以求出三次或更多次的貝茲曲線,公式就直接看維基百科上的吧!(可以運用〈巴 斯卡三角形〉來協助記憶!)

實作三次貝茲曲線

在繪圖軟體中,基本上都會提供貝茲曲線工具,因為只要操作 P0、P1、P2…這些點,就可以改變曲線的樣貌,不用數學公式就可以設計出自己想要的曲線。

OpenSCAD 本身並沒有內建貝茲曲線的功能,你可以試著寫出一個通用的貝茲曲線程式,不過,因為 OpenSCAD 不能使用滑鼠,有太多點在設定時也不是那麼方便,對於簡單的模型,像是 Customizable Bezier vase 之類的,三次貝茲曲線就夠用了,以下是求出三次貝茲曲線中各點的實作:

function bezier_coordinate(t, n0, n1, n2, n3) = 
    n0 * pow((1 - t), 3) + 3 * n1 * t * pow((1 - t), 2) + 
        3 * n2 * pow(t, 2) * (1 - t) + n3 * pow(t, 3);

function bezier_point(t, p0, p1, p2, p3) = 
    [
        bezier_coordinate(t, p0[0], p1[0], p2[0], p3[0]),
        bezier_coordinate(t, p0[1], p1[1], p2[1], p3[1]),
        bezier_coordinate(t, p0[2], p1[2], p2[2], p3[2])
    ];


function bezier_curve(t_step, p0, p1, p2, p3) = 
    [for(t = [0: t_step: 1 + t_step]) bezier_point(t, p0, p1, p2, p3)];

算出來的各個點,可以使用〈線 段〉建立起來的 polyline 模組來繪製。例如:

function bezier_coordinate(t, n0, n1, n2, n3) = 
    n0 * pow((1 - t), 3) + 3 * n1 * t * pow((1 - t), 2) + 
        3 * n2 * pow(t, 2) * (1 - t) + n3 * pow(t, 3);

function bezier_point(t, p0, p1, p2, p3) = 
    [
        bezier_coordinate(t, p0[0], p1[0], p2[0], p3[0]),
        bezier_coordinate(t, p0[1], p1[1], p2[1], p3[1]),
        bezier_coordinate(t, p0[2], p1[2], p2[2], p3[2])
    ];


function bezier_curve(t_step, p0, p1, p2, p3) = 
    [for(t = [0: t_step: 1 + t_step]) bezier_point(t, p0, p1, p2, p3)];


module line(point1, point2, width = 1, cap_round = true) {
    angle = 90 - atan((point2[1] - point1[1]) / (point2[0] - point1[0]));
    offset_x = 0.5 * width * cos(angle);
    offset_y = 0.5 * width * sin(angle);

    offset1 = [-offset_x, offset_y];
    offset2 = [offset_x, -offset_y];

    if(cap_round) {
        translate(point1) circle(d = width, $fn = 24);
        translate(point2) circle(d = width, $fn = 24);
    }

    polygon(points=[
        point1 + offset1, point2 + offset1,  
        point2 + offset2, point1 + offset2
    ]);
}

module polyline(points, width = 1) {
    module polyline_inner(points, index) {
        if(index < len(points)) {
            line(points[index - 1], points[index], width);
            polyline_inner(points, index + 1);
        }
    }

    polyline_inner(points, 1);
}

t_step = 0.05;
width = 2;

p0 = [0, 0];
p1 = [40, 60];
p2 = [-50, 90];
p3 = [0, 200];

points = bezier_curve(t_step, 
    p0, p1, p2, p3
);

polyline(points, width);

描繪出來的曲線就是:

貝茲曲線

那麼,試著挑戰看看吧!想想看 Customizable Bezier vase 是如何結合上面的程式設計出來呢?