想像有一隻海龜在沙灘上爬行,爬過的路線會留下痕跡,基本上這就是海龜繪圖,使用海龜繪圖時,我們僅操作海龜前進、轉彎等,相關的計算,會隱藏 在這些操作的底層,相關的計算,可以參考〈二 維海龜繪圖法〉。
在我的作品當中,Turtle Spiral Generator 就是基於海龜繪圖實作出來:
不同的語言下以不同的風格實作海龜繪圖會有不同的方式,OpenSCAD 因為是 Functional Programming 風格,變數值一旦設定就不可變動值了,向量也是不可變動內容的,如果你沒接觸過 Functional Programming,一開始可能會遇上一些困難。
設定位置與角度
雖然操作時,我們僅操作海龜前進、轉彎等,不過,在底層會必須記錄海龜的目前的 x、y 座標、面向的角度等,我打算將這些資料,使用 [[x,
y], angle]
這樣的資料結構組合在一起,為了方便,就使用一個 turtle
函式,只要指定 x
、y
與角度 angle
,就會自動傳
回 [[x, y], angle]
:
function turtle(x, y, angle) = [[x, y], angle];
function get_x(turtle) = turtle[0][0]; // 取得 x
function get_y(turtle) = turtle[0][1]; // 取得 y
function get_xy(turtle) = turtle[0]; // 取得 [x, y]
function get_angle(turtle) = turtle[1]; // 取得 angle
如果已經有個海龜資料,你會想要設定它的位置,但不改變它的角度,因為 OpenSCAD 的向量一但建立就不可變動,因此,你沒辦法如下操作:
t = turtle(0, 0, 0);
// 設定至 [10, 10] 座標
t[0][0] = 10;
t[0][1] = 10;
怎麼辦呢?你可以建立一個新的向量,當中包含新座標以及原角度:
function set_point(turtle, point) = [point, get_angle(turtle)];
如此一來,你就可以這麼使用:
t = turtle(0, 0, 0);
// 設定至 [10, 10] 座標
new_t = set_point(t, [10, 10]);
原本的 t
資料還是不變,因此如果你要根據座標改變後的海龜進行操作,你要使用 new_t
。
為了方便,你也可以建立 set_x
、set_y
以及 set_angle
函式:
function set_x(turtle, x) = [[x, get_y(turtle)], get_angle(turtle)]; // 設定 x
function set_y(turtle, y) = [[get_x(turtle), y], get_angle(turtle)]; // 設定 y
function set_angle(turtle, angle) = [get_xy(turtle), angle]; // 設定 angle
由目前位置移動指定長度
如果海龜從目前位置移動 leng
長度,就會在經過的路徑畫上一條線,在非 Functional
Programming 的語言中,很容易可以實作出一個 forward(leng)
的函式,呼叫過後就會畫上一條線,不過,在 OpenSCAD 中,你沒辦法這麼做!
因為 OpenSCAD 中會有副作用的操作,具體來說,就是會有繪圖輸出的操作,一定要定義在 module
中,而且 module
不能傳回值,如果要傳回值,必須使用 function
。
因此,你沒辦法寫一個 forward(leng)
模組,移動 leng
長度之後,畫上一條線,而且傳回移動後的海龜座標,怎麼辦呢?想像一下,在非
Functional Programming
的語言中,你是怎麼做的,你會保留海龜原座標、移動海龜後得到新座標,然後使用原座標與新座標畫線。
使用原座標與新座標畫線的部份,可以使用〈線段〉中談到的 polyline
模組,因此剩下的,就是取得移動後的新座標了:
function forward(turtle, leng) =
turtle(
get_x(turtle) + leng * cos(get_angle(turtle)),
get_y(turtle) + leng * sin(get_angle(turtle)),
get_angle(turtle)
);
因此,如果海龜從目前位置移動 leng
長度,想在經過的路徑畫上一條線,在 OpenSCAD
中要分兩個操作來完成:
leng = 10;
width = 1;
t = turtle(0, 0, 0);
new_t = forword(t, leng);
polyline([get_xy(t), get_xy(new_t)], width);
如果想讓目前海龜游至指定座標呢?既然你有新座標了,不就可以直接使用 polyline
嗎?因此可以這麼做:
width = 1;
t = turtle(0, 0, 0);
// 設定至 [10, 10] 座標
new_t = set_point(t, [10, 10]);
polyline([get_xy(t), get_xy(new_t)], width);
旋轉海龜
看過以上的討論,這個問題應該超簡單了吧!
function turn(turtle, angle) = [get_xy(turtle), get_angle(turtle) + angle];
那麼,就如同〈二 維海龜繪圖法〉中談到的,試著使用我們的程式庫,讓海龜畫個正三角形如何?
function turtle(x, y, angle) = [[x, y], angle];
function get_x(turtle) = turtle[0][0];
function get_y(turtle) = turtle[0][1];
function get_xy(turtle) = turtle[0];
function get_angle(turtle) = turtle[1];
function set_point(turtle, point) = [point, get_angle(turtle)];
function set_x(turtle, x) = [[x, get_y(turtle)], get_angle(turtle)];
function set_y(turtle, y) = [[get_x(turtle), y], get_angle(turtle)];
function set_angle(turtle, angle) = [get_xy(turtle), angle];
function forward(turtle, leng) =
turtle(
get_x(turtle) + leng * cos(get_angle(turtle)),
get_y(turtle) + leng * sin(get_angle(turtle)),
get_angle(turtle)
);
function turn(turtle, angle) = [get_xy(turtle), get_angle(turtle) + angle];
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);
}
side_leng = 10;
angle = 120;
width = 1;
t = turtle(0, 0, 0);
t_p1 = forward(t, side_leng); // 前進 side_leng
polyline([get_xy(t), get_xy(t_p1)], width); // 畫線
t_p2 = forward(turn(t_p1, angle), side_leng); // 轉 angle 並前進 side_leng
polyline([get_xy(t_p1), get_xy(t_p2)], width); // 畫線
t_p3 = forward(turn(t_p2, angle), side_leng); // 轉 angle 並前進 side_leng
polyline([get_xy(t_p2), get_xy(t_p3)], width); // 畫線
這隻海龜畫出來的正三角形如下:
跟非 Functional Programming 的語言有些不同吧!一開始可能會不習慣,不過習慣了之後,其實會有一些程式設計上不同的想法啟發,在〈電 腦圖學入門〉中有一些使用海龜繪圖的例子,試著改改看吧!