欢迎光临散文网 会员登陆 & 注册

【Aegisub】随机烟雾、随机地形与柏林噪声简介

2022-09-06 20:12 作者:多华宫与火火里  | 我要投稿

        下面这些效果都是利用柏林噪声做出来的:

        是的没错,柏林噪声可以做火焰模拟、消融效果、定向溶解、定点溶解、平滑抖动、随机曲线、图形扭曲、放射速度线、流场效果、随机烟雾、随机地形、湍流效果、体积云、电流闪电描边等等等等各种各样的效果,有着极其丰富的应用。别忘了,之前我讲过如何绘制隐式方程,也就是说,只要不是要专门做像素效果,那么你就可以直接绘制出相应的绘图,比如燃烧效果,就可以直接绘制出图形,而不是用一堆像素来做!

        好了,那么就开始具体讲了。首先,现在说的"噪声"其实指的就是随机值。那比如说,现在就随便生成几十个或几百个随机数,然后可以横着把它们排出来、比如每隔一个单位画出一个随机点,然后把这些点直接连起来就会大概是下图这样的,这应该很容易理解

像是这样的噪声可以叫做一维噪声。那很显然,完全随机的一些数是乱七八糟的、毫无美感,那么如果想要更平滑过渡的随机数呢?当然就可以用插值了。

        先介绍值噪声(即Value Noise)。现在可以隔一段距离生成一个随机数,比如隔20个单位生成一个随机数,然后再用线性插值得到每个单位的对应数值,这样看起来就会不那么杂乱了

而如果想要更顺滑的话,就可以不用线性插值,而是使用其它的插值函数,比如fade(t)=3t²−2。原本的线性插值是a+t*(b-a)(即从a到b在走到 t 处时的数值),而如果用上fade函数就变成了a + fade(t) * (b-a),因为现在说fade(t)=3t²−2t³,所以从a到b在走到 t 处时的数就是a + (3t²−2t³) * (b-a)了。比如每隔20单位生成一个随机数,假设第一个随机数是0.66、第二个随机数是0.23,即0对应的0.66、20对应的0.23,那么比如7对应的就该是0.66 + fade(7/20) * (0.23-0.66)了。总的来说,这样就可以得到更加平滑的一维随机值噪声了:

插值函数fade(t)可以有各种各样的选择,不过一般需要有几个要求:fade(0)必须等于0、fade(0.5)必须等于0.5、fade(1)必须等于1,刚刚的fade(t)=3t²−2t³就满足这些要求,再比如fade(t)=0.5*(1-cos(t*π))也是可以的,再比如fade(t)=6*t^515*t^4+10*t^3也可以。

        那么二维的值噪声其实也是同样的道理。如果一维噪声是横轴上每个位置对应一个随机值的话,那二维噪声当然就是平面上每个点对应一个随机值了。假设随机值的范围是0到255的话,就可以和颜色对应上,如果每个像素对应的随机值完全随机(指的是直接用random函数的意思)的话,就会得到这样的图像:

显然也是杂乱无章的,这样的噪声被称为白噪声(即White Noise),而如果要更加平滑的噪声的话,当然还是要用插值了。二维的值噪声和一维的是同样的道理,一维的是每隔一段距离取一个随机数、然后中间的部分就插值、相当于把数轴分成一段段的,那么二维的话,就可以把平面分成网格、在每个方格顶点生成一个随机数,然后平面上的其它点就用插值得到:

比如x的间隔和y的间隔都是一样的,那么每个格子就是正方形了,当然也可以x间隔和y的间隔不一样。然后在每个顶点生成一个随机数

然后其它点就可以利用这些随机数来插值得到。比如就看一个格子里的某一点

那怎么插值呢?其实就是插三次,左上和右上插值得到点B、左下和右下插值得到点A、最后A和B之间插值得到P

假设每次插值都是线性的话,就可以得到这样的效果(200X200共4万个像素,格子大小是20X20)

这样的效果看起来像毛玻璃,不过线性插值的过渡不够平滑,所以用fade插值函数的话,就会得到这样的效果

200X200共4万个像素,格子大小是20X20

这样就得到了二维值噪声了,当然格子刚刚说了也可以不是正方形的,反正都是一样的插值而已

200X200共4万个像素,格子大小是40X20

虽然现在过渡平滑了,但显然值噪声看起来和自然噪声区别不小,所以在80年代一个名叫Perlin的老外提出了柏林噪声。二维柏林噪声的生成方法当然就有些许不同,二维值噪声是在格子顶点处生成一个随机数,而二维柏林噪声是在格子顶点处生成一个随机梯度(即一个向量),然后利用点乘来做插值

比如要求上图P这个点的随机值,红色向量是在格子顶点生成的随机梯度,然后每个顶点的红色向量和蓝色向量做点乘,然后用点乘得到的4个数值来插值、当然是插三次就得到了P点处需要的随机值了。举例比如对于左上的顶点就是向量a1和向量P1P进行点乘,其它顶点的同理。哦对了,蓝色向量当然就是从格子顶点连到P这个像素点的一个向量。

        这样就有了二维柏林噪声,那三维的也是同理。三维的就把空间划分为多个立方体,然后插值即可,当然三维就要插值7次了(一维插值1次、二维插3次),插的方法是同理的。

        不过现在想一想,网格顶点都要生成一个随机梯度,那么如果要求平面上任意一点的噪声值,就需要有布满整个平面的一张网格,就有无数个格子顶点,那难道要提前生成无数个随机梯度吗?很显然,不管是一维还是二维还是几维,都需要提前生成一些随机的东西,但是不可能生成无数个随机的东西,所以柏林噪声的作者在此后提出了优化算法。

        现在比如三维的柏林噪声,每个立方体顶点就不生成完全随机的梯度向量了,而是预设好一些向量,然后在这些向量里随机选一个作为这个顶点的随机梯度即可。比如,预设有12个不同的向量:

每个顶点在生成随机梯度的时候,不再是生成任意的随机向量了,而是在这12个里面随便选一个当做随机梯度。不过由于为了在写代码时方便用位运算,所以可以把预设的向量个数增加到16个,在刚刚的基础上再添加4个

当然这4个和刚刚的12个是重复的,但这无所谓,反正在这16个向量里随便选一个作为顶点处的梯度向量即可。然后由于还是不可能记录保存下无数个随机梯度,所以需要任给一个顶点坐标都能返回一个确定的随机梯度,那就需要用到排列表和哈希值了。现在将排列表取名为perm表,perm表装有512个范围是0到255的随机数,然后每个点的哈希值用取余计算得到,然后因为预设16个向量,就在这16个里选,所以哈希值和16取余得到相应的梯度。那由于本质上是用的取余运算,所以就算是无数个立方体顶点,每个顶点也能算出一个随机梯度,而且当然,对于相同的perm随机表、每个顶点得到的随机梯度就是相同的。相当于,只要生成了一个随机的排列表perm,空间中任意一个点的噪声值都可以算出来了,而且对于同一个perm排列表、每一点的噪声值都一样。所以你想生成不同的噪声值,只需要生成不同的perm表即可。假设你无法理解这一段,也没有关系,在老外的https://adrianb.io/2014/08/09/perlinnoise.html 这篇文章里本身应该已经讲得很清楚了,因为连代码都可以说是帮你写好了,再不济,你把它翻译成Lua语言你不会吗?并且,这算法得到的噪声值的范围在-1到1,这样也算是非常方便了,比如你缩放平移一下就能得到0到255的数

        在刚刚的这个算法中,显然每个坐标是整数的点都对应生成一个随机的梯度,什么意思,不就是在说,立方体是边长为1的正方体吗,如果用二维的来说就是每个格子的边长是1。但是显然,在刚刚所有举例中,格子的边长都大于1,比如x间隔20y间隔40这样的,你现在每个方向都间隔1,那你要怎么算每个像素的噪声值呢、每个像素的坐标不就是"间隔1"的吗,你都没地方插值了啊?嗯,那当然是缩放啊!仔细想想这是不是废话?如果这真的是废话,这证明什么?当然是证明我喜欢说废话啊!可是同样一句废话,对于每个人来说是一样的吗,也许对于一些人,废话不是废话呢?那我讲的是废话吗?喂来碗泡椒牛肉炒面,我正在研究在外星人头上骑摩托车呢!很好,这就对了。缩放是什么意思,比如如果格子的边长是30,你可以算出来每个像素的噪声值,但是现在格子边长是1,相当于画面缩放成了30分之一,比如你要求原来(23,66)这个像素的噪声值,现在就求(23/30,66/30)这个坐标的噪声值即可,然后你显示图像的时候,就还是显示在(23,66)这个位置不就行了?所以,这么看的话,格子边长是1实际上更方便了,因为利用缩放你可以更轻松地得到各种各样边长的格子,比如你想x间隔是40y间隔是20,那求(233,666)这个点的噪声值就是求(233/40,666/20)这个点在边长是1的格子里的噪声值。

        也就是说,现在"标准"柏林噪声算法划分的格子的边长是1,其它任意边长的格子都可以缩放得到,反正现在空间中任意一点的噪声值都已经是可以计算出来的。并且任意一点的噪声值的取值范围是-1到1。那比如你想要随机值的范围是0到1,你当然不能直接对随机值取绝对值,虽然-1到1取绝对值以后它的范围就落在0到1,但是这样做你就改变了原本的分布情况了。为了不改变随机数在区间里的分布,只能对区间进行缩放和平移、不能对区间进行翻折(比如取绝对值)。所以把区间缩小一半然后平移0.5即可,假设你的噪声值是val,那么用val/2+0.5就可以得到在0到1这个范围的随机数了。同样的,如果要让噪声值的范围是0到255,只需要把0到1缩放成0到255即可,很简单。所以每个像素都可以对应一个灰度,即一个颜色,r、g、b的值都是一样的。比如6C6C6C就是r、g、b都是6C,比如FFFFFF就是r、g、b都是FF、而FFFFFF当然是白色了,而000000是黑色(可以理解为黑色是0白色是1),那现在每个像素对应一个颜色的话,就可以有:

200x200共4万个像素,格子大小是30x30

        同样的,你还可以绘制等值线,比如就让噪声值的范围是-1到1,可以画出大于0的部分:

        为了配合柏林噪声的范围,也可以让其它噪声的范围也在-1到1,比如让值噪声的范围是-1到1,这样统一设定一下就会更方便记忆和使用。并且也同样设定用来生成值噪声的格子边长也是1

        现在再来更加"具体"、"形象"的理解一下噪声。首先生成随机的排列表perm、表里数字的范围是0到255,比如:

对于一维噪声,就是给出一个数字然后得到一个随机值,即使用func(x)返回一个-1到1的随机数,而对于同一个排列表,func(x)得到的数当然是一样的、比如不停地用func(233),得到的结果永远是相同的、比如不停地用func(666),得到的结果也永远是相同的func(666),对于不同的perm排列表,同一个x的话,func(x)当然也是不同的。其实就像random函数一样,如果设定同样的随机数种子,那么random函数得到的随机数就是一样的。现在同一个排列表就能得到同样的随机值。然后,别忘了咱们用柏林噪声的理由,是为了得到平滑过渡的噪声值,所以显然,同一个排列表要用很多次,比如现在每次得到随机数时不重新生成新的随机排列表,那么func(x)首先得到一个-1到1的随机数,而你用func(x+0.01)也会得到一个随机数,你当然会发现得到的随机数和func(x)得到的数很接近,因为已经讲得很清楚了,噪声值现在是平滑过渡的

而如果你取func(x)和func(x+2)的话,你就可能会发现得到的两个数相差很多。因为上面已经讲了,现在生成噪声的采样间隔是1(如网格格子边长是1),所以你取点的时候只要相差小于1,那么得到的随机数就会比较接近了。同样的,可以应用缩放的思想,你不想要采样间隔是1,你想采样间隔是20,那么你使用函数的时候就可以是用func(x/20)和func( (x+9)/20 )这样的,这样当然也会得到平滑过渡的随机值了。

        对于二维噪声同理。同一个perm表,用func(x,y)得到的随机数和func(x+0.01,y)得到的随机数差不多、也和func(x-0.01,y+0.003)得到的随机数差不多、也和fiunc(x+0.0123,y+0.023)差不了多少。当然你觉得这样不清晰,也可以缩放网格格子,比如让格子大小是23x66的,那么你可以算每个像素点对应的噪声值,即func(x/23,y/66)。所以三维噪声也是同样的,本身算法里设定是立方体边长是1,方便缩放,所以你用func(x/77,y/5,z/21)就说明你想设定的是x间隔77y间隔5z间隔21。所以四维噪声也同理,如func(x,y,z,w)和func(x+0.01,y+0.0001,z+0.04,w-0.0233)就很接近,因为默认采样间隔是1。

        所以知道了采样间隔是1方便缩放以后,就能利用缩放得到不同的噪声图了。比如150x150的像素图、设定格子边长是30x30可以得到

而设定格子边长是20x20就有:

设定格子边长是10x10就有:

显然格子边长越小整个图看起来就越"密集",这可以直接从缩放来理解

格子相对于噪声图越大则噪声图看起来就越"不密集",格子相对于噪声图越小则噪声图看起来就越"密集"。从缩放的角度看,各种各样的格子都是从1x1的格子缩放得到的,比如30X30的格子不就是把默认的1x1的格子在x、y方向都缩放了30吗,所以你就可以发现以30x30为格子的噪声图是以20x20的噪声图的一部分。当然在缩放的时候,排列表当然是不变的,因为同一个排列表对应同样的噪声,所以当然不能改变排列表了,这刚刚应该已经讲得很清楚了,什么时候可以改变排列表、什么时候不能改变排列表。

        当然缩放的时候,可以x、y缩放不一样,比如还是150x150的噪声图,格子设定是60x20

比如把格子设定成150x20:

比如把格子设定成150x5

利用不同的缩放就能得到各种需要的噪声图,比如利用上面细的一条条就可以做速度线效果

        一般的random函数得到的随机数在整个取值范围是均匀分布的,而噪声函数则不一样,它是类似正态分布的。也就是说,比如范围是-1到1,生成的大部分噪声值是靠近0的,而靠近-1和1的非常少

        再总的重复啰嗦一遍,在"标准的"、"优化的"柏林噪声算法中,"格子"间隔是1、对于同一个排列表会得到同样的噪声、噪声值的范围在-1到1

        那知道了每个点可以生成一个噪声值,那动态的效果是怎么做的呢,就算平面上每一点都有一个噪声值,可是又怎么让它变起来呢?这当然是增加一个维度了,比如你想让一维的随机曲线动来动去,就可以利用二维噪声,而如果你想二维的噪声动来动去,当然就要用三维噪声了:

显然三维噪声是空间中每一个点都对应一个噪声值,所以你可以截出一个面,这个面当然就是一张噪声图了,而如果连续的朝一个方向截取一个个面,当然就可以做出一张"动来动去"的噪声图了。这应该很好理解,比如对于一维噪声来说,一个点附近的点对应的噪声值和这一点的噪声值很接近,对于二维噪声来说,一个点周围的其他点(如上下左右)的噪声值和这个点的噪声值很接近,对于三维噪声来说,一个点周围的其他点(如上下左右前后)的噪声值和这个点的噪声值很接近,因为这些噪声都是平滑过渡的。所以想要噪声"动来动去"只需要增加一个维度即可,比方说你随便在一个高度取一堆x、y得到一张噪声图、然后将高度增加一些又取一堆x、y又得到一张噪声图,以此类推。同样的道理,你也可以使噪声图看起来有平移效果,这当然不需要增加维度,而是直接朝相应的方向偏移即可

因为过渡是平滑的,所以你在平面任意画一条曲线,然后一点点的取得曲线上的点,这些点对应的噪声值当然也是平滑过渡的

所以如果你在噪声图上画一个圆,那么一开始的噪声值和最后的噪声值就可以一样了,相当于首尾相接了

这意味着你可以做随机的循环效果,比如可以让一个点随机的走动,然后整个过程可以循环起来。

        在有了柏林噪声以后,就可以利用分形布朗运动( 即fbm )使得噪声看起来更加"自然"

这很简单,就是叠加噪声而已。比如有一个正弦波

然后又有一个正弦波

然后叠加起来就有

所以要得到像是山脉一样的东西,只需要将噪声叠加一下即可

比如原本的函数是func(x,y),现在设定一个新的函数func2(x,y)是func(x,y)+func(x*0.5,y*0.5)这样不就是叠加了吗?显然叠加的次数越多,就会有越多的"细节",不过叠加次数越多计算量当然就越大,所以一般叠加最多8次就行了。

噪声图相加就是字面意思的相加,就是在噪声图A的点(x,y)对应某噪声值1、在噪声图B的这点(x,y)对应某噪声值2、在噪声图C的这点(x,y)对应某噪声值3,将噪声值1、2、3都加起来就得到这个点的新的噪声值了,当然加起来可能最后会使得噪声值范围不在-1到1,所以最后要控制一下最大的幅度。这很简单,一个范围是-1到1的数加上一个范围是-0.5到0.5的数以后,得到的数的范围就是-1.5到1.5,那么这个数只要再除以1.5就直接让它的范围在-1到1了。所以现在这些噪声的最大幅度都知道,最开始噪声值范围是-1到1、所以振幅是1,然后叠加振幅0.5的噪声,然后又加振幅0.25的噪声,然后又加振幅是0.125的,以此类推,然后为了控制使得得到的噪声值的范围是-1到1,只需要将结果除以(1+0.5+0.25+0.125+......)即可,应该讲的很清楚了。

        最后再简单提一下simplex noise,刚刚讲了柏林噪声,而simplex noise其实也是柏林噪声的作者Perlin提出来的,是的,Perlin提出了perlin noise然后又优化了perlin noise然后为了使得噪声算法更优化,他本人又提出了simplex noise。

        咱们已经知道在二维中是如何在四个点(正方形的四个角)之间插值的;,对于三维和四维我们需要插入 8 个和 16 个点。也就是说对于 N 维你需要插入 2 的 n 次方个点(2^N)。尽管很显然填充屏幕的形状应该是方形,在二维中最简单的形状却是等边三角形。所以Perlin他把正方形网格(才刚讲了怎么用)替换成了单纯形等边三角形的网格。

这时 N 维的形状就只需要 N + 1 个点了。也就是说在二维中少了 1 个点,三维中少了 4 个,四维中则少了 11 个!那么怎么得到三角形网格呢?可以先把常规的四角网格分成两个等腰三角形,然后再把三角形歪斜成等边三角形。

其实就是坐标变换一下而已。有了网格以后,就是插值了。

        simplex noise 比之前的算法有如下优化:

        有着更低的计算复杂度和更少乘法计算。

        可以用更少的计算量达到更高的维度。

        制造出的 noise 没有明显的人工痕迹。

        有着定义得很精巧的连续的 gradients(梯度),可以大大降低计算成本。


        最后,关于噪声的应用实在是过于的多,多得数不过来,所以各种各样的应用就在以后再讲了。

        然后代码啥的照旧在相应的视频里讲

【Aegisub】随机烟雾、随机地形与柏林噪声简介的评论 (共 条)

分享到微博请遵守国家法律