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

0x00 隨機

咱們不能預測天空中烏雲的樣子,由於它的紋理老是具備不可預測性。這種不可預測性叫作隨機 (random)。算法

在計算機圖形學中,咱們一般使用隨機來模擬天然界中的噪聲。如何得到一個隨機值呢,讓咱們從下面的函數入手:dom

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

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

咱們能夠用這個函數來生成隨機值。可是,這裏的隨機是僞隨機的,由於對於相同的 x 計算的結果都相同。spa

0x01 噪聲

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

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

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

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

這樣的噪聲被稱做白噪聲,完整代碼以下:ip

#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 平滑

和白噪聲相比,天然界中的噪聲並不是如此雜亂無序。好比本文開頭的烏雲,就沒有白噪聲這樣的顆粒感。所以,咱們須要進一步加工,讓噪聲圖變得更平滑,點與點之間的過渡更天然。ci

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

對於本來歸一化的 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);
}

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


參考資料:

相關文章
相關標籤/搜索