遮罩與蜂巢迷宮

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

想要使用遮罩基本上也是可以的,例如,使用方才的「迷」字遮罩:

遮罩與蜂巢迷宮

你可以試著做出以上的迷宮,然後看看怎麼利用遮罩資料加上外框之類,讓迷宮有不同的變化。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter