Marching squares(三)


Marching squares(一)Marching squares(二) 談到了如何建立等值線,現在的問題是,若想要的是這樣的輪廓圖呢?

Marching squares(三)

單純地將等值線加粗,沒辦法構成這樣的圖形,你需要的圖形就像梯田,在一個高度的下界與上界之間,畫出一個平面的帶狀區域,

Marching squares 的等值線演算,可以擴展為等值帶演算,你會指定高度的下界與上界,因此每一格資料不僅是被標示為 0 與 1,而會是個三元值,分別代表小於下界、在上下界之間、高於上界,若分別標示為 0、1、2,那麼以維基百科 Marching squares 中的圖來說,就會有 82 種可能性。

Marching squares(三)

上圖是將維基百科 Marching squares 中的圖,加上了細胞角落標示的編號,等值帶是藍色區域的部份(也就是 1111 那個顏色),乍看 82 種可能性,要為等值帶建立頂點很繁雜,不過有些頂點旋轉後是相同的,圖中將它們分類以虛線框圍起來了,這麼一來,基本的等值帶頂點就只有 34 種,其餘就旋轉頂點,就能得到相對應的等值帶。

在程式實作方法類似等值線,首先標示每一格的資料,作為細胞的一角:

function mapToCorners(values, threshold) {
    const corners = []
    for(let r = 0; r < values.length; r++) {
        const row = [];
        for(let c = 0; c < values[r].length; c++) {
            const v = values[r][c];
            row.push({
                vt : createVector(c, r, values[r][c]),
                thresholdLabel: v > threshold.upper ? '2' : (v <= threshold.upper && v >= threshold.lower ? '1' : '0')
            });
        }
        corners.push(row);
    }
    return corners;
}

再來建立細胞資料,每個細胞資料會包含四個角落的向量,以及角落值的編號:

function flatMapToCells(corners) {
    const cells = [];
    for(let r = 0; r < corners.length - 1; r++) {
        for(let c = 0; c < corners[r].length - 1; c++) {
            cells.push({
                vts: [
                    corners[r][c].vt,
                    corners[r + 1][c].vt,
                    corners[r + 1][c + 1].vt,
                    corners[r][c + 1].vt
                ], 
                cornerCase: cornerCase([
                    corners[r][c],
                    corners[r + 1][c],
                    corners[r + 1][c + 1],
                    corners[r][c + 1]
               ])
            });
        }
    }
    return cells;
}

function cornerCase(corners) {
    return corners.map(corner => corner.thresholdLabel).join('');
}

有了細胞資料之後,接下來就是建立等值帶了,這邊需要更細心一些,雖然歸納為 34 種了,也還是很多啦!總之也是小心座標點別對應錯誤了:

function case1222(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[3], U)]];
}

function case2111(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[0], vts[1], U), vts[1], vts[2], vts[3]]];
}

function case0111(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[0], vts[1], L), vts[1], vts[2], vts[3]]];
}

function case1000(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[3], L)]];
}

function case0222(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[3], U)]];
}

function case2211(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), vts[2], vts[3]]];
}

function case0011(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), vts[2], vts[3]]];
}

function case2000(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[3], L)]];
}

function case2201(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], vts[3], L), vts[3]]];
}

function case2210(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), vts[2], inter_pt(vts[2], vts[3], L), inter_pt(vts[0], vts[3], L)]];
}

function case0012(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), vts[2], inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case0021(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], vts[3], U), vts[3]]];
}

function case1120(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(vts[0], vts[3], L)]];
}

function case1102(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[2], vts[3], L), inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case1012(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], L), inter_pt(vts[1], vts[2], L), vts[2], inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case2121(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)], [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], cp, L), inter_pt(vts[3], cp, L), inter_pt(vts[2], cp, L), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]], 
        [inter_pt(vts[2], vts[1], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], cp, L), inter_pt(vts[1], cp, L), inter_pt(vts[0], cp, L), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), vts[1]]
    ];
}

function case0101(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z < L) {
        return [
            [inter_pt(vts[0], vts[1], L), vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[1], cp, L)], [inter_pt(vts[0], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], cp, U), inter_pt(vts[3], cp, U), inter_pt(vts[2], cp, U), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]], 
        [inter_pt(vts[2], vts[1], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], cp, U), inter_pt(vts[1], cp, U), inter_pt(vts[0], cp, U), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), vts[1]]
    ];
}

function case2020(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)], 
            [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[0], vts[3], L)]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], L), inter_pt(cp, vts[2], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[0], vts[3], L)]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[3], L)], 
        [inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[2], L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(cp, vts[2], U), inter_pt(vts[2], vts[3], U)]
    ];
}

function case2021(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], cp, U), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)],
            [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L),  inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L),  inter_pt(vts[0], cp, L), inter_pt(vts[3], cp, L),  inter_pt(vts[2], cp, L),  inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
    ];
}

function case0201(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z < L) {
        return [
            [inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, L)], 
            [inter_pt(vts[0], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[1], cp, U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], cp, U), inter_pt(vts[3], cp, U), inter_pt(vts[2], cp, U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
    ];
}

function rotated(vts, n) {
    return vts.slice(n, vts.length).concat(vts.slice(0, n));
}

function isobands(cell, threshold) {
    const vts = cell.vts;
    const U = threshold.upper;
    const L = threshold.lower;
    switch(cell.cornerCase) {
        case '2222': case '0000': 
            return [];
        case '1111':
            return [vts];

        // 底下每四個一組,逆時針轉動頂點就可以了  
        case '1222':
            return case1222(vts, threshold);
        case '2122':
            return case1222(vts, threshold, 1);     
        case '2212':
            return case1222(vts, threshold, 2);         
        case '2221':
            return case1222(vts, threshold, 3);

        case '2111':
            return case2111(vts, threshold);
        case '1211':
            return case2111(vts, threshold, 1);
        case '1121':
            return case2111(vts, threshold, 2);
        case '1112':
            return case2111(vts, threshold, 3); 

        case '0111':
            return case0111(vts, threshold);    
        case '1011':
            return case0111(vts, threshold, 1);
        case '1101':
            return case0111(vts, threshold, 2);
        case '1110':
            return case0111(vts, threshold, 3); 

        case '1000':
            return case1000(vts, threshold);
        case '0100':
            return case1000(vts, threshold, 1);
        case '0010':
            return case1000(vts, threshold, 2);
        case '0001':
            return case1000(vts, threshold, 3); 

        case '0222':
            return case0222(vts, threshold);
        case '2022':
            return case0222(vts, threshold, 1);
        case '2202':
            return case0222(vts, threshold, 2);
        case '2220':
            return case0222(vts, threshold, 3);

        case '2211':
            return case2211(vts, threshold);
        case '1221':
            return case2211(vts, threshold, 1);
        case '1122':
            return case2211(vts, threshold, 2);
        case '2112':
            return case2211(vts, threshold, 3);

        case '0011':
            return case0011(vts, threshold);
        case '1001':
            return case0011(vts, threshold, 1);
        case '1100':
            return case0011(vts, threshold, 2);
        case '0110':
            return case0011(vts, threshold, 3);

        case '2000':
            return case2000(vts, threshold);
        case '0200':
            return case2000(vts, threshold, 1);
        case '0020':
            return case2000(vts, threshold, 2);
        case '0002':
            return case2000(vts, threshold, 3);

        case '2201':
            return case2201(vts, threshold);
        case '1220':
            return case2201(vts, threshold, 1);
        case '0122':
            return case2201(vts, threshold, 2);
        case '2012':
            return case2201(vts, threshold, 3);

        case '2210':
            return case2210(vts, threshold);
        case '0221':
            return case2210(vts, threshold, 1);
        case '1022':
            return case2210(vts, threshold, 2);
        case '2102':
            return case2210(vts, threshold, 3);

        case '0012':
            return case0012(vts, threshold);
        case '2001':
            return case0012(vts, threshold, 1);
        case '1200':
            return case0012(vts, threshold, 2);
        case '0120':
            return case0012(vts, threshold, 3);;

        case '0021':
            return case0021(vts, threshold);
        case '1002':
            return case0021(vts, threshold, 1);
        case '2100':
            return case0021(vts, threshold, 2);
        case '0210':
            return case0021(vts, threshold, 3);

        case '1120':
            return case1120(vts, threshold);
        case '0112':
            return case1120(vts, threshold, 1);
        case '2011':
            return case1120(vts, threshold, 2);
        case '1201':
            return case1120(vts, threshold, 3);

        case '1102':
            return case1102(vts, threshold);
        case '2110':
            return case1102(vts, threshold, 1);
        case '0211':
            return case1102(vts, threshold, 2);
        case '1021':
            return case1102(vts, threshold, 3);

        case '1012':
            return case1012(vts, threshold);
        case '2101':
            return case1012(vts, threshold, 1);
        case '1210':
            return case1012(vts, threshold, 2);
        case '0121':
            return case1012(vts, threshold, 3);

        // 鞍部每個 case 有三種情況,另一 case 逆時針轉動頂點就可以了  
        case '2121':
            return case2121(vts, threshold);
        case '1212':
            return case2121(vts, threshold, 1);

        case '0101':
            return case0101(vts, threshold);
        case '1010':
            return case0101(vts, threshold, 1);

        case '2020':
            return case2020(vts, threshold);        
        case '0202':
            return case2020(vts, threshold, 1); 

        // 鞍部每個 case 有三種情況,另三 case 逆時針轉動頂點就可以了  
        case '2021':
            return case2021(vts, threshold);
        case '1202':
            return case2021(vts, threshold, 1);
        case '2120':
            return case2021(vts, threshold, 2);
        case '0212':
            return case2021(vts, threshold, 3);

        case '0201':
            return case0201(vts, threshold);
        case '1020':
            return case0201(vts, threshold, 1);
        case '0102':
            return case0201(vts, threshold, 2);
        case '2010':
            return case0201(vts, threshold, 3);
    }
}

這麼一來,就可以建立一個 contours 函式來取得全部的等值帶:

function contours(values, threshold) {
    const corners = mapToCorners(values, threshold);
    return flatMapToCells(corners)
         .map(cell => isobands(cell, threshold))
         .filter(lines => lines.length > 0)
         .flat();
}

底下完整的範例,結合了〈Perlin 雜訊〉畫出的等值帶(這邊沒有使用 p5.js-widget,因為它載入太長的程式碼會有問題):

function setup() {
    createCanvas(300, 300);
    noLoop();
    noStroke();
}

function draw() {
    background(220);

    const values = [];
    for(let x = 0; x < width; x++) {
        const row = [];
        for(let y = 0; y < height; y++) {
            const c = 255 * noise(x / 100, y / 100);
            row.push(c);
        }  
        values.push(row);
    } 

    for(let t = 255; t > 0; t -= 20) {
        fill(t, 255 - t, 0);
        contours(values, {lower: t - 20, upper: t}).forEach(band => {
            beginShape();
            band.forEach(p => vertex(p.x, p.y));
            endShape(CLOSE);
        });
    }  
}

function contours(values, threshold) {
    const corners = mapToCorners(values, threshold);

    return flatMapToCells(corners)
         .map(cell => isobands(cell, threshold))
         .filter(lines => lines.length > 0)
         .flat();
}

function mapToCorners(values, threshold) {
    const corners = []
    for(let r = 0; r < values.length; r++) {
        const row = [];
        for(let c = 0; c < values[r].length; c++) {
            const v = values[r][c];
            row.push({
                vt : createVector(c, r, values[r][c]),
                thresholdLabel: v > threshold.upper ? '2' : (v <= threshold.upper && v >= threshold.lower ? '1' : '0')
            });
        }
        corners.push(row);
    }
    return corners;
}

function flatMapToCells(corners) {
    const cells = [];
    for(let r = 0; r < corners.length - 1; r++) {
        for(let c = 0; c < corners[r].length - 1; c++) {
            cells.push({
                vts: [
                    corners[r][c].vt,
                    corners[r + 1][c].vt,
                    corners[r + 1][c + 1].vt,
                    corners[r][c + 1].vt
                ], 
                cornerCase: cornerCase([
                    corners[r][c],
                    corners[r + 1][c],
                    corners[r + 1][c + 1],
                    corners[r][c + 1]
               ])
            });
        }
    }
    return cells;
}

function cornerCase(corners) {
    return corners.map(corner => corner.thresholdLabel).join('');
}

function case1222(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[3], U)]];
}

function case2111(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[0], vts[1], U), vts[1], vts[2], vts[3]]];
}

function case0111(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[0], vts[1], L), vts[1], vts[2], vts[3]]];
}

function case1000(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[3], L)]];
}

function case0222(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[3], U)]];
}

function case2211(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), vts[2], vts[3]]];
}

function case0011(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), vts[2], vts[3]]];
}

function case2000(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[3], L)]];
}

function case2201(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], vts[3], L), vts[3]]];
}

function case2210(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], U), inter_pt(vts[1], vts[2], U), vts[2], inter_pt(vts[2], vts[3], L), inter_pt(vts[0], vts[3], L)]];
}

function case0012(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), vts[2], inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case0021(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[inter_pt(vts[0], vts[3], L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], vts[3], U), vts[3]]];
}

function case1120(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(vts[0], vts[3], L)]];
}

function case1102(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[2], vts[3], L), inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case1012(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    return [[vts[0], inter_pt(vts[0], vts[1], L), inter_pt(vts[1], vts[2], L), vts[2], inter_pt(vts[2], vts[3], U), inter_pt(vts[0], vts[3], U)]];
}

function case2121(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)], [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), vts[1], inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], cp, L), inter_pt(vts[3], cp, L), inter_pt(vts[2], cp, L), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]], 
        [inter_pt(vts[2], vts[1], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], cp, L), inter_pt(vts[1], cp, L), inter_pt(vts[0], cp, L), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), vts[1]]
    ];
}

function case0101(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z < L) {
        return [
            [inter_pt(vts[0], vts[1], L), vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[1], cp, L)], [inter_pt(vts[0], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), vts[1], inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], cp, U), inter_pt(vts[3], cp, U), inter_pt(vts[2], cp, U), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]], 
        [inter_pt(vts[2], vts[1], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], cp, U), inter_pt(vts[1], cp, U), inter_pt(vts[0], cp, U), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), vts[1]]
    ];
}

function case2020(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)], 
            [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[0], vts[3], L)]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], L), inter_pt(cp, vts[2], U), inter_pt(vts[2], vts[3], U), inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[0], vts[3], L)]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[3], L)], 
        [inter_pt(vts[2], vts[3], L), inter_pt(cp, vts[2], L), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(cp, vts[2], U), inter_pt(vts[2], vts[3], U)]
    ];
}

function case2021(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z > U) {
        return [
            [inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L), inter_pt(vts[1], cp, U), inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, U)],
            [inter_pt(vts[0], vts[3], U), inter_pt(cp, vts[3], U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, L),  inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], U), inter_pt(vts[0], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], vts[1], L),  inter_pt(vts[0], cp, L), inter_pt(vts[3], cp, L),  inter_pt(vts[2], cp, L),  inter_pt(vts[1], vts[2], L), inter_pt(vts[1], vts[2], U), inter_pt(vts[2], cp, U), inter_pt(vts[2], vts[3], U), vts[3]]
    ];
}

function case0201(cellVts, threshold, r = 0) {
    const vts = rotated(cellVts, r);
    const U = threshold.upper;
    const L = threshold.lower;
    const cp = center(vts);

    if(cp.z < L) {
        return [
            [inter_pt(vts[0], vts[1], L), inter_pt(vts[1], cp, U), inter_pt(vts[0], vts[1], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], cp, L)], 
            [inter_pt(vts[0], vts[3], L), inter_pt(cp, vts[3], L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }
    else if(cp.z <= U && cp.z >= L) {
        return [
            [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[1], cp, U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
        ];
    }

    return [
        [inter_pt(vts[0], vts[3], L), inter_pt(vts[0], cp, L), inter_pt(vts[0], vts[1], L), inter_pt(vts[0], vts[1], U), inter_pt(vts[0], cp, U), inter_pt(vts[3], cp, U), inter_pt(vts[2], cp, U), inter_pt(vts[1], vts[2], U), inter_pt(vts[1], vts[2], L), inter_pt(vts[2], cp, L), inter_pt(vts[2], vts[3], L), vts[3]]
    ];
}

function rotated(vts, n) {
    return vts.slice(n, vts.length).concat(vts.slice(0, n));
}

function isobands(cell, threshold) {
    const vts = cell.vts;
    const U = threshold.upper;
    const L = threshold.lower;
    switch(cell.cornerCase) {
        case '2222': case '0000': 
            return [];
        case '1111':
            return [vts];

        // 底下每四個一組,逆時針轉動頂點就可以了  
        case '1222':
            return case1222(vts, threshold);
        case '2122':
            return case1222(vts, threshold, 1);     
        case '2212':
            return case1222(vts, threshold, 2);         
        case '2221':
            return case1222(vts, threshold, 3);

        case '2111':
            return case2111(vts, threshold);
        case '1211':
            return case2111(vts, threshold, 1);
        case '1121':
            return case2111(vts, threshold, 2);
        case '1112':
            return case2111(vts, threshold, 3); 

        case '0111':
            return case0111(vts, threshold);    
        case '1011':
            return case0111(vts, threshold, 1);
        case '1101':
            return case0111(vts, threshold, 2);
        case '1110':
            return case0111(vts, threshold, 3); 

        case '1000':
            return case1000(vts, threshold);
        case '0100':
            return case1000(vts, threshold, 1);
        case '0010':
            return case1000(vts, threshold, 2);
        case '0001':
            return case1000(vts, threshold, 3); 

        case '0222':
            return case0222(vts, threshold);
        case '2022':
            return case0222(vts, threshold, 1);
        case '2202':
            return case0222(vts, threshold, 2);
        case '2220':
            return case0222(vts, threshold, 3);

        case '2211':
            return case2211(vts, threshold);
        case '1221':
            return case2211(vts, threshold, 1);
        case '1122':
            return case2211(vts, threshold, 2);
        case '2112':
            return case2211(vts, threshold, 3);

        case '0011':
            return case0011(vts, threshold);
        case '1001':
            return case0011(vts, threshold, 1);
        case '1100':
            return case0011(vts, threshold, 2);
        case '0110':
            return case0011(vts, threshold, 3);

        case '2000':
            return case2000(vts, threshold);
        case '0200':
            return case2000(vts, threshold, 1);
        case '0020':
            return case2000(vts, threshold, 2);
        case '0002':
            return case2000(vts, threshold, 3);

        case '2201':
            return case2201(vts, threshold);
        case '1220':
            return case2201(vts, threshold, 1);
        case '0122':
            return case2201(vts, threshold, 2);
        case '2012':
            return case2201(vts, threshold, 3);

        case '2210':
            return case2210(vts, threshold);
        case '0221':
            return case2210(vts, threshold, 1);
        case '1022':
            return case2210(vts, threshold, 2);
        case '2102':
            return case2210(vts, threshold, 3);

        case '0012':
            return case0012(vts, threshold);
        case '2001':
            return case0012(vts, threshold, 1);
        case '1200':
            return case0012(vts, threshold, 2);
        case '0120':
            return case0012(vts, threshold, 3);;

        case '0021':
            return case0021(vts, threshold);
        case '1002':
            return case0021(vts, threshold, 1);
        case '2100':
            return case0021(vts, threshold, 2);
        case '0210':
            return case0021(vts, threshold, 3);

        case '1120':
            return case1120(vts, threshold);
        case '0112':
            return case1120(vts, threshold, 1);
        case '2011':
            return case1120(vts, threshold, 2);
        case '1201':
            return case1120(vts, threshold, 3);

        case '1102':
            return case1102(vts, threshold);
        case '2110':
            return case1102(vts, threshold, 1);
        case '0211':
            return case1102(vts, threshold, 2);
        case '1021':
            return case1102(vts, threshold, 3);

        case '1012':
            return case1012(vts, threshold);
        case '2101':
            return case1012(vts, threshold, 1);
        case '1210':
            return case1012(vts, threshold, 2);
        case '0121':
            return case1012(vts, threshold, 3);

        case '2121':
            return case2121(vts, threshold);
        case '1212':
            return case2121(vts, threshold, 1);

        case '0101':
            return case0101(vts, threshold);
        case '1010':
            return case0101(vts, threshold, 1);

        case '2020':
            return case2020(vts, threshold);        
        case '0202':
            return case2020(vts, threshold, 1); 

        case '2021':
            return case2021(vts, threshold);
        case '1202':
            return case2021(vts, threshold, 1);
        case '2120':
            return case2021(vts, threshold, 2);
        case '0212':
            return case2021(vts, threshold, 3);

        case '0201':
            return case0201(vts, threshold);
        case '1020':
            return case0201(vts, threshold, 1);
        case '0102':
            return case0201(vts, threshold, 2);
        case '2010':
            return case0201(vts, threshold, 3);
    }
}

function inter_pt(v1, v2, threshold) {
    return p5.Vector.lerp(v1, v2, (threshold - v1.z) / (v2.z - v1.z));
}

function center(vts) {
    return p5.Vector.add(vts[0], vts[1]).add(vts[2]).add(vts[3]).div(4);
}

等值線、帶並不單只是用於等高線、等壓線之類的場合,我曾經實現了 OpenSCAD 版本的 Marching squares,並寫了個簡單的切片程式 Image slicer,可以從圖片生成可列印的三維線稿模型:

Marching squares(三)