|
|
|
Mojang 的生成器和别人的生成器
目前,网络上能找到的各种世界生成器的教程/资源,按照以下的方式生成一个地图:
- 使用噪波函数生成一串随机但是连续的数字
- 使用这些数字的大小表示地形的高度/湿度/其他属性
- 使用这些属性决定生物群系
什么是噪波函数呢?
噪波函数(这是我的叫法,这篇教程也会这么叫),别人把他叫做噪声函数,基本上是一个种子随机发生器。它需要一个数作为参数,然后根据这个参数返回一个随机数。如果两次都传同一个参数进来,它就会产生两次相同的数。这个性质决定了 Minecraft 使用相同的种子总是生成相同的地形,
如果每个相近的参数生成的随机数相差太大,那么 Minecraft 的地形将是无比混乱的。噪波函数在传入连续的数字时,返回的随机数的差值是不大的,整体数值呈现随机但连续起伏。
关于噪波函数,你可以去这里看看。如果你能够硬肛全洋文文档,你也可以去这里看看,这一篇详细的讲解了各种噪波的区别,
那么 Mojang 的生成器是如何运作的呢?
根据土球的答案,我们可以了解到 Minecraft 的地形生成与上文不同,是先生成生物群系,再通过群系来决定地形(如高度)。生成的地形分为两个大阶段,Generation 时期生成主要的地形,Population 时期生成点缀,比如树。
土球回答的下面也有一个答案,那个答案比较详细的讲述了 Minecraft 的地形生成过程。
我们的生成器
我们采用地形决定群系的方式。首先是生成地形。常用的噪波函数有 Perlin Noise 和 Simplex,我们采用 Simplex。
public class NormalGenerator extends ChunkGenerator { private SimplexOctaveGenerator noise; @Override public ChunkData generateChunkData(World world, Random random, int chunkX, int chunkZ, BiomeGrid biome) { ChunkData chunkData = createChunkData(world); // 我们需要的噪声生成器 if (noise == null) { noise = new SimplexOctaveGenerator(world.getSeed(), 1); // 我们需要更平缓的地形,所以需要设置 scale // 该值越大,地形变化更大 // 微调即可 noise.setScale(0.005D); } for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { // 方块的真实坐标 int realX = chunkX * 16 + x; int realZ = chunkZ * 16 + z; // noise 方法返回 -1 到 1 之间的随机数 double noiseValue = noise.noise(realX, realZ, 0.5D, 0.5D); // 将 noise 值放大,作为该点的高度 int height = (int) (noiseValue * 40D + 100D); // 底层基岩 chunkData.setBlock(x, 0, z, Material.BEDROCK); // 中间石头 for (int y = 0; y < height - 1; y++) { chunkData.setBlock(x, y, z, Material.STONE); } // 表面覆盖泥土和草方块 chunkData.setBlock(x, height - 1, z, Material.DIRT); chunkData.setBlock(x, height, z, Material.GRASS_BLOCK); } } return chunkData; } } |
有点单调,除了默认生成的生物以外没有任何东西,并且地形看起来也很单调。
加点东西
如果只是小小地点缀一下地图,BlockPopulator 是再适合不过的选择了。
重写 getDefaultPopulators 方法,返回我们自定义的:树!
@Override public List<BlockPopulator> getDefaultPopulators(World world) { return ImmutableList.of(new TreePopulator()); } private static class TreePopulator extends BlockPopulator { @Override public void populate(World world, Random random, Chunk chunk) { // 假设只有 1/4 的区块生成树 if (random.nextInt(4) < 1) { // 假设每个区块生成 1-3 颗树 int amount = random.nextInt(3) + 1; for (int i = 0; i < amount; i++) { // 随机生成树的坐标 int x = random.nextInt(16); int z = random.nextInt(16); int y = 255; // 找到最高的方块来生成树 while (chunk.getBlock(x, y, z).getType() == Material.AIR) y--; // 生成树 world.generateTree(chunk.getBlock(x, y, z).getLocation(), // 搞点有趣的,我们随机选择不同的树生成 TreeType.values()[random.nextInt(TreeType.values().length)]); } } } } |
(最终还是没有蘑菇树,果然还是不行呢)
地面上不是那么单调了,但是 Minecraft 的特色可不止地面上。现在,我们往这个世界加一点矿物:
@Override public List<BlockPopulator> getDefaultPopulators(World world) { return ImmutableList.of(new TreePopulator(), new DiamondPopulator()); } private static class DiamondPopulator extends BlockPopulator { @Override public void populate(World world, Random random, Chunk chunk) { // 假设每个区块只有一个钻石矿 // 钻石矿脉随机生成在高度 16 以下 int x = random.nextInt(16); // 不要生成在基岩上 int y = random.nextInt(15) + 1; int z = random.nextInt(16); // 继续生成的几率 while (random.nextDouble() < 0.8D) { // 只替换岩石 if (chunk.getBlock(x, y, z).getType() == Material.STONE) { chunk.getBlock(x, y, z).setType(Material.DIAMOND_ORE); } // 向某个方向随机继续生成 switch (random.nextInt(6)) { case 0: x++; break; case 1: y++; break; case 2: z++; break; case 3: x--; break; // 不要生成到基岩下面去了 case 4: y = Math.max(y-1, 0); break; default: z--; break; } } } } |
你可以仿照这个方法生成其他矿物。
读了上面几章以后,你一定对代码中出现的 PerlinNoiseGenerator / SimplexOctaveGenerator 感到疑惑了。这一章将会教会你 噪波函数 的各种概念,以及如何使用。
如果你能够访问 archive.org 并且可以阅读英文,那么你可以看看这一篇文章。
噪波函数?
各位相比用过随机数生成器,即 Java 的 Random 类,虽然这个类很好的满足了我们对于不可预测性的需求,但是它的输出过于随机;在这种情况下,Ken Perlin 发明了 Perlin 噪波函数。Perlin 函数看起来是这样的:
如果传入更连续的参数,最终的结果会是这样的:
噪波函数的形状被我们用于生成地形。
函数的一些特性
这是一个普通的正弦波
amplitude:振幅,为波的高度
wavelength:波长,为每个峰之间的距离
frequency:频率,为 1/波长
这是一个噪波函数
每个红点代表函数的随机值
振幅为函数可能取得的最大值和最小值的差值
波长为每个红点之间的距离
频率仍然为 1/波长
现在,脑补一个随机的噪波,脑补一下增加/减少它的频率,增加/减少它的振幅
当振幅减少时,函数将会变「矮」,当频率增加时,函数起伏更加剧烈。
如果我们把低频高幅的函数和高频低幅的函数混合起来:
就会得到和原有单调的噪波函数完全不同的、更为复杂的函数图像:
数学表达式?
我们将 噪波函数 定义为 noise(x) ,返回值为 0-1 的小数。
那么上文的混合函数写出来可能就是这样的
f(x) = 1*noise(x) + 0.5*noise(2x) + 0.25*noise(4x) + 0.125*noise(8x) + 0.0625*noise(16x) ......
在数学表达式中,我们可以简单的把 2x 4x 8x 里的数字称为频率,将 0.5 0.25 0.125 称为振幅
到这里,有心的读者可能会注意到了,这个 f(x) 最后的值不是可以大于 1 了,还能叫噪波函数吗?
别担心,记住这个问题,继续往下看。
代码?
上文的混合函数可以写成这样:
/** * 假设此函数返回 0-1 之间的随机数,并且满足噪波函数的相关定义 * * @param x 参数 * @return 0-1 的随机数 */ static double noise(double x) { return 0; } /** * 噪波函数 * * @param x 参数 * @param freq 频率 * @param amp 振幅 * @return 函数值 */ static double f(double x, double freq, double amp) { return amp * noise(x * freq); } static double f(double x) { return f(x, 1, 1) + f(x, 2, 0.5) + f(x, 4, 0.25) + f(x, 8, 0.125); } |
维基百科: 分形噪声是上述 Perlin 1985年的文章中提出的将符合上文所述三条件的噪声通过计算分形和构造更复杂效果的算法。 在一维的情况下,设噪声函数为noise(x),则通过 noise(2x), noise(4x) 等就可以构造更高频率的噪声。 |
static double f(double x, double freq, double amp, int octaves, boolean normalized) { double result = 0.0D; double a = 1.0D; double f = 1.0D; double max = 0.0D; for (int i = 0; i < octaves; ++i) { result += f(x, f, a); max += amp; f *= freq; a *= amp; } if (normalized) { result /= max; } return result; } |
这 normalized 参数是干啥的?上文中我(也可能是读者你)提出了一个问题,这个参数即是解决这个问题的。仔细想想,是不是这样。
NoiseGenerator 与 OctaveGenerator
到了这里,我们只剩一个问题了:最开始的代码里的 noise 方法,如何实现呢?
org.bukkit.util.noise 包内有 4 个类,用于提供生成噪波函数的方法。
static double bukkitNoise(double x, double freq, double amp, int octaves, boolean normalized) { SimplexNoiseGenerator generator = new SimplexNoiseGenerator(new Random()); return generator.noise(x, octaves, freq, amp, normalized); } static double bukkitOctave(double x, double freq, double amp, int octaves, boolean normalized) { SimplexOctaveGenerator generator = new SimplexOctaveGenerator(new Random(), octaves); return generator.noise(x, freq, amp, normalized); } |
几点需要注意的地方:
- Bukkit 的噪波函数类返回 -1 到 1 的值,与上文的 0 到 1 不同,自行处理即可
- Bukkit 的噪波函数类默认频率很大,所以需要使用小频率(OctaveGenerator 的 setScale(0.005))
- 对于给定的相同种子,调用同一个点的函数总返回相同值
最后一个问题:为什么会有 PerlinXxxxGenerator 和 SimplexXxxxGenerator 两种呢?
Perlin 是初代噪波函数,Simplex 基于 Perlin 优化,得到的图像更好看,在高维度的速度也更快。
1. Math.pow
这个函数的图像是这样的:
你可以用这个函数让山峰更加陡峭,让山谷更加平坦
n1 = new PerlinNoiseGenerator(random); double e = n1.noise(x * 0.01F, z * 0.01F, 6, 2.0F, 0.5F); e = Math.pow(e, 2.5); elevation[x][z] = (int) (64 + e * 64F); |
2. Math.abs
你可以用此创建锋利的山脊
function ridgenoise(x, z) { return 2 * (0.5 - abs(0.5 - noise(x, z))); } e0 = 1 * ridgenoise(1 * x, 1 * z); e1 = 0.5 * ridgenoise(2 * x, 2 * z) * e0; e2 = 0.25 * ridgenoise(4 * x, 4 * z) * (e0+e1); e = e0 + e1 + e2; elevation[x][z] = Math.pow(e, 2.5); |
[groupid=1330]PluginsCDTribe[/groupid]