噪声笔记#4 梯度噪声_Simplex Noise

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_38275140/article/details/84205711

柏林噪声很好用,但是它计算需要的顶点数是\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

猜你喜欢

转载自blog.csdn.net/qq_38275140/article/details/84205711