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

创造自己的世界——Minecraft 1.18的地形生成(一)

2022-01-23 14:05 作者:Nickid2018  | 我要投稿

在1.18新版本中,MC的地形发生了翻天覆地的变化。它们采用了新的算法用于雕刻地形、应用群系等。接下来,我将会一步一步讲解这个地形算法。

警告:下文可能有非常多且复杂的函数公式!

一.三次插值函数(Cubic Spline)

为了地形生成有比较平滑的效果,对于输入的数据,MC使用了三次Hermite插值(有意思的是类名写的是三次样条,但是这两个不是一个东西)。它的定义如下:

它有两个子类,一个是Constant,代表了常量;另一个是Multipoint,采用了Hermite插值。

先说说Constant,它返回的就是value这个值,不会进行任何插值计算。

另一个,Multipoint,它是根据locations(插值点自变量的值)、value(插值点因变量的值)和derivatives(插值点的一阶导数值)采用分段两点三次Hermite插值计算。这个算法的数学表述如下:

假设有x_0%2Cx_1%2Cx_2....x_n个插值点,

如果x_%7Bi-1%7D%3C%20x%20%3C%20x_%7Bi%7D

H_%7B3%7D(x)%3Df_%7Bi-1%7D%5CBig(1%2B2%5Cfrac%7Bx-x_%7Bi-1%7D%7D%7Bx_i-x_%7Bi-1%7D%7D%20%5CBig)%0A%5CBig(%5Cfrac%7Bx-x_i%7D%7Bx_%7Bi-1%7D-x_i%7D%5CBig)%5E2%2Bf_i%5CBig(1%2B2%5Cfrac%7Bx-x_i%7D%7Bx_%7Bi-1%7D-x_i%7D%20%5CBig)%5CBig(%5Cfrac%7Bx-x_%7Bi-1%7D%7D%7Bx_i-x_%7Bi-1%7D%7D%5CBig)%5E2%2Bf%5E%7B'%7D_%7Bi-1%7D(x-x_%7Bi-1%7D)%5CBig(%5Cfrac%7Bx-x_i%7D%7Bx_%7Bi-1%7D-x_i%7D%5CBig)%5E2%2Bf%5E%7B'%7D_i(x-x_i)%5CBig(%5Cfrac%7Bx-x_%7Bi-1%7D%7D%7Bx_i-x_%7Bi-1%7D%7D%5CBig)%5E2

如果x%5Cleq%20x_0

H_3(x)%3Df_0%2Bf%5E%7B'%7D_0(x-x_0)

如果x%5Cgeq%20x_n

H_3(x)%3Df_n%2Bf%5E%7B'%7D_n(x-x_n)

具体代码如下:

二.伪随机数源

Java中内置了Random类提供伪随机数。在1.18之前,MC一直使用的也是它。

Java内置的随机数算法是线性同余算法(LCG),它的算法代码如下:

可以看出,这个算法会利用long中的后48位作为有效位,因此对于2%5E%7B48%7D同余的种子在经过算法计算后会产生同样的结果,这导致了在老版本的世界生物群系生成中海洋图像种子只要与2%5E%7B48%7D同余就会相同(但是因为另一个图像奇偶性问题使世界不会一模一样),在更老的版本中甚至导致了世界一模一样。

老版本的世界生物群系生成:

LCG的安全性比较低,可以逆推得到种子。

在1.18,加入了一种新的伪随机数算法,Xoroshiro128++伪随机算法。对于生成高斯分布的随机数,采用了Marsaglia Polar算法。Xoroshiro128++伪随机算法如下:

当然,加入新的伪随机数源不意味着舍弃了旧的源。当我们在噪声生成器内将legacy_random_source设置为true时就会使用原先的伪随机数发生器,默认是false,也就是使用新的发生器。

三.噪声

1.18地形的最大更新就是“噪声”(其实之前也有)。噪声是一个图形学概念,用于模拟自然情况下的随机数。在MC中使用了很多种噪声,主要使用的是Perlin噪声、Simplex噪声和它们的组合。

下面是一个简单的Perlin噪声的图像,使用了MC中ImprovedNoise的源码转换成GLSL语言编写而成:

代码+演示:https://www.shadertoy.com/view/7sXyRn,RNG=JDK LCG,seed=514

可以看到单个柏林噪声的图像并不太符合真正的地形参数,所以MC采用了分叠加,将不同的柏林噪声叠加起来。它的代码是这样的:

解读一下这段代码:

这段代码要求输入一个随机数源、一个起始倍频、一个振幅列表和是否使用新的随机数源(这个通常是true)。以MC内置的普通温度(temperature)噪声参数举例,传入这个构造函数的参数是:

根据上面的代码,首先,我们创建2个柏林噪声(因为在0的分量上不会计算),它们的随机数源由随机数源工厂指定。

接着,计算出第一个噪声输入因子和第一个结果乘法因子。

接下来,我们把lowestFreqInputFactor简写成f_0,lowestFreqValueFactor简写成v_0,将输入的x、y、z坐标简写成输入向量%5Cvec%7Bp%7D%20,各振幅值使用a_0...a_n代替,各个基元柏林噪声函数用noise_0...noise_n代替。那么,getValue的值可以用下面的公式表示:

getValue(%5Cvec%20p)%3D%5Csum%5Cnolimits_%7Bi%3D0%7D%5En%20%5Cfrac%20%7Ba_iv_0%7D%7B2%5Ei%7Dnoise_i(2%5Eif_0%5Cvec%20p)

可以看到,振幅值越靠后对于噪声的贡献越小。

在这个例子中,这个分形噪声的计算如下:

t_1(%5Cvec%20p)%3D%5Cfrac%7B16%7D%7B21%7Dnoise_0(%5Cfrac%20%7B%5Cvec%20p%7D%7B1024%7D)%2B%5Cfrac%7B8%7D%7B63%7Dnoise_2(%5Cfrac%7B%5Cvec%20p%7D%7B256%7D)

但是分形噪声还不够,MC最终采用的是两个分形噪声混合叠加,最终形成了现在的噪声生成器。

设两个分形噪声是fbm_1(%5Cvec%20p)%2Cfbm_2(%5Cvec%20p)(注意这两个噪声构建时的代码看起来一样,但是因为RNG被计算了几次,所以两个噪声不一样),最大的倍频和最小的倍频分别为o_%7Bmax%7D%2Co_%7Bmin%7D,上面的代码转换成公式就是这样:

getValue(%5Cvec%20p)%3D%5Cfrac%20%7B5(o_%7Bmax%7D-o_%7Bmin%7D%2B1)%7D%20%7B3(o_%7Bmax%7D-o_%7Bmin%7D%2B2)%7D(fbm_1(%5Cvec%20p)%2Bfbm_2(1.0181268882175227%5Cvec%20p))

对于上面的温度噪声,就是这样:

t(%5Cvec%20p)%3D%5Cfrac%20%7B5(t_1(%5Cvec%20p)%2Bt_2(1.0181268882175227%5Cvec%20p))%7D%7B4%7D

对于这个怪异的常量,我们最终也找不到它是怎么组成的,暂且认为是Mojang瞎打的吧)

下面,是这个噪声的图像表示(y取0,这也是MC的做法):

代码+演示:https://www.shadertoy.com/view/fdXyz8,RNG=Xoroshiro128++,seed=514,噪声值经过了线性变换2n-0.8,范围[0, 256]

对照真实的MC世界,我们可以看出它们基本一致:

1.18默认世界配置,seed=514,范围[0, 1024],从中间明显的分割线可以看出相似点

四.区块生成次序

讲完了基础的数学方面,我们来看看MC是怎么组织区块的生成的。

区块生成有一定阶段,在ChunkStatus里面是这样写的:

  • EMPTY("empty") - 区块没有被加载,也不在生成当中,只存在于内存中,区块内部的所有数据未知

  • STRUCTURES_STARTS("structures_starts") - 区块没有被加载,但是在生成(之后的所有阶段都是在生成之中)。这个阶段计算结构生成起始点。这里的结构指村庄、要塞、神殿、宝藏等的多方块联合复杂结构。

  • STRUCTURE_REFERENCES("structure_references") - 将结构生成起始点保存到地物存储中。

  • BIOMES("biomes") - 计算群系并保存,此时地形地貌甚至任何方块都没有生成。

  • NOISE("noise") - 计算地形噪声,生成起伏的地形,也生成了1.18特有的洞穴、含水层与矿脉,但是地形表面和地物都没有生成。

  • SURFACE("surface") - 计算地形的表面生成。地表的景物随着群系不同而不同,例如冻洋的冰,恶地的陶瓦等。

  • CARVERS("carvers") - 老版本的洞穴雕刻生成,生成普通的洞穴结构。

  • LIQUID_CARVERS("liquid_carvers") - 在1.18中这个阶段没有意义。

  • FEATURES("features") - 地物生成。地物包括了很多东西,矿物的生成、树的生成、结构的放置都属于这个阶段的事。注意结构虽然在最开始就确定了位置,但是在这个阶段才开始生成。高度表也在这时生成。

  • LIGHT("light") - 启动光照引擎计算区块光照。

  • SPAWN("spawn") - 生成区块附带的生物,如草原上的原生动物。结构生物不在此阶段生成,它们和结构一样在上一阶段就生成了。

  • HEIGHTMAPS("heightmaps") - 1.18中这个阶段没有意义,在这个阶段之前高度表就生成了。

  • FULL("full") - 区块生成结束,现在这个区块就是一个可以被玩家加载,并且在内部游玩的区域了。

接下来,我们会讲讲主要的部分。先来讲一下群系的判断。

五.群系生成

群系的生成有关于很多参数,接下来我们将一一说明。但是首先,我们要了解一下区块是怎么被划分之后生成的。

在区块生成的时候,每个区块被分成了更小的单位用于群系生成。区块首先被分为一块块16x16x16方块的区域,这就是区块段(Chunk Section),这是MC世界数据保存的最小单位。它又被分成了4x4x4的区域,每个区域都会计算一次群系,下面将这个区域的坐标叫群系生成坐标。群系生成坐标的计算就是XYZ轴的坐标除以4,因此我们可以看到噪声图像和真实生成有1:4的比例。

每个群系生成区域都有特定的数值用于计算群系和地形等,下面列举一下它含有的各个值(坐标指群系生成坐标,而不是方块坐标,计算忽略Blender影响):

Offset(偏移) 分为X轴偏移和Z轴偏移,是XZ坐标加上一个噪声得出的,下面的公式给出了关系:

o_x%3Dx%2B4o(x%2C0%2Cz)%3Bo_z%3Dz%2B4o(z%2Cx%2C0)

其中噪声o的参数如下:

Continentalness(大陆性)代表了这个区域的海陆关系,它的值可以在F3中找到——Multinoise行的C,和Biome Builder行的C,用来代表陆地类型。当值大于-1.2且小于-1.05时显示为“Mushroom fields”(蘑菇岛);当值大于-1.05且小于-0.455时显示为“Deep ocean”(深海);当值大于-0.455且小于-0.19时显示为“Ocean”(海洋);当值大于-0.19且小于-0.11时显示为“Coast”(海岸);当值大于-0.11且小于0.03时显示为“Near inland”(浅内陆);当值大于0.03且小于0.3时显示为“Mid inland”(中内陆);当值大于0.3时显示为“Far inland”(深内陆)。

它的计算仅和XZ坐标有关,或者更确切地说是XZ偏移。

c%3Dc(o_x%2C0%2Co_z)

噪声c参数如下:

Weirdness(奇异性) 代表了地形的奇异程度,例如竹林和丛林仅有奇异度不同,因为竹林相当于丛林的变种,所以奇异度更高。在F3中也能找到它的身影,Multinoise行的W。

计算仅和XZ坐标有关,公式与参数如下:

w%3Dw(o_x%2C0%2Co_z)

Erosion(侵蚀度) 代表地形被侵蚀的程度。值越低代表被侵蚀的越强,形成峡谷;值越高代表侵蚀弱,形成平原。值可以在F3中找到,在Multinoise行的E。

计算也只关于XZ坐标。

e%3De(o_x%2C0%2Co_z)

Ridge(山脊性) 代表地形隆起程度,它的另一个名称是PV(Peaks and Valleys)。在F3中也能看到它的值,Terrain行的PV;另一项是Boime Builder的PV,值小于-0.85时,该项显示为“Valley”(山谷);当值大于-0.85且小于-0.2时显示为“Low”(低地);当值大于-0.2且小于0.2时显示为“Mid”(中等高度地形);当值大于0.2且小于0.7时显示为“High”(高地);当值大于0.7时显示为“Peak”(山峰)。

计算时只关于奇异性。

r%3D1-%5Cvert3%20%5Cvert%20w%20%5Cvert%20-2%20%5Cvert%20

Terrain Offset(地形偏移)与之前的XZ偏移不一样,这个偏移是用来计算地形的,它的具体含义在之后介绍。它的计算和之前的值计算不一样,它使用了插值函数。由于默认的插值函数过于复杂,这里就不给出它的公式了(具体公式在TerrainShaper里面)。它有关于大陆性、侵蚀度和山脊性。在F3中表示为Terrain行的O。

Factor(地形因子) 同上,也是地形参数。有关于大陆性、侵蚀度、奇异性和山脊性。公式也是过于复杂所以略去。F3中显示为Terrain行的F。

Jaggedness(粗糙度)也属于地形参数,也有关于前面的4个参数。公式略去。它在F3中Terrain行的JA。

Depth(深度) 位置的深度。1.18拥有垂直群系,所以深度也决定了群系类型。值越大,代表着越深。它的计算公式如下:

d%3D1-%5Cfrac%7By%7D%7B32%7D%2Bo_%7Bterrain%7D

Temperature(温度)决定这个位置上的温度。温度决定了群系的温度类型,比如寒冷或者温暖。它只和XZ轴有关

t%3Dt(o_x%2C0%2Co_z)

Humidity(湿度)决定这个位置上的湿度。湿度决定了群系的湿度,是干旱还是湿润。它也只和XZ轴有关。

h%3Dh(o_x%2C0%2Co_z)

知道了这些值,我们就可以选取这个位置上应该存在的群系。

群系的选择是这样的:群系本身有个“最佳区间”,例如冻河(FROZEN_RIVER)的最佳区间是温度-1.0~-0.45、任意湿度区间、大陆性-0.11~0.03、侵蚀度-1.0~-0.375,奇异性-0.05~0.05,深度0或1。如果噪声点正好位于某个最佳区间内,那么直接选中表示的群系,但是我们的噪声结果不一定能正好放到这些区间上,所以系统会选取噪声图上离这个噪声点最近的群系最佳区间,并设置群系为这个区间的群系。为了减少一些群系的生成,让他们更稀有,又加入了一个offset偏移用于增加和其他点的距离(普通群系这项是0),这样就会更加稀有。

群系的内置设定都在类OverworldBiomeBuilder内部,但是内部的代码实在是过于杂乱并且区间很多,这里就不给出具体的各个群系的最佳区间了。这里说明几个规律:

1. 蘑菇岛,它只需要大陆性为-1.2~-1.05(这也可以解释为什么蘑菇岛都在离海岸极远的地方),除了深度之外无视其他参数(准确来说是其他参数的区间都被包括在内,无论什么值都在区间之内都能达到最佳区间)。

2. 在地表的群系深度的最佳值是0和1,在地底的群系深度最佳区间是0.2~0.9,这意味着两件事:一是地底群系不能无限向下延伸,二是地表也有可能出现地底群系。如果深度噪声因为地形偏移被强制覆盖(比如设置成很大的值),那么可能出现地底群系的生成高度上升或下降。

3. 温度(T)、湿度(H)、侵蚀度(E)有对应的简化值,这些简化值在F3的Biome Builder行能看到。这些简化值实质上是数组的序号,数组的内容如下:

这里有一个特殊的区间,也就是温度-1.0~-0.45。这个区间代表了地表结冰,简化值是0。在寒冷生态群系中,基本T都是0;而当T是1~4时通常不会产生积雪等地表景物。

4. 奇异性决定是否为某个群系的变种。河流的奇异性在-0.05到0.05之间(这也说明了奇异性的噪声图像能大概看出河流位置),变种的奇异值大于0,而非变种的奇异性小于0。这也可以看出件事:变种群系和非变种群系基本不在河流的同一岸。又因为奇异性决定了山脊性,所以可以看出为什么河流奇异性是接近0的:因为在这时山脊性最小,接近-1。

5. 巨大化生物群系只修改了大陆性、温度、湿度、侵蚀度噪声,准确来说是每个噪声都下调了两个数量级(firstOctave被减了2),但是奇异性不变。这导致了奇异性决定的一些事物不会变化,最明显的是河流生成位置是相同的!(在1.18前的版本之中,巨大化生物群系和普通生成的河流图像也是一样的,可能是为了同步两个版本)

至此,群系的初步生成就结束了。

下面是几种参数的噪声图像和真实生成的比较:

群系真实生成和理论的噪声图像比较。seed=514,RNG=Xoroshiro128++

更加清晰的版本:

https://sm.ms/image/FigCvdPQEDk1r4t - 侵蚀度噪声图像

https://sm.ms/image/NcHBptkxRi57ldW - 湿度噪声图像

https://sm.ms/image/GNYjkw1vySQELZs - 大陆性噪声图像

https://sm.ms/image/AYLg2j7OKpnZ1hI - 奇异性噪声图像

https://sm.ms/image/bjN1kXI6YgonaeJ - 温度噪声图像

每个群系生成的单位在计算完群系之后,就要从4x4x4的网格放大到16x16x16的区块段上,这样才可以让群系的过渡更平滑。这个工作由BiomeManager完成,由于代码比较复杂,在这就不做演示了。

六.结构起始点生成

结构在生成之前要决定放置位置,也就是结构起始点生成。在区块生成的步骤中它是第一项,而真正的结构生成被包含在后面的地物生成,这时会删除结构起始点代表结构已经生成完毕。

结构的定位使用了几种不同的算法,但是大多数都使用了一个算法定位。首先介绍的也就是这个算法:

为了保证结构之间不能离得太近也不能离得太远,MC提供了两个参数用于控制结构的距离:

  • spacing(空位) - 两个同类型结构距离的平均值。

  • separation(间隔)- 两个同类型结构距离的最小值,要求必须小于spacing。

通过这两个参数,MC能将结构控制在一个合理的密度,不至于两个结构离得太近。这两个值的具体应用见下方代码:

从这段代码可以看出,MC其实是将世界分成了一个一个边长为spacing的格子计算结构的。每个格子内部只能有一个结构,并且每个格子内的结构都不会超出以格子起始点为左上角边长为spacing-separation的方格。这也就代表着,如果X轴/Z轴的区块坐标有一项遵照了下面的公式:

pos%20%5Cin%20%5Bspacing%5Ctimes%20k-separation%2Cspacing%5Ctimes%20k)%2Ck%5Cin%20Z

那么这个结构无论如何也不会在这里生成。理论上,一个结构在

8%5Csqrt%7B(spacing-separation)%5E2%2B(3spacing-separation)%5E2%7D%20

格内肯定有另一个同种类型的结构,两个相同结构的距离不会超过

16%5Csqrt%7B(spacing-separation)%5E2%2B(2spacing-separation)%5E2%7D%20

格。

在这段代码中,还有一个参数之前没有提到:salt。salt指盐,它在密码学中是一种辅助散列化的值,在MC中它用于辅助结构生成的随机化。在这里,盐作用于随机数引擎的种子上,它是这样计算的:

现在这段代码里面只差一个方法没有解释了。linearSeparation方法返回布尔值,代表最短间隔是否遵循线性规律。这是个硬编码在代码里的值,不能通过我们配置修改。在使用了这种方式生成的结构只有三种结构不使用线性规律,它们是末地城、海底神殿和林地府邸。

下面我们使用这段代码生成一下海底神殿的位置图。它的平均间隔是32,最小间隔是5,盐是10387313。也就是说理论上,一个海底神殿在759.4格范围内必有另一个海底神殿,两个相邻海底神殿的最大间隔不会超过1038.1格,X轴和Z轴区块坐标符合pos%20%5Cin%20%5B32k-5%2C32k)%2Ck%20%5Cin%20Z的坐标不会生成海底神殿。

左:实际生成;右:程序生成。seed=514,region=X[-2560,-512],Z[-512,1536]

对比这两张图我们可以发现左面的坐标右面也都有,可是右面却多出了几个点。这些点是被过滤掉了,因为海底神殿只能在海洋生物群系生成。但是一个区块可是有很多群系决定点的,哪个才是它判断的点呢?下面这段代码给出了答案:

可以看到检查的是区块中心点(8,8)的最高点处群系。

到这里,结构生成点的判断就结束了。

除了这种方式外,还有几种结构生成点生成算法。

对于埋藏的宝藏结构,它拥有一个“概率值”用于生成。在默认情况下这个值是0.01,也就是说每个区块都有1%的几率可能生成埋藏的宝藏。在可能的前提下检查群系,如果群系是沙滩或者积雪沙滩那么就能生成。

结构起始点生成就说到这里,结构的具体生成和地物的具体生成在这系列的专栏不会详细说明。

这篇专栏就到这里,下一篇将讲述高度表生成、洞穴生成等。

有错误可以在评论区指出。

(MC的代码真的是一团糟)

创造自己的世界——Minecraft 1.18的地形生成(一)的评论 (共 条)

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