水晶杂谈3:生物群系大家族,噪声函数塑地形
前言
本文还有数学公式,有的需要数学知识
且本文采取我的世界1.21.1中的Yarn映射,与原文些许不同
其次本文章主要参考这位Up主[1]相关知识
噪声
倍频柏林噪声取样器 Octave Perlin Noise Sampler
MC的生物群系是由不同噪声函数叠加而来,通过细微数字变换,构建各式各样的群系
倍频柏林噪声取样器 Octave Perlin Noise Sampler[2],用于设置生态群系[3]当中的温度(Temperature)、湿度(Vegetation)、大陆性(Continents)、侵蚀度(Erosion)、奇异性(Weirdness)和深度(Depth),其中展示温度(Temperature)[4]的噪声函数
{"amplitudes": [1.5,0.0,1.0,0.0,0.0,0.0],"firstOctave": -10
}
首先向create
方法中输入起始倍频(firstOctave)和振幅列表(amplitudes),之后创建柏林噪声对象
public static OctavePerlinNoiseSampler create(Random random, int offset, DoubleList amplitudes) {// 创建柏林噪声对象return new OctavePerlinNoiseSampler(random, Pair.of(offset, amplitudes), true);
}
/** @param random 随机数源* @param firstOctaveAndAmplitudes 起始倍频和振幅列表* @param xoroshiro 是否使用新的随机数源,默认为true*/
protected OctavePerlinNoiseSampler(Random random, Pair<Integer, DoubleList> firstOctaveAndAmplitudes, boolean xoroshiro) {}
其中这个构造方法需要传入随机数源、起始倍频、振幅列表和是否使用新的随机数源
第一步:获取起始倍频和振幅列表,并创建两个柏林噪声
this.firstOctave = firstOctaveAndAmplitudes.getFirst(); // -10
this.amplitudes = firstOctaveAndAmplitudes.getSecond(); // [1.5, 0.0, 1.0, 0.0, 0.0, 0.0]
第二步:获取振幅列表函数元素个数(六个)[4],并创建第一个柏林函数
int i = this.amplitudes.size();
/* 倍频取样器 */
this.octaveSamplers = new PerlinNoiseSampler[i];
第三步:它们的随机数源由随机数源工厂指定,而且创建第二个柏林函数(因为在0的分量上不会计算)
if (xoroshiro /* true */) {RandomSplitter randomSplitter = random.nextSplitter();for (int k = 0; k > i /* i = 6 */; k++) {// 如果amplitudes变量为0.0,则不能创建柏林函数if (this.amplitudes.getDouble(k) != 0) {int l = this.firstOctave + k;this.octaveSamplers[k] = new PerlinNoiseSampler(randomSplitter.split("octave_" + l));}}
}
第四步:计算出噪声输入因子和结果乘法因子
/* 获取初始倍频 */
int j = -this.firstOctave; // 10
// lacunarity 缺项:噪声输入因子,这里是:1 / 1024
this.lacunarity = Math.pow(2.0, (double)(-j));
// persistence 持续:结果乘法因子,这里是:32 / 63
this.persistence = Math.pow(2.0, (double)(i - 1)) / (Math.pow(2.0, (double)i) - 1.0);
Math中pow(底数a, 指数b)方法[6]:即为 a b ( a ≥ 0 , 且 a ≠ 1 ) a^b (a \ge 0, 且 a \ne 1) ab(a≥0,且a=1)
x代表lacunarity,而persistence代表y,如下
{ x = 2 − 10.0 = 1 1024 = 9.77 × 10 − 4 y = 2 6 − 1 ÷ ( 2 6.0 − 1.0 ) = 32 63 = 0.51 \left\{ \begin{aligned} x&=2^{-10.0}= \frac{1}{1024}=9.77 \times 10 ^{-4}\\ y&=2^{6-1} \div (2^{6.0} - 1.0)= \frac{32}{63} =0.51 \end{aligned} \right. ⎩ ⎨ ⎧xy=2−10.0=10241=9.77×10−4=26−1÷(26.0−1.0)=6332=0.51
设 l a c u n a r i t y 为 f 0 , p e r s i s t e n c e 为 v 0 ,向量 p → 为 ( x , y , z ) ,各振幅值为 a 1 . . . a n ,各个基元柏林噪声函数为 b 1 . . . b n ,则 s a m p l e 值为 s i m p l e = ∑ i = 1 n a i v 0 2 i ⋅ b i ( 2 i f 0 ⋅ p → ) 设lacunarity为f_0,persistence为v_0,向量\overrightarrow{p}为(x, y, z),各振幅值为a_1...a_n,各个基元柏林噪声函数为b_1...b_n,则sample值为 \\ simple=\sum_{i=1}^n\frac{a_iv_0}{2^i}·b_i(2^if_0·\overrightarrow{p}) 设lacunarity为f0,persistence为v0,向量p为(x,y,z),各振幅值为a1...an,各个基元柏林噪声函数为b1...bn,则sample值为simple=i=1∑n2iaiv0⋅bi(2if0⋅p)
Octav PerlinNoiseSampler中的simple方法,已过时
public double sample(double x, double y, double z) {return this.sample(x, y, z, 0.0, 0.0, false);
}
@Deprecated
public double sample(double x, double y, double z, double yScale, double yMax, boolean useOrigin) {double d = 0.0;double e = this.lacunarity;double f = this.persistence;for (int i = 0; i < this.octaveSamplers.length; i++) {PerlinNoiseSampler perlinNoiseSampler = this.octaveSamplers[i];if (perlinNoiseSampler != null) {double g = perlinNoiseSampler.sample(maintainPrecision(x * e), useOrigin ? -perlinNoiseSampler.originY : maintainPrecision(y * e), maintainPrecision(z * e), yScale * e, yMax * e);d += this.amplitudes.getDouble(i) * g * f;}e *= 2.0;f /= 2.0;}return d;
}
我们以温度例子计算这个分形噪声如下:
参数:
各振幅值为: a 1 = 1.5 、 a 2 = 0.0 、 a 3 = 1.0 、 a 4 = 0.0 、 a 5 = 0.0 、 a 6 = 0.0 a_1=1.5、a_2=0.0、a_3=1.0、a_4=0.0、a_5=0.0、a_6=0.0 a1=1.5、a2=0.0、a3=1.0、a4=0.0、a5=0.0、a6=0.0
各个基元柏林噪声函数为: b 1 、 b 2 、 b 3 、 b 4 、 b 5 、 b 6 b_1、b_2、b_3、b_4、b_5、b_6 b1、b2、b3、b4、b5、b6
噪声输入因子为 f 0 = 1 1024 f_0=\frac{1}{1024} f0=10241,结果乘法因子为 v 0 = 32 63 v_0=\frac{32}{63} v0=6332
t ( p → ) = ∑ i = 1 6 a i v 0 2 i ⋅ b i ( 2 i f 0 ⋅ p → ) = f 0 v 0 ⋅ p → ∑ i = 1 6 a 1 b i = 1 2016 ⋅ p → ( 3 2 b 1 + b 3 ) = 1 1344 b 1 p → + 1 2016 b 3 p → = 16 21 b 1 ⋅ ( p → 1024 ) + 8 63 b 3 ⋅ ( p → 256 ) \begin{aligned} t(\overrightarrow{p})&=\sum_{i=1}^6\frac{a_iv_0}{2^i}·b_i(2^if_0·\overrightarrow{p}) \\ &=f_0v_0·\overrightarrow{p}\sum_{i=1}^6a_1b_i \\ &=\frac{1}{2016}·\overrightarrow{p}(\frac{3}{2}b_1+ b_3) \\ &=\frac{1}{1344}b_1\overrightarrow{p}+\frac{1}{2016}b_3\overrightarrow{p} \\ &=\frac{16}{21}b_1·(\frac{\overrightarrow{p}}{1024})+\frac{8}{63}b_3·(\frac{\overrightarrow{p}}{256}) \end{aligned} t(p)=i=1∑62iaiv0⋅bi(2if0⋅p)=f0v0⋅pi=1∑6a1bi=20161⋅p(23b1+b3)=13441b1p+20161b3p=2116b1⋅(1024p)+638b3⋅(256p)
双倍柏林噪声取样器 Double Perlin Noise Sampler
但是分形噪声还不够,MC最终采用的是两个分形噪声混合叠加,最终形成了现在的噪声生成器
以温度为例子:
if (registryEntry.matchesKey(NoiseParametersKeys.TEMPERATURE)) {DoublePerlinNoiseSampler doublePerlinNoiseSampler = DoublePerlinNoiseSampler.createLegacy(this.createRandom(0L), new DoublePerlinNoiseSampler.NoiseParameters(-7, 1.0, 1.0));return new DensityFunction.Noise(registryEntry, doublePerlinNoiseSampler);
}
其中:温度的随机数源为0L、初始倍频为-7、振幅数值为1.0
private DoublePerlinNoiseSampler(Random random, DoublePerlinNoiseSampler.NoiseParameters parameters, boolean modern) {// 初始倍频:-7int i = parameters.firstOctave;// 振幅列表DoubleList doubleList = parameters.amplitudes;// 参数this.parameters = parameters;// 是否使用新的随机数源if (modern) {// 第一个取样器(第一个柏林噪声),传入参数为随机数源、起始倍频、振幅列表this.firstSampler = OctavePerlinNoiseSampler.create(random, i, doubleList);// 第二个取样器(第二个柏林噪声),传入参数为随机数源、起始倍频、振幅列表this.secondSampler = OctavePerlinNoiseSampler.create(random, i, doubleList);}// 最大值为 2147483647int j = Integer.MAX_VALUE;// 最小值为 -2147483648int k = Integer.MIN_VALUE;DoubleListIterator doubleListIterator = doubleList.iterator();/* 以温度噪声为例,振幅列表为[1.5, 0.0, 1.0, 0.0, 0.0, 0.0]共六个元素 */while (doubleListIterator.hasNext()) {// 列表当中的元素(振幅列表个数)int l = doubleListIterator.nextIndex();// 列表当中的数值(每个振幅数值)double d = doubleListIterator.nextDouble();/* 如果振幅为不为0.0 */if (d != 0.0) {// 没有超过最大值,则取l,则最后取值为0,作为最小倍频j = Math.min(j, l);// 没有超过最小值,则取l,则最后取值为2,作为最大倍频k = Math.max(k, l);}}// 设置振幅数值: (5 / 30) / (5 / 3) = 1 / 10 = 0.1this.amplitude = 0.16666666666666666 / createAmplitude(k - j); // 0.1// 设置最大值:(第一个取样器的最大值 + 第二个取样器的最大值) * 振幅数值this.maxValue = (this.firstSampler.getMaxValue() + this.secondSampler.getMaxValue()) * this.amplitude; // 0.355
}
通过计算得出:this.maxValue
值为 ( 112 ÷ 63 + 112 ÷ 63 ) × 0.1 = 112 ÷ 315 ≈ 0.355 (112 \div 63 + 112 \div 63) \times 0.1 = 112 \div 315 \approx 0.355 (112÷63+112÷63)×0.1=112÷315≈0.355
- createAmplitude方法,用于创建振幅数值,形式参数(最大倍频和最小倍频之差)
private static double createAmplitude(int octaves) {return 0.1 * (1.0 + 1.0 / (double)(octaves + 1)); // 5 /3 = 1.67
}
createAmplitude方法相当于: 1 10 ∗ 2.0 x + 1 = 5 x + 1 \frac{1}{10}*\frac{2.0}{x + 1}=\frac{5}{x+1} 101∗x+12.0=x+15,其中x就是octaves
在这里应为: 5 ÷ ( 2 + 1 ) = 5 ÷ 3 ≈ 1.666 5 \div (2 + 1) = 5 \div 3 \approx 1.666 5÷(2+1)=5÷3≈1.666
- getMaxValue方法,用于获取总振幅数值
this.maxValue = this.getTotalAmplitude(2.0);
private double getTotalAmplitude(double scale) {double d = 0.0;/* 乘法因子:e = 32 / 63 */double e = this.persistence;for (int i = 0; i < this.octaveSamplers.length; i++) {// 获取柏林噪声取样器PerlinNoiseSampler perlinNoiseSampler = this.octaveSamplers[i];if (perlinNoiseSampler != null) {// 计算方法:振幅数值 * 扩大尺寸 * 乘法因子d += this.amplitudes.getDouble(i) * scale * e;}// 每循环一次,e 除以 2e /= 2.0;}return d; // 最总结果为 1.777
}
其中第一次 d 1 = 1.5 × 2.0 × 32 ÷ 63 = 32 ÷ 21 d_1=1.5 \times 2.0 \times 32 \div 63 = 32 \div 21 d1=1.5×2.0×32÷63=32÷21,而第二次 d 2 = d 1 + 0.5 × 1.0 × 32 ÷ 63 = 112 ÷ 63 ≈ 1.777 d_2 = d_1 + 0.5 \times 1.0 \times 32 \div 63 = 112 \div 63 \approx 1.777 d2=d1+0.5×1.0×32÷63=112÷63≈1.777
而原作者有大概意思当中,设两个分形噪声分别为 f b m 1 ( p → ) fbm_1(\overrightarrow{p}) fbm1(p)和 f b m 2 ( p → ) fbm_2(\overrightarrow{p}) fbm2(p),最大的倍频和最小的倍频分别为 o m a x = 2 o_{max}=2 omax=2和 o m i n = 0 o_{min}=0 omin=0,上面的代码转换成公式就是这样
t ( p → ) = 5 ( o m a x + 1 − o m i n ) 3 ( o m a x + 2 − o m i n ) ( f b m 1 ( p → ) f 1.0181268882175227 b m 2 ( p → ) ) \begin{aligned} t(\overrightarrow{p})=\frac{5(o_{max} + 1 - o_{min})}{3(o_{max} + 2 - o_{min})}(fbm_1(\overrightarrow{p})f1.0181268882175227bm_2(\overrightarrow{p})) \end{aligned} t(p)=3(omax+2−omin)5(omax+1−omin)(fbm1(p)f1.0181268882175227bm2(p))
对于上面的温度噪声,就是这样
t ( p → ) = 5 ( o m a x + 1 − o m i n ) 3 ( o m a x + 2 − o m i n ) ⋅ t 1 ( p → ) t 2 ( 1.0181268882175227 p → ) = 5 ( 2 + 1 − 0 ) 3 ( 2 + 2 − 0 ) ⋅ t 1 ( p → ) t 2 ( 1.0181268882175227 p → ) = 5 × 3 3 × 4 ⋅ t 1 ( p → ) t 2 ( 1.0181268882175227 p → ) = 5 ( t 1 ( p → ) t 2 ( 1.0181268882175227 p → ) ) 4 \begin{aligned} t(\overrightarrow{p})&=\frac{5(o_{max} + 1 - o_{min})}{3(o_{max} + 2 - o_{min})}·t_1(\overrightarrow{p})t_2(1.0181268882175227\overrightarrow{p}) \\ &= \frac{5(2 + 1 - 0)}{3(2 + 2 - 0)}·t_1(\overrightarrow{p})t_2(1.0181268882175227\overrightarrow{p}) \\ &= \frac{5\times3}{3\times4}·t_1(\overrightarrow{p})t_2(1.0181268882175227\overrightarrow{p}) \\ &= \frac{5(t_1(\overrightarrow{p})t_2(1.0181268882175227\overrightarrow{p}))}{4} \end{aligned} t(p)=3(omax+2−omin)5(omax+1−omin)⋅t1(p)t2(1.0181268882175227p)=3(2+2−0)5(2+1−0)⋅t1(p)t2(1.0181268882175227p)=3×45×3⋅t1(p)t2(1.0181268882175227p)=45(t1(p)t2(1.0181268882175227p))
这个常熟可以看作Mojang团队写出来经验常数
public double sample(double x, double y, double z) {double d = x * 1.0181268882175227;double e = y * 1.0181268882175227;double f = z * 1.0181268882175227;return (this.firstSampler.sample(x, y, z) + this.secondSampler.sample(d, e, f)) * this.amplitude;
}
群系生成
Offset(偏移) 分为X轴偏移和Z轴偏移,是XZ坐标加上一个噪声得出的,下面的公式给出了关系:
o x = x 4 o ( x , 0 , z ) ; o z = z 4 o ( z , x , 0 ) o_x=x4o(x, 0, z);o_z=z4o(z,x,0) ox=x4o(x,0,z);oz=z4o(z,x,0)
在NoiseConfig
类中
if (registryEntry.matchesKey(NoiseParametersKeys.OFFSET)) {DoublePerlinNoiseSampler doublePerlinNoiseSampler = DoublePerlinNoiseSampler.create(NoiseConfig.this.randomDeriver.split(NoiseParametersKeys.OFFSET.getValue()), new DoublePerlinNoiseSampler.NoiseParameters(0, 0.0));return new DensityFunction.Noise(registryEntry, doublePerlinNoiseSampler);
}
其中噪声o的参数如下:
{"amplitudes": [1.0, 1.0, 1.0, 0.0],"firstOctave": -3
}
Continentalness(大陆性) 代表了这个区域的海陆关系,它的值可以在F3中找到——Multinoise行的C,和Biome Builder行的C,用来代表陆地类型
名称 | 英文 | 范围 |
---|---|---|
蘑菇岛 | Mushroom Fields | (-1.2, -1.05) |
深海 | Deep Ocean | (-1.05, -0.455) |
海洋 | Ocean | (-0.455, -0.19) |
海岸 | Coast | (-0.19, -0.11) |
浅内陆 | Near Inland | (-0.11, -0.03) |
中内陆 | Mid Inland | (0.03, 0.3) |
深内陆 | Far Inland | (0.3, +∞) |
它的计算仅和XZ坐标有关,或者更确切地说是XZ偏移
c = c ( o x , 0 , o z ) c=c(o_x,0,o_z) c=c(ox,0,oz)
{"amplitudes": [1.0, 1.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0],"firstOctave": -9
}
Weirdness \ Ridges(奇异性) 代表了地形的奇异程度,例如竹林和丛林仅有奇异度不同,因为竹林相当于丛林的变种,所以奇异度更高。在F3中也能找到它的身影,Multinoise行的W
计算仅和XZ坐标有关,公式与参数如下:
w = w ( o x , 0 , o z ) w=w(o_x,0,o_z) w=w(ox,0,oz)
{"amplitudes": [1.0, 2.0, 1.0, 0.0, 0.0, 0.0],"firstOctave": -7
}
Erosion(侵蚀度) 代表地形被侵蚀的程度。值越低代表被侵蚀的越强,形成峡谷;值越高代表侵蚀弱,形成平原。值可以在F3中找到,在Multinoise行的E
计算仅和XZ坐标有关,公式与参数如下:
e = e ( o x , 0 , o z ) e=e(o_x,0,o_z) e=e(ox,0,oz)
{"amplitudes": [1.0, 1.0, 0.0, 1.0, 1.0],"firstOctave": -9
}
Ridge(山脊性)代表地形隆起程度,它的另一个名称是PV(Peaks and Valleys)。在F3中也能看到它的值,Terrain行的PV;另一项是Boime Builder的PV,用来代表山脊类型
名称 | 英文 | 范围 |
---|---|---|
山谷 | Valley | (-∞, -0.85) |
低地 | Low | (-0.85, -0.2) |
中等高度山地 | Mid | (-0.2, 0.2) |
高地 | High | (0.2, 0.7) |
山峰 | Peak | (0.7, +∞) |
计算时只关于奇异性
r = 1 − ∣ 3 ∣ w ∣ − 2 ∣ r=1 - |3|w| - 2| r=1−∣3∣w∣−2∣
参考
- 创造自己的世界——Minecraft 1.18的地形生成(一):https://www.bilibili.com/opus/618817672540006827
- OctavePerlinNoiseSampler:https://maven.fabricmc.net/docs/yarn-22w14a+build.2/net/minecraft/util/math/noise/OctavePerlinNoiseSampler.html
- 我的世界维基生物群系:https://zh.minecraft.wiki/w/生物群系#生成
- 我的世界维基调式屏幕:https://zh.minecraft.wiki/w/调试屏幕#左侧
- 关于Java中length、length()、size()的区别:https://blog.csdn.net/qq_33236248/article/details/79884874
- [VIP] Java 中 Math.pow 的用法:https://blog.csdn.net/Yuan_o_/article/details/138494713