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