正如〈貝茲曲線〉中談到的,只要有適當的數學公式,就可以算出每個點的位置然 後畫出圖形,然而有時候,我們數學不夠好,難以找出數學公式來表現想要的「曲面」,怎麼辦呢?
既然,貝茲曲線可以用來近似出我們想要的曲線,那麼可不可以進一步擴充貝茲曲線,想辦法近似出我們想要的曲面?
上面這個曲面,使用了 16 個控制點(綠色的點)來建立貝茲曲面,改變控制點的位置,就可以改變曲面。
四條貝茲曲線
許多繪圖軟體在建立曲線時,大多都是使用三次貝茲曲線,也就是使用四個控制點來建立曲線,對於複雜的曲線,就是用多條三次貝茲曲線連接在一起, 每條三次貝茲曲線會有彼此共用的控制點,例如:
在上圖中可以看到,這段曲線其實是由三條三次貝茲曲線構成,第一條的第四個控制點,其實是第二條的第一個控制點,為了構成平滑的曲線,圖中顯示 的三個控制點構成了一條切線,通過了兩條貝茲曲線的銜接點。
因此為了便於控制,這邊在擴充貝茲曲線為貝茲曲面時,就使用三次貝茲曲線作為基礎,我們使用四條三次貝茲曲線,因此會需要 16 個控制點。
在這之前,為了將焦點集中在曲面的構造上,我已經將〈貝茲曲線〉中的範例程式
碼做了進一步的封裝,成為 bezier_curve
函式,這個函式會傳回貝茲曲線路徑上所有的點,因此,只要結合〈3D 線段
〉中繪製 3D 線段的程式碼,就可以繪製貝茲曲線,一樣地,為了集中焦點在曲面構造上,我將 3D 線段的程式碼也抽取為 polyline3d
模組了,它們都是我的程式庫 dotSCAD
的一部份。
現在先簡單地用 16 個控制點來畫出四條貝茲曲線:
include <line3d.scad>;
include <polyline3d.scad>;
include <bezier_curve.scad>;
ctrl_pts = [
[[0, 0, 20], [60, 0, -35], [90, 0, 60], [200, 0, 5]],
[[0, 50, 30], [100, 60, -25], [120, 50, 120], [200, 50, 5]],
[[0, 100, 0], [60, 120, 35], [90, 100, 60], [200, 100, 45]],
[[0, 150, 0], [60, 150, -35], [90, 180, 60], [200, 150, 45]]
];
thickness = 2;
t_step = 0.05;
for(i = [0:len(ctrl_pts) - 1]) {
bezier_pts = bezier_curve(t_step, ctrl_pts[i]);
polyline3d(bezier_pts, thickness);
}
從線到面
現在我們有四條貝茲曲線了,從圖中看來的感覺,若是有塊布可以罩住這些線,就會是個曲面了?來試試,將產生的每個點連成面,這會用到〈函
式圖形〉中談到的方式,同樣地,我將程式碼重構為一個 function_grapher
模組,以便將焦點集中在曲面的構造上:
include <bezier_curve.scad>;
include <function_grapher.scad>;
ctrl_pts = [
[[0, 0, 20], [60, 0, -35], [90, 0, 60], [200, 0, 5]],
[[0, 50, 30], [100, 60, -25], [120, 50, 120], [200, 50, 5]],
[[0, 100, 0], [60, 120, 35], [90, 100, 60], [200, 100, 45]],
[[0, 150, 0], [60, 150, -35], [90, 180, 60], [200, 150, 45]]
];
thickness = 2;
t_step = 0.05;
g_pts = [for(i = [0:len(ctrl_pts) - 1])
bezier_curve(t_step, ctrl_pts[i])
];
function_grapher(g_pts, thickness);
X 軸方向看來很平滑,感覺很接近了,不過,在 Y 軸方向上,因為還是只有四個控制點,因而看來仍是直線而不像是曲線。
用貝茲來做貝茲
在 Y 軸方向上仍不平滑,如果 Y 軸上的控制點也是由貝茲曲線產生,是不是就可以平滑了呢?這是什麼意思?來想像一下,在現有的四條貝茲曲線上,每個對應的步進線段上各取一個點,是不是也可以得到四個點?
如果對於每個對應的四個點也再做一條貝茲曲線會如何呢?
include <line3d.scad>;
include <polyline3d.scad>;
include <bezier_curve.scad>;
ctrl_pts = [
[[0, 0, 20], [60, 0, -35], [90, 0, 60], [200, 0, 5]],
[[0, 50, 30], [100, 60, -25], [120, 50, 120], [200, 50, 5]],
[[0, 100, 0], [60, 120, 35], [90, 100, 60], [200, 100, 45]],
[[0, 150, 0], [60, 150, -35], [90, 180, 60], [200, 150, 45]]
];
thickness = 2;
t_step = 0.05;
// 第一次畫四條貝茲曲線
g_pts = [for(i = [0:len(ctrl_pts) - 1])
bezier_curve(t_step, ctrl_pts[i])
];
for(i = [0:len(g_pts) - 1]) {
polyline3d(g_pts[i], thickness);
}
// 四條貝茲曲線上各自對應的四個點
crtl_j = 5;
color("red") union() {
polyline3d([for(i = [0:len(g_pts) - 1]) g_pts[i][crtl_j]], thickness);
for(i = [0:3]) {
translate(g_pts[i][5]) sphere(r = thickness * 2);
}
}
// 用四條貝茲曲線上各自對應的四個點再畫貝茲曲線
for(j = [0:len(g_pts[0]) - 1]) {
ctrl_pts2 = [for(i = [0:len(g_pts) - 1]) g_pts[i][j]];
bezier_pts = bezier_curve(t_step, ctrl_pts2);
color(j == crtl_j ? "blue" : "green") polyline3d(bezier_pts, thickness);
}
綠線部份在 X 軸與 Y 軸上,看來是不是都平滑了呢?
為了更清楚說明,除了綠線之外,在上圖中,在原先畫出的四條貝茲曲線上,各找出第五個對應的點,也就是紅球的部份,用這四個點畫出了藍色的貝茲 曲線,每條綠線就是這樣一一找出四個點,然後畫出來的貝茲曲線。
現在,不畫線了,直接把找到的綠線上每個點餵給 function_grapher
模組:
include <bezier_curve.scad>;
include <function_grapher.scad>;
ctrl_pts = [
[[0, 0, 20], [60, 0, -35], [90, 0, 60], [200, 0, 5]],
[[0, 50, 30], [100, 60, -25], [120, 50, 120], [200, 50, 5]],
[[0, 100, 0], [60, 120, 35], [90, 100, 60], [200, 100, 45]],
[[0, 150, 0], [60, 150, -35], [90, 180, 60], [200, 150, 45]]
];
thickness = 2;
t_step = 0.05;
bezier_pts = [for(i = [0:len(ctrl_pts) - 1])
bezier_curve(t_step, ctrl_pts[i])
];
g_pts = [for(j = [0:len(bezier_pts[0]) - 1])
bezier_curve(t_step,
[for(i = [0:len(bezier_pts) - 1]) bezier_pts[i][j]]
)
];
function_grapher(g_pts, thickness);
你可以進一步將每個點計算的相關細節封裝起來,就像我的 bezier_surface
函式 那樣,這麼一來,你就可以簡單地描述你的貝茲曲面了:
include <bezier_curve.scad>;
include <bezier_surface.scad>;
include <function_grapher.scad>;
t_step = 0.05;
thickness = 0.5;
ctrl_pts = [
[[0, 0, 20], [60, 0, -35], [90, 0, 60], [200, 0, 5]],
[[0, 50, 30], [100, 60, -25], [120, 50, 120], [200, 50, 5]],
[[0, 100, 0], [60, 120, 35], [90, 100, 60], [200, 100, 45]],
[[0, 150, 0], [60, 150, -35], [90, 180, 60], [200, 150, 45]]
];
g = bezier_surface(t_step, ctrl_pts);
function_grapher(g, thickness);
從貝茲曲線到貝茲曲面,就數學上本身是個值得探討的過程,而就程式面上也很有趣,你可以看到,為了將焦點集中在目前想要解決的任務上,你得適當 地封裝一些基礎操作,基於上頭繼續解決更高層的任務,這樣你才不會每次都被一些雜亂的程式碼所干擾。
至於該怎麼封裝才會有彈性?有足夠的隔離性?這就得融合一些語言上的特性,持續不斷地經驗累積與思考,動手實作並改進了。
這就是為什麼我會持續創作一些作品,持續記錄想法,並且試著做出 dotSCAD 這個程式庫的原因了,如果有興趣,可以看看我的程式庫原始碼,以及提交歷史,你會發現,其中就有著許多我的作品與文件中想法的影子。