Worley 雜訊(三)


到目前為止,傳回的雜訊值都是離最接近點的距離,其實也可以取不同的距離傳回,例如,第二接近點、第三接近點等,若用這些雜訊值來作為灰階值,會構成不同的圖案。

Steven Worley 的〈A Cellular Texture Basis Function〉就提到,第二接近距離減去第一接近距離,也就是被稱為 F2 - F1 的方式,圖案會有突起的效果,例如:

Worley 雜訊(三)

其他的距離值也可以嘗試看看,例如,若計算距離時,不是採取直線距離呢?像是曼哈頓距離

function manhattan(x1, y1, x2, y2) {
    return abs(x1 - x2) + abs(y1 - y2); 
}

為了可以指定距離的計算方式,來修改一下 worleyNoise 可以接受距離計算函式:

function worleyNoise(points, x, y, distance = dist) {
    let mDist = Infinity;
    for(let i = 0; i < points.length; i++) {
        mDist = min(mDist, distance(points[i].x, points[i].y, x, y));
    }
    return mDist;
}

然後 Worleynoise 方法也可以接受距離函式:

class Worley {
    constructor(cellWidth, seed = random(255)) {
        this.cellWidth = cellWidth;
        this.seed = seed;
        this.cellPoints = cellPoints(cellWidth, seed);
    }

    neighbors(x, y) {
        const xi = floor(x / this.cellWidth);
        const yi = floor(y / this.cellWidth);
        return [
            [-1, -1], [0, -1], [1, -1],
            [-1, 0],  [0, 0], [1, 0],
            [-1, 1], [0, 1], [1, 1]
        ].map(idx => { 
            const r = this.cellPoints[idx[1] + yi];
            return r === undefined ? r : r[idx[0] + xi];      
        }).filter(p => p); 
    }

    noise(x, y, distance = dist) {
        return worleyNoise(this.neighbors(x, y), x, y, distance);
    }

    nearest(x, y, distance = dist) { 
        return nearest(this.neighbors(x, y), x, y, distance);
    }
}

來看看畫出來的效果:

現在有個問題,有辦法只繪製細胞的邊緣嗎?目前的 worleyNoise 實作行不通,因為它計算最接近點的距離,而這會構成圓的勢力範圍,若你打算基於某個距離閥值來畫邊緣,結果只會有圓的空洞產生:

Worley 雜訊(三)

其實上圖也是一種風格,將閥值條件反過來,就可以構成這種圖案:

Worley 雜訊(三)

如果你依 Steven Worley 的做法,採 F2 - F1 的話,因為圖案會有突起的效果,邊緣會比較明顯,效果會好一些:

Worley 雜訊(三)

之所以會有這樣的結果,原因在於在兩個細胞的邊界處,F2 - F1 一定會是 0,像素越靠近其中一點,F2 - F1 的值會有較大的變化;然而,這並不是很精確的做法,畢竟只是單純地計算距離差,而不是去計算像素與細胞邊界的距離。

只不過,若要更準確地描繪邊界,計算像素與細胞邊界的距離會是必要的,在 2012 年, Inigo Quilez 撰寫的〈voronoi edges〉中提到利用兩個最接近點,計算它們的中點後,取像素與中點的投影,確實地計算出細胞邊界的距離。

Worley 雜訊(三)

為此,可以修改 worleyNoise 如下:

function worleyNoise(points, x, y, option = {distance : dist}) {
    const distance = option.distance || dist;
    const edge = option.edge;

    if(edge) {
        const sorted = points.map(p => [p, distance(p.x, p.y, x, y)])
                             .sort((e1, e2) => e1[1] - e2[1]);
        return edge(sorted, x, y);
    }

    let mDist = Infinity;
    for(let i = 0; i < points.length; i++) {
        mDist = min(mDist, distance(points[i].x, points[i].y, x, y));
    }
    return mDist;
}

function border(points, x, y) {
    const p1 = points[0][0];
    const p2 = points[1][0];
    const mp = p5.Vector.add(p1, p2).mult(0.5);
    return createVector(x, y).sub(mp).dot(p5.Vector.sub(p1, mp));
}

function f2f1(points, x, y) {
    const p1 = points[0][0];
    const p2 = points[1][0];
    const mp = p5.Vector.add(p1, p2).mult(0.5);
    return mag(p2.x - x, p2.y - y) - mag(p1.x - x, p1.y - y);
}

optionedge 可以接受函式,該函式的參數可接受排序後的點,表示像素距離的遠近,據以計算邊界距離,上頭提供了兩個實作,borderf2f1,你也可以試著用其他的實作來看看是否會產生什麼特別的圖案。

如果採取 border 的話,會產生的圖案是:

Worley 雜訊(三)

可以跟更上頭的 F2 - F1 圖案比較看看,會發現突起更為明顯,若小於某個閥值才繪製像素,就會產生更精確的邊界:

Worley 雜訊(三)

以下是完整的程式實作: