本文分享的是如何使用噪聲生成紋理。
首先,什麼是噪聲呢?在上篇文章中我介紹過一個生成隨機數的函式,利用隨機技巧我們生成了一個類似剪紙的圖案,那在自然界中,這種離散的隨機也是比較常見的,比如蟬鳴突然響起又突然停下,比如雨滴隨機落在一個位置,但是隨機和連續並存是更常見的情況,比如山脈的走向是隨機的,但山峰之間的高度又是連續的,比如天上的雲朵、水面的波紋等等。
那麼這種把隨機和連續結合起來,就形成了噪聲。
透過利用噪聲,我們就可以去模擬真實自然的圖案。
接下來就介紹幾種生成噪聲的常用演算法。
插值噪聲
首先是比較容易理解的插值噪聲,Value noise。
一維噪聲
我們先來看一個小例子。
// 隨機函式
float random (float x) {
return fract(sin(x * 1243758.5453123));
}
void main() {
vec2 st = vUv - vec2(0.5);
st *= 10.0;
float i = floor(st.x);
float f = fract(st.x);
// d直接等於隨機函式返回值,這樣d不連續
float d = random(i); // 取出10個不同的'd'值(0~1)
// st.y: -5 ~ +5
// 1. d < st.y - 0.1 或 d > st.y + 0.1,值為0,為黑色(st.y > d+0.1 或 st.y < d-0.1)
// 2. st.y - 0.1 < d < st.y + 0.1 時, 值為0->1->0,為黑到白再到黑的過渡色
gl_FragColor.rgb = (smoothstep(st.y - 0.10, st.y, d) - smoothstep(st.y, st.y + 0.10, d)) * vec3(1.0);
gl_FragColor.a = 1.0;
}
透過這段程式碼我們在畫布上繪製了10條線段,那麼這10條線段是怎麼生成的呢?
我們來看程式碼,首先透過減去vec2(0.5)
,相當於把紋理座標軸的原點挪到了(0.5, 0,5)
的位置,然後乘以10,就是把 st 座標值放大10倍,得到一個10 x 10的網格,這裡也有一個生成偽隨機數的函式,可以看出是根據片元所在的網格、在X軸方向的索引,生成了一個隨機數,所以也就是說整個畫布的片元去運算,會得到10個不同的 d 值。
然後我們看這個生成偽隨機數的函式,它的返回值的範圍,其實是在0到1之間,也就是說後面的d,它是一個0到1之間的值。
最後我們看這個色值的計算,當 st.y > d+0.1 或者 st.y < d-0.1時,這個值是0,也就為黑色,而st.y的範圍本來就在-5到5之間,所以我們可以看到這個隨機計算出來的10條線段是靠近X軸的。
在上面的程式碼中,我們生成的是10條離散的線段,如果我們想將計算出來的離散的值連起來,我們可以使用mix函式。
// mix(a, b, c):線性插值函式。a和b是兩個輸入的顏色或值,c是一個介於0和1之間的浮點數,表示插值的權重
// 當c接近0時,返回a;當c接近1時,mix函式返回b;當c在0和1之間時,返回a和b的插值結果。
float d = mix(random(i), random(i + 1.0), f);
這樣我們就會得到一段連續的折線。f 是取st的小數部分,是片元在它自身所在的網格內的X座標,它的範圍是在0到1之間。
我們看到折線雖然是連續的,但它看上去並不夠自然,因此我們可以改用smoothstep或者三次多項式f*f*(3.0-2.0*f)
,這樣就能得到一條連續且平滑的曲線。
float d = mix(random(i), random(i + 1.0), smoothstep(0.0, 1.0, f));
// float d = mix(random(i), random(i + 1.0), f * f * (3.0 - 2.0 * f));
隨機加連續,所以這就是噪聲函式了。
二維噪聲
可以看到,這個噪聲的生成方式,是在首尾兩個點之間進行插值。用了一個座標去生成一個隨機值,這是一維噪聲。如果要生成二維的圖案,我們需要使用二維噪聲,需要對平面畫布上 方形區域 的四個頂點,分別從x、y方向進行兩次插值。
比如下面這個例子:
float random(vec2 st) {
return fract(
sin(
dot(st.xy, vec2(12.9898, 78.233))
)
*
43758.5453123
);
}
// 二維噪聲,對st與方形區域的四個頂點插值
highp float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
vec2 u = f * f * (3.0 - 2.0 * f); // 0~1
return mix(
mix(random(i + vec2(0.0, 0.0)), random(i + vec2(1.0, 0.0)), u.x),
mix(random(i + vec2(0.0, 1.0)), random(i + vec2(1.0, 1.0)), u.x),
u.y
);
}
void main() {
vec2 st = vUv * 20.0;
gl_FragColor.rgb = vec3(noise(st));
gl_FragColor.a = 1.0;
}
這個例子中我們是透過 st 這個向量與一個常量的向量得到一個隨機值,也就是說隨機值由x座標和y座標同時決定,而不是像一維噪聲的例子中,僅由一個座標決定。
noise這個函式可以看出,當u的值分別接近四個頂點時,用來計算隨機值的向量都不同,這樣我們就得到一個插值,得到二維噪聲。這是一個比較模糊的噪聲圖案。
梯度噪聲
很顯然,這裡生成的噪聲有很明顯的缺點,最直觀的表現就是,影像有明顯的“塊狀“特點,不夠平滑。這是因為它的值的梯度不均勻。如果我們追求更平滑的噪聲效果,可以改為使用梯度噪聲,Gradient Noise。
梯度噪聲是對隨機的二維向量來插值,而不是一維的隨機數。我們來看下面的例子。
vec2 random2(vec2 st) {
st = vec2(
dot(st, vec2(127.1, 311.7)),
dot(st, vec2(269.5, 183.3))
);
return -1.0 + 2.0 * fract(sin(st) * 43758.5453123); // x和y:-1~1
}
// Gradient Noise by Inigo Quilez - iq/2013
// https://www.shadertoy.com/view/XdXGW8
float noise(vec2 st) {
vec2 i = floor(st);
vec2 f = fract(st);
vec2 u = f * f * (3.0 - 2.0 * f); // 0~1
return mix(
mix(
dot(random2(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),
dot(random2(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)),
u.x
),
mix(
dot(random2(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
dot(random2(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)),
u.x
),
u.y
);
}
void main() {
vec2 st = vUv * 20.0;
gl_FragColor.rgb = vec3(0.5 * noise(st) + 0.5);
gl_FragColor.a = 1.0;
}
在這個程式碼中,random函式生成的不再是一維的隨機數float,而是二維的隨機向量vec2,在噪聲函式Noise中透過點積dot將二維座標轉為一個數字,得到一個噪聲值。
可以看到最終的效果中,黑白的過渡明顯平滑多了,不再呈現塊狀。因此許多有趣的模擬自然界特效的視覺實現都採用了梯度噪聲。
下面我們來看一個雲霧效果的視覺。
它將噪聲疊加6次,在每次疊加的時候範圍擴大一倍,但是權重減半。配合色相變化,就能到類似飛機航拍的效果。
#define OCTAVES 6
float mist(vec2 st) {
// Initial values
float value = 0.0;
float amplitude = 0.5;
// 疊加6次
for(int i = 0; i < OCTAVES; i ++) {
// 每次範圍擴大一倍,權重減半
value += amplitude * noise(st);
st *= 2.0;
amplitude *= 0.5;
}
return value;
}
// 配合色相的變化
void main() {
vec2 st = vUv;
st.x += 0.1 * uTime;
gl_FragColor.rgb = hsb2rgb(vec3(mist(st), 1.0, 1.0));
gl_FragColor.a = 1.0;
}
我們可以分別使用插值噪聲和梯度噪聲看效果,雖然色值不一樣,但是可以明顯看出,在使用插值噪聲的函式時,雲霧效果中會出現明顯的”塊狀“的特點。
Simplex Noise
接下來介紹一個Simplex Noise演算法,相比前面兩種噪聲函式,這個演算法比較新,它有非常明顯的優勢,它有更低的計算複雜度,可以用更少的計算量達到更高的維度,並且它所製造出的噪聲非常自然。
不同於前面的函式是對四邊形進行插值,Simplex Noise演算法是對三角網格進行插值,所以大大降低了計算量,提升了執行效能。它所包含的數學技巧比較高深,可以參考Book of Shaders的文章來學習。下面我們來看一個例子體會一下。
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289((x * 34.0 + 1.0) * x); }
//
// Description : GLSL 2D simplex noise function
// Author : Ian McEwan, Ashima Arts
// Maintainer : ijm
// Lastmod : 20110822 (ijm)
// License :
// Copyright (C) 2011 Ashima Arts. All rights reserved.
// Distributed under the MIT License. See LICENSE file.
// https://github.com/ashima/webgl-noise
//
float noise(vec2 v) {
// Precompute values for skewed triangular grid
const vec4 C = vec4(0.211324865405187,
// (3.0 - sqrt(3.0))/6.0
0.366025403784439,
// 0.5 * (sqrt(3.0) - 1.0)
-0.577350269189626,
// -1.0 + 2.0 * C.x
0.024390243902439);
// 1.0 / 41.0
// First corner (x0)
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
// Other two corners(x1, x2)
vec2 i1 = vec2(0, 0);
i1 = (x0.x > x0.y)? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec2 x1 = x0.xy + C.xx - i1;
vec2 x2 = x0.xy + C.zz;
// Do some permutations to avoid
// truncation effects in permutation
i = mod289(i);
vec3 p = permute(
permute(i.y + vec3(0.0, i1.y, 1.0))
+ i.x + vec3(0.0, i1.x, 1.0)
);
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x1, x1), dot(x2, x2)), 0.0);
m = m * m;
m = m * m;
// Gradients:
// 41 pts uniformly over a line, mapped onto a diamond
// (在一條線上均勻分佈 41 個點,對映到一個菱形上。)
// The ring size 17*17 = 289 is close to a multiple
// of 41(41 * 7 = 287)
// (環的大小17 * 17等於289,接近41的倍數(41 * 7等於287)。)
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
// Normalise gradients implicitly by scaling m
// Approximation of: m *= inversesqrt(a0 * a0 + h * h)
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
// Compute final noise value at P
vec3 g = vec3(0.0);
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * vec2(x1.x, x2.x) + h.yz * vec2(x1.y, x2.y);
return 130.0 * dot(m, g);
}
void main() {
vec2 st = vUv * 20.0;
gl_FragColor.rgb = vec3(0.5 * noise(st) + 0.5);
gl_FragColor.a = 1.0;
}
與梯度噪聲生成的圖案相比,它顯得更清晰一點。
網格噪聲
最後我們來看一個網格噪聲,這是將噪聲與網格結合使用來生成紋理。也是來看一個例子。
vec2 random2(vec2 st) {
st = vec2(
dot(st, vec2(127.1, 311.7)),
dot(st, vec2(269.5, 183.3))
);
return fract(sin(st) * 43758.5453123); // x和y:0~1
}
void main() {
vec2 st = vUv * 10.0;
float d = 1.0;
vec2 i_st = floor(st);
vec2 f_st = fract(st);
vec2 p = random2(i_st); // 特徵點
d = distance(f_st, p);
gl_FragColor.rgb = vec3(d);
gl_FragColor.a = 1.0;
}
首先我們使用網格技術在畫布上生成10 x 10的網格。然後構建距離場,使用隨機技術我們在每個網格內部會得到一個特徵點,在這個距離場中我們定義的距離就是片元到它所在網格的特徵點的距離。
這樣我們就使用隨機技術得到了一個紋理圖案,但是這裡每個網格很明顯是互相獨立的,是界限分明的。如果我們想要他們的邊界過渡更圓滑,那麼我們可以透過以下這種方式來處理。
就是除了當前片元所在網格的特徵點之外,還要計算片元與相鄰8個網格特徵點的距離,然後取其中的最小值。這樣看上去就是平滑的過渡。
我們還可以加上uTime,讓網格動起來,同時把特徵點也給顯示出來。這樣得到的視覺效果就會非常類似動態的生物細胞。
void main() {
vec2 st = vUv * 10.0;
float d = 1.0;
vec2 i_st = floor(st);
vec2 f_st = fract(st);
for (int i = -1; i <= 1; i ++) {
for (int j = -1; j <= 1; j ++) {
vec2 neighbor = vec2(float(i), float(j)); // 座標x和y:-1~1
vec2 p = random2(i_st + neighbor); // 9個隨機特徵點在自身網格內的座標(座標x和y:0~1)
p = 0.5 + 0.5 * sin(uTime + 6.2831 * p); // 隨時間動態變化(0~1)
// 當前點和9個特徵點 最近的距離
d = min(d, distance(f_st, neighbor + p)); // neighbor+p(座標X和Y:-1~2)
}
}
gl_FragColor.rgb = vec3(d) + step(d, 0.03); // 顯示特徵點
gl_FragColor.a = 1.0;
}
總結
透過前面的例子,可以看出噪聲還是非常有意思的技術,實際上它是一種程式化的紋理生成技術,基本思路就是對離散的隨機數進行平滑處理。可以模擬很多有趣的效果。關於噪聲這一塊的內容呢,是比較偏向技巧性的,需要更多去動手實踐,我們也可以透過去看更多的創作案例,來得到更多的啟發,比如Shadertoy.com上就有很多的著色器創作分享。
效果參考
完整程式碼