主世界的生成——浅谈Minecraft地形生成(四)
现在,我们已经了解了密度函数是如何影响地形生成的,那么现在让我们把目光重新看向主世界的简化版的密度函数:
{ "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 } } } } }
其中我们主要将洞穴相关的噪声函数去除了。如果你在misode的预览器中观察它,你会看到大概这样的图像:
紫色的部分就是>0的部分,大于0的密度函数将会生成默认方块,在主世界就是石头了。在图中,你可以看到哪些地方是山谷或者海洋,哪些地方是山峰。
大致浏览一次这个密度函数后,我们会发现它还调用了一些其他的函数,比如minecraft:overworld/sloped_cheese
。继续深挖,我们会发现更多的调用函数。那么,让我们直接前往函数调用的最底部,看看这个世界是怎么生成的。
Contents
minecraft:overworld/offset
{ "type": "minecraft:flat_cache", "argument": { "type": "minecraft:cache_2d", "argument": { "type": "minecraft:add", "argument1": { "type": "minecraft:mul", "argument1": { "type": "minecraft:blend_offset" }, "argument2": { "type": "minecraft:add", "argument1": 1.0, "argument2": { "type": "minecraft:mul", "argument1": -1.0, "argument2": { "type": "minecraft:cache_once", "argument": { "type": "minecraft:blend_alpha" } } } } }, "argument2": { "type": "minecraft:mul", "argument1": { "type": "minecraft:add", "argument1": -0.5037500262260437, "argument2": { "type": "minecraft:spline", "spline": {...} } }, "argument2": { "type": "minecraft:cache_once", "argument": { "type": "minecraft:blend_alpha" } } } } } }
观察这个密度函数。我们省略了它的样条插值部分,因为真的特别特别多,加起来总共一千多行的JSON,在这里是绝对看不完的。有兴趣的读者可以自行打开源文件研究一下子。除了插值以外,我们可以看到它还有一些其他的密度函数,但是又一看,都是blend_xxx
函数。而这些函数都是用来处理新旧世界区块交接的,对于全新的世界生成来说,这些就是常数而已。cache_once
用于增强性能,不会影响函数的值。因此,经过简单的加减法即可发现,除了样条插值以外的所有部分都被约为了0,也就是说这个函数的值只取决于这个样条插值函数。
最后得到的结果就是上面这个条形码一样的图案。其中的每一个小细条都是成百上千格的距离,世界的起源就蕴含在这些小小的条纹中。
如果你把视图切换为俯视图,你还能看出河流和山脊。在这里,大陆性(continents)和侵蚀性(erosion)两个噪声共同参与了样条采样的过程,通过控制三次样条的控制点从而控制样条曲线,进而实现了地形的沟壑纵横或山脉起伏。同时,生物群系的生成同样使用了大陆性和侵蚀性两个参数,因此在对应的山脉或者峡谷地形,就会生成对应的生物群系。也就是说,大陆性和侵蚀性两个噪声,同时决定了地形生成和生物群系的生成两个过程。
minecraft:overworld/depth
{ "type": "minecraft:add", "argument1": { "type": "minecraft:y_clamped_gradient", "from_value": 1.5, "from_y": -64, "to_value": -1.5, "to_y": 320 }, "argument2": "minecraft:overworld/offset" }
这个函数中,我们已经看到了起伏的山脉,而这个函数却惊人的简单——只有一个加法运算。第一个参数表示了一个从y轴的-64到320,值从1.5变化到-1.5的函数。让这个函数的值加上我们之前看到的条纹噪声的值,就可以得到上图这样的函数了。
为什么能得到这样的函数呢?因为第一个函数的噪声值和y轴成线性关系,即value1 = (y+64)/384*3-1.5,而第二个密度函数的值仅仅和xz轴有关。那么假设第二个密度函数在此处的值为value2,所以可以求出,如果要密度函数大于0,那么需要value1+value2大于0,即(y+64)/384*3-1.5+value2 > 0,即y > (1.5-value2)/3*384-64,其实也是一个线性关系。如此,就把offset的条纹码转换为了高低起伏的地形。
现在,我们在y轴为64的位置,也就是通常的海平面位置,观察俯视图,就可以更明显地看出河流、海洋、大陆和山脉的位置(绿色的部分就是低于0,也就是海洋和河流的位置,而蓝紫色则是大于0,表示这里应该是大陆或者山脉)
minecraft:overworld/jaggedness
{ "type": "minecraft:flat_cache", "argument": { "type": "minecraft:cache_2d", "argument": { "type": "minecraft:add", "argument1": 0.0, "argument2": { "type": "minecraft:mul", "argument1": { "type": "minecraft:blend_alpha" }, "argument2": { "type": "minecraft:add", "argument1": -0.0, "argument2": { "type": "minecraft:spline", "spline": {...} } } } } } }
这个密度函数看起来很繁杂,其实一点也不简单。首先,cache_2d
是用于缓存以加快运算速度的,所以我们这里可以不用管,而blend_alpha
则是用于区分新版本和旧版本区块,用于处理加载旧版本世界的时候,新旧区块交界处的,所以我们可以把它看作一个常数。因此经过简单计算以后,这个密度函数就只和被我用{...}
代替的这个样条插值有关了。具体的插值过程有兴趣的读者可以自己下去研究,如果有合适的计算工具的话,其实计算这个插值过程并不复杂。上图分别是这个这个密度函数的俯视图和横向剖面图。这个函数的结果永远是大于0的。
minecraft:overworld/sloped_cheese
{ "type": "minecraft:add", "argument1": { "type": "minecraft:mul", "argument1": 4, "argument2": { "type": "minecraft:quarter_negative", "argument": { "type": "minecraft:mul", "argument1": { "type": "minecraft:add", "argument1": "minecraft:overworld/depth", "argument2": { "type": "minecraft:mul", "argument1": "minecraft:overworld/jaggedness", "argument2": { "type": "minecraft:half_negative", "argument": { "type": "minecraft:noise", "noise": "minecraft:jagged", "xz_scale": 1500, "y_scale": 0 } } } }, "argument2": "minecraft:overworld/factor" } } }, "argument2": "minecraft:overworld/base_3d_noise" }
这个函数需要我们慢慢拆开一层层一看。首先,我们把目光转向最内层:这里一个叫做jagged
的噪声在xz方向进行放缩后,进行了half_negative
计算,这个计算让噪声大于0的部分保持不变,同时让噪声小于0的部分减半。接着,它与overworld/jaggedness
相乘。由于overworld/jaggedness
大部分区域的值实际上为0,因此这个步骤相当的作用是将jagged
噪声限制在了一定的范围内。下图从左到右分别展示了同一种子的同一位置,在进行half_negative
计算后的jagged
密度函数,overworld/jaggedness
密度函数和进行上述计算后的结果(图的位置可能稍有偏移,y轴为64):
jagged的意思是层次不齐的,而jaggedness的意思是粗糙度。通过简单的加和操作,这个部分用于为我们已经初具地形模样的overworld/depth
函数添加更多的变化。下图是原来的overworld/depth
密度函数,以及加上我们刚刚计算结果后的样子:
接下来是overworld/factor
密度函数。这个函数我们之前没有介绍过。它的内部也是一个三次样条,而具体的形状在下图的左侧(剖面图)。这个函数的用途我推测是用于给后续叠加三维噪声创造缺口,为什么这么说我们之后再解释。下方中部的那张图就是加上overworld/factor
后的密度函数。而左侧则是加上三维噪声后的密度函数:
可以看到,叠加了overworld/factor
后,密度函数的这个地方的颜色更加接近绿色,这也就意味着这个地方的密度绝对值较低,因此更容易受到三维噪声的影响。右图中可以看到,在overworld/factor
影响的位置,地形出现了更加明显的崎岖不平,甚至出现了小山峰。在某些极端情况下,这会导致空岛这种极端的地形。
主世界密度函数
现在我们终于看完了所有引用的函数,是时候看看主世界的生成过程了。这里是主世界的密度函数,和我们文章开头的时候看到的一样,是简化版的:
{ "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 } } } } }
我们还是从里面往外面看。首先是一个range_choice
计算,这个计算让密度函数大于1的部分都变成了1。随后进行了blend_density
,即新旧版本区块的过渡,这一步我们可以不用管。之后便是至关重要的一步,interpolated
,插值。插值可以让地形变得更加的平滑,否则你的地形中将会全是以4*4小格子为单位的地形。下面分别展示了插值前后地形的变化。
此后,便只有简单的数值运算了。挤压(squeeze
)操作会将地形进一步进行一些细小的微调,挤压的函数图像如下图:
最后,我们就得到了简单的一个主世界的地形,有河流,有山丘,有海洋。但是,还是缺少了一些东西,比如噪声洞穴。有兴趣的读者可以自行用我们文章中的这种思路,分析主世界密度函数的完整版,此处我们就不再赘述了。