根据噪声特征创建地图

我的网站上最受欢迎的文章之一是关于多边形地图生成 (Habré中的翻译 )。 创建这样的卡需要大量的努力。 但是我不是从这个开始的,而是从一个简单得多的任务开始的,我将在这里进行描述。 这种简单的技术使您可以用少于50行的代码创建此类卡:


我不会解释如何绘制此类卡:它取决于语言,图形库,平台等。 我将仅说明如何使用地图数据填充数组

噪音


生成2D贴图的标准方法是使用有限频段的噪声作为构建块,例如Perlin噪声或单工噪声。 噪声函数如下所示:

图片

我们为地图上的每个点分配一个介于0.0到1.0之间的数字。 在此图像中,0.0是黑色,而1.0是白色。 以下是使用类C语言的语法设置每个网格点颜色的方法:

for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 

该循环在Javascript,Python,Haxe,C ++,C#,Java和大多数其他流行语言中都将起作用,因此我将以类似C的语法显示它,以便将其转换为所需的语言。 在本教程的其余部分中,我将展示添加新函数时循环主体的变化(线value[y][x]=… )。 该演示将显示一个完整的示例。

在某些库中,有必要对结果值进行移位或相乘,以便将它们返回的范围从0.0到1.0。

身高


噪声本身只是数字的集合。 我们需要赋予它意义 。 您可以想到的第一件事是将噪声值绑定到高度(这称为“高度图”)。 让我们以上面显示的噪声并将其绘制为高度:



除了内部循环外,代码几乎相同。 现在看起来像这样:

 elevation[y][x] = noise(nx, ny); 

是的,就是这样。 地图数据保持不变,但现在我将其称为elevation (高度),而不是value

我们有很多山丘,但仅此而已。 怎么了

频次


噪声可以在任何频率下产生。 到目前为止,我只选择了一种频率。 让我们看看它是如何影响的。

尝试使用滑块 (在原始文章中) 更改值,并查看在不同频率下会发生什么:


它只是改变比例。 起初,这似乎不是很有用,但事实并非如此。 我还有另外一本教程 (用Habré 翻译 )解释了该理论 :诸如频率,幅度,八度,粉红色和蓝色噪声等概念。

 elevation[y][x] = noise(freq * nx, freq * ny); 

有时召回与频率成反比的波长有时也很有用。 当频率增加一倍时,大小只会减半。 波长加倍,所有光子都加倍。 波长是以像素/图块/米或您为地图选择的任何其他单位测量的距离。 它与频率有关: wavelength = map_size / frequency

八度


为了使高度图更有趣,我们将添加不同频率的噪声



 elevation[y][x] = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 2 * ny); 

让我们在一张地图中混合大型低频山丘和小型高频山丘。 移动滑块 (在原始文章中),以向混合添加小丘陵:


现在,它更像我们需要的分形浮雕! 我们可以获得丘陵和崎rough的山脉,但仍然没有平坦的平原。 为此,您还需要其他一些东西。

重新分配


噪声函数为我们提供0到1之间的值(或从-1到+1,取决于库)。 要创建平坦的平原,我们可以将高度提高到幂移动滑块 (在原始文章中)获得不同的角度。


 e = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 4 * ny); elevation[y][x] = Math.pow(e, exponent); 

较高的值会降低到达平原的平均高度 ,而较低的值会提高到达山峰的平均高度。 我们需要省略它们。 我使用幂函数是因为它们更简单,但是您可以使用任何曲线。 我有一个更复杂的演示

现在我们有了一个逼真的海拔图,让我们添加生物群落!

生物群落


噪声给出数字,但是我们需要一张包含森林,沙漠和海洋的地图。 您可以做的第一件事是将小高地变成水:


 function biome(e) { if (e < waterlevel) return WATER; else return LAND; } 

哇,这已经变得像程序产生的世界了! 我们有水,草和雪。 但是,如果我们需要更多呢? 让我们来序列水,沙,草,森林,大草原,沙漠和雪:



根据身高的救济

 function biome(e) { if (e < 0.1) return WATER; else if (e < 0.2) return BEACH; else if (e < 0.3) return FOREST; else if (e < 0.5) return JUNGLE; else if (e < 0.7) return SAVANNAH; else if (e < 0.9) return DESERT; else return SNOW; } 

哇,看起来很棒! 对于您的游戏,您可以更改值和生物群系。 孤岛危机将会有更多的丛林。 天际有更多的冰雪。 但是,无论您如何更改数字,此方法都非常有限。 凸版类型对应于高度,因此形成条状。 为了使它们更有趣,我们需要基于其他内容来选择生物群落。 让我们为湿度创建第二个噪声图。



上面是高处的噪音; 底部-湿度噪声

现在,让我们一起使用高度和湿度。 在下面显示的第一个图像中,y轴是高度(从上图获取),x轴是湿度(第二个图像更高)。 这给了我们一个引人注目的地图:



基于两个噪声值的缓解

小高度是海洋和海岸。 大高度是岩石和下雪。 在这之间,我们得到了各种各样的生物群落。 代码如下:

 function biome(e, m) { if (e < 0.1) return OCEAN; if (e < 0.12) return BEACH; if (e > 0.8) { if (m < 0.1) return SCORCHED; if (m < 0.2) return BARE; if (m < 0.5) return TUNDRA; return SNOW; } if (e > 0.6) { if (m < 0.33) return TEMPERATE_DESERT; if (m < 0.66) return SHRUBLAND; return TAIGA; } if (e > 0.3) { if (m < 0.16) return TEMPERATE_DESERT; if (m < 0.50) return GRASSLAND; if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; return TEMPERATE_RAIN_FOREST; } if (m < 0.16) return SUBTROPICAL_DESERT; if (m < 0.33) return GRASSLAND; if (m < 0.66) return TROPICAL_SEASONAL_FOREST; return TROPICAL_RAIN_FOREST; } 

如有必要,您可以根据游戏要求更改所有这些值。

如果我们不需要生物群落,那么平滑的渐变(请参阅本文 )可以创建颜色:



对于生物群系和梯度,单个噪声值不能提供足够的可变性,但是两个就足够了。

气候状况


在上一节中,我用海拔代替了温度 。 高度越高,温度越低。 但是,地理纬度也会影响温度。 让我们同时使用高度和纬度来控制温度:


在两极(大纬度)附近,气候较冷,而在山顶(大高度)上,气候也较冷。 到目前为止,我的工作还不是很辛苦:要正确使用这些参数,您需要进行许多细微的设置。

还有季节性的气候变化。 在夏季和冬季,北半球和南半球变暖和变冷,但在赤道,情况变化不大。 在这里也可以做很多事情,例如,可以模拟盛行的风和洋流,生物群落对气候的影响以及海洋对温度的平均影响。

岛屿


在某些项目中,我需要地图的边框为水。 这将世界变成一个或多个岛屿。 有很多方法可以做到这一点,但是我在多边形地图生成器中使用了一个非常简单的解决方案:我将高度更改为e = e + a - b*d^c ,其中d是距中心的距离(比例为0-1)。 另一种选择是更改e = (e + a) * (1 - b*d^c) 。 常数a使所有事物升高, b使边缘降低,而c控制下降率。


我对此并不完全满意,还有很多事情要做。 应该是曼哈顿距离还是欧几里得距离? 它应该取决于到中心的距离还是到边缘的距离? 距离应该是平方的,还是线性的,或具有其他程度? 应该是加/减,乘/除,还是其他? 在原始文章中, 尝试加,a = 0.1,b = 0.3,c = 2.0,或尝试乘以a = 0.05,b = 1.00,c = 1.5。 适合您的选项取决于您的项目。

为什么要坚持使用标准数学函数? 正如我在有关RPG中的损坏 (在Habré上翻译 )中所讲的那样,每个人(包括我在内)都使用数学函数,例如多项式,指数分布等,但是在计算机上,我们不能局限于此。 我们可以使用查找表e = e + height_adjust[d]来获取任何形成函数并在此处使用。 到目前为止,我还没有研究过这个问题。

尖刺的噪音


除了将高度提高到幂之外,我们可以使用绝对值创建尖峰:

 function ridgenoise(nx, ny) { return 2 * (0.5 - abs(0.5 - noise(nx, ny))); } 

要增加八度,我们可以改变高频的幅度,以便只有山峰才能接收到增加的噪声:

 e0 = 1 * ridgenoise(1 * nx, 1 * ny); e1 = 0.5 * ridgenoise(2 * nx, 2 * ny) * e0; e2 = 0.25 * ridgenoise(4 * nx, 4 * ny) * (e0+e1); e = e0 + e1 + e2; elevation[y][x] = Math.pow(e, exponent); 


我对这项技术没有太多的经验,所以我需要进行实验以学习如何很好地使用它。 将尖峰的低频噪声与非尖峰的高频噪声混合可能也很有趣。

梯田


如果将高度四舍五入到下n个水平,则会得到梯田:


这是应用e = f(e)形式的高度重新分配函数的结果。 上面,我们使用e = Math.pow(e, exponent)来锐化山峰; 在这里,我们使用e = Math.round(e * n) / n创建平台。 如果使用非步进功能,则梯形可以倒圆或仅出现在特定高度。

树的位置


通常我们使用分形噪声来表示高度和湿度,但也可以用来放置空间不均匀的物体,例如树木和石头。 对于高度,我们使用低频的高振幅(“红色噪声”)。 要放置物体,您需要使用高频高振幅(“蓝噪声”)。 左边是蓝色的噪音图案。 右边是噪声大于相邻值的地方:


 for (int yc = 0; yc < height; yc++) { for (int xc = 0; xc < width; xc++) { double max = 0; //     for (int yn = yc - R; yn <= yc + R; yn++) { for (int xn = xc - R; xn <= xc + R; xn++) { double e = value[yn][xn]; if (e > max) { max = e; } } } if (value[yc][xc] == max) { //    xc,yc } } } 

为每个生物群系选择不同的R,我们可以获得可变的树木密度:



可以使用这种噪声来放置树是很棒的,但是其他算法通常更有效并且可以创建更均匀的分布:泊松点,范图块或图形抖动。

超越无限


在位置(x,y)的生物群系的计算独立于所有其他位置的计算。 这种局部计算具有两个方便的属性:可以并行计算,并且可以用于无尽的地形。 将鼠标光标放在左侧的小地图上 (在原始文章中),以在右侧生成地图。 您可以生成卡的任何部分而无需生成(甚至不存储)整个卡。



实作


使用噪声生成地形是一种流行的解决方案,在Internet上,您可以找到针对许多不同语言和平台的教程。 用于生成不同语言的卡的代码大致相同。 这是三种不同语言的最简单循环:

  • Javascript:

     let gen = new SimplexNoise(); function noise(nx, ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2D(nx, ny) / 2 + 0.5; } let value = []; for (let y = 0; y < height; y++) { value[y] = []; for (let x = 0; x < width; x++) { let nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • C ++:

     module::Perlin gen; double noise(double nx, double ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.GetValue(nx, ny, 0) / 2.0 + 0.5; } double value[height][width]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • Python:

     from opensimplex import OpenSimplex gen = OpenSimplex() def noise(nx, ny): # Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2d(nx, ny) / 2.0 + 0.5 value = [] for y in range(height): value.append([0] * width) for x in range(width): nx = x/width - 0.5 ny = y/height - 0.5 value[y][x] = noise(nx, ny) 

所有的噪声库都差不多。 对于Python ,请尝试opensimplex; 对于C ++ ,请尝试libnoise;对于Javascript,请尝试simplex-noise 。 对于大多数流行的语言,有许多噪声库。 或者,您可以了解Perlin噪音的工作原理或亲自了解噪音。 我没做

在针对您的语言的不同噪声库中,应用程序的详细信息可能会略有不同(一些返回值的范围是0.0到1.0,其他返回值的范围是-1.0到+1.0),但是基本思想是相同的。 对于真实的项目,可能需要将noise函数和gen对象包装在一个类中,但是这些细节无关紧要,因此我将它们设置为全局。

对于这样一个简单的项目,使用什么噪声都没有关系:Perlin噪声,单纯形噪声,OpenSimplex噪声,值噪声,中点偏移,菱形算法或傅立叶逆变换。 它们每个都有其优缺点,但是对于类似的卡生成器,它们都创建或多或少相同的输出值。

地图的渲染取决于平台和游戏,因此我没有实现它。 该代码仅用于生成高度和生物群落,其渲染取决于游戏中使用的样式。 您可以在项目中复制,移植和使用它。

实验


我研究了混合八度音阶,将度数提升至幂,以及将高度与湿度结合起来以创建生物群系。 在这里,您可以研究一个交互式图形,该图形可以让您试验所有这些参数,从而显示代码组成:


这是一个示例代码:

 var rng1 = PM_PRNG.create(seed1); var rng2 = PM_PRNG.create(seed2); var gen1 = new SimplexNoise(rng1.nextDouble.bind(rng1)); var gen2 = new SimplexNoise(rng2.nextDouble.bind(rng2)); function noise1(nx, ny) { return gen1.noise2D(nx, ny)/2 + 0.5; } function noise2(nx, ny) { return gen2.noise2D(nx, ny)/2 + 0.5; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var nx = x/width - 0.5, ny = y/height - 0.5; var e = (1.00 * noise1( 1 * nx, 1 * ny) + 0.50 * noise1( 2 * nx, 2 * ny) + 0.25 * noise1( 4 * nx, 4 * ny) + 0.13 * noise1( 8 * nx, 8 * ny) + 0.06 * noise1(16 * nx, 16 * ny) + 0.03 * noise1(32 * nx, 32 * ny)); e /= (1.00+0.50+0.25+0.13+0.06+0.03); e = Math.pow(e, 5.00); var m = (1.00 * noise2( 1 * nx, 1 * ny) + 0.75 * noise2( 2 * nx, 2 * ny) + 0.33 * noise2( 4 * nx, 4 * ny) + 0.33 * noise2( 8 * nx, 8 * ny) + 0.33 * noise2(16 * nx, 16 * ny) + 0.50 * noise2(32 * nx, 32 * ny)); m /= (1.00+0.75+0.33+0.33+0.33+0.50); /* draw biome(e, m) at x,y */ } } 

有一个困难:对于高湿噪声,必须使用不同的种子,否则它们将变成相同的种子,并且卡片看起来不会那么有趣。 在Javascript中,我使用prng-parkmiller库 ; 在C ++中,可以使用两个单独的linear_congruential_engine对象 ; 在Python中,您可以创建random.Random类的两个单独的实例。

思想


我喜欢这种方法来简化地图生成。 它速度快,只需要很少的代码即可产生不错的结果。

我不喜欢他在这种方法上的局限性。 本地计算意味着每个点都独立于所有其他点。 地图的不同区域没有相互连接 。 地图上的每个地方“似乎”都一样。 没有全球性的限制,例如“在地图上应该有3至5个湖泊”或全球性的特征,例如从最高峰的顶部流入海洋的河流。 另外,我不喜欢这样的事实,为了获得良好的画面,您需要长时间配置参数。

我为什么推荐它? 我认为这是一个很好的起点,特别是对于独立游戏和游戏果酱。 我的两个朋友短短30天的游戏大赛中写下了“狂神王国”的最初版本。 他们要求我帮助创建地图。 我使用了这种技术(加上一些其他功能,这些功能原来并不是很有用),并为它们绘制了地图。 几个月后,在收到玩家的反馈并仔细研究了游戏的设计之后,我们基于Voronoi多边形创建了一个更高级的地图生成器, 在此进行了介绍( 译自 Habré)。 此卡生成器不使用本文所述的技术。 它使用噪音以完全不同的方式创建地图。

附加信息


噪声功能可以完成很多很酷的事情。 如果您在Internet上搜索,则可以找到各种选项,例如湍流,波涛,脊形多重分形,振幅阻尼,阶梯形,voronoi噪声,分析导数,域扭曲等。 您可以使用此页面作为灵感来源。 我在这里不考虑它们;我的文章集中在简单性上。

该项目受到我以前的地图生成项目的影响:

  • 我将Perlin的整体噪音用于“狂神”卡生成器的第一个领域 。 我们在前六个月的alpha测试中使用了它,然后将其替换为Voronoi多边形上的地图生成器该生成器是专门为在alpha测试期间确定的游戏要求而创建的。 生物群落及其文章的颜色均来自这些项目。
  • 在研究音频信号的处理时,我写了一个噪声教程 ,解释了诸如频率,幅度,八度和噪声的“颜色”等概念。 适用于声音的相同概念也适用于基于噪声的卡生成。 当时,我创建了一些原始的演示浮雕生成器 ,但是我没有完成它们。
  • 有时我会尝试寻找界限。 我想知道创建引人注目的地图最少需要多少代码。 在这个小型项目中,我达到了行代码-一切都由图像滤镜完成(湍流,阈值,颜色渐变)。 这让我感到高兴和悲伤。 图像过滤器可以在多大程度上执行地图生成? 在足够大。 以上关于“平滑颜色渐变方案”的所有描述均来自此实验。 噪声层是湍流图像滤波器。 八度音阶是彼此叠加的图像; 学位工具在Photoshop中称为“曲线校正”。

让我感到困扰的是,游戏开发人员为基于噪声的地形生成(包括中点位移)编写的大多数代码都与声音和图像过滤器中的相同。 另一方面,它仅用几行代码即可产生相当不错的结果,这就是我撰写本文的原因。 这是一个快速简便的参考点 。 通常,我不会长时间使用此类卡,但是一旦发现哪种类型的卡更适合游戏设计,就立即用更复杂的地图生成器替换它们。 这是我的标准模式:从非常简单的内容开始,然后在我更好地了解要使用的系统之后才进行替换。

在噪声中,还有很多事情可以做,在本文中我只提到了一些。 尝试Noise Studio交互式测试各种功能。

Source: https://habr.com/ru/post/zh-CN430384/


All Articles