實作海龜繪圖


想像有一隻海龜在沙灘上爬行,爬過的路線會留下痕跡,基本上這就是海龜繪圖,使用海龜繪圖時,我們僅操作海龜前進、轉彎等,相關的計算,會隱藏 在這些操作的底層,相關的計算,可以參考〈二 維海龜繪圖法〉。

在我的作品當中,Turtle Spiral Generator 就是基於海龜繪圖實作出來:

Zentangle bracelet

不同的語言下以不同的風格實作海龜繪圖會有不同的方式,OpenSCAD 因為是 Functional Programming 風格,變數值一旦設定就不可變動值了,向量也是不可變動內容的,如果你沒接觸過 Functional Programming,一開始可能會遇上一些困難。

設定位置與角度

雖然操作時,我們僅操作海龜前進、轉彎等,不過,在底層會必須記錄海龜的目前的 x、y 座標、面向的角度等,我打算將這些資料,使用 [[x, y], angle] 這樣的資料結構組合在一起,為了方便,就使用一個 turtle 函式,只要指定 xy 與角度 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_xset_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 的語言有些不同吧!一開始可能會不習慣,不過習慣了之後,其實會有一些程式設計上不同的想法啟發,在〈電 腦圖學入門〉中有一些使用海龜繪圖的例子,試著改改看吧!