Skip to main content

steel_worldgen/noise/
perlin_noise.rs

1//! Octave-based Perlin noise implementation matching vanilla Minecraft's `PerlinNoise.java`
2//!
3//! This combines multiple `ImprovedNoise` instances at different frequencies (octaves)
4//! to create more natural-looking noise with detail at multiple scales.
5
6use std::ops;
7use std::simd::cmp::{SimdPartialEq, SimdPartialOrd};
8use std::simd::num::SimdFloat;
9use std::simd::{Mask, Simd, SimdCast, SimdElement, StdFloat, f64x4};
10
11use crate::noise::ImprovedNoise;
12use crate::random::{PositionalRandom, Random, RandomSource, RandomSplitter, name_hash::NameHash};
13use steel_math::{wrap, wrap_simd};
14
15/// Octave-based Perlin noise generator.
16///
17/// Combines multiple [`ImprovedNoise`] instances at different frequencies
18/// to create noise with detail at multiple scales.
19#[derive(Debug, Clone)]
20pub struct PerlinNoise {
21    /// Noise generators for each octave (None if amplitude is 0).
22    /// Kept for [`Self::get_octave_noise`] which is indexed by original octave order.
23    noise_levels: Vec<Option<ImprovedNoise>>,
24    /// Amplitude multipliers for each octave.
25    /// Kept for [`Self::max_broken_value`] which recomputes the edge value.
26    amplitudes: Vec<f64>,
27    /// Pre-computed per-octave data for the hot sampling loop. Only contains
28    /// entries with non-zero amplitude, in original (low → high frequency) order
29    /// so that floating-point accumulation matches the legacy iteration.
30    active_octaves: Vec<ActiveOctave>,
31    /// Factor applied to output values for the lowest frequency octave.
32    /// Used by [`Self::max_broken_value`] which recomputes the edge value.
33    lowest_freq_value_factor: f64,
34    /// Maximum possible output value
35    max_value: f64,
36}
37
38/// Pre-computed octave data for the hot sampling path.
39///
40/// Avoids per-iteration `*= 2.0` / `/= 2.0` factor reductions and the per-octave
41/// `Option` check in [`PerlinNoise::get_value_with_y_params`].
42#[derive(Debug, Clone)]
43struct ActiveOctave {
44    noise: ImprovedNoise,
45    /// `lowest_freq_input_factor * 2^i` for original octave index `i`.
46    input_factor: f64,
47    /// `amplitude * (lowest_freq_value_factor / 2^i)` — combines the per-octave
48    /// amplitude with the value factor reduction so the inner loop does one
49    /// multiply + one add per octave.
50    output_factor: f64,
51}
52
53impl PerlinNoise {
54    /// Create a new [`PerlinNoise`] from a positional random splitter (hash-based seeding).
55    ///
56    /// Each octave gets its seed from `splitter.with_hash_of("octave_{level}")`.
57    /// This is a convenience method; for vanilla-matching behavior within [`NormalNoise`],
58    /// use [`create_from_random`](Self::create_from_random) instead.
59    #[must_use]
60    pub fn create(splitter: &RandomSplitter, first_octave: i32, amplitudes: &[f64]) -> Self {
61        let octaves = amplitudes.len();
62        let zero_octave_index = (-first_octave) as usize;
63
64        let mut noise_levels = vec![None; octaves];
65
66        for i in 0..octaves {
67            if amplitudes[i] != 0.0 {
68                let octave = first_octave + i as i32;
69                let name = format!("octave_{octave}");
70                let mut octave_random = splitter.with_hash_of(&NameHash::new(&name));
71                noise_levels[i] = Some(ImprovedNoise::new(&mut octave_random));
72            }
73        }
74
75        Self::from_parts(noise_levels, amplitudes, zero_octave_index)
76    }
77
78    /// Create a new [`PerlinNoise`] from a mutable sequential random source.
79    ///
80    /// This matches vanilla's [`PerlinNoise`] constructor for [`XoroshiroRandomSource`]:
81    /// 1. Consume 262 values from the random (to advance state)
82    /// 2. Fork a new positional random from the current state
83    /// 3. Use hash-based seeding for each octave from the forked positional
84    ///
85    /// This is critical for [`NormalNoise`] where the first and second [`PerlinNoise`]
86    /// must get different seeds from the same sequential random source.
87    #[must_use]
88    pub fn create_from_random(
89        random: &mut RandomSource,
90        first_octave: i32,
91        amplitudes: &[f64],
92    ) -> Self {
93        let octaves = amplitudes.len();
94        let zero_octave_index = (-first_octave) as usize;
95
96        // Match vanilla's useNewInitialization=true path:
97        // `forkPositional()` consumes 2 longs from the random source
98        let splitter = random.next_positional();
99
100        let mut noise_levels = vec![None; octaves];
101
102        for i in 0..octaves {
103            if amplitudes[i] != 0.0 {
104                let octave = first_octave + i as i32;
105                let name = format!("octave_{octave}");
106                let mut octave_random = splitter.with_hash_of(&NameHash::new(&name));
107                noise_levels[i] = Some(ImprovedNoise::new(&mut octave_random));
108            }
109        }
110
111        Self::from_parts(noise_levels, amplitudes, zero_octave_index)
112    }
113
114    /// Create a [`PerlinNoise`] using the legacy nether biome initialization path.
115    ///
116    /// Unlike [`create_from_random`](Self::create_from_random) which uses positional/hash-based
117    /// seeding, this creates `ImprovedNoise` instances directly from a sequential random source.
118    /// Matches vanilla's `PerlinNoise(random, pair, useNewInitialization=false)`.
119    #[must_use]
120    pub fn create_legacy_for_nether(
121        random: &mut RandomSource,
122        first_octave: i32,
123        amplitudes: &[f64],
124    ) -> Self {
125        let octaves = amplitudes.len();
126        let zero_octave_index = (-first_octave) as usize;
127
128        let mut noise_levels = vec![None; octaves];
129
130        if zero_octave_index < octaves && amplitudes[zero_octave_index] != 0.0 {
131            noise_levels[zero_octave_index] = Some(ImprovedNoise::new(random));
132        } else {
133            random.consume_count(262);
134        }
135
136        for ix in (0..zero_octave_index).rev() {
137            if ix < octaves && amplitudes[ix] != 0.0 {
138                noise_levels[ix] = Some(ImprovedNoise::new(random));
139            } else {
140                random.consume_count(262);
141            }
142        }
143
144        Self::from_parts(noise_levels, amplitudes, zero_octave_index)
145    }
146
147    /// Build a [`PerlinNoise`] from pre-computed noise levels.
148    #[must_use]
149    fn from_parts(
150        noise_levels: Vec<Option<ImprovedNoise>>,
151        amplitudes: &[f64],
152        zero_octave_index: usize,
153    ) -> Self {
154        let octaves = amplitudes.len();
155
156        // Calculate frequency factors
157        // lowest_freq_input_factor = 2^(-zero_octave_index)
158        let lowest_freq_input_factor = 2.0_f64.powi(-(zero_octave_index as i32));
159
160        // lowest_freq_value_factor = 2^(octaves-1) / (2^octaves - 1)
161        let lowest_freq_value_factor =
162            2.0_f64.powi((octaves - 1) as i32) / (2.0_f64.powi(octaves as i32) - 1.0);
163
164        // Calculate max value
165        let max_value = Self::edge_value(amplitudes, lowest_freq_value_factor, 2.0);
166
167        // Pre-compute per-octave factors for the hot path. Mirrors the legacy
168        // iteration order so summation is bit-identical.
169        let mut active_octaves = Vec::with_capacity(noise_levels.len());
170        let mut input_factor = lowest_freq_input_factor;
171        let mut value_factor = lowest_freq_value_factor;
172        for (i, noise_opt) in noise_levels.iter().enumerate() {
173            if let Some(noise) = noise_opt {
174                active_octaves.push(ActiveOctave {
175                    noise: noise.clone(),
176                    input_factor,
177                    output_factor: amplitudes[i] * value_factor,
178                });
179            }
180            input_factor *= 2.0;
181            value_factor /= 2.0;
182        }
183
184        Self {
185            noise_levels,
186            amplitudes: amplitudes.to_vec(),
187            active_octaves,
188            lowest_freq_value_factor,
189            max_value,
190        }
191    }
192
193    /// Calculate the theoretical maximum value for the given amplitudes.
194    fn edge_value(amplitudes: &[f64], lowest_freq_value_factor: f64, noise_value: f64) -> f64 {
195        let mut value = 0.0;
196        let mut value_factor = lowest_freq_value_factor;
197
198        for &amplitude in amplitudes {
199            if amplitude != 0.0 {
200                value += amplitude * noise_value * value_factor;
201            }
202            value_factor /= 2.0;
203        }
204
205        value
206    }
207
208    /// Sample the noise at the given coordinates.
209    #[inline]
210    #[must_use]
211    pub fn get_value(&self, x: f64, y: f64, z: f64) -> f64 {
212        let mut value = 0.0;
213
214        for octave in &self.active_octaves {
215            let input_factor = octave.input_factor;
216            let noise_val = octave.noise.noise(
217                wrap(x * input_factor),
218                wrap(y * input_factor),
219                wrap(z * input_factor),
220            );
221            value += octave.output_factor * noise_val;
222        }
223
224        value
225    }
226
227    /// Sample the noise at `(x, 0.0, z)`.
228    #[inline]
229    #[must_use]
230    pub fn get_value_xz(&self, x: f64, z: f64) -> f64 {
231        let mut value = 0.0;
232
233        for octave in &self.active_octaves {
234            let input_factor = octave.input_factor;
235            let noise_val = octave
236                .noise
237                .noise_xz(wrap(x * input_factor), wrap(z * input_factor));
238            value += octave.output_factor * noise_val;
239        }
240
241        value
242    }
243
244    /// Sample the noise at `(x, y, 0.0)`.
245    #[inline]
246    #[must_use]
247    pub fn get_value_xy(&self, x: f64, y: f64) -> f64 {
248        let mut value = 0.0;
249
250        for octave in &self.active_octaves {
251            let input_factor = octave.input_factor;
252            let noise_val = octave
253                .noise
254                .noise_xy(wrap(x * input_factor), wrap(y * input_factor));
255            value += octave.output_factor * noise_val;
256        }
257
258        value
259    }
260
261    /// Calculate Perlin noise value using SIMD vectors.
262    #[inline]
263    #[must_use]
264    pub fn get_value_simd<F, const N: usize>(
265        &self,
266        x: Simd<F, N>,
267        y: Simd<F, N>,
268        z: Simd<F, N>,
269    ) -> Simd<F, N>
270    where
271        F: SimdElement + SimdCast,
272        Simd<F, N>: SimdFloat<Cast<i32> = Simd<i32, N>>
273            + SimdPartialOrd
274            + SimdPartialEq<Mask = Mask<<F as SimdElement>::Mask, N>>
275            + ops::Add<Output = Simd<F, N>>
276            + ops::Sub<Output = Simd<F, N>>
277            + ops::Mul<Output = Simd<F, N>>
278            + ops::Div<Output = Simd<F, N>>
279            + ops::Neg<Output = Simd<F, N>>
280            + StdFloat,
281    {
282        let mut value = Simd::splat(0.0).cast();
283
284        for octave in &self.active_octaves {
285            let input_factor = Simd::splat(octave.input_factor).cast();
286            let noise_val = octave.noise.noise_simd(
287                wrap_simd(x * input_factor),
288                wrap_simd(y * input_factor),
289                wrap_simd(z * input_factor),
290            );
291            value += Simd::splat(octave.output_factor).cast() * noise_val;
292        }
293
294        value
295    }
296
297    /// Sample the noise with Y scaling parameters.
298    ///
299    /// # Arguments
300    /// * `x`, `y`, `z` - Coordinates to sample
301    /// * `y_scale` - Y scaling factor for terrain
302    /// * `y_fudge` - Y fudge factor for floor snapping
303    /// * `y_flat_hack` - If true, use `-yo` instead of wrapped y (for legacy biomes)
304    #[must_use]
305    pub fn get_value_with_y_params(
306        &self,
307        x: f64,
308        y: f64,
309        z: f64,
310        y_scale: f64,
311        y_fudge: f64,
312        y_flat_hack: bool,
313    ) -> f64 {
314        let mut value = 0.0;
315
316        for octave in &self.active_octaves {
317            let input_factor = octave.input_factor;
318            let noise = &octave.noise;
319            let noise_val = noise.noise_with_y_scale(
320                wrap(x * input_factor),
321                if y_flat_hack {
322                    -noise.yo
323                } else {
324                    wrap(y * input_factor)
325                },
326                wrap(z * input_factor),
327                y_scale * input_factor,
328                y_fudge * input_factor,
329            );
330            value += octave.output_factor * noise_val;
331        }
332
333        value
334    }
335
336    /// SIMD form of [`Self::get_value_with_y_params`] that processes 4 Y values
337    /// at a fixed `(x, z)` per call. Used by transpiled density-function trees
338    /// that batch 4 cell-corner Ys in one pass.
339    ///
340    /// Per-lane math is identical to the scalar path — same operation order,
341    /// same wrapping, same octave loop — so the 4 returned lanes are
342    /// bit-identical to four scalar calls at the same Y values.
343    #[must_use]
344    pub fn get_value_with_y_params_4x(
345        &self,
346        x: f64,
347        ys: f64x4,
348        z: f64,
349        y_scale: f64,
350        y_fudge: f64,
351        y_flat_hack: bool,
352    ) -> f64x4 {
353        let mut value = f64x4::splat(0.0);
354
355        for octave in &self.active_octaves {
356            let input_factor = octave.input_factor;
357            let noise = &octave.noise;
358            let x_w = wrap(x * input_factor);
359            let z_w = wrap(z * input_factor);
360            let ys_for_call = if y_flat_hack {
361                f64x4::splat(-noise.yo)
362            } else {
363                wrap_simd(ys * f64x4::splat(input_factor))
364            };
365            let y_fudges = f64x4::splat(y_fudge * input_factor);
366            let noise_val = noise.noise_with_y_scale_simd(
367                x_w,
368                ys_for_call,
369                z_w,
370                y_scale * input_factor,
371                y_fudges,
372            );
373            value += f64x4::splat(octave.output_factor) * noise_val;
374        }
375
376        value
377    }
378
379    /// Generic N-lane form of [`Self::get_value_with_y_params_4x`]. Per-lane
380    /// math is identical to the scalar path at any supported width.
381    #[must_use]
382    pub fn get_value_with_y_params_simd<const N: usize>(
383        &self,
384        x: f64,
385        ys: Simd<f64, N>,
386        z: f64,
387        y_scale: f64,
388        y_fudge: f64,
389        y_flat_hack: bool,
390    ) -> Simd<f64, N> {
391        let mut value = Simd::splat(0.0);
392
393        for octave in &self.active_octaves {
394            let input_factor = octave.input_factor;
395            let noise = &octave.noise;
396            let x_w = wrap(x * input_factor);
397            let z_w = wrap(z * input_factor);
398            let ys_for_call = if y_flat_hack {
399                Simd::splat(-noise.yo)
400            } else {
401                wrap_simd(ys * Simd::splat(input_factor))
402            };
403            let y_fudges = Simd::splat(y_fudge * input_factor);
404            let noise_val = noise.noise_with_y_scale_simd(
405                x_w,
406                ys_for_call,
407                z_w,
408                y_scale * input_factor,
409                y_fudges,
410            );
411            value += Simd::splat(octave.output_factor) * noise_val;
412        }
413
414        value
415    }
416
417    /// Get the maximum possible output value.
418    #[inline]
419    #[must_use]
420    pub const fn max_value(&self) -> f64 {
421        self.max_value
422    }
423
424    /// Calculate the maximum "broken" value for `BlendedNoise`.
425    ///
426    /// Used by `BlendedNoise` to determine the theoretical max output.
427    /// Java reference: `PerlinNoise.maxBrokenValue(double)`
428    #[must_use]
429    pub fn max_broken_value(&self, y_scale: f64) -> f64 {
430        Self::edge_value(
431            &self.amplitudes,
432            self.lowest_freq_value_factor,
433            y_scale + 2.0,
434        )
435    }
436
437    /// Get the noise generator for a specific octave (by index from highest frequency).
438    ///
439    /// Index 0 is the highest frequency octave.
440    #[must_use]
441    pub fn get_octave_noise(&self, i: usize) -> Option<&ImprovedNoise> {
442        self.noise_levels
443            .get(self.noise_levels.len() - 1 - i)
444            .and_then(|opt| opt.as_ref())
445    }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::random::{Random, xoroshiro::Xoroshiro};
452    use std::simd::f64x4;
453
454    #[test]
455    fn test_perlin_noise_deterministic() {
456        let mut rng = Xoroshiro::from_seed(12345);
457        let splitter = rng.next_positional();
458
459        let amplitudes = [1.0, 1.0, 1.0];
460        let noise1 = PerlinNoise::create(&splitter, -3, &amplitudes);
461        let noise2 = PerlinNoise::create(&splitter, -3, &amplitudes);
462
463        let v1 = noise1.get_value(100.0, 64.0, 100.0);
464        let v2 = noise2.get_value(100.0, 64.0, 100.0);
465        assert!((v1 - v2).abs() < 1e-15);
466    }
467
468    #[test]
469    fn test_get_value_matches_zero_y_params_path() {
470        let mut rng = Xoroshiro::from_seed(12345);
471        let splitter = rng.next_positional();
472
473        let noise = PerlinNoise::create(&splitter, -4, &[1.0, 0.0, 1.0, 1.0]);
474
475        for (x, y, z) in [
476            (0.0, 0.0, 0.0),
477            (100.0, 64.0, -100.0),
478            (-4096.25, -32.5, 1024.75),
479        ] {
480            assert!(
481                (noise.get_value(x, y, z)
482                    - noise.get_value_with_y_params(x, y, z, 0.0, 0.0, false))
483                .abs()
484                    < 1e-15
485            );
486        }
487    }
488
489    #[test]
490    fn test_get_value_simd_matches_scalar() {
491        let mut rng = Xoroshiro::from_seed(12_345);
492        let splitter = rng.next_positional();
493        let noise = PerlinNoise::create(&splitter, -6, &[1.0, 0.0, 1.0, 1.0, 0.5]);
494        let xs = [0.0, 1.25, -1000.0, 33_554_431.5];
495        let ys = [0.0, 64.5, -32.25, 255.75];
496        let zs = [0.0, -30.75, 4096.5, -33_554_432.25];
497
498        let simd = noise.get_value_simd(
499            f64x4::from_array(xs),
500            f64x4::from_array(ys),
501            f64x4::from_array(zs),
502        );
503
504        for i in 0..4 {
505            let scalar = noise.get_value(xs[i], ys[i], zs[i]);
506            #[expect(
507                clippy::float_cmp,
508                reason = "SIMD path must be bit-identical to scalar noise for vanilla determinism"
509            )]
510            let matches = scalar == simd[i];
511            assert!(
512                matches,
513                "Mismatch at ({}, {}, {}): scalar={}, simd={}",
514                xs[i], ys[i], zs[i], scalar, simd[i],
515            );
516        }
517    }
518
519    #[test]
520    fn test_perlin_noise_spatial_variation() {
521        let mut rng = Xoroshiro::from_seed(42);
522        let splitter = rng.next_positional();
523
524        let noise = PerlinNoise::create(&splitter, -4, &[1.0, 1.0, 1.0, 1.0]);
525
526        // Sample at different locations
527        let values: Vec<f64> = (0..10)
528            .map(|i| noise.get_value(f64::from(i) * 50.0, 64.0, f64::from(i) * 50.0))
529            .collect();
530
531        // Check there's variation
532        let min = values.iter().copied().fold(f64::INFINITY, f64::min);
533        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
534        assert!(max - min > 0.01, "Noise should have spatial variation");
535    }
536
537    #[test]
538    fn test_create_from_random_different_seeds() {
539        let mut rng = Xoroshiro::from_seed(12345);
540        let splitter = rng.next_positional();
541        let mut random = splitter.with_hash_of(&NameHash::new("test_noise"));
542
543        let amplitudes = [1.0, 1.0, 1.0];
544        let noise1 = PerlinNoise::create_from_random(&mut random, -3, &amplitudes);
545        let noise2 = PerlinNoise::create_from_random(&mut random, -3, &amplitudes);
546
547        // These should produce different values since the random state advanced
548        let v1 = noise1.get_value(100.0, 64.0, 100.0);
549        let v2 = noise2.get_value(100.0, 64.0, 100.0);
550        assert!(
551            (v1 - v2).abs() > 0.001,
552            "Two PerlinNoise from sequential random should differ: v1={v1}, v2={v2}",
553        );
554    }
555
556    #[test]
557    fn test_zero_axis_helpers_match_full_noise() {
558        let mut rng = Xoroshiro::from_seed(98_765);
559        let splitter = rng.next_positional();
560        let noise = PerlinNoise::create(&splitter, -6, &[1.0, 0.0, 1.0, 1.0, 0.5]);
561        let samples = [
562            (0.0, 0.0),
563            (1.25, -30.75),
564            (-1000.0, 4096.5),
565            (33_554_431.5, -33_554_432.25),
566            (-0.000_000_1, 0.000_000_1),
567        ];
568
569        for &(a, b) in &samples {
570            #[expect(
571                clippy::float_cmp,
572                reason = "zero-axis helpers must be bit-identical to the full scalar path"
573            )]
574            {
575                assert_eq!(noise.get_value_xz(a, b), noise.get_value(a, 0.0, b));
576                assert_eq!(noise.get_value_xy(a, b), noise.get_value(a, b, 0.0));
577            }
578        }
579    }
580}