Unity 生成各类型噪声图

一维噪声

首先需要的是自己来实现一个随机函数,并且这个随机函数是 ”可控的“ ,相同的输入要得到相同的输出。

这是sin(x)取小数部分的函数图像,可以明显的看出来是有一定规律的

fract(sin(x))

fract(sin(x)) 函数图像

然后我们在sin(x) 后面乘上10,让sin(x)频率变得更快的同时取小数部分的间隔也更小了,但是还是能看出明显的重复区间和sin(x)最大值和最小值位置。但是根据这个规律可以发现只要间隔足够的小,那么在x轴上面取的sin(x)小数部分的取值波动就会更大的,试试乘上一个大数。

fract(sin(x) * 10)

fract(sin(x) * 10) 函数图像

在sin(x)乘上1000之后的函数图像已经基本上看不出任何的规律了,这个1000也可以换成任意的大数,理论上这个数越大,相对越小步长取值的波动也会更加随机。同时使用固定的大数也可以来做为随机的种子,使得随机是”可控的“,固定的输入取到的随机数也是固定的,这样一维的固定随机取值就完成了。

fract(sin(x) * 1000)

fract(sin(x) * 1000) 函数图像

随机值对应透明度映射到x轴上

把这个函数命名为noise,使用1000作为种子的随机函数,将数轴上的整数 floor(x) 作为参数传入,可以得到一个一维的随机分别函数图像

noise(floor(x))

使用 fract(sin(x) * 1000) 生成的随机值

我们可以在这些随机的点之间使用x轴上的小数部分fract(x) 进行线性插值,来形成一个更加连续的函数图像

lerp(noise(floor(x )),noise(floor(x + 1)),fract(x))

使用 lerp(noise(floor(x )),noise(floor(x + 1)),fract(x)) 生成函数图像

也可以用缓和函数来代替普通的线性插值来使得两个随机值之间的过渡更加的平滑,形成波浪的效果

t = fract(x)
u = t * t * (3 - 2 * t)
lerp(noise(floor(x )),noise(floor(x + 1)),u)

使用 3t^2 - 2t^3 经行插值

利用这个原理,也可以让这个不规则的波浪动起来

float p = floor(uv.x + _Time.y);
float t = fract(uv.x + _Time.y);
float u = t * t * (3 - 2 * t);
float y = lerp(noise(p),noise(p + 1),u);
fixed4 color = (1,1,1,step(uv.y,y));

随着时间改变x值,实现波浪的效果

二维噪声

我们已经掌握了一维噪声的生成方法,二维噪声理论上只需要在原来的基础上在增加一个维度并且在他们之间插值就能实现。还是相同的方法,这一次先把uv同时夸大10倍,形成一个个正方形的晶格接下来的二维噪声基本上都是要基于这个晶格来进行制作的。

float2 uv = i.uv * 10;
float2 coordinate= frac(uv);
fixed4 color = (coordinate,1,1);

uv扩大10倍后形成的晶格

接下来我们需要用噪声函数在每个晶格的交接处生成一个随机的值。同样的使用floor(uv)和frac(uv)来获取每个晶格的索引以及相对位置的uv值,使用索引作为输入在噪声函数中取得对应的随机值。每个晶格对应了四个点,相邻角的索引只需要通过在对应的轴上增加索引即可。最后通过小数部分也就是晶格局部uv来对四个点分别在x轴和y轴上面进行插值,完成之后就获得了下图的噪声。

float2 index = floor(uv);
float2 coordinate= frac(uv);
//四个角的随机值
float buttomLeft = Noise(index );
float buttomRight = Noise(index  + float2(1,0));
float TopLeft = Noise(index  + float2(0,1));
float TopRight = Noise(index  + float2(1,1));
//获取插值
float value = lerp(lerp(buttomLeft ,buttomRight ,coordinate.x),lerp(TopLeft ,TopRight ,coordinate.x),coordinate.y);

通过晶格四个点的随机值进行线性插值形成的噪声图

可以看出来直接插值晶格的边缘变化还是太明显了,这里再次使用之前的缓和曲线3t^2 - 2t^3来进行插值,边缘变化的问题解决了。这种完全使用随机值之间进行插值生成的噪声也就成为 Value Noise ,由于是在晶格之间进行点和点之间随机数的插值,所以还是能够明显的感受到其中的块状结构。

float u = coordinate * coordinate * (3 - 2 * coordinate); //使用缓和曲线
float value = lerp(lerp(buttomLeft ,buttomRight ,u.x),lerp(TopLeft ,TopRight ,u.x),u.y);

使用缓和曲线之后的 Value Noise 噪声图

为了让晶格之间的关系更加的自然,Ken Perlin 在 1985 年开发了一种新的噪声生成算法 Gradient Noise ,他使用一个二维的随机向量代替了原来的随机值,让各个点之间产生一定的梯度变换,使生成的噪声图几乎感觉不到明显的块状感。

单个晶格内生成随机向量的分布

那么如何获取晶格中像素点p的取值呢?新的生成算法的原理也是非常的容易理解,首先将原先的四个顶点的随机值变成二维的向量,并且同时把原来的随机值生成范围从【0,1】映射到【-1,1】来使得随机生成的方向能够布满整个圆(图中的红色向量)。然后我们再将晶格中对应的像素点P与晶格的四个顶点连接起来形成四个从顶点指向P点的新向量(图中的蓝色向量)。这样最终得到的就是4个随机的向量以及4个指向像素点的向量,只需要将4个随机生成的向量分别投影到各自对应顶点指向像素点的向量上得到四个标量值,然后在这四个值之间插值即可的到对应像素点的值,这种方法生成的噪声也被称为 Perlin Noise,这种新的方式使得晶格内外之间具有明显的梯度变化,不再像 Value Noise 一样只有在边缘部分进行插值从而形成块状感。

//将原来的随机方法返回一个二维向量,并且映射到【-1,1】之间
float2 Noise(float2 i){
    return -1 + 2 * frac(sin(float2(dot(i,float2(152.123,125.127)),dot(i,float2(112.123,156.12)))) * 1000);
}
//======================fragment=========================
float2 index = floor(uv);
float2 coordinate= frac(uv);
//四个角的随机向量投影
float buttomLeft = dot(Noise(index),coordinate - float2(0,0));
float buttomRight = dot(Noise(index  + float2(1,0)),coordinate - float2(1,0));
float TopLeft = dot(Noise(index  + float2(0,1)),coordinate - float2(0,1));
float TopRight = dot(Noise(index  + float2(1,1)),coordinate - float2(1,1));
//缓和曲线
float u = coordinate * coordinate * ( 3 - 2 * coordinate);
//获取插值
float value = lerp(lerp(buttomLeft ,buttomRight ,u.x),lerp(TopLeft ,TopRight ,u.x),u.y);
//映射回到【0,1】之间
value = value * 0.5 + 0.5 

Perlin Noise

在 Ken Perlin 发表了他的噪声生成方法五年之后,Steven Worley 发表了另一种全新的噪声生成方法,基于特征点的噪声生成。在一定范围内生成几个特征点,然后对每一个像素,计算离他最近特征点的距离并保留最小值。这样生成的噪声图就和细胞一样,也称他为 Cell Noise 或者 Worley Noise。

随机生成的特征点和像素点

实现的逻辑也是非常简单,随机生成4个特征点(图中红色点),像素点p连接各个特征点,遍历形成的向量,求得所有向量模的最小值(图中蓝色向量)即为p点的值。

float distance = 1;
for(float j = 1,j < 5;j++){
    distance = min(distance,length(uv - Noise(uv))) //获取距离像素最近的特征点距离
}
return distance

使用4个特征点生成的 Worley Noise

同样的逻辑也可以插入数量较多的特征点来实现更密集的结构,插入50个控制点之后形成的结构显得十分密集,但是需要遍历的次数也上升到了50次。并且也会出现部分区域控制点较多或者较少的情况。

使用50个特征点生成的 Worley Noise

为了实现特征点的均匀分布同时减少每次像素计算遍历的向量数量,我们可以利用之前生成Value Noise的方法,将空间划分成一个个晶格,每个晶格内生成一个位置随机的特征点。晶格内部的像素点每次计算只需要考虑包含自己和周围8个晶格在内的9个特征点,这样一来均匀分布和遍历次数的问题都解决了。

包含自身9个晶格的特征点

创建晶格并且获取索引和晶格内的相对坐标我们在之前的部分已经说明过了,晶格内的特征点位置只需要通过对应的索引随机生成即可,然后通过两个循环来遍历包含自身在内的9个特征点计算出距离像素最近的值。

float2 index = floor(uv);
float2 coordinate= frac(uv);  
float distance = 1
//计算包含自己和周围8个晶格一共九个特征点的最小距离
for(float x = -1 ; x <= 1 ; ++ ){
   for(float y = -1 ; y <= 1 ; y++){
       float2 offset = float2(x,y); //周围的偏移
       float2 point = Noise(index + offset);  //生成随机特征点
       distance = min(distance ,length((neighbor + point) - coordinate)); //保留最小距离
   }
}
return distance

划分成10个网格之后生成的 Worley Nise

显示网格以及中心点

同样的我们也可以让其随着时间进行变化,或者改变计算距离的方法,来实现一些特殊的效果。

   point = sin(_Time.y + 6.2831 * point ) * 0.5 + 0.5 ;  //特征点随时间变化

生成的 Voronoi 动图

在 2001 年的 Siggraph 上 Ken Perlin 发表了他新的噪声算法 Simplex Noise ,它有着更低的计算复杂度以及更好的显示效果。这里的 Simpelx 指的是单形,也就是用来填充N维空间中需要的最简单的多边形,它有着N+1个顶点。Ken 将原来用来插值的四边形改成了填充二维空间中最简单的形状正三角形,在三个顶点之间经行插值。

使用单形的晶格

Simplex Noise 中像素点的值也是需要通过插值得到,只不过从原来的正方形变成了三角形。在正方形网格中可以很方便的获取像素点所在晶格的坐标,那么在三角形网格中该如何处理呢?

偏斜示意图

我们可以通过偏斜的方法将正三角形偏斜成原来正方形的一半,使图像变回原来的正方形网格,这样就能够简单的确认顶点的位置关系,确认完之后再偏斜回原来的正三角形。那么首先需要知道偏斜的方法和它逆操作的方法。

将正三角形中的点偏斜成正方形中的点 :

x′=x+(x+y+...)⋅K , y′=y+(x+y+...)⋅K

其中 K=(n+1−1)n 在二维空间下为 K=(3−1)2

偏斜回正三角形的点:

x=x′−(x′+y′+...)⋅G , y=y′−(x′+y′+...)⋅G

其中 G=(1n+1−1)n 在二维空间下为 G=(3−3)6

偏斜了之后通过floor方法可以轻松的获取第一个点,A点的坐标(晶格的索引),那么该如何获取B点或者D点的坐标呢?

区分上下两个三角形

通过观察发现在偏斜之后的正方形中,可以通过对角线来划分两个三角形,也就是可以通过偏斜后x和y的关系来确认点是处于上面的三角形还是下面的三角形,当 x < y 时,处于上面的三角形,对应第二个点也就是B点的坐标 A +(0,1),当 x > y 时,处于下面的三角形 ,对应第二个点也就是D点的坐标 A +(1,0)。至于第三个点则是正方形固定右上角C点的坐标 A +(1,1)。这样一来我们就获取了偏斜之后正方形网格中三角形三个顶点对应的位置,再通过一个逆操作将他们偏斜回原来的单形网格中的三角形,即可获得单形网格中三角形三个顶点的坐标了。

value=(r2−|dist|2)4×dot(dist,grad)

获取了单形网格中三角形三个顶点之后我们则需要通过上面的这个公式来获取三角形中每个点相对的贡献度,其中dist对应了像素点到三个点的距离,r2 代表了衰减半径,grad代表了顶点上的随机向量。那么这个 r2 的取值该如何定义呢?

保证连续的衰减半径

可以发现为了保证图像的连续性,在正三角形中点到顶点的距离范围是有限的,最大值即为顶点到对边的距离也就是三角形的高。而要求这个高首先需要知道正三角形的边长,通过之前的偏移函数我们可以知道在单形网格下三角形的坐标 A(0,0),B (-G, 1 - G),对应的 |AB|2=|A2−B2| 其中

A2=0

B2=G2+(1−G)2

=2G2−2G+1

=2((3−3)6)2−2(3−3)6+1

=(18−12∗3+6)−(36−12∗3)+3636

=23

AB=A2−B2=0−23=23

确定了三角形的边长之后根据正三角形的特质,可以轻松的求出三角形的高。

等边三角形

r2=(23)2−(322)2=12

将求出来的 r2 带入到公式当中即可得到对应点的贡献值,只需要把3个顶点的贡献值加起来就能得到最终的值,为了将他可视化显示出来,我们最还需要把它映射到 【-1, 1】 的区间内。想要映射需要知道对应的最大值,也就是最大的衰减半径,正好和我们之前求的高是一样的,当点位于边的中点时取到最大值,此时到三个点的距离分别为

A=r=22

B=C=22∗33=66

由于 A2 的值超过了 r2 不参与计算,所以对应的值等于B,C两边的权重相加,这里随机值的模取最大值 2

valuemax=((0.5−16)4×66∗2)∗2≈0.01425

所以最后我们需要将结果乘上 10.01425≈70 来映射到【-1, 1】,最后附上代码

float k = 0.366025 ; //(sqrt(3) - 1 ) / 2;
float g = 0.211324 ;//(3 - sqrt(3)) / 6;

float2 squareP = uv + (uv.x + uv.y) * k;
float2 squareA = floor(squareP);
                
float2 triangleA = uv - (squareA - (squareA.x + squareA.y) * g);                
float2 squareB = triangleA.x < triangleA.y ? float2(0,1) : float2(1,0);

float2 triangleB = triangleA - (squareB - (squareB.x + squareB.y) * g);
                    
float2 triangleC = triangleA - (float2(1,1) - (1 + 1) * g);

float3 m = max(0.5 - float3(dot(triangleA,triangleA),dot/(triangleB,triangleB),dot(triangleC,triangleC)) ,0);                
float3 n = m * m * m * m * float3(dot(triangleA,Noise(squareA)),dot(triangleB,Noise(squareA + squareB)),dot(triangleC,Noise(squareA + float2(1,1))));

n = dot(float3(70,70,70),n) * 0.5 + 0.5;

生成的 Simplex Noise 噪声图


 

编辑于 2022-10-25 00:30

猜你喜欢

转载自blog.csdn.net/weixin_42565127/article/details/128814811