Skip to main content

steel_worldgen/noise/
perlin_simplex_noise.rs

1//! Multi-octave simplex noise matching vanilla's `PerlinSimplexNoise`.
2//!
3//! Used for biome temperature calculations. Not to be confused with
4//! `PerlinNoise` which uses improved (gradient) noise octaves.
5
6use std::collections::BTreeSet;
7
8use crate::noise::SimplexNoise;
9use crate::random::legacy_random::LegacyRandom;
10use crate::random::{Random, RandomSource};
11
12/// Multi-octave simplex noise generator.
13///
14/// Matches vanilla's `net.minecraft.world.level.levelgen.synth.PerlinSimplexNoise`.
15/// Created from a set of octave levels; each octave uses a separate `SimplexNoise`.
16///
17/// Array layout matches vanilla: index 0 = highest frequency octave (largest
18/// octave number), increasing index = decreasing frequency.
19pub struct PerlinSimplexNoise {
20    noise_levels: Vec<Option<SimplexNoise>>,
21    highest_freq_input_factor: f64,
22    highest_freq_value_factor: f64,
23}
24
25impl PerlinSimplexNoise {
26    /// Create from a random source and a list of octave levels.
27    ///
28    /// Matches vanilla's constructor exactly: the zero octave is created first,
29    /// then negative octaves (lower frequency) consume the same random, and
30    /// positive octaves (higher frequency) use a derived random from the zero
31    /// octave's self-evaluation.
32    ///
33    /// # Panics
34    ///
35    /// Panics if `octaves` is empty.
36    #[must_use]
37    pub fn new(random: &mut RandomSource, octaves: &[i32]) -> Self {
38        let octave_set: BTreeSet<i32> = octaves.iter().copied().collect();
39        assert!(!octave_set.is_empty(), "Need some octaves");
40
41        // SAFETY: assert above guarantees non-empty
42        let first_octave = *octave_set.first().expect("non-empty octave set");
43        let last_octave = *octave_set.last().expect("non-empty octave set");
44        let high_freq_octaves = last_octave;
45        let total = (last_octave - first_octave + 1) as usize;
46
47        // Zero octave is always created first (consuming random state),
48        // matching vanilla's construction order
49        let zero_octave = SimplexNoise::new(random);
50        let zero_index = high_freq_octaves; // as i32, can be negative
51
52        let mut noise_levels: Vec<Option<SimplexNoise>> = vec![None; total];
53
54        // Compute seed for positive octaves before moving zero_octave
55        let hf_seed = if high_freq_octaves > 0 {
56            Some(
57                (zero_octave.get_value_3d(zero_octave.xo, zero_octave.yo, zero_octave.zo)
58                    * 9.223_372_036_854_776e18) as i64,
59            )
60        } else {
61            None
62        };
63
64        // Place zero octave if octave 0 is in the set and index is valid
65        if zero_index >= 0 && (zero_index as usize) < total && octave_set.contains(&0) {
66            noise_levels[zero_index as usize] = Some(zero_octave);
67        }
68
69        // Lower-frequency octaves (negative octave numbers, array indices > zero_index)
70        let start = (zero_index + 1).max(0) as usize;
71        for (i, level) in noise_levels.iter_mut().enumerate().skip(start) {
72            let octave_level = zero_index - i as i32;
73            if octave_set.contains(&octave_level) {
74                *level = Some(SimplexNoise::new(random));
75            } else {
76                random.consume_count(262);
77            }
78        }
79
80        // Higher-frequency octaves (positive octave numbers, indices < zero_index)
81        // Uses a separate random derived from the zero octave
82        if let Some(seed) = hf_seed {
83            let mut hf_random = RandomSource::Legacy(LegacyRandom::from_seed(seed as u64));
84
85            for ix in (0..zero_index as usize).rev() {
86                let octave_level = zero_index - ix as i32;
87                if octave_set.contains(&octave_level) {
88                    noise_levels[ix] = Some(SimplexNoise::new(&mut hf_random));
89                } else {
90                    hf_random.consume_count(262);
91                }
92            }
93        }
94
95        Self {
96            noise_levels,
97            highest_freq_input_factor: 2.0f64.powi(last_octave),
98            highest_freq_value_factor: 1.0 / (2.0f64.powi(total as i32) - 1.0),
99        }
100    }
101
102    /// Sample the 2D noise at the given coordinates.
103    ///
104    /// Matches vanilla's `getValue(x, z, false)` path (no offset applied).
105    #[must_use]
106    pub fn get_value(&self, x: f64, z: f64) -> f64 {
107        let mut sum = 0.0;
108        let mut factor = self.highest_freq_input_factor;
109        let mut amplitude = self.highest_freq_value_factor;
110
111        for noise in &self.noise_levels {
112            if let Some(n) = noise {
113                sum += n.get_value_2d(x * factor, z * factor) * amplitude;
114            }
115            factor /= 2.0;
116            amplitude *= 2.0;
117        }
118
119        sum
120    }
121}