遮罩與蜂巢迷宮
March 15, 2022迷宮一定得是方形的嗎?當然不是!這就是為什麼 dotSCAD 的 mz_square
函式,傳回的是細胞資料,而不是直接繪製迷宮的原因,這邊就來談談兩個基本的迷宮變化。
遮罩迷宮
製造不同形狀的迷宮,最簡單的方式是,將迷宮演算結合遮罩,原理很簡單,迷宮是一組細胞組成,事先設定某些細胞不能造訪,就可以將迷宮塑造為不同的形狀。例如:
這個簡單的心形迷宮,是基於以下的遮罩資料建立的,0 不能作為迷宮細胞,1 可以作為迷宮細胞,也就是可以創建道路的部份(0 與 1 我會這麼設計,是因為在灰階影像,0 代表著黑色):
mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
為了能開始走訪迷宮,走訪起點必須是在遮罩中 1 的位置,如果希望迷宮能走訪全部的 1,記得 1 彼此之間必須接續。
dotSCAD 的 mz_square
函式傳回的細胞資料中,牆面型態有可能是 "MASK"
,不過 mz_square
函式沒有接受遮罩資料的參數,然而有接受初始細胞資料的 init_cells
參數,想建立初始細胞資料,可以透過 mz_square_initialize
函式。例如:
use <maze/mz_square_initialize.scad>
mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
init_cells = mz_square_initialize(mask = mask);
mz_square_initialize
函式可以接受遮罩資料,另外設計個 mz_square_initialize
函式的原因在於,我不想讓 mz_square
有過多的參數,因此將細胞初始的職責分離出來罷了。
接著只要將 init_cells
餵給 mz_square
就可以了,例如:
use <maze/mz_square.scad>
use <maze/mz_squarewalls.scad>
use <maze/mz_square_initialize.scad>
use <polyline_join.scad>
mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
start = [3, 3];
cell_width = 5;
wall_thickness = 2;
init_cells = mz_square_initialize(mask = mask);
cells = mz_square(start = start, init_cells = init_cells);
walls = mz_squarewalls(cells, cell_width, false, false);
// 繪製迷宮牆面
for(wall = walls) {
polyline_join(wall)
square(wall_thickness, center = true);
}
// 繪製遮罩的 0
rows = len(mask);
columns = len(mask[0]);
mask_width = cell_width + wall_thickness;
translate([-wall_thickness / 2, -wall_thickness / 2])
for(r = [0:rows - 1], c = [0:columns - 1]) {
if(mask[r][c] == 0) {
translate([cell_width * c, cell_width * (rows - r - 1)])
square(mask_width);
}
}
因為細胞資料最多只會有兩個牆面資訊,mz_squarewalls
的 left_border
、bottom_border
可以決定要不要繪底部與左邊的牆,設為 false
的話就不會繪製,遮罩的細胞也不會有牆,這時若要繪製迷宮邊緣的牆,可以像以上簡單地將遮罩的 0 以方塊繪製,完成的就會是上面看到的迷宮,或者是基於遮罩建立一個框:
use <maze/mz_square.scad>
use <maze/mz_squarewalls.scad>
use <maze/mz_square_initialize.scad>
use <polyline_join.scad>
use <hollow_out.scad>
mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
start = [3, 3];
cell_width = 5;
wall_thickness = 2;
init_cells = mz_square_initialize(mask = mask);
cells = mz_square(start = start, init_cells = init_cells);
walls = mz_squarewalls(cells, cell_width, false, false);
// 繪製迷宮牆面
for(wall = walls) {
polyline_join(wall)
square(wall_thickness, center = true);
}
// 基於遮罩建一個框
rows = len(mask);
columns = len(mask[0]);
mask_width = cell_width + wall_thickness;
translate([-wall_thickness / 2, -wall_thickness / 2])
hollow_out(wall_thickness)
for(r = [0:rows - 1], c = [0:columns - 1]) {
if(mask[r][c] == 1) {
translate([cell_width * c, cell_width * (rows - r - 1)])
square(mask_width);
}
}
這會完成以下的迷宮:
問題是在於,如何能簡單地建立遮罩呢?可以用影像軟體,用點陣式的黑白圖片來畫。例如 30 x 30 的「迷」字圖:
因為 OpenSCAD 沒有提供讀入像素資料的功能,圖片轉換我寫了個 img2binary 來處理,上圖會轉出以下的資料:
mask = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0],
[0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0],
[0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
將以上的 mask
取代方才程式的 mask
,就可以建立以下的迷宮:
蜂巢迷宮
蜂巢狀迷宮,是指每個細胞的外觀都是正六角形,如蜂巢般排列,例如:
有些人第一眼的想法可能是,因為每個細胞的外觀是正六角形,行進路徑有六個方向,對吧?
其實不用,仔細觀察一下這個迷宮中每個細胞的排列方式,還是基於行列,將每一列以不同顏色表示,很容易就能看出了:
既然是基於行列,就表示只要改變細胞的繪製就可以了,在六角形結構下,往上或往下走沒有問題,就是打通六角形的上或下邊,那麼往左或往右呢?
往左或往右時,可以看到依行的不同,打通的牆會不同,以往右為例,若行索引以 0 開始,那麼偶數索引必須打通右下牆,奇數索引必須打通右上牆。
簡單來說,mz_square_cells
傳回的資料,完全可以用來繪製蜂巢迷宮,只是需要一些計算,對計算方式有興趣,可參考〈玩轉 p5.js〉的〈蜂巢狀迷宮〉。
如果你使用 dotSCAD 的 mz_hexwalls
函式,就不用管怎麼計算了,它的使用跟 mz_squarewalls
類似,只不過傳回的線段資料,可用來繪製蜂巢狀迷宮:
use <maze/mz_square.scad>
use <maze/mz_hexwalls.scad>
use <polyline_join.scad>
rows = 10;
columns = 12;
cell_width = 5;
wall_thickness = 2;
cells = mz_square(rows, columns);
walls = mz_hexwalls(cells, cell_width);
for(wall = walls) {
polyline_join(wall)
circle(wall_thickness, $fn = 6);
}
想要使用遮罩基本上也是可以的,例如,使用方才的「迷」字遮罩:
你可以試著做出以上的迷宮,然後看看怎麼利用遮罩資料加上外框之類,讓迷宮有不同的變化。