對于普通人而言鞠苟,噪聲通常是都是有害的,而在圖形學中秽之,噪聲卻經(jīng)常被用來生成一些非常優(yōu)美的效果当娱,比如天空的云層,地形考榨,水面波形等跨细,還可以用于生成迷宮。
對于圖形學而言河质,噪聲通常會用作程序化效果生成(procedural generation冀惭,如前面列舉的地形水面云層等),其最開始在圖形學中引進掀鹅,是為了代替貼圖給物件添加紋理以解決電腦內存不足的問題(不過噪聲的計算通常比貼圖采樣要慢一點散休,因此在內存重組的現(xiàn)在通常是直接使用噪聲貼圖來代替shader的隨機數(shù)計算),但是并不是所有的噪聲都是有用的乐尊,只有那些數(shù)據(jù)具有一定的連貫性的噪聲才算是有用的噪聲戚丸,而如果噪聲不連貫的話,在進行貼圖采樣后扔嵌,得到的結果就會呈現(xiàn)一種混亂的狀態(tài)限府,這種對于程序化生成而言并沒有什么作用,因此圖形學中的一個理想的噪聲應該具備如下幾個特性:
- 偽隨機(不變性):所謂的噪聲只是看起來隨機而已痢缎,實際上胁勺,需要保證在同樣的輸入下,肯定能夠得到同樣的輸出牺弄,否則可能出現(xiàn)渲染的結果隨著時間或者觀察位置而變化姻几,這就不夠物理了,而且結果不可控也跟實際需要不符合势告。
- 只返回一個float值蛇捌,不管輸入是幾維的,只返回一個float咱台。
- 噪聲通常是帶限的(band-limited)络拌,噪聲頻率過高通常會導致鋸齒(鏡頭旋轉等情況下常見),因此通常其頻率范圍都是有限的回溺,不過對于一些平緩(大尺寸)變化的情形需要一些低頻噪聲春贸,而對于一些細節(jié)變化則需要一些高頻噪聲混萝。
- 噪聲需要具有一定的連續(xù)性,比如某些情況下需要計算噪聲的導數(shù)萍恕,甚至需要計算高階微分逸嘀,因此對于噪聲的連續(xù)性有一定的要求。
- 四方連續(xù)允粤,為了保證tiling時不會出現(xiàn)肉眼可辯的縫隙崭倘,需要保證上下左右四個方向都是連續(xù)的(如果使用了大量tiling可能會導致重復紋樣,而解決重復的做法就是將tiling尺寸設得足夠大类垫,雖然可能會引入其他問題司光,但是這個問題可以通過其他方式來規(guī)避)。
圖形學中常用的噪聲種類很多悉患,我們這邊可能無法一一覆蓋残家,只能先介紹幾種已經(jīng)遇到過的噪聲的實現(xiàn)算法,后面再遇到新的噪聲會不斷添加進來售躁。
1. Value Noise
Value Noise是最簡單的一類噪聲坞淮,其實現(xiàn)算法非常簡單,以2D為例迂求,我們在一個規(guī)整的2D網(wǎng)格上的每個頂點(如下圖中的每個紅色小圓點)放置一個隨機數(shù)(通常范圍在[0, 1]之間)碾盐,之后使用線性插值填充每個小方格,得到的結果就是Value Noise揩局。
Value Noise的生成算法可以用如下的代碼表示:
vec3 valueNoise(vec2 uv)
{
//int position used for random number generation
vec2 intPos = floor( uv );
//frac position used for interpolation
vec2 fracPos = fract( uv );
//get the interpolation weights(u) and weights derivatives(du)
#if 1
// quintic interpolation
vec2 u = fracPos * fracPos * fracPos * (fracPos *( fracPos * 6.0 - 15.0) + 10.0);
vec2 du = 30.0 * fracPos * fracPos * (fracPos * (fracPos - 2.0) + 1.0);
#else
// cubic interpolation
vec2 u = fracPos * fracPos * (3.0 - 2.0 * fracPos);
vec2 du = 6.0 * fracPos *( 1.0 - fracPos);
#endif
//generate 4 different random value on 4 neighbor vertices
float va = hash2d( intPos + vec2(0.0, 0.0) );
float vb = hash2d( intPos + vec2(1.0, 0.0) );
float vc = hash2d( intPos + vec2(0.0, 1.0) );
float vd = hash2d( intPos + vec2(1.0, 1.0) );
float k0 = va;
float k1 = vb - va;//horizontal
float k2 = vc - va;//vertical
float k4 = va - vb - vc + vd;
//mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y);
float value = k0 + k1 * u.x + k2 * u.y + k4 * u.x * u.y;
//vec2(d value / du.xy)
vec2 derivative = du * (u.yx * k4 + vec2(k1, k2));
return vec3(value, derivative);
}
注釋中有比較詳細的解釋毫玖,這里就不贅述其實現(xiàn)原理了,輸出結果如下圖所示:
其中左側是僅僅輸出Value Noise的效果凌盯,右邊則是輸出了Gradient后的效果
2. Gradient Noise
前面介紹的Value Noise是通過對周邊頂點的隨機Value進行插值來得到噪聲貼圖的付枫,而Gradient Noise的實現(xiàn)原理與Value Noise類似,不同的是驰怎,這里是通過對周邊頂點的Gradient(梯度阐滩,可以理解為某個點的速度,常用向量來表示)進行插值來輸出噪聲貼圖县忌。
根據(jù)插值頂點選取算法的不同掂榔,這里又有不同的細分,Perlin Noise與前面的Value Noise類似症杏,都是選取周邊四個頂點(如果是3D的装获,就是周邊8個頂點,以此類推)的數(shù)據(jù)進行插值厉颤,而Simplex Noise則不同穴豫,選取的是等邊三個頂點的數(shù)據(jù)(如果是3D,選取的就是正四面體的四個頂點進行插值)逼友,下面來看一下實現(xiàn)細節(jié)精肃。
對梯度進行插值秤涩,這里有一個問題需要解決,那就是對向量的插值司抱,得到的結果肯定還是向量筐眷,而前面說過,噪聲的輸出結果應該是一個浮點數(shù)状植,那么要怎么實現(xiàn)這二者的轉換呢浊竟?
這里的做法是將當前像素點到對應頂點的連線作為一個向量怨喘,與這個頂點的梯度進行點乘津畸,就得到了對應的浮點數(shù),之后再對這個浮點數(shù)應用與Value Noise一樣的插值算法必怜,就能得到對應的噪聲結果了肉拓。
2.1 Perlin Noise
Perlin Noise是一種非常常見的Gradient Noise,其實現(xiàn)算法給出如下梳庆,其插值算法與Value Noise相同暖途,不同的只是插值是將梯度與當前點到對應頂點的方向向量的點乘結果作為數(shù)據(jù):
// return gradient noise (in x) and its derivatives (in yz)
// https://www.shadertoy.com/view/XdXBRH
vec3 perlin2DNoise(in vec2 uv, bool revert)
{
vec2 i = floor(uv);
vec2 f = fract(uv);
#if 1
// quintic interpolation
vec2 u = f * f * f * (f * (f * 6.0 - 15.0)+10.0);
vec2 du = 30.0 * f * f * (f * (f - 2.0)+1.0);
#else
// cubic interpolation
vec2 u = f * f * (3.0 - 2.0 * f);
vec2 du = 6.0 * f * (1.0 - f);
#endif
//random gradients
vec2 ga = hash2d2(i + vec2(0.0,0.0));
vec2 gb = hash2d2(i + vec2(1.0,0.0));
vec2 gc = hash2d2(i + vec2(0.0,1.0));
vec2 gd = hash2d2(i + vec2(1.0,1.0));
//random values by random gradients
float va = dot(ga, f - vec2(0.0,0.0));
float vb = dot(gb, f - vec2(1.0,0.0));
float vc = dot(gc, f - vec2(0.0,1.0));
float vd = dot(gd, f - vec2(1.0,1.0));
//mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y);
float value = va + u.x * (vb - va) + u.y * (vc - va) + u.x * u.y * (va - vb - vc + vd);
//mix(mix(ga, gb, u.x), mix(gc, gd, u.x), u.y);
vec2 derivatives = ga + u.x * (gb - ga) + u.y * (gc - ga) + u.x * u.y * (ga - gb - gc +gd) + du * (u.yx * (va - vb - vc + vd) + vec2(vb,vc) - va);
if(revert)
value = 1.0 - 2.0 * value;
return vec3(value, derivatives);
}
對應的結果圖如下所示:
根據(jù)這個思路,還可以將噪聲繼續(xù)擴展到3d膏执,基本實現(xiàn)沒有太大區(qū)別驻售,這里就直接跳過了。
2.2 Simplex Noise
實際上更米,Simplex噪聲跟Perlin噪聲都是Ken Perlin發(fā)明的欺栗,后者是對前者的優(yōu)化替代,Simplex實際上是一種算法征峦,既可以用于實現(xiàn)Value Noise迟几,同樣也可以用于實現(xiàn)Gradient Noise,不過由于Gradient Noise的應用范圍更廣栏笆,因此這里我們就直接跳過Value Noise部分类腮,只介紹用于實現(xiàn)Gradient Noise的部分。
Simplex Noise與Perlin Noise的區(qū)別在于其插值時所選取的周邊頂點的算法不同蛉加,具體而言蚜枢,是選取此像素所從屬的grid中的正三角形(等邊三角形)的三個頂點(即將Perlin Noise中的插值正方形沿著對角線一分為二,選取當前像素所在的那個正三角形的三個頂點针饥,對應到3D空間厂抽,Perlin使用的是立方體的8個頂點,而Simplex使用的則是連接相鄰三個面的對角線組成的四面體轉換后的正立方體的四個頂點)作為插值的數(shù)據(jù)源打厘。
相對Perlin Noise修肠,Simplex的實現(xiàn)更為簡潔,其成本也更低户盯。與前面計算某個像素對應的噪聲值需要通過對周邊頂點數(shù)據(jù)進行插值不同嵌施,Simplex采用的是衰減函數(shù)饲化,比如根據(jù)某個頂點到此像素的距離來計算此頂點數(shù)據(jù)對于此像素的貢獻,之后將周邊頂點的貢獻進行累加就得到了最終的輸出結果吃靠。
雖然也可以使用衰減來計算Perlin等噪聲,但是如上面兩圖所示足淆,使用衰減函數(shù)來計算Perlin噪聲等通過hypercube(正方形巢块,立方體以及更多維的超立方體,統(tǒng)稱hypercube)進行影響的算法輸出的結果會存在問題巧号,結果并不一致族奢。
前面說到,Simplex噪聲來自于正三角形(正四面體)的數(shù)據(jù)衰減丹鸿,那么這個正三角形是怎么來的呢越走?我們知道,一個2D平面靠欢,既可以使用正方形進行無縫平鋪廊敌,這種tiling方式對應的就是前面Value/Perlin Noise的計算基礎,同時也可以使用正三角形進行平鋪门怪,而這對應的則是Simplex噪聲的實現(xiàn)基礎骡澈,這里的一個問題就是這二者是如何轉換的,畢竟我們平常使用的基本上都是grid掷空,也就是正方形的平鋪方式肋殴。
這個轉換過程,在Simplex Noise, keeping it simple有比較詳細的說明拣帽,這里我們做一下簡單的搬運疼电。
如上圖所示,一個正方形减拭,經(jīng)過一定的skew(擠壓形變)之后蔽豺,可以變換成兩個正三角形,維持左下角頂點位置以及擠壓方向所對應的正方形的對角線(圖中兩個藍色正三角形重疊的邊)方向不變拧粪,我們可以推斷修陡,右上角的頂點的位移長度肯定是左上角以及右下角頂點的位移長度的兩倍(將正方形沿著連接左上右下兩個頂點的對角線進行劃分,剛好將正方形一分為二可霎,在這種布局下沿著右上-左下對角線進行擠壓魄鸦,就能算出 右上頂點的擠壓幅度正好等于處于對角線上的兩個頂點的擠壓幅度的兩倍)。
實際上癣朗,相對于上面這種口頭描述的變換關系拾因,正三邊形與正四邊形之間的變換還有一種公式化的理論推導,Simplex noise -wikipedia中說明了多維空間中從單形(在2D空間中,對應的就是等邊三角形)到正超晶格體(在2D空間中绢记,對應的就是正方形)之間的skew變換是有一套固有的公式的扁达。
對于等邊三角形上的某個點<x, y>,變換到四邊形上的某點<x1, y1>蠢熄,有:
反過來跪解,則有:
有了這兩者之間的變換關系,那么我們就可以來進行相應的變換以及噪聲計算了
如上圖所示签孔,在正方形空間的三個頂點A<0, 0>叉讥,B<1, 0>,C<1,1>經(jīng)過變換后饥追,變成了上圖右小圖中的藍色等邊三角形图仓,那么最終的噪聲計算程序就如下所示:
// uv lies in triangle space
float simplexNoise(in vec2 uv)
{
//transform from triangle to quad
const float K1 = 0.366025404; // (sqrt(3)-1)/2;
//transform from quad to triangle
const float K2 = 0.211324865; // (3 - sqrt(3))/6;
//Find the rectangle vertex
vec2 quadIntPos = floor(uv + (uv.x + uv.y)*K1);
//relative coorindates from origin vertex A
vec2 vecFromA = uv - quadIntPos + (quadIntPos.x + quadIntPos.y)*K2;
float IsLeftHalf = step(vecFromA.y,vecFromA.x);
vec2 quadVertexOffset = vec2(IsLeftHalf,1.0 - IsLeftHalf);
//vecFromA - (quadVertexOffset + (quadVertexOffset.x + quadVertexOffset.y ) * K2)
vec2 vecFromB = vecFromA - quadVertexOffset + K2;
//vecFromA - (vec(1, 1) + (1 + 1 ) * K2)
vec2 vecFromC = vecFromA - 1.0 + 2.0 * K2;
vec3 falloff = max(0.5 - vec3(dot(vecFromA,vecFromA), dot(vecFromB,vecFromB), dot(vecFromC,vecFromC)), 0.0);
vec2 ga = hash2d2(quadIntPos + 0.0);
vec2 gb = hash2d2(quadIntPos + quadVertexOffset);
vec2 gc = hash2d2(quadIntPos + 1.0);
float simplexGradient = vec3(dot(vecFromA,ga), dot(vecFromB,gb), dot(vecFromC, gc));
vec3 n = falloff * falloff * falloff * falloff * simplexGradient;
//blend all vertices' contribution
return dot(n, vec3(70.0));
}
這個程序相對此前的實現(xiàn)略顯復雜,這里做一個簡單的解釋判耕,可以結合注釋進行理解:
- 所有的計算包括輸入默認是在三角形空間中進行的
- 先將uv坐標從等邊三角形空間轉換到正方形空間透绩,轉換參數(shù)F對應的是K1,得到對應的正方形空間的左下角頂點的坐標
- 根據(jù)左下角坐標壁熄,計算當前uv坐標到三角形空間中三個頂點的方向向量:vecFromA/B/C
- 根據(jù)到三個頂點的距離,計算衰減系數(shù)falloff
- 使用隨機函數(shù)計算三個頂點上的隨機梯度向量
- 使用點乘計算三個梯度對應的噪聲結果
- 使用衰減系數(shù)計算真實的貢獻值
- 使用點乘將三個頂點的貢獻累加到一起
最終結果如下圖所示(左邊輸出的是Simplex Noise碳竟,右邊輸出的則是三個頂點的貢獻值):
3. Voronoi Noise與Worley Noise
Voronoi Noise與Worley Noise在形態(tài)上十分相似草丧,在圖形學中的應用也基本一致,比如同樣用于進行云層創(chuàng)建莹桅,水底焦散現(xiàn)象模擬等昌执,那同樣的噪聲為什么會有兩個名字呢?
實際上圖形學中最開始使用的是Voronoi噪聲诈泼,只是這種噪聲的實現(xiàn)算法消耗比較高懂拾,后面Steven Worley對齊進行了改進,提出了以其名字命名的Worley噪聲铐达。下面我們一起來看一下這兩種噪聲的實現(xiàn)算法岖赋。
如上圖所示,Voronoi噪聲是通過在空間中生成隨機分布的多個特征點瓮孙,之后對于每個需要計算的像素唐断,對所有的特征點進行遍歷,找到距離其最近的特征點杭抠,以其對應的特征值作為此像素的值進行輸出脸甘。算法思路很簡單,但是由于需要對每個特征點進行遍歷偏灿,整個算法的復雜度就變得很高了丹诀,為了降低計算的消耗,Worley噪聲就應運而生了。
Worley噪聲是通過將空間(2D/3D)劃分成一個個的cell(正方形/立方體)铆遭,在每個cell中的隨機位置隨機生成一個特征點扁藕,之后對于每個待計算的像素,搜尋周邊的cell疚脐,找到距離其最近的噪點亿柑,之后以距離此噪點的距離作為當前像素的噪聲結果,就得到了對應的Worley噪聲棍弄。相對于Voronoi噪聲望薄,Worley算法的改進點在于將搜尋范圍從所有特征點限定在了周邊的若干個cell之中(理論上最正確的搜索范圍是周邊25個cell,但實際上如果噪聲函數(shù)選取得當呼畸,使用九宮格進行搜索也能得到正確的結果)
Worly噪聲在2D空間中的示例代碼如下所示:
float Worley2D(vec2 uv, bool revert)
{
float Dist = 16.0;
vec2 intPos = floor(uv);
vec2 fracPos = fract(uv);
//search range
const int Range = 2;
for(int X = -Range; X <= Range; X++)
{
for(int Y = -Range; Y <= Range; Y++)
{
float D = distance(hash2d2(intPos + vec2(X,Y)) + vec2(X,Y), fracPos);
// take the feature point with the minimal distance
Dist = min(Dist,D);
}
}
//use the distance as output
if(revert)
return 1.0 - 2.0 * Dist;
else
return Dist;
}
輸出結果如下圖所示:
注意這里使用的搜索范圍為周邊25個cell痕支,如果將之更換成9個cell,會發(fā)現(xiàn)結果會存在異常蛮原,這是因為在某些隨機函數(shù)作用下卧须,九宮格搜索會漏掉一些正確解導致:
4. Noise FBM
有時候單一頻率的噪聲不足以滿足需求,會需要使用多級噪聲累加的結果來實現(xiàn)程序化生成儒陨,這種方式我們稱之為Fractal Brownian Motion花嘶,簡稱FBM,下圖展示了FBM的基本形式蹦漠,簡單來說就是將多個不同頻率的噪聲按照不同的振幅進行混合:
下面以Worley噪聲為例椭员,給出混合的代碼實現(xiàn):
float Worley2DFBM(vec2 uv, int octave, bool revert)
{
float noise = 0.0;
float frequency = 1.0;
float amplitude = 1.0;
for(int i = 0; i < octave; i++)
{
noise += Worley2D(uv * frequency, revert) * amplitude;
frequency *= 2.0;
amplitude *= 0.5;
}
return noise;
}
輸出結果如下圖所示:
對Simplex噪聲應用FBM,得到結果如下圖:
對Perlin噪聲應用FBM笛园,得到結果如下圖:
5. Curl Noise
Curl噪聲在圖形學中有著廣泛的應用隘击,比如可以用于對粒子位置進行調制,使之產(chǎn)生卷曲的效果研铆;比如可以對煙霧水流效果進行調制埋同,生成湍流擾動效果等。
相對于其他的流體模擬算法棵红,Curl Noise的生成算法算是十分簡單的凶赁,但是應用起來效果卻并沒有減色多少。
Curl噪聲中的Curl可以看成是跟加減乘除號同等的一種運算符號窄赋,其輸入數(shù)據(jù)是一個向量哟冬,經(jīng)過curl運算之后,就得到了一個divergence free(無散度)的向量場忆绰,這里先介紹下什么是向量的divergence浩峡,divergence的中文稱謂是‘散度’:
散度指的是向量三個分量在對應坐標軸方向上的偏微分之和,從物理上來說错敢,指的是一個向量場在某個給定的位置散開或者說收斂的程度翰灾,日常生活中常見的流體比如水流缕粹,空氣,煙霧等都是divergence-free(無散)的纸淮。
curl噪聲從物理上來說平斩,可以用來表征用于對向量進行轉向的力的大小。
下面我們來介紹一下Curl噪聲的實現(xiàn)算法咽块,對一個潛在的3D向量場而言绘面,令:
我們可以計算出其Curl Velocity算子:
而對于2D情景而言,向量場的Curl算子是一個標量侈沪,可以用如下公式進行計算:
根據(jù)流體力學原理可以得知揭璃,上述的速度場是無散的,即亭罪。
具體要怎么做呢瘦馍,以2D為例,我們這里取2D Perlin噪聲作為向量場应役,那么最終的Curl噪聲就可以用如下的公式表示:
相關實現(xiàn)代碼給出如下:
vec2 curlNoise(vec2 uv)
{
float eps = 0.00001;
float x = uv.x;
float y = uv.y;
//Find rate of change in X direction
int firstOctave = 3;
int accumOctaves = 3;
bool revertPerlin = false;
float n1 = perlin2DNoise(vec2(x, y + eps), revertPerlin).x;
float n2 = perlin2DNoise(vec2(x, y - eps), revertPerlin).x;
//Average to find approximate derivative
float a = (n1 - n2)/(2.0 * eps);
//Find rate of change in Y direction
float n3 = perlin2DNoise(vec2(x + eps, y), revertPerlin).x;
float n4 = perlin2DNoise(vec2(x - eps, y), revertPerlin).x;
//Average to find approximate derivative
float b = (n3 - n4)/(2.0 * eps);
//Curl
return vec2(a, -b);
}
輸出的Curl Noise如下圖所示:
將之用速度向量來表示情组,如下圖所示:
其中灰色部分表示的是原始的Perlin噪聲,而白色箭頭表示的則是Curl噪聲向量的方向與大小箩祥。
提高噪聲頻率院崇,得到的結果如下面兩圖所示:
6. White Noise
白噪聲(White Noise)是一種在各個頻率上的強度都十分均勻的噪聲,這種噪聲并不平滑滥比,而自然界的各種紋理實際上都是連續(xù)的亚脆,因此通常不適合用于貼圖生成(比如生成樹皮紋路)。
實際上盲泛,所謂的白噪聲并不是特指的某一種噪聲,而是一種信號的統(tǒng)計模型键耕。在離散采樣中寺滚,白噪聲具有如下特點:
- 各個采樣點之間完全沒有數(shù)值上的聯(lián)系
- 信號的均值為0,方差有限屈雄。
實現(xiàn)白噪聲最簡單的算法就是直接使用一個隨機數(shù)作為返回值村视,比如我們采用如下的算法:
vec3 whiteNoise(vec2 uv)
{
return vec3(hash2dsin(uv));
}
得到的結果如下圖所示:
此外,通過對算法進行仔細設計酒奶,還可以保證輸出的白噪聲在各個數(shù)值上的概率基本一致蚁孔。
Source Code
本文的所有源碼與對應的效果,都已放入到shadertoy中惋嚎,有興趣的同學可前往測試修正杠氢。
Reference
1. Waterworld - frankenburgh
2. Clouds - iq
3. Designer Worlds: Procedural Generation of Infinite Terrain from Real-World Elevation Data
4. Value Noise and Procedural Patterns: Part 1
5. Generative designs
6. Simplex Noise, keeping it simple
7. Simplex Noise(一)
8. Simplex noise -wikipedia
9. Worley Noise的Shader生成
10. White noise -wikipedia
11. Curl Noise - Peter Werner
12. Divergence(散度) of a vector field
13. Intro to Curl Noise
14. Curl-Noise for Procedural Fluid Flow - Siggraph 2007