1. 程式人生 > >噪聲筆記#4 梯度噪聲_Simplex Noise

噪聲筆記#4 梯度噪聲_Simplex Noise

柏林噪聲很好用,但是它計算需要的頂點數是\small 2^n,隨著維度的增加,需要的頂點數急劇上升。於是在2001 年的 Siggraph上,他展示了 “simplex noise”,它比perlin有更低的計算量和優化。

Simplex是單形的意思

通俗解釋單形的話,可以認為是在N維空間裡,選出一個最簡單最緊湊的多邊形,讓它可以平鋪整個N維空間。我們可以很容易地想到一維空間下的單形是等長的線段(1-單形),把這些線段收尾相連即可鋪滿整個一維空間。在二維空間下,單形是三角形(2-單形),我們可以把等腰三角形連線起來鋪滿整個平面。三維空間下的單形,即3-單形就是四面體。更高維空間的單形也是存在的。

 總之記住構成單形的定點數是n+1就行了,所以3維只要4個點,4維只要5個點,和perlin噪聲相比分別省了5個點和11個點,維度越高優勢越大。

另外n!個單形可以構成一個超晶格體,也就是perlin噪聲中用到的晶格,在simplex噪聲中單形轉化成超晶格會用到,比如在2d中,2個(2!)單形構成一個晶格,在3d中則需要6個(3!)。

看起來只是單純的換個取點的晶格,但理解起來真的腦闊疼,我以2D的simplex noise為例,把這個過程一步步分開。

一、  通過偏斜(Skewing)把單形晶格轉換成容易計算的正晶格

就像這張圖一樣,注意是從紅色的單形轉換成黑色的正方形網格。轉換公式如下:

\small \\x'=x+(x+y+...)*F \\y'=y+(x+y+...)*F \\F=\frac{\sqrt{n+1}-1}{n}


本來想看這個公式怎麼推出來的,結果我找了半天只找到偏斜座標 https://en.wikipedia.org/wiki/Skew_coordinates 然後我被勸退了,也不知道是不是同一個意思 。。。。


float F=0.366025404 // (sqrt(3)-1)/2



float2 SkewPos = p + (p.x + p.y) * F;
float2 Skewi=floor(SkewPos);
float2 Skewf=frac(SkewPos);

/*
PS:在這裡我遇到了一個誤區,
我以為i和Skewi;f和Skewf是對應的,後面就混著用了。但實際上,只有p和SkewPos是對應的
因為floor和frac都是基於1的計算,但是偏斜座標裡的1和正常座標裡的1不是同一單位!!!!
雖然我覺得這個錯誤很蠢,但我還是記錄一下,所以這裡標了出來,在實際運用中,我們只會用到Skewi和skewf。   i和f是沒有意義的
*/

float2 i=floor(p);
float2 f=frac(p);

二、找到正晶格中對應的單形晶格

也就是找到該畫素點插值所需要的n+1個頂點。一個正晶格中有n!個單形晶格,在2D中,一個正晶格由2個單形晶格組成。

怎麼找呢?

比如畫素點的f向量是(x,y,...),我們把f向量的各個值從大到小排序,然後先從(0,...0)開始,然後是維度中最大的值,比如說y最大,那麼就把y值設為1,第二個頂點,就是(0,1,...,0),x第二大,那麼第二個頂點的基礎上把x的維度設為1.依次推類得到n+1個頂點。


 我的理解就是一點點縮小範圍,最後得到確定的單形,

以3D為例,如果畫素點的位置是(0.2,0.4,0.3)

第一個頂點是(0,0,0),此時6個單形都可能是目標單形,換句話說 xyz的大小排列組合有6種

y軸部分的值最大,所以第二個頂點為(0,1,0),剔除了x>y>z;x>z>y;z>y>x;z>x>y這4個晶格,因為這些晶格是不可能包括(0,1,0)這個頂點的,也不可能包含畫素點。

z軸部分第二大,所以第三個頂點為(0,1,1),剔除了y>x>z的情況,這時候晶格已經確定了。

最後一個頂點(1,1,1)收尾

就像正晶格中的單形晶格的數量是n!=n*(n-1)..*2*1一樣,一開始有n!個,確定一個點後還剩是(n-1)!個,不停的確定點,最後只剩下1!個。


2D確認單形的程式碼塊如下

   float2 p0=float2(0,0);
   float2 p1 = (Skewf.x < Skewf.y) ? float2(0.0, 1.0) : float2(1.0, 0.0);
   float2 p2=float2(1,1);

三、根據頂點得到梯度向量

這步和perlin噪聲一樣,以permutation排列表為索引得到頂點梯度向量,當然我又偷懶了,直接隨機取值吧

float2 grad1=hash2(p0+Skewi);
float2 grad2=hash2(p1+Skewi);
float2 grad3=hash2(p2+Skewi);

四、得到畫素點和頂點的距離向量

計算距離時我們要求的是,未偏斜前的頂點和畫素點的距離,所以要先把偏斜頂點位置轉換成原位置,這裡就需要用到前面的偏斜公式F的逆函式:

\small \\x=x'-(x'+y'+...)*G \\y=y'-(x'+y'+...)*G \\G=\frac{1-\frac{1}{\sqrt{n+1}}}{n}

但是像o1=p1-(p1.x+p1.y)*G這樣得到每個頂點位置然後再求距離比較麻煩(後面兩個頂點要先加再算),所以我們用只算一個然後推導另外兩個。

計算圖

首先為了方便計算,我們把A點偏移到原點,因為距離是這幾點之間的相對關係,所以不會受影響,b點共有兩種情況,所以用(bx-G,by-G)來同一表示B點,同時我們的畫素點P為(fx,fy),f就是先前求的座標的小數部分,P同時也表示和第一個點(0,0)的距離,最後是C(1-2G,1-2G);

點都有了接下來開始求距離

\small \\\underset{AP}{\rightarrow}=(fx,fy) \\\underset{BP}{\rightarrow}=\underset{AP}{\rightarrow}-\underset{AB}{\rightarrow}=(fx-bx+G,fy-by+G) \\\underset{CP}{\rightarrow}=\underset{AP}{\rightarrow}-\underset{AC}{\rightarrow}=(fx-1+2G,fy-1+2G)

OK,帶入就行了,3D思路一樣

float G = 0.211324865; // (3-sqrt(3))/6;

float2 d1=p - (Skewi - (Skewi.x + Skewi.y) * G);
float2 d2=d1-p1+G;
float2 d3=d1-1+2*G;

五、插值求和

處理之前講的插值的Hermite函式3次變四次之外,由於是單形了,所以權值的計算肯定不能像perlin一樣,直接用f值了。

Simplex利用求和替代了Perlin噪聲的插值,使用一個精.心選擇的徑向衰減函式對每個頂點的貢獻值進行計算然後求和,計算頂點貢獻值的函式如下:

 \small (max(0,r^2-|dist|^2))^4\times dot(dist,grad)

dist是距離向量,grad是梯度向量,r^的取值一般是0.5 ,因為dist^2最大為0.5(所以如果確認用0.5 max()感覺可以不用),但設為0.6是效果可能會更好。

//h=r^2-dist^2 
float3 h = max(0.5 - float3(dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0);

float3 n = h * h * h * h * float3(dot(d1, grad1), dot(d2, grad2), dot(d3, grad3));

//=>70*(n.x+n.y+n.z)
return dot(float3(70,70,70),n);//這裡返回的是(-1,1),用於顏色值要對映成(0,1)

兩個補充:

1、dist^2為啥最大為0.5: 距離最大就是點到對邊(面)的距離,也就是高,正三角形高是邊的\small \frac{\sqrt{3}}{2}倍,轉化後的正方形邊長為1,可以得到原來正三角形的邊長為\small \sqrt{\frac{2}{3}}【算(1,1)頂點對應的原位置即可得到】相乘高為\small \sqrt{\frac{1}{2}},平方即為0.5.

2、為啥最後要乘70:因為我們必須把返回值的範圍控制在[-1,1]之間,而算出來的絕對值最大卻不是1,而當點在每一條邊的中點時,值最大,前面我們已經知道了邊長和高,隨便算一下,就知道三個距離為(\small \sqrt{\frac{1}{2}},\small \sqrt{\frac{1}{6}},\small \sqrt{\frac{1}{6}}),當r^2=0.5時,h=(0,\small \frac{1}{3},\small \frac{1}{3}),梯度向量和距離向量點乘部分想要最大,方向相同 cos=1,相反-1(這也是為什麼範圍是(-1,1)而不是(0,1)的原因)兩個向量模最大的時候,距離向量前面已經確定是,而梯度向量模最大為\small \sqrt{2}


PS:關於梯度模的最大值,這裡是因為隨機取值,所以(1,1)最大,為 \small \sqrt{2},如果按照標準的預設梯度向量,模應該都是一,我看到一篇文章它用的預設梯度向量的方法,但最終還是乘以70,最後我發現它取梯度向量時,是用的3d梯度向量中的前兩個值xy,

http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf

double t0 = 0.5 - x0*x0-y0*y0;

if(t0<0) n0 = 0.0; else { t0 *= t0; n0 = t0 * t0 * dot(grad3[gi0], x0, y0);

// (x,y) of grad3 used for 2D gradient }

所以乘最終值時,還是應該考慮到具體取值的的情況


分別計算並相加三個值,所以最後的最大值:

\small \\\frac{1}{3}^4*(\frac{1}{\sqrt{6}}*\sqrt{2}+\frac{1}{\sqrt{6}}*\sqrt{2}+0) \\=\frac{1}{3}^4*(\frac{1}{\sqrt{6}}*\sqrt{2})*2 \\\approx \frac{1}{70}

這是r^2=0.5的時候,如果r^2=0.6,應該乘24.5左右。如果3D的話,邊長 \small \frac{\sqrt{3}}{2} ,0.5,最後應該乘31.32。


2D Simplex噪聲

Simplex2D

Shader "Custom/SimplexNoise2D" {
	Properties{
		_Scale("Scale",Range(3,50)) = 10
	}
	SubShader{
		Pass{
			CGPROGRAM
			#include "UnityCG.cginc"
			#pragma vertex vert
			#pragma fragment frag
			#define F 0.366025404 
			#define G 0.211324865
			float _Scale;
			struct v2f {
				float4 pos:SV_POSITION;
				half2 uv:TEXCOORD0;
			};
			v2f vert(appdata_base v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = v.texcoord;
				return o;
			}

			float2 random(float2 p) {
				p = float2(dot(p, float2(127.1, 311.7)),
					dot(p, float2(269.5, 183.3)));

				return -1.0 + 2.0 * frac(sin(p)*43758.5453123);
			}

			
			float simplexNoise(float2 p) {
				float2 SkewPos = p + (p.x + p.y) * F;
				float2 Skewi = floor(SkewPos);
				float2 Skewf = frac(SkewPos);

				float2 p1 = (Skewf.x < Skewf.y) ? float2(0.0, 1.0) : float2(1.0, 0.0);

				float2 d1 = p - (Skewi - (Skewi.x + Skewi.y) * G);
				float2 d2 = d1 - p1 + G;
				float2 d3 = d1 - 1 + 2 * G;

				float3 h = max(0.5 - float3(dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0);
				float3 n = h * h * h * h * float3(dot(d1, random(Skewi)), dot(d2, random(Skewi + p1)), dot(d3, random(Skewi + 1)));

				return dot(float3(70, 70, 70), n);
			}

			fixed4 frag(v2f i) :SV_Target{
				float noise = (simplexNoise(i.uv*_Scale)+1)*.5;
				return float4(noise, noise, noise, 1);
			}
			ENDCG
		}
	}
	FallBack "Diffuse"
}

3D Simplex噪聲

simplex3D

Shader "Custom/SimplexNoise3D" {
	Properties{
		_Scale("Scale",Range(3,50)) = 10
	}
	SubShader{
		Pass{
			CGPROGRAM

			#include "UnityCG.cginc"
			#pragma vertex vert
			#pragma fragment frag
			//注意這裡,這裡一定要用小數,不要直接1/3 1/6 會出錯的,害我檢查半天!
			#define F 1.0/3.0
			#define G 1.0/6.0
			float _Scale;
			struct v2f {
				float4 pos:SV_POSITION;
				half2 uv:TEXCOORD0;
			};
			v2f vert(appdata_base v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = v.texcoord;
				return o;
			}


			float3 random(float3 p) {
				p = float3(dot(p, float3(127.1, 311.7, 74.7)),
					dot(p, float3(269.5, 183.3, 246.1)),
					dot(p, float3(113.5, 271.9, 124.6)));
				return -1.0 + 2.0 * frac(sin(p)*43.5453123);
			}


			float simplexNoise(float3 p) {
				float3 SkewPos = p + (p.x + p.y+p.z) * F;
				float3 Skewi = floor(SkewPos);
				float3 Skewf = frac(SkewPos);

				float3 p1;
				float3 p2;
				/*if (Skewf.x>Skewf.y) {
					if (Skewf.y > Skewf.z) {//xyz
						p1 = float3(1, 0, 0);
						p2 = float3(1, 1, 0);
					}
					else if (Skewf.x > Skewf.z) {//xzy
						p1 = float3(1, 0, 0);
						p2 = float3(1, 0, 1);
					}
					else {//zxy
						p1 = float3(0, 0, 1);
						p2 = float3(1, 0, 1);
					}
				}
				else {//y>x
					if (Skewf.y < Skewf.z) {//zyx
						p1 = float3(0, 0, 1);
						p2 = float3(0, 1, 1);
					}
					else if (Skewf.x<Skewf.z){//yzx
						p1 = float3(0, 1, 0);
						p2 = float3(0, 1, 1);
					}
					else {//yxz
						p1 = float3(0, 1, 0);
						p2 = float3(1, 1, 0);
					}
				}*/
				
				//這裡我用step代替if,計算量可能會小點
				float xy = step(Skewf.y , Skewf.x);//等價於(x>y)?1:0
				float xz = step(Skewf.z , Skewf.x);
				float yx = step(Skewf.x , Skewf.y);
				float yz = step(Skewf.z , Skewf.y);
				float zx = step(Skewf.x , Skewf.z);
				float zy = step(Skewf.y , Skewf.z);
				//相乘只有最大數的結果才是1;
				p1 = float3(xy*xz,yx*yz,zx*zy);
				//相加結果是210,減去p1的100 結果就是110
				p2 = float3(xy+xz, yx+yz, zx+zy)-p1;
				

				float3 d1 = p - (Skewi - (Skewi.x + Skewi.y+ Skewi.z) * G);
				float3 d2 = d1 - p1 + G;
				float3 d3 = d1 - p2 + 2 * G;
				float3 d4 = d1 - 1 + 3 * G;

				float4 h = max(0.6 - float4(dot(d1, d1), dot(d2, d2), dot(d3, d3), dot(d4, d4)), 0.0);
				float4 n = h * h * h * h * float4(dot(d1, random(Skewi)), dot(d2, random(Skewi + p1)), dot(d3, random(Skewi + p2)), dot(d4, random(Skewi + 1)));

				return 32*(n.x+n.y+n.z+n.w);
				
			}

			fixed4 frag(v2f i) :SV_Target{
				float3 pos = float3(i.uv.xy*_Scale,_Time.y);
				float noise = (simplexNoise(pos) + 1)*.5;
				return float4(noise, noise, noise, 1);
			}
			ENDCG
		}
	}
		FallBack "Diffuse"
}

參考內容:http://staffwww.itn.liu.se/~stegu/simplexnoise/simplexnoise.pdf

https://blog.csdn.net/yolon3000/article/details/78106203

https://blog.csdn.net/candycat1992/article/details/50346469