Book of Shaders 03 - 學習隨機與噪聲生成演算法

drnkcff發表於2020-10-03

0x00 隨機

我們不能預測天空中烏雲的樣子,因為它的紋理總是具有不可預測性。這種不可預測性叫做隨機 (random)。

在計算機圖形學中,我們通常使用隨機來模擬自然界中的噪聲。如何獲得一個隨機值呢,讓我們從下面的函式入手:

y = fract(sin(x) * 10000.0);

這裡,sin(x) 乘以了一個很大的數:10000.0,使得 x 值的一點微小變化也會引起計算結果的劇烈變動。同時,根據 sin 的圖形我們可以知道,在一個小範圍內,sin 函式的變化率總是不同的。結合這兩點,再使用 fract() 函式提取整個表示式的小數部分,這樣就能得到的一系列呈現出隨機狀態的值。

我們可以用這個函式來生成隨機值。但是,這裡的隨機是偽隨機的,因為對於相同的 x 計算的結果都相同。

0x01 噪聲

上面函式得到的隨機值是在一個維度變化的。為了將其應用到二維中,還需要將二維的座標值轉化為一維的浮點數。這一步可以使用點乘來實現。

y = fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453);

你可能注意到上面的表示式中有三個很奇怪的數字。有什麼特殊含義嗎?沒有!這三個數字是我從其他地方複製貼上來的。事實上,這三個數並不是固定的。也可以對其進行修改,從而得到不同的隨機效果。

將 uv 座標代入上面的表示式中,並將返回的結果賦值給該座標點的顏色,就能得到一張噪聲圖了。這樣生成的噪聲並不具備連續性,點與點之間的值存在著很大的差異。

這樣的噪聲被稱作白噪聲,完整程式碼如下:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

float random(vec2 pos) {
    return fract(sin(dot(pos.xy, vec2(12.9898,78.233)))* 43758.5453123);
}

void main() {
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    float f = random(st);
    gl_FragColor = vec4(vec3(f), 1.0);
}

0x02 平滑

和白噪聲相比,自然界中的噪聲並非如此雜亂無序。比如本文開頭的烏雲,就沒有白噪聲這樣的顆粒感。因此,我們需要進一步加工,讓噪聲圖變得更平滑,點與點之間的過渡更自然。

一種通常的做法是,對於任何一個點,都求它所在的單位格子的四個頂點的值。再使用平滑插值函式:\(f(x) = 6x^5 - 15x^4 + 10x^3\) 對這四個點的值進行插值,將插值的結果賦給原先的這個點。

對於原本歸一化的 uv 座標來說,這樣會得到一幅完全平滑過渡的圖,少了一些隨機性。所以在程式碼中,還需要將 uv 的範圍擴大,這樣就能得到更多變化。

這樣的噪聲被稱作 Value Noise,具體程式碼如下:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

float random(vec2 pos) {
    return fract(sin(dot(pos.xy, vec2(12.9898,78.233)))* 43758.5453123);
}

vec2 smooth(vec2 t) {
    return t * t * t * (t * (6.0 * t - 15.0) + 10.0);
}

float noise(in vec2 st) {
    // vec2 i = floor(st);
    vec2 s = smooth(fract(st));

    // float bl = random(i);
    // float br = random(i + vec2(1.0, 0.0));
    // float tl = random(i + vec2(0.0, 1.0));
    // float tr = random(i + vec2(1.0, 1.0));

    float tl = random(vec2(floor(st.x), ceil(st.y)));
    float tr = random(vec2(ceil(st.x), ceil(st.y)));
    float bl = random(vec2(floor(st.x), floor(st.y)));
    float br = random(vec2(ceil(st.x), floor(st.y)));

    float t = mix(tl, tr, s.x);
    float b = mix(bl, br, s.x);
    return mix(b, t, s.y);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 10.0;
    // st *= 100.0;
    float n = noise(st);
    gl_FragColor = vec4(vec3(n), 1.0);
}

上面程式碼中註釋的部分來自 The Book of Shader 給出的例子,該例子對於格子的邊界如 y = 3.0 處,將會取到 floor(3.0) + 1.0 = 4.0 的值,如果兩個點的噪聲值差距較大,則會造成格子間出現明顯的分割線。因此,改用 ceil 來計算原點所處格子右邊和上邊的邊界值,可以保證其位於格子中。

另外,不妨試著將 st 乘上更大的係數,比如再乘上 100。可以發現,當 st 來到一個更大值的時候,噪聲圖又會重新變成白噪聲。

0x03 柏林

Perlin Noise (柏林噪聲) 是由 Ken Perlin 發明的自然噪聲生成演算法。簡單來說,將空間劃分成大小相同的格子。對於一個輸入點 (x, y),取該點所在格子的每個頂點的梯度向量與頂點到該點的方向向量的點乘,作為一個頂點對於該點的貢獻值。最後使用類似 Value Noise 的插值方式計算出輸入點的值。

下圖中的紅色向量即是每個頂點的梯度,綠色向量是四個頂點到輸入位置的方向向量。對於梯度,可以使用預先計算的梯度表,也可以使用隨機函式計算出一個隨機的二維向量。

為了簡單,這裡使用隨機方法生成頂點的梯度。完整程式碼如下:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

vec2 random2(vec2 pos) {
    vec2 vec = vec2(dot(pos, vec2(12.9898,78.233)));
    return -1.0 + 2.0 * fract(sin(vec) * 43758.5453123);
}

float grad(vec2 vert, vec2 pos) {
    return dot(random2(vert), pos - vert);
}

vec2 smooth(vec2 t) {
    return t * t * t * (t * (6.0 * t - 15.0) + 10.0);
}

float perlinNoise(in vec2 st) {
    vec2 s = smooth(fract(st));

    float tl = grad(vec2(floor(st.x), ceil(st.y)), st);
    float tr = grad(vec2(ceil(st.x), ceil(st.y)), st);
    float bl = grad(vec2(floor(st.x), floor(st.y)), st);
    float br = grad(vec2(ceil(st.x), floor(st.y)), st);

    float t = mix(tl, tr, s.x);
    float b = mix(bl, br, s.x);
    return mix(b, t, s.y);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 10.0;
    float n = perlinNoise(st) + 0.5;
    gl_FragColor = vec4(vec3(n), 1.0);
}

最終生成的噪聲圖效果如下。


參考資料:

相關文章