Delaunay 三角分割(四)


在〈Delaunay 三角分割(一)〉談過,如果有一堆隨機散佈的點,建立了 Delaunay 三角分割,找出相鄰三角形的外接圓,將圓心連接起來,就可以構成 Voronoi 圖形。

既然是相鄰三角形,就表示有共用頂點,實際上一個 Voronoi 細胞的細胞核,就是一組相鄰三角形的共用頂點,例如下圖中,紅色細胞核是六個三角形的共用頂點:

Delaunay 三角分割(四)

因此要找出一個 Voronoi 細胞,是由哪些相鄰三角形的外接圓心連接而成,就是找出哪些三角形包含該細胞核作為頂點之一,然後求得其外接圓心,以逆時針(或順時針)排序就可以定義出一個 Voronoi 細胞的多邊形了。

因為在〈Delaunay 三角分割(三)〉中,已經實作出 Delaunay 三角分割了,Delaunay 的實例本身,帶有 triangles 特性,可以從中找出某個點作為頂點的三角形,也因為有 Delaunay 實例也有 circles 特性,可用來取得外接圓心,想實作出 Voronoi 並不是難事,只是尋找過程有沒有效率罷了。

若要有效率地尋找以細胞核作為頂點的三角形並排序,可以在找到三角形時,在頂點上做點調整,例如某個細胞核的三角形們若頂點編號如下:

Delaunay 三角分割(四)

對於這些三角形,給予新的頂點順序,例如,(b, c, a)、(c, d, a)、(d, e, a)、(e, f, a)、(f, g, a)、(g, b, a):

Delaunay 三角分割(四)

每個三角形的新頂點順序,最後一個頂點都是做為細胞核的頂點 a,這樣的好處是,若找到某個以 a 頂點的三角形,該三角形第 2 個頂點,若為某三角形的第 1 個點,那它就是下個以 a 頂點的三角形。

例如,找到 (d, e, a) 以細胞核 a 為頂點,這個三角形的第 2 個頂點是 e,而以 e 為第 1 個頂點的三角形為 (e, f, a),正好是下個以細胞核 a 為頂點的三角形,依此類推下去,就可以依序找出圍繞 a 的三角形了。

現在,可以來將任務分解為:

  • 圍繞某點的三角形頂點關係
  • 建立細胞頂點關係

首先在 Delaunay 中定義 voronoi 方法,實作第一個任務,說明就直接寫在註解中了:

class Delaunay {
    ...

    voronoi() {
        const tris = Array.from(this.triangles.keys());
        // 收集外接圓心(Voronoi 細胞頂點)
        const vertices = tris.map(t => this.circles.get(t).center);

        // 計算圍繞某點的三角形關係
        // connectedTris: 收集最後頂點連著 i 的三角形
        // triIndices: 三角形與第 i 個外接圓的對應
        const {connectedTris, triIndices} = connectedTrisIndices(this, tris);
        ...
    }
}

function connectedTrisIndices(d, tris) {
    // 收集最後頂點連著 i 的三角形
    const connectedTris = [];
    for(let i = 0; i < d.coords.length; i++) {
        connectedTris.push([]);    
    }       
    // 三角形與第 i 個外接圓心的對應
    const triIndices = new Map();  
    for(let i = 0; i < tris.length; i++) {
        const [a, b, c] = tris[i];

        // rt1、rt2、rt3 都代表 tris[i],只是頂點順序不同
        const rt1 = [b, c, a];
        const rt2 = [c, a, b];
        const rt3 = [a, b, c];

        // connectedTris 的索引就是三角形最後一個頂點索引
        // 這可用於後續依序走訪連接至某點的三角形
        connectedTris[a].push(rt1); 
        connectedTris[b].push(rt2); 
        connectedTris[c].push(rt3); 

        // rt1、rt2、rt3 都代表 tris[i],外接圓心都是 vertices 的第 i 個
        triIndices.set(rt1, i);
        triIndices.set(rt2, i);
        triIndices.set(rt3, i);
    }

    return {connectedTris, triIndices};
}

接著就是收集細胞頂點:

class Delaunay {
    ...

    voronoi() {
        ...
        const {connectedTris, triIndices} = connectedTrisIndices(this, tris);

        // 收集各細胞的頂點索引
        const cells = [];
        // 從 4 開始是因為不包含自設的矩形頂點
        for(let i = 4; i < this.coords.length; i++) {
            // 連接 i 點的三角形們構成的細胞
            cells.push(indicesOfCell(connectedTris[i], triIndices));
        }

        return {vertices, cells};
    }
}

function indicesOfCell(iTris, triIndices) {
    // 取得其中一個三角形的首個頂點,這邊取第 0 個三角形的首個頂點
    let vi = iTris[0][0];
    const indices = [];
    for(let _ = 0; _ < iTris.length; _++) {
        // 找到一個以 vi 為起點的三角形,這邊取第 0 個
        const t = iTris.filter(t => t[0] === vi)[0];
        // 收集細胞頂點索引
        indices.push(triIndices.get(t));  
        // 目前三角形的下個頂點,就是下個三角形的起點
        vi = t[1];
    }
    return indices;
}

如果只是想繪製細胞,那麼有個傳回細胞頂點座標的方法會比較方便:

class Delaunay {
    ...

    pointsOfVoronoiCells() {
        const {vertices, cells} = this.voronoi();
        return cells.map(cell => cell.map(i => vertices[i]));
    }
}

這麼一來,就可以用傳回的細胞頂點座標來繪圖了,底下的範例可以使用滑鼠來決定每個點,同繪製 Delaunay 三角分割與 Voronoi 圖形: