Skip to main content

steel_worldgen/noise/
blended_noise.rs

1//! Float-based blended terrain noise from vanilla 26.3.
2
3use crate::noise::ImprovedNoise;
4use crate::random::RandomSource;
5use std::array;
6use std::simd::cmp::SimdPartialEq;
7use std::simd::num::SimdFloat;
8use std::simd::{Select, Simd};
9use steel_math::clamped_lerp;
10
11const BASE_SCALE: f64 = 684.412;
12
13#[derive(Debug, Clone)]
14struct SmearedLayer {
15    noise: ImprovedNoise,
16    frequency: f64,
17    amplitude: f32,
18    fudge_y_scale: f64,
19}
20
21#[derive(Debug, Clone)]
22struct SmearedStack {
23    layers: Box<[SmearedLayer]>,
24}
25
26impl SmearedStack {
27    fn create(
28        random: &mut RandomSource,
29        first_octave: i32,
30        smear_y_scale: f64,
31        mut value_factor: f64,
32    ) -> Self {
33        let octaves = (-first_octave + 1) as usize;
34        value_factor /= 2.0_f64.powi(octaves as i32) - 1.0;
35        let mut frequency = 1.0;
36        let mut layers = Vec::with_capacity(octaves);
37
38        // `BlendedNoise.createFbm` initializes the highest octave first.
39        for _ in (0..octaves).rev() {
40            layers.push(SmearedLayer {
41                noise: ImprovedNoise::new(random),
42                frequency,
43                amplitude: value_factor as f32,
44                fudge_y_scale: smear_y_scale * frequency,
45            });
46            frequency *= 0.5;
47            value_factor *= 2.0;
48        }
49
50        Self {
51            layers: layers.into_boxed_slice(),
52        }
53    }
54
55    #[inline]
56    fn sample(&self, x: f64, y: f64, z: f64) -> f32 {
57        let mut value = 0.0_f32;
58        for layer in &self.layers {
59            value += layer.amplitude
60                * layer.noise.smeared_noise(
61                    x * layer.frequency,
62                    y * layer.frequency,
63                    z * layer.frequency,
64                    layer.fudge_y_scale,
65                );
66        }
67        value
68    }
69
70    #[inline]
71    fn sample_y_simd<const N: usize>(&self, x: f64, ys: Simd<f64, N>, z: f64) -> Simd<f32, N> {
72        let mut value = Simd::splat(0.0_f32);
73        for layer in &self.layers {
74            value += Simd::splat(layer.amplitude)
75                * layer.noise.smeared_noise_y_simd(
76                    x * layer.frequency,
77                    ys * Simd::splat(layer.frequency),
78                    z * layer.frequency,
79                    layer.fudge_y_scale,
80                );
81        }
82        value
83    }
84}
85
86/// Runtime counterpart of vanilla's 26.3 `BlendedNoise` sampler.
87///
88/// The old terrain sampler used double octave sums. Vanilla now builds three
89/// `NoiseStack`s and carries float values through each layer and the final lerp.
90#[derive(Debug, Clone)]
91pub struct BlendedNoise {
92    min_limit_noise: SmearedStack,
93    max_limit_noise: SmearedStack,
94    main_noise: SmearedStack,
95    xz_multiplier: f64,
96    y_multiplier: f64,
97    main_xz_scale: f64,
98    main_y_scale: f64,
99}
100
101impl BlendedNoise {
102    /// Creates the three seeded `NoiseStack`s used for terrain generation.
103    #[must_use]
104    pub fn new(
105        random: &mut RandomSource,
106        xz_scale: f64,
107        y_scale: f64,
108        xz_factor: f64,
109        y_factor: f64,
110        smear_scale_multiplier: f64,
111    ) -> Self {
112        let xz_multiplier = BASE_SCALE * xz_scale;
113        let y_multiplier = BASE_SCALE * y_scale;
114        let limit_smear_scale_y = y_multiplier * smear_scale_multiplier;
115        let main_smear_scale_y = limit_smear_scale_y / y_factor;
116
117        Self {
118            min_limit_noise: SmearedStack::create(
119                random,
120                -15,
121                limit_smear_scale_y,
122                f64::from(0.999_984_74_f32),
123            ),
124            max_limit_noise: SmearedStack::create(
125                random,
126                -15,
127                limit_smear_scale_y,
128                f64::from(0.999_984_74_f32),
129            ),
130            main_noise: SmearedStack::create(random, -7, main_smear_scale_y, 12.75),
131            xz_multiplier,
132            y_multiplier,
133            main_xz_scale: xz_multiplier / xz_factor,
134            main_y_scale: y_multiplier / y_factor,
135        }
136    }
137
138    #[inline]
139    /// Samples the blended terrain density at a block coordinate.
140    #[must_use]
141    #[expect(
142        clippy::float_cmp,
143        reason = "Vanilla selects exact lerp endpoints before interpolation"
144    )]
145    pub fn compute(&self, block_x: f64, block_y: f64, block_z: f64) -> f32 {
146        let limit_x = block_x * self.xz_multiplier;
147        let limit_y = block_y * self.y_multiplier;
148        let limit_z = block_z * self.xz_multiplier;
149        let main = self.main_noise.sample(
150            block_x * self.main_xz_scale,
151            block_y * self.main_y_scale,
152            block_z * self.main_xz_scale,
153        );
154        let alpha = (main + 0.5_f32).clamp(0.0, 1.0);
155        if alpha == 0.0 {
156            return self.min_limit_noise.sample(limit_x, limit_y, limit_z);
157        }
158        if alpha == 1.0 {
159            return self.max_limit_noise.sample(limit_x, limit_y, limit_z);
160        }
161        let minimum = self.min_limit_noise.sample(limit_x, limit_y, limit_z);
162        let maximum = self.max_limit_noise.sample(limit_x, limit_y, limit_z);
163        clamped_lerp(minimum, maximum, alpha)
164    }
165
166    #[inline]
167    fn compute_y_simd<const N: usize>(
168        &self,
169        block_x: f64,
170        block_ys: Simd<f64, N>,
171        block_z: f64,
172    ) -> Simd<f32, N> {
173        let limit_x = block_x * self.xz_multiplier;
174        let limit_ys = block_ys * Simd::splat(self.y_multiplier);
175        let limit_z = block_z * self.xz_multiplier;
176        let main = self.main_noise.sample_y_simd(
177            block_x * self.main_xz_scale,
178            block_ys * Simd::splat(self.main_y_scale),
179            block_z * self.main_xz_scale,
180        );
181        let alpha = (main + Simd::splat(0.5_f32))
182            .simd_max(Simd::splat(0.0))
183            .simd_min(Simd::splat(1.0));
184        if alpha.simd_eq(Simd::splat(0.0)).all() {
185            return self
186                .min_limit_noise
187                .sample_y_simd(limit_x, limit_ys, limit_z);
188        }
189        if alpha.simd_eq(Simd::splat(1.0)).all() {
190            return self
191                .max_limit_noise
192                .sample_y_simd(limit_x, limit_ys, limit_z);
193        }
194        let minimum = self
195            .min_limit_noise
196            .sample_y_simd(limit_x, limit_ys, limit_z);
197        let maximum = self
198            .max_limit_noise
199            .sample_y_simd(limit_x, limit_ys, limit_z);
200        let interpolated = minimum + alpha * (maximum - minimum);
201        let result = alpha
202            .simd_eq(Simd::splat(0.0))
203            .select(minimum, interpolated);
204        alpha.simd_eq(Simd::splat(1.0)).select(maximum, result)
205    }
206
207    /// Samples one X/Z column into the supplied float buffer.
208    pub fn compute_column(&self, block_x: i32, block_ys: &[i32], block_z: i32, out: &mut [f32]) {
209        let len = block_ys.len().min(out.len());
210        let mut index = 0;
211        // Four-lane batches are faster on baseline targets; retain eight for AVX-512.
212        #[cfg(target_feature = "avx512f")]
213        while index + 8 <= len {
214            let ys: Simd<f64, 8> =
215                Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
216            out[index..index + 8].copy_from_slice(
217                &self
218                    .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
219                    .to_array(),
220            );
221            index += 8;
222        }
223        while index + 4 <= len {
224            let ys: Simd<f64, 4> =
225                Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
226            out[index..index + 4].copy_from_slice(
227                &self
228                    .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
229                    .to_array(),
230            );
231            index += 4;
232        }
233        if index + 2 <= len {
234            let ys: Simd<f64, 2> =
235                Simd::from_array(array::from_fn(|lane| f64::from(block_ys[index + lane])));
236            out[index..index + 2].copy_from_slice(
237                &self
238                    .compute_y_simd(f64::from(block_x), ys, f64::from(block_z))
239                    .to_array(),
240            );
241            index += 2;
242        }
243        for (&block_y, value) in block_ys[index..len].iter().zip(&mut out[index..len]) {
244            *value = self.compute(f64::from(block_x), f64::from(block_y), f64::from(block_z));
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::BlendedNoise;
252    use crate::random::{RandomSource, legacy_random::LegacyRandom};
253
254    #[test]
255    fn vanilla_lerp_endpoint_and_main_coordinate_rounding() {
256        let mut random = RandomSource::Legacy(LegacyRandom::from_seed(0));
257        let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
258
259        for (x, y, z, expected) in [
260            (0.0, -56.0, -10_000.0, 0.410_753_88_f32),
261            (20_000_068.0, 296.0, -19_999_796.0, 0.013_388_243_f32),
262        ] {
263            assert_eq!(noise.compute(x, y, z).to_bits(), expected.to_bits());
264        }
265    }
266
267    #[test]
268    fn compute_column_matches_scalar_lanes() {
269        let mut random = RandomSource::Legacy(LegacyRandom::from_seed(0));
270        let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
271        let ys = [
272            -64, -56, -48, -40, -32, -24, -16, -8, 0, 8, 16, 24, 32, 40, 48, 56, 64,
273        ];
274        let mut column = [0.0_f32; 17];
275        // Exercise every 8/4/2/scalar tail combination and short output buffers.
276        for len in 0..=ys.len() {
277            noise.compute_column(20_000_068, &ys, -19_999_796, &mut column[..len]);
278            for (&y, &value) in ys.iter().zip(&column[..len]) {
279                assert_eq!(
280                    value.to_bits(),
281                    noise
282                        .compute(20_000_068.0, f64::from(y), -19_999_796.0)
283                        .to_bits(),
284                    "length={len}, Y={y}"
285                );
286            }
287        }
288    }
289
290    #[test]
291    fn simd_endpoint_shortcuts_preserve_each_lane() {
292        use std::array;
293        use std::simd::{Simd, num::SimdFloat};
294
295        let mut saw_minimum = false;
296        let mut saw_maximum = false;
297        let mut saw_mixed = false;
298        for seed in [0, 42, 13579] {
299            let mut random = RandomSource::Legacy(LegacyRandom::from_seed(seed));
300            let noise = BlendedNoise::new(&mut random, 0.25, 0.125, 80.0, 160.0, 8.0);
301            for x in [-20_000_068.0, -128.0, 0.0, 128.0, 20_000_068.0] {
302                for z in [-19_999_796.0, -256.0, 0.0, 256.0, 19_999_796.0] {
303                    for base_y in (-64..256).step_by(64) {
304                        let ys = Simd::from_array(array::from_fn::<_, 8, _>(|lane| {
305                            f64::from(base_y) + lane as f64 * 8.0
306                        }));
307                        let main = noise.main_noise.sample_y_simd(
308                            x * noise.main_xz_scale,
309                            ys * Simd::splat(noise.main_y_scale),
310                            z * noise.main_xz_scale,
311                        );
312                        let all_minimum = main.reduce_max() <= -0.5;
313                        let all_maximum = main.reduce_min() >= 0.5;
314                        saw_minimum |= all_minimum;
315                        saw_maximum |= all_maximum;
316                        saw_mixed |= !all_minimum && !all_maximum;
317                        let actual = noise.compute_y_simd(x, ys, z);
318                        for (lane, y) in ys.to_array().into_iter().enumerate() {
319                            assert_eq!(
320                                actual[lane].to_bits(),
321                                noise.compute(x, y, z).to_bits(),
322                                "seed {seed}, ({x}, {y}, {z})"
323                            );
324                        }
325                    }
326                }
327            }
328        }
329        assert!(saw_minimum && saw_maximum && saw_mixed);
330    }
331}