chunkgen 0.2.0

Minecraft world generation library
Documentation
use std::collections::BTreeSet;

use crate::util::noise::SimplexNoise;
use crate::util::rng::RandomSource;
use crate::util::rng::legacy_random::LegacyRandomSource;

const POSITIVE_OCTAVE_SEED_SCALE: f64 = 9.223_372_036_854_776e18;

pub struct PerlinSimplexNoise {
    noise_levels: Vec<Option<SimplexNoise>>,
    highest_freq_input_factor: f64,
    highest_freq_value_factor: f64,
}

impl PerlinSimplexNoise {
    pub fn new(random: &mut RandomSource, octaves: &[i32]) -> Self {
        let octave_set: BTreeSet<i32> = octaves.iter().copied().collect();
        assert!(!octave_set.is_empty(), "Need some octaves");

        let low_freq_octaves = -*octave_set.first().expect("non-empty octave set");
        let high_freq_octaves = *octave_set.last().expect("non-empty octave set");
        let octave_count = (low_freq_octaves + high_freq_octaves + 1) as usize;

        let zero_octave = SimplexNoise::new(random);
        let zero_octave_self_sample =
            zero_octave.get_value_3d(zero_octave.xo, zero_octave.yo, zero_octave.zo);
        let zero_octave_index = high_freq_octaves;

        let mut noise_levels: Vec<Option<SimplexNoise>> = vec![None; octave_count];
        if zero_octave_index >= 0
            && (zero_octave_index as usize) < octave_count
            && octave_set.contains(&0)
        {
            noise_levels[zero_octave_index as usize] = Some(zero_octave);
        }

        for i in (zero_octave_index + 1)..octave_count as i32 {
            if i >= 0 && octave_set.contains(&(zero_octave_index - i)) {
                noise_levels[i as usize] = Some(SimplexNoise::new(random));
            } else {
                random.consume_count(262);
            }
        }

        if high_freq_octaves > 0 {
            let positive_octave_seed =
                (zero_octave_self_sample * POSITIVE_OCTAVE_SEED_SCALE) as i64;
            let mut high_freq_random =
                RandomSource::Legacy(LegacyRandomSource::from_seed(positive_octave_seed as u64));

            for i in (0..zero_octave_index).rev() {
                if (i as usize) < octave_count && octave_set.contains(&(zero_octave_index - i)) {
                    noise_levels[i as usize] = Some(SimplexNoise::new(&mut high_freq_random));
                } else {
                    high_freq_random.consume_count(262);
                }
            }
        }

        Self {
            noise_levels,
            highest_freq_input_factor: 2.0f64.powi(high_freq_octaves),
            highest_freq_value_factor: 1.0 / (2.0f64.powi(octave_count as i32) - 1.0),
        }
    }

    pub fn get_value(&self, x: f64, z: f64) -> f64 {
        let mut value = 0.0;
        let mut factor = self.highest_freq_input_factor;
        let mut value_factor = self.highest_freq_value_factor;

        for noise_level in &self.noise_levels {
            if let Some(noise) = noise_level {
                value += noise.get_value_2d(x * factor, z * factor) * value_factor;
            }
            factor /= 2.0;
            value_factor *= 2.0;
        }

        value
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::util::rng::xoroshiro::XoroshiroRandomSource;

    fn rng(seed: u64) -> RandomSource {
        RandomSource::Xoroshiro(XoroshiroRandomSource::from_seed(seed))
    }

    #[test]
    fn spatial_variation() {
        let noise = PerlinSimplexNoise::new(&mut rng(42), &[-2, -1, 0]);
        let values: Vec<f64> = (0..10)
            .map(|i| noise.get_value(f64::from(i) * 10.0, f64::from(i) * 5.0))
            .collect();
        let min = values.iter().copied().fold(f64::INFINITY, f64::min);
        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
        assert!(max - min > 0.001);
    }

    #[test]
    fn deterministic_for_same_seed() {
        let n1 = PerlinSimplexNoise::new(&mut rng(7), &[-3, -2, -1, 0, 1]);
        let n2 = PerlinSimplexNoise::new(&mut rng(7), &[-3, -2, -1, 0, 1]);
        assert!((n1.get_value(12.5, -4.25) - n2.get_value(12.5, -4.25)).abs() < 1e-15);
    }
}