Book of Shaders 04 - 網格噪聲:Worley Noise

drnkcff發表於2020-10-04

0x00 思路

假設要生成 4 個網格,可以先在空間中指定 4 個特徵點。對於每個畫素點,計算它到最近特徵點的距離,將這個距離當作結果值輸出。

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;

    // 先指定 4 個特徵點
    vec2 point[4];
    point[0] = vec2(0.83, 0.75);
    point[1] = vec2(0.60, 0.07);
    point[2] = vec2(0.28, 0.64);
    point[3] =  vec2(0.31, 0.26);

    // 計算畫素點到 4 個特徵點的最小距離
    float m_dist = 1.0;
    for (int i = 0; i < 4; i++) {
        float dist = distance(st, point[i]);
        m_dist = min(m_dist, dist);
    }

    // 輸出結果
    vec3 color = vec3(0.0);
    color += m_dist;
    gl_FragColor = vec4(color, 1.0);
}

效果展示:

0x01 優化

迴圈對著色器很不友好,遍歷次數過多的迴圈會顯著降低著色器的效能。在 Steven Worley 發表的一篇論文《A Cellular Texture Basis Function》中描述了優化的方法。我們可以將空間分割成網格,每個網格對應一個特徵點,每個畫素點只計算到相鄰網格中的特徵點的距離。這樣,每個畫素點就只需要計算到九個特徵點的距離,它自身所在的網格的特徵點和相鄰的八個網格的特徵點。

另外,還可以改用每個網格的整數座標來構造隨機的特徵點。省去了手動指定特徵點的麻煩,同時也帶來了更多的隨機性。

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

vec2 random2(vec2 pos) {
    float x = dot(pos, vec2(127.1, 311.7));
    float y = dot(pos, vec2(269.5, 183.3));
    return fract(sin(vec2(x, y)) * 43758.5453);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 9.0;

    vec2 i = floor(st);
    vec2 f = fract(st);

    float m_dist = 1.0;
    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            // 相鄰格子
            vec2 neighbor = vec2(float(x), float(y));
            // 生成隨機特徵點
            vec2 point = random2(i + neighbor);
            // 計算距離
            float dist = length(neighbor + point - f);
            // 更新最短距離
            m_dist = min(m_dist, dist);
        }
    }

    vec3 color = vec3(0.0);
    color += m_dist;
    gl_FragColor = vec4(color, 1.0);
}

效果展示:

0x02 擴充套件

Inigo Quilez 寫了一篇文章,提出了他稱之為 Voro Noise 的噪聲,可以將常規噪聲和網格噪聲組合在一起。

在 Voro Noise 中,額外使用兩個引數,不妨稱之為 u 和 v。其中,u 用來決定最終的噪聲更像常規噪聲還是網格噪聲,簡單來說:當 u 接近 0 時,生成的噪聲更接近常規噪聲;當 u 接近 1 時,生成的噪聲更接近網格噪聲。v 提供類似常規噪聲中的線性插值和網格噪聲中最短距離值的功能(ps:最短距離的演算法是非連續的,在 iq 的另一篇文章 Smooth Voronoi 中提供瞭解決這一問題的辦法)。

// 初版插值方法。其中,64 是一個連續性比較好的值,詳見 Smooth Voronoi 一文。
// float ww = pow( 1.0 - smoothstep(0.0, 1.414, sqrt(d)), 64.0 - 63.0 * v);

// 在初版基礎上提高函式的階數以獲得更平滑的表現。
// 64.0 - 63.0 * v => 1.0 + 63.0 * pow(1.0 - v, 4.0)
float ww = pow( 1.0 - smoothstep(0.0, 1.414, sqrt(d)), 1.0 + 63.0 * pow(1.0 - v, 4.0));

完整程式碼:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;

vec3 random3( vec2 p ) {
    float x = dot(p, vec2(127.1, 311.7));
    float y = dot(p, vec2(269.5, 183.3));
    float z = dot(p, vec2(419.2, 371.9));
    return fract(sin(vec3(x, y, z)) * 43758.5453);
}

float voroNoise(in vec2 x, float u, float v ) {
    vec2 i = floor(x);
    vec2 f = fract(x);

    float k = 1.0 + 63.0 * pow(1.0 - v, 4.0);

    // 下面兩個引數用於計算加權平均值,wa 統計總值,wt 統計總單位數
    float wa = 0.0;
    float wt = 0.0;

    // 擴大搜尋範圍,進一步提高連續性
    for (int y = -2; y <= 2; y++) {
        for (int x = -2; x <= 2; x++) {
            // 相鄰格子
            vec2 neighbor = vec2(float(x), float(y));
            // 隨機生成 point,其中 point.xy 表示特徵點,point.z 表示該點的灰度值
            vec3 point = random3(i + neighbor) * vec3(u, u, 1.0);
            // 根據距離計算貢獻值
            float dist = length(neighbor - f + point.xy);
            float ww = pow(1.0 - smoothstep(0.0, 1.414, dist), k);
            wa += point.z * ww;
            wt += ww;
        }
    }

    return wa/wt;
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    st *= 10.0;

    // 用滑鼠位置控制 voroNoise 中的 u 和 v
    float n = voroNoise(st, u_mouse.x/u_resolution.x, u_mouse.y/u_resolution.y);
    gl_FragColor = vec4(vec3(n), 1.0);
}

效果展示:

u 從 0 到 1

v 從 0 到 1

uv 一起變化


參考資料:

相關文章