只要有適當的數學公式,就可以算出每個點的位置然後畫出圖形,除了在〈3D 線段〉 中看過的螺紋或球形曲線之外,之後也會看到利用數學公式來畫〈阿基米德螺線〉。
然而有時候,我們數學不夠好,難以找出數學公式來表現想要的曲線,怎麼辦呢?這時,可以使用〈貝 茲曲線〉來近似出我們想要的曲線,例如,我的作品 Customizable Bezier vase,就使用了三次貝茲曲線,你可以自訂四個點,客製你想要的花瓶曲線。
用直線來描述曲線
如果對微積分還有些印象,大概會記得曲線的微分,可以用來求某點的切線斜率,如果忘了,可以這麼想,如果每次只看曲線中的一小段,那麼這一小段 曲線只要夠短,小到趨近於 0 的極限,那麼幾乎就可以看成是一段直線:
更具體地說,如果曲線的函數是 f(x)
,而曲線上有某點 (x, y)
,若於 X
方向前進 Δx
距離,會得到 Δy = f(x + Δx) - f(x)
,剛才說
到一小段曲線只要夠短的意思是,若 Δx
趨近於 0,這時這一小段曲線變化就可以看成是直線,而直線的斜率是 Δy / Δx
(這段描述其實就是微分的定義,而這一小段直線,就是曲線在點 (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 是如何結合上面的程式設計出來呢?