浅析Minecraft地形生成(三)

上一篇中,我们介绍了噪声,那么这一章,我们就要开始进入最艰难的部分之一了,也就是噪声是如何具体生成地形的。

密度

首先,我们引入一个叫做密度的概念。前面说到过,在Minecraft中地形是由噪声产生的。噪声有两个/三个输入值,即我们的坐标,而输出值就是所谓的密度了。密度的正负则决定了这里是空气还是方块,如果为正值,则放置方块(一般是石头),如果为负值,则放置空气。

未经处理的噪声可能会显得很杂乱,因此我们会额外使用一些计算来对噪声直接产生的密度值进行二次处理,而这计算密度用的函数,就被称为密度函数。当然,计算噪声本身的过程,也可以被称作密度函数。事实上,wiki中是这样定义密度函数的:

密度函数(Density Function)用于根据一定算法将一个方块坐标转换为一个数,其以JSON文件的形式存储在数据包data/<命名空间>/worldgen/density_function目录下。

密度函数 – Minecraft Wiki_BWIKI_哔哩哔哩 (biligame.com)

也就是说,在世界生成的时候,游戏会对每一个坐标进行一次密度的计算,根据最终的值,决定这里应该是空气还是石头。

密度函数的Json格式如下:

看起来非常简单,不是吗?但是这个type键大有玄机,如果你看了wiki,你就会看到下方长长的一串各种函数。它们之间还能进行嵌套计算,从而能让整个密度函数的json显得格外复杂。例如,下面是简化以后的主世界的密度函数:

{
    "type": "minecraft:squeeze",
    "argument": {
        "type": "minecraft:mul",
        "argument1": 0.64,
        "argument2": {
            "type": "minecraft:interpolated",
            "argument": {
                "type": "minecraft:blend_density",
                "argument": {
                    "type": "minecraft:range_choice",
                    "input": "minecraft:overworld/sloped_cheese",
                    "min_inclusive": -1000000,
                    "max_exclusive": 1.5256,
                    "when_in_range": {
                        "type": "minecraft:min",
                        "argument1": "minecraft:overworld/sloped_cheese",
                        "argument2": 1
                    },
                    "when_out_of_range": 1
                }
            }
        }
    }
}

注意,这已经是极致简化过的了。事实上的密度函数比这个要冗长地多,长到如果把它写在这里的话,你可能需要花一些时间用鼠标滚轮滚过它。

好啦,废话不多说。有一点是很明显的,那就是,密度函数的核心就是type中的内容,也就是wiki页面下的那一大串具体的函数。接下来,我们将会详细介绍它们。

密度函数的热力图

misode的生成器能够让你清晰地预览这个密度函数的情况。例如下面就是一个例子。左边是密度函数json文件的可视化编辑,右边则是密度函数的实时预览。你可以清楚地看到密度函数在不同坐标上的值(当然在二维中你只能看到xy轴的截面图,z轴是被固定的),借此你可以预览你的密度函数,也可以通过修改参数,观察各种密度函数有何用途。

密度函数

如果直接写Json文件格式可能会让人难以理解,而且也不便于阅读。为了方便起见,我们先研究一下密度函数的统一格式,然后用一种更简便的方式来表达它。

和代码中的函数一样,密度函数首先有一个函数名,也就是type后面直接跟着的键值。此外密度函数可能会有一个或多个参数,当然也可能没有参数。你可以从json格式看出来:

这是没有参数的
这是有两个参数的

所以其实我们可以直接把密度函数的格式写成这个样子来表示:函数名(参数1, 参数2)。密度函数至少会有一个三维坐标作为默认输入,然后返回一个浮点数作为密度值。此外,根据密度函数的不同,还可能会有其他更多的参数,这些参数通常会写在json文件当中。而对于参数的类型,额外值得注意的便是,我们会用Density表示密度,在wiki的Json格式中,它通常被描述为“可以为一个密度函数ID,或者一个密度函数的JSON对象形式或常数形式”。这代表了三种形式的密度函数:

//一个密度函数ID
Density densityFunction(pos, args1){
    return AnotherDensityFunction(pos, args2);
}

//一个密度函数的JSON对象形式
Density densityFunction(pos, args1){
    //...处理input的计算
    return resultDensity;
}

Density densityFunction(pos, args1){
    //返回一个常数
    return const;
}

此外,下文中参数格式中的参数名和json格式中的键名相吻合,除了argument被简写为了arg

简单计算的密度函数

针对简单的数学运算,这里有一系列密度函数可供调用。

函数名参数格式描述
constantconstant(double arg)返回这个常数。
和直接把密度函数写成一个常数效果相同
addadd(Density arg1, Density arg2)将两个密度相加,即a+b
mulmul(Density arg1, Density arg2)将两个密度相乘,即a*b
minmin(Density arg1, Density arg2)取两个密度的最小值
maxmax(Density arg1, Density arg2)取两个密度的最大值
absabs(Density arg)取绝对值,即|a|
squaresquare(Density arg)计算它的平方,即a^2
cubecube(Density arg)计算它的三次方,即a^3

复合计算的密度函数

除了简单的数学运算以外,还有一些复合的数学运算也被包装在了单独的函数中。

half_negative(Density arg):如果输入的密度函数的值大于零,则返回该值本身;否则返回该值的一半。

quarter_negative(Density arg):如果输入的密度函数值大于零,则返回该值本身;否则返回该值的1/4。

squeeze(Density arg):首先把输入值钳制在-1到1的区间内(小于-1的视为-1,大于1的视为1),然后将它代入公式x/2 - x*x*x/24并返回。

复杂计算的密度函数

有些密度函数的内部计算较为复杂,甚至需要除密度以外的参数类型来辅助计算。

old_blended_noise(double xz_scale, double y_scale, double xz_factor, double y_factor, double smear_scale_multiplier)

旧版的噪声算法。

  • xz_scale:xz轴方向的缩放。取值为0.001到1000.0的闭区间。
  • y_scale:y轴方向的缩放。取值同上。
  • xz_scale:作用未知。取值同上。
  • y_scale:作用位置。取值同上。

noise(string noise, double xz_scale, double y_scale)

对噪声进行一次采样。

  • noise:一个噪声的命名空间ID。
  • xz_scale:xz方向的缩放。
  • y_scale:y方向的缩放。

weird_scaled_sampler(string rarity_value_mapper, string noise, Density input)

根据输入的另一个密度函数,对指定噪声的特定区域进行缩放、减弱增强,并取绝对值后返回。

  • rarity_value_mapper:填type_1(最小缩放增强率为0.75,最大为2.0)或type_2(最小缩放增强率为0.5,最大为3.0)。
  • noise:一个噪声的命名空间ID。
  • input:用于处理噪声的密度函数。

shifted_noise(string noise, double xz_scale, double y_scale, Density shift_x, Density shift_y, Density shift_z)

类似noise,但是先缩放并偏移输入的坐标。

  • noise:一个噪声的命名空间ID。
  • xz_scale:xz方向的缩放。
  • y_scale:y方向的缩放。
  • shift_x:用于偏移x坐标。
  • shift_y:用于偏移y坐标。
  • shift_z:用于偏移z坐标。

range_choice(Density input, double min_inclusive, double max_exclusive, Density when_in_range, Density when_out_of_range)

根据input在此坐标的计算结果,判断是否在一定数值范围,即[min_exclusive, max_exclusive)内,如果在范围内则返回when_in_range的计算结果,如果在范围外则返回when_out_of_range的计算结果。

  • input:用于判断范围的密度。
  • min_inclusive:范围的下限(闭区间)
  • max_exclusive:范围的上限(开区间)
  • when_in_range:如果input值在范围内返回的密度
  • when_out_of_range:如果input值在范围外返回的密度

shift_a(string arg)

设输入的三维坐标为(x, y, z),在(x/4, 0, z/4)处对指定的噪声进行采样后,对采样所得的值乘上4。

  • argument:一个噪声的命名空间ID。

shift_b(string arg)

同上,但是是在(x/4, y/4, z/4)处进行噪声采样后乘上4。

  • argument:一个噪声的命名空间ID。

shift(string arg)

同上,但是是在(x/4, y/4, z/4)处进行噪声采样后乘上4。

  • argument:一个噪声的命名空间ID。

clamp(Density input, double min, double max)

将输入值钳制在一个范围内。

  • input:输入的密度值。
  • min:范围的最小值。取值为-1000000.0到1000000.0的闭区间。
  • max:范围的最大值。取值为-1000000.0到1000000.0的闭区间。

spline

根据输入的密度函数和采样点,进行一次三次样条计算。由于此密度函数包含复合对象,因此直接用json文件的形式展示

  • spline:一个样条函数对象。如果为常数,则此密度函数永远返回一个常数,效果和将密度函数直接设置为一个常数相同。
    • coordinate:密度。用于计算初始密度,从而能放入样条函数中进行二次映射。
    • point:一个包含三个采样点的列表。用于拟合样条函数。
      • location:该采样点的密度值(横坐标)。
      • value:该采样点映射的密度值(纵坐标)可以为另一个样条对象,也就是再将这个点进行一次样条函数映射。
      • derivative:该采样点处应有的斜率。

y_clamped_gradient(int from_y, int to_y, double from_value, double to_value)

将Y坐标钳制后线性映射到一个区间上返回,计算过程大致为:

//density是传入的坐标
y_clamped_gradient(Pos pos ,int from_y, int to_y, double from_value, double 
to_value){
    if(pos.y < from_y) return from_value;
    if(pos.y > to_y) return to_value;
    return (pos.y-from_y)/(to_y-from_y)*(to_value-from_value) + from_value;
}
  • from_y:Y坐标映射区间的开始值。取值为-4064,到4062的闭区间。
  • to_y:Y坐标映射区间的结束值。取值为-4064到4062的闭区间。
  • from_valuefrom_y对应的映射值。取值为-1000000.0到1000000.0的闭区间。
  • to_valueto_y对应的映射值。取值为-1000000.0到1000000.0的闭区间。

标记函数

这些密度函数没有参数,计算过程完全被包装在内,仅有另一个密度函数作为输入。因此下文中都不会再说明参数。

interpolated:根据周围元胞(Cell)的密度函数值,对元胞里的每个方块进行插值。每个元胞的大小为size_horizontal * 4size_vertical * 4(通常为4*4)。

flat_cache:对输入的密度函数在Y=0的值按照元胞缩放大小进行缓存。

cache_2d:对输入的密度函数值进行缓存,直到当前计算的坐标发生变化。

cache_once:对输入的密度函数值进行缓存,直到下一次插值运算。

cache_all_in_cell:在游戏内部对final_density使用。对输入的密度函数值按元胞为单位进行缓存,存入区块的内存数据中。

杂项密度函数

blend_alpha():目前在原版用于旧版区块与新版区块间的过渡。如果区域内全部为新版区块,返回1。

blend_offset():目前在原版用于旧版区块与新版区块间的过渡。如果区域内全部为新版区块,返回0。

blend_density(Density arg):目前在原版用于旧版区块与新版区块间的过渡。

beardifier():向结构中添加额外的地形(见结构设置中的adapt_noise)。不需要在噪声设置中额外声明,默认会被加至噪声设置中的final_density

end_islands():使用末地岛屿的噪声算法对当前水平坐标进行采样。其最小值为-0.84375,最大值为0.5625。一般决定了什么位置会有岛屿。

末地岛屿的密度函数

你应该在上面看到了,有一种叫做末地岛屿的噪声算法。末地的生成分为两部分,也就是中央末路之地岛屿(64区块内)以及外侧的岛屿(64区块外)。

末地岛屿的密度函数俯视图

在外岛的计算过程中,将会通过计算单形噪声的方式确认岛屿中心所在的位置。然后在各个坐标上,根据与岛屿中心的距离分别计算密度,然后对各个岛屿中心计算出的密度取最大值。因此你可以看到末地岛屿通常都有圆形的要素。

总结

这一章节中我们详细介绍了密度函数以及各种密度函数的用法和用途。别忘了我们开头给出的主世界的密度函数,接下来,我们将会用一整个章节来分析这个密度函数。相信在分析完毕这个例子后,你会对密度函数有更深的理解。

类似文章

发表回复