碎形之一(Sierpinski 三角形)


在我的作品中,有個蕨葉造形的濾器,可以用在咖啡上灑巧克力粉之類的用途,灑不灑得漂亮是另一回事啦!

Fern leaf stencil

蕨葉造形的部份是具有碎形的特徵,也就是若看造形中某個部份,就像是整體縮小後的形狀,而該部份中更小的部份,仍舊像是整體的一部份,如果沒有尺寸上的限制,可以這麼一直看下去…

你也會碎形

如果直接看蕨葉造形的程式碼,或者是其他碎形的程式碼,像是〈非關語言: 電腦圖學入門〉中的〈科赫曲線〉、〈樹木曲線〉、〈雪花曲線〉…你可能會覺得很神奇,這是怎麼構想出來的?

別想這麼多,其實你也會碎形,像是…同心圓!

$fn = 48;
width = 1;

module frame(thickness) {
    difference() {
        children();
        offset(r = -thickness) children();
    }
}

frame(width) circle(40);
frame(width) circle(20);
frame(width) circle(10);
frame(width) circle(5);

碎形之一

同心圓是碎形?為什麼不是?大圓之中包含半徑為二分之一的小圓,若看其中的小圓,又包含更小的圓,只要半徑一直縮小下去,也是符合碎形的定義,只不過,上面的寫法太沒效率,而且,實際上由於物理性的限制,也不可能無限縮小半徑地繪製下去,因此,改寫成遞迴並設定半徑的終止條件吧!

radius = 40;
r_limit = 4;
width = 1;

module frame(thickness) {
    difference() {
        children();
        offset(r = -thickness) children();
    }
}

module concentric_circles(radius, r_limit, width) {
    $fn = 48;
    if(radius >= r_limit) {
        frame(width) circle(radius);
        concentric_circles(radius / 2, r_limit, width);
    }
}

concentric_circles(radius, r_limit, width);

這個程式碼的繪製結果,跟之前的圖是一樣的!

改成三角形吧!

如果把方才的程式碼中的 $fn 改為 3,結果會是如何呢?

碎形之一

大三角中包含小三角形,小三角形包含更小的三角形,這也是碎形,如果把 concentric_circles(radius / 2, r_limit, width) 改為 rotate(60) concentric_circles(radius / 2, r_limit, width),也就是每次繪製的小三角,必須轉動 60 度,結果會如何呢?

碎形之一

這也是碎形喔!

運用海龜繪圖

接下來我想要做的是,不只是建立同心的三角形,而是每個可見的小三角形,都要能看到上面的圖案,例如,想要上圖中左下的小三角,也能有上圖的整體圖案。

當然,我可以試著計算左下的小三角中心在哪,然後以它為中心畫同心圓,類似地,我也可以找出右下小三角的中心如泡製…總之…每個小三角的中心都找出來…嗚!有點麻煩。

其實你不用費心地找出每個小三角的中心,使用海龜繪圖的話,可以省去這類麻煩,還記得〈實作海龜繪圖〉中寫過畫三角形的程式嗎?我們試著將之建立為模組:

// …要用到海龜繪圖的程式碼...自己在〈實作海龜繪圖〉中找吧!

module triangle(t, side_leng, width) {    
    angle = 120; 
    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);  // 畫線
}

然後,用它來改寫先前的 concentric_circles,因為現在其實是在繪製三角形,就改名為 concentric_triangles 吧!

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        triangle(t, side_len, width);
        // 前進一半邊長,並轉動 60 度
        next_t = turn(forward(t, side_len / 2), 60); 
        // 進行下一次的同心三角形
        concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

side_len = 50;
len_limit = 4;
width = 1;
t = turtle(0, 0, 0);

concentric_triangles(t, side_len, len_limit, width);

結果會是:

碎形之一

開始變化!

現在要開始變化了,首先來問自己好了,這個同心三角形的整體部份是什麼?一個三角形中有個轉 60 度的小三角形,對吧!我們將整體部份抽取出來,成為一個 two_triangles 模組:

module triangle(t, side_leng, width) {    
    angle = 120; 
    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);  // 畫線
}

module two_triangles(t, side_len, len_limit, width) {
    angle = 60;
    triangle(t, side_len, width);
    // 前進一半邊長,並轉動 60 度
    next_t = turn(forward(t, side_len / 2), angle); 
    // 轉 60 度的小三角
    triangle(next_t, side_len / 2, width);
}

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        next_t = turn(forward(t, side_len / 2), 60); 

        concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

side_len = 50;
len_limit = 4;
width = 1;
t = turtle(0, 0, 0);

concentric_triangles(t, side_len, len_limit, width);

繪製出來的圖是一樣的,只不過有些邊會重複繪製,要去除重複繪製是也可以,不過為了簡化程式碼,也為了容易觀察模式,就暫且不理重複繪製的部份了。

接下來把焦點集中在左下三角形,想要在其中畫個同心三角形,其實就是以最大三角形的二分之一邊長畫同心三角形,對吧!因此,在 two_triangles 模組繪製之後,就來處理左下同心三角形吧!

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        // 左下同心三角形
        concentric_triangles(t, side_len / 2, len_limit, width);

        next_t = turn(forward(t, side_len / 2), 60); 

        concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

出來的圖案是:

碎形之一

那麼,接下來處理右下同心三角形,這只要讓海龜前進一半邊長,然後以最大三角形的二分之一邊長畫同心三角形就可以了:

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        // 左下同心三角形
        concentric_triangles(t, side_len / 2, len_limit, width);

        // 右下同心三角形
        concentric_triangles(forward(t, side_len / 2), side_len / 2, len_limit, width);

        next_t = turn(forward(t, side_len / 2), 60); 

        concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

出來的圖案長這樣:

碎形之一

然後最上面的同心三角形呢?讓海龜轉 60 度,前進一半長度,轉 -60 度,以二分之一邊長畫同心三角形:

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        // 左下同心三角形
        concentric_triangles(t, side_len / 2, len_limit, width);

        // 右下同心三角形
        concentric_triangles(forward(t, side_len / 2), side_len / 2, len_limit, width);

        // 最上面的同心三角形
        concentric_triangles(turn(forward(turn(t, 60), side_len / 2), -60), side_len / 2, len_limit, width);

        next_t = turn(forward(t, side_len / 2), 60); 

        concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

出來的圖形是這樣:

碎形之一

這什麼啦?這是碎形啦!看不出來,因為變成都是規則的三角形了,好像只是重複地堆疊而已嘛!是的,為了更能清楚地看出整體與部份的關係,我們把一開始正中間三角形不斷重複同心的程式碼註解掉:

module concentric_triangles(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        // 左下同心三角形
        concentric_triangles(t, side_len / 2, len_limit, width);

        // 右下同心三角形
        concentric_triangles(forward(t, side_len / 2), side_len / 2, len_limit, width);

        // 最上面的同心三角形
        concentric_triangles(turn(forward(turn(t, 60), side_len / 2), -60), side_len / 2, len_limit, width);

        // 正中的同心三角形
        // next_t = turn(forward(t, side_len / 2), 60); 
        // concentric_triangles(next_t, side_len / 2, len_limit, width);
    }
}

清楚多了!

碎形之一

然後,這個三角形有個看似很酷的名字「謝爾賓斯基三角形(Sierpinski triangle)」,我是不知道謝爾賓斯基繪出這個三角形時的思路順序是怎樣,只是藉由這個從簡單同心圓到謝爾賓斯基三角形的過程,可以清楚地瞭解到,碎形並不是一步登天就構造出來,你可以從一個簡單的想法開始(或者是一個已知的模式,無論那是簡單或複雜),然後逐步加入變化,得到你想要的結果。

當然,這只是建立碎形的其中一種方式,因此這篇的題目才取名為〈碎形之一〉,如果你一直認為碎形不易理解或者是實作,這會是個出發點,可以讓你有能力去探索更多的碎形構成方式。

以下是方才完成的圖案之完整程式碼,順便畫個大一點的謝爾賓斯基三角形:

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);
}

module triangle(t, side_leng, width) {    
    angle = 120; 
    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);  // 畫線
}

module two_triangles(t, side_len, len_limit, width) {
    angle = 60;
    triangle(t, side_len, width);
    // 前進一半邊長,並轉動 60 度
    next_t = turn(forward(t, side_len / 2), angle); 
    // 轉 60 度的小三角
    triangle(next_t, side_len / 2, width);
}

module sierpinski_triangle(t, side_len, len_limit, width) {
    if(side_len >= len_limit) {
        two_triangles(t, side_len, len_limit, width);

        // 左下同心三角形
        sierpinski_triangle(t, side_len / 2, len_limit, width);

        // 右下同心三角形
        sierpinski_triangle(forward(t, side_len / 2), side_len / 2, len_limit, width);

        // 最上面的同心三角形
        sierpinski_triangle(turn(forward(turn(t, 60), side_len / 2), -60), 
            side_len / 2, len_limit, width);
    }
}

side_len = 150;
len_limit = 4;
width = 0.5;
t = turtle(0, 0, 0);

sierpinski_triangle(t, side_len, len_limit, width);

出來的圖案是:

碎形之一

最後,想想看吧!如何從簡單到複雜,畫出 Fern leaf stencil 中的蕨葉呢?也可以參考〈非關語言: 電腦圖學入門〉中的說明喔!