在我的作品中,有個蕨葉造形的濾器,可以用在咖啡上灑巧克力粉之類的用途,灑不灑得漂亮是另一回事啦!
蕨葉造形的部份是具有碎形的特徵,也就是若看造形中某個部份,就像是整體縮小後的形狀,而該部份中更小的部份,仍舊像是整體的一部份,如果沒有尺寸上的限制,可以這麼一直看下去…
你也會碎形
如果直接看蕨葉造形的程式碼,或者是其他碎形的程式碼,像是〈非關語言: 電腦圖學入門〉中的〈科赫曲線〉、〈樹木曲線〉、〈雪花曲線〉…你可能會覺得很神奇,這是怎麼構想出來的?
別想這麼多,其實你也會碎形,像是…同心圓!
$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 中的蕨葉呢?也可以參考〈非關語言: 電腦圖學入門〉中的說明喔!