貝茲曲面


正如〈貝茲曲線〉中談到的,只要有適當的數學公式,就可以算出每個點的位置然 後畫出圖形,然而有時候,我們數學不夠好,難以找出數學公式來表現想要的「曲面」,怎麼辦呢?

既然,貝茲曲線可以用來近似出我們想要的曲線,那麼可不可以進一步擴充貝茲曲線,想辦法近似出我們想要的曲面?

貝茲曲面

上面這個曲面,使用了 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 這個程式庫的原因了,如果有興趣,可以看看我的程式庫原始碼,以及提交歷史,你會發現,其中就有著許多我的作品與文件中想法的影子。