一个独立开发游戏者。个人开发小游戏有:僵尸消灭计划、怪物连一连和割草英雄传说分享心路历程、开发经验与教程,欢迎加入我的社群。
我们从1D噪声开始。想象一个平面坐标轴,x轴代表变量(比如时间或距离),y轴的值就是对应的一维噪声值。我们首先在x轴上标记出0、1、2等整数格点,并为每个点随机指定一个-1到1之间的数值。这个数值代表函数经过该点时的斜率,也就是我们常说的“梯度”。
那么,如何将两侧的梯度综合成一个最终的噪声值呢?这时就需要引入“插值”的概念。插值的本质是给左右两边的贡献分配“权重”。当采样点靠近左侧时,左侧梯度的影响权重更大;靠近右侧时,右侧权重更大。这个权重分配的过程不是僵硬的,而是平滑过渡的。变量t的范围定义为0到1。插值公式:f(t)=a+t(b-a)。
这就是线性插值后的效果。聪明的你可能发现了一个问题:线性插值的公式明明是一个线性方程,为什么绘制出来却像二次函数的抛物线呢?
我们取x轴的一段来讲解:左侧为A点,右侧为B点,当前采样点位于A、B之间。A点处的梯度g0对采样点的位移贡献为t,B点处的梯度g1贡献则为t-1。此时,左侧贡献值yA=g0*t,右侧贡献值yB=g1*(t-1)。将这两个值代入插值公式f(t)=(1-t)*yA+t*y_B展开整理后,你会发现它确实是一个关于t的二次函数。这也就是为什么它的轨迹是一条抛物线。
虽然二次函数自带弯曲,但在x等于2或4这样的整数格点上,我们可以明显看到“折痕”。这说明两个区间在衔接处不够丝滑。要解决这个问题,我们需要引入“导数”。
导数是描述函数变化快慢的数学量。刚才提到的“折痕”,本质上就是导数在衔接瞬间发生了突变。函数y= f(x)的导数公式如下(展示极限定义公式),简单来说,它就是y的变化量除以x的变化量。当x的变化量趋近于零时,得到的就是该时刻的瞬时变化率。导数的正负和大小反映了函数的增减方向和变化快慢。特别需要注意的是:当导数为0时,函数处于平稳点,比如二次函数的顶点。这给了我们启示:如果我们希望曲线在交汇点实现平滑衔接,那么在交汇瞬间,它的导数为0是最理想的状态。
因此,实现平滑边界的条件就是:在AB区间的边界(即t=0和t=1处),插值函数的导数必须为零。
这里我们要引入“平滑函数”的概念。在代码调用插值时,通常传入三个参数:左侧贡献、右侧贡献和当前t值。核心就在这个t值上。如果我们直接使用线性插值,实际传入的平滑函数就是S(t)=t。现在我们对其求导,代入边界发现:当t=0时,导数值为g0-g1;当t=1时,导数值为g1-g0。除非两点梯度完全相等,否则导数几乎不可能为0。这从数学上证明了,直接使用S(t) =t无法实现边界的平滑衔接。
聪明的你一定想到了:难道我们不能换一个更高级的平滑函数吗?思路完全正确!早期柏林噪声采用了S(t) =3t^2-2t^3,也就是“三次方平滑函数”。我们将这个函数代入插值公式后求导,发现它在t=0和t=1处的导数确实都变成了0。这完美达成了平滑衔接的目的。
如图所示,使用三次方平滑后,区间衔接处变得优雅了。但这是最终答案吗?还不是。虽然衔接处光滑了,但我们还可以追求更自然的过渡。这里涉及到了导数的导数——“二阶导数”。
一阶导数表示梯度的变化率,而二阶导数则表示“变化率的变化率”,通俗地说就是“弯曲程度”或“曲率”。如果二阶导数在衔接处也能平稳过渡,那么视觉上的弯曲感会更加自然。
我们来看看三次函数的表现:它的一阶导数在边界虽然为0,但二阶导数在t=1处却是-12*(g1-g0)。这意味着除非g1等于g0,否则弯曲程度在衔接处依然会发生突跳。
于是,我们需要引入终极平滑函数:S(t)=6t^5-15t^4+ 10t^3。通过计算可以证明,这个五次方函数的一阶导数和二阶导数在t=0和t=1处全部为零。它让梯度的变化率和弯曲程度都达到了极致的自然。这就是柏林噪声最精妙的数学设计。
仔细观察引入五次方平滑后的曲线,是不是感觉呼吸感更加自然了?
我们用一张图来对比:橙色是僵硬的线性插值,淡紫色是改善后的三次方平滑,荧光绿色则是我们最终的五次方平滑。显然,最终版本是最优雅、最符合自然规律的。
此外,不同的柏林噪声是可以直接叠加的。根据导数的线性性质,两个平滑函数相加后的导数,等于它们各自导数的和。因此,多个平滑噪声叠加后的结果依然是极致平滑的。我们可以利用不同振幅、频率的波形叠加出具有丰富细节的曲线,如绿色曲线所示。
综合运用一维柏林噪声,我们可以生成类似《泰拉瑞亚》这种横版游戏的地形。可以看到,生成的地貌非常优雅自然。
理解了1D,2D就非常容易了。在2D平面中,我们划分出很多网格。如果采样点P处在网格ABCD中,它会受到4个格点梯度的共同影响。此时梯度变为了“向量”,我们使用“点积”来计算每个梯度对P点的贡献。接着,先沿着x轴在AB和CD之间进行两次插值,最后再沿着 y轴进行最后一次插值。相比1D,2D噪声涉及4个顶点和总共3次插值运算。
我们可以用2D噪声生成平面的地形图,衔接非常自然,你可以根据噪声值填充不同的美术素材。
甚至,我们可以将2D噪声映射到3D空间的高度轴上,从而生成起伏的3D山峦。
最后,我们可以将其推广到3D柏林噪声。3D环境下采样点受周围8个顶点的影响,需要进行7次插值。
当我们要生成类似《我的世界》这样的大型世界时,3D噪声不仅能生成连绵的地形,甚至能通过噪声密度的变化,在山体内部“挖出”错综复杂的随机洞穴。
可以点击“阅读全文”查看我在B站上分享的该内容视频,也可以看我的视频号的内容,两个平台内容一致。