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, StdFloat, f32x4, 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 = f64::from(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 = f64::from(
236                octave
237                    .noise
238                    .noise_xz(wrap(x * input_factor), wrap(z * input_factor)),
239            );
240            value += octave.output_factor * noise_val;
241        }
242
243        value
244    }
245
246    /// Sample the noise at `(x, y, 0.0)`.
247    #[inline]
248    #[must_use]
249    pub fn get_value_xy(&self, x: f64, y: f64) -> f64 {
250        let mut value = 0.0;
251
252        for octave in &self.active_octaves {
253            let input_factor = octave.input_factor;
254            let noise_val = f64::from(
255                octave
256                    .noise
257                    .noise_xy(wrap(x * input_factor), wrap(y * input_factor)),
258            );
259            value += octave.output_factor * noise_val;
260        }
261
262        value
263    }
264
265    /// Calculate Perlin noise value using SIMD vectors.
266    #[inline]
267    #[must_use]
268    pub fn get_value_simd<const N: usize>(
269        &self,
270        x: Simd<f32, N>,
271        y: Simd<f32, N>,
272        z: Simd<f32, N>,
273    ) -> Simd<f32, N>
274    where
275        Simd<f32, N>: SimdFloat<Cast<i32> = Simd<i32, N>>
276            + SimdPartialEq<Mask = Mask<i32, N>>
277            + SimdPartialOrd<Mask = Mask<i32, N>>
278            + ops::Add<Output = Simd<f32, N>>
279            + ops::Sub<Output = Simd<f32, N>>
280            + ops::Mul<Output = Simd<f32, N>>
281            + ops::Div<Output = Simd<f32, N>>
282            + ops::Neg<Output = Simd<f32, N>>
283            + StdFloat,
284    {
285        let mut value = Simd::splat(0.0);
286
287        for octave in &self.active_octaves {
288            let input_factor = Simd::splat(octave.input_factor as f32);
289            let noise_val = octave.noise.noise_simd(
290                wrap_simd(x * input_factor),
291                wrap_simd(y * input_factor),
292                wrap_simd(z * input_factor),
293            );
294            value += Simd::splat(octave.output_factor as f32) * noise_val;
295        }
296
297        value
298    }
299
300    /// Sample the noise with Y scaling parameters.
301    ///
302    /// # Arguments
303    /// * `x`, `y`, `z` - Coordinates to sample
304    /// * `y_scale` - Y scaling factor for terrain
305    /// * `y_fudge` - Y fudge factor for floor snapping
306    /// * `y_flat_hack` - If true, use `-yo` instead of wrapped y (for legacy biomes)
307    #[must_use]
308    pub fn get_value_with_y_params(
309        &self,
310        x: f64,
311        y: f64,
312        z: f64,
313        y_scale: f64,
314        y_fudge: f64,
315        y_flat_hack: bool,
316    ) -> f32 {
317        let mut value = 0.0_f32;
318
319        for octave in &self.active_octaves {
320            let input_factor = octave.input_factor;
321            let noise = &octave.noise;
322            let noise_val = noise.noise_with_y_scale(
323                wrap(x * input_factor),
324                if y_flat_hack {
325                    -noise.yo
326                } else {
327                    wrap(y * input_factor)
328                },
329                wrap(z * input_factor),
330                y_scale * input_factor,
331                y_fudge * input_factor,
332            );
333            value += (octave.output_factor as f32) * noise_val;
334        }
335
336        value
337    }
338
339    /// SIMD form of [`Self::get_value_with_y_params`] that processes 4 Y values
340    /// at a fixed `(x, z)` per call. Used by transpiled density-function trees
341    /// that batch 4 cell-corner Ys in one pass.
342    ///
343    /// Per-lane math is identical to the scalar path — same operation order,
344    /// same wrapping, same octave loop — so the 4 returned lanes are
345    /// bit-identical to four scalar calls at the same Y values.
346    #[must_use]
347    pub fn get_value_with_y_params_4x(
348        &self,
349        x: f64,
350        ys: f64x4,
351        z: f64,
352        y_scale: f64,
353        y_fudge: f64,
354        y_flat_hack: bool,
355    ) -> f32x4 {
356        let mut value = f32x4::splat(0.0);
357
358        for octave in &self.active_octaves {
359            let input_factor = octave.input_factor;
360            let noise = &octave.noise;
361            let x_w = wrap(x * input_factor);
362            let z_w = wrap(z * input_factor);
363            let ys_for_call = if y_flat_hack {
364                f64x4::splat(-noise.yo)
365            } else {
366                wrap_simd(ys * f64x4::splat(input_factor))
367            };
368            let y_fudges = f64x4::splat(y_fudge * input_factor);
369            let noise_val = noise.noise_with_y_scale_simd(
370                x_w,
371                ys_for_call,
372                z_w,
373                y_scale * input_factor,
374                y_fudges,
375            );
376            value += f32x4::splat(octave.output_factor as f32) * noise_val;
377        }
378
379        value
380    }
381
382    /// Generic N-lane form of [`Self::get_value_with_y_params_4x`]. Per-lane
383    /// math is identical to the scalar path at any supported width.
384    #[must_use]
385    pub fn get_value_with_y_params_simd<const N: usize>(
386        &self,
387        x: f64,
388        ys: Simd<f64, N>,
389        z: f64,
390        y_scale: f64,
391        y_fudge: f64,
392        y_flat_hack: bool,
393    ) -> Simd<f32, N> {
394        let mut value = Simd::splat(0.0_f32);
395
396        for octave in &self.active_octaves {
397            let input_factor = octave.input_factor;
398            let noise = &octave.noise;
399            let x_w = wrap(x * input_factor);
400            let z_w = wrap(z * input_factor);
401            let ys_for_call = if y_flat_hack {
402                Simd::splat(-noise.yo)
403            } else {
404                wrap_simd(ys * Simd::splat(input_factor))
405            };
406            let y_fudges = Simd::splat(y_fudge * input_factor);
407            let noise_val = noise.noise_with_y_scale_simd(
408                x_w,
409                ys_for_call,
410                z_w,
411                y_scale * input_factor,
412                y_fudges,
413            );
414            value += Simd::splat(octave.output_factor as f32) * noise_val;
415        }
416
417        value
418    }
419
420    /// Get the maximum possible output value.
421    #[inline]
422    #[must_use]
423    pub const fn max_value(&self) -> f64 {
424        self.max_value
425    }
426
427    /// Calculate the maximum "broken" value for `BlendedNoise`.
428    ///
429    /// Used by `BlendedNoise` to determine the theoretical max output.
430    /// Java reference: `PerlinNoise.maxBrokenValue(double)`
431    #[must_use]
432    pub fn max_broken_value(&self, y_scale: f64) -> f64 {
433        Self::edge_value(
434            &self.amplitudes,
435            self.lowest_freq_value_factor,
436            y_scale + 2.0,
437        )
438    }
439
440    /// Get the noise generator for a specific octave (by index from highest frequency).
441    ///
442    /// Index 0 is the highest frequency octave.
443    #[must_use]
444    pub fn get_octave_noise(&self, i: usize) -> Option<&ImprovedNoise> {
445        self.noise_levels
446            .get(self.noise_levels.len() - 1 - i)
447            .and_then(|opt| opt.as_ref())
448    }
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::random::{Random, xoroshiro::Xoroshiro};
455    use std::simd::f32x4;
456
457    #[test]
458    fn test_perlin_noise_deterministic() {
459        let mut rng = Xoroshiro::from_seed(12345);
460        let splitter = rng.next_positional();
461
462        let amplitudes = [1.0, 1.0, 1.0];
463        let noise1 = PerlinNoise::create(&splitter, -3, &amplitudes);
464        let noise2 = PerlinNoise::create(&splitter, -3, &amplitudes);
465
466        let v1 = noise1.get_value(100.0, 64.0, 100.0);
467        let v2 = noise2.get_value(100.0, 64.0, 100.0);
468        assert!((v1 - v2).abs() < 1e-15);
469    }
470
471    #[test]
472    fn test_get_value_matches_zero_y_params_path() {
473        let mut rng = Xoroshiro::from_seed(12345);
474        let splitter = rng.next_positional();
475
476        let noise = PerlinNoise::create(&splitter, -4, &[1.0, 0.0, 1.0, 1.0]);
477
478        for (x, y, z) in [
479            (0.0, 0.0, 0.0),
480            (100.0, 64.0, -100.0),
481            (-4096.25, -32.5, 1024.75),
482        ] {
483            assert!(
484                (noise.get_value(x, y, z)
485                    - f64::from(noise.get_value_with_y_params(x, y, z, 0.0, 0.0, false)))
486                .abs()
487                    < 1e-6
488            );
489        }
490    }
491
492    #[test]
493    fn test_get_value_simd_matches_scalar() {
494        let mut rng = Xoroshiro::from_seed(12_345);
495        let splitter = rng.next_positional();
496        let noise = PerlinNoise::create(&splitter, -6, &[1.0, 0.0, 1.0, 1.0, 0.5]);
497        let xs = [0.0_f32, 1.25, -1000.0, 4095.5];
498        let ys = [0.0_f32, 64.5, -32.25, 255.75];
499        let zs = [0.0_f32, -30.75, 4096.5, -4096.5];
500
501        let simd = noise.get_value_simd(
502            f32x4::from_array(xs),
503            f32x4::from_array(ys),
504            f32x4::from_array(zs),
505        );
506
507        for i in 0..4 {
508            let scalar = noise.get_value(f64::from(xs[i]), f64::from(ys[i]), f64::from(zs[i]));
509            assert!(
510                (scalar - f64::from(simd[i])).abs() < 1e-5,
511                "Mismatch at ({}, {}, {}): scalar={}, simd={}",
512                xs[i],
513                ys[i],
514                zs[i],
515                scalar,
516                simd[i],
517            );
518        }
519    }
520
521    #[test]
522    fn test_perlin_noise_spatial_variation() {
523        let mut rng = Xoroshiro::from_seed(42);
524        let splitter = rng.next_positional();
525
526        let noise = PerlinNoise::create(&splitter, -4, &[1.0, 1.0, 1.0, 1.0]);
527
528        // Sample at different locations
529        let values: Vec<f64> = (0..10)
530            .map(|i| noise.get_value(f64::from(i) * 50.0, 64.0, f64::from(i) * 50.0))
531            .collect();
532
533        // Check there's variation
534        let min = values.iter().copied().fold(f64::INFINITY, f64::min);
535        let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
536        assert!(max - min > 0.01, "Noise should have spatial variation");
537    }
538
539    #[test]
540    fn test_create_from_random_different_seeds() {
541        let mut rng = Xoroshiro::from_seed(12345);
542        let splitter = rng.next_positional();
543        let mut random = splitter.with_hash_of(&NameHash::new("test_noise"));
544
545        let amplitudes = [1.0, 1.0, 1.0];
546        let noise1 = PerlinNoise::create_from_random(&mut random, -3, &amplitudes);
547        let noise2 = PerlinNoise::create_from_random(&mut random, -3, &amplitudes);
548
549        // These should produce different values since the random state advanced
550        let v1 = noise1.get_value(100.0, 64.0, 100.0);
551        let v2 = noise2.get_value(100.0, 64.0, 100.0);
552        assert!(
553            (v1 - v2).abs() > 0.001,
554            "Two PerlinNoise from sequential random should differ: v1={v1}, v2={v2}",
555        );
556    }
557
558    #[test]
559    fn test_zero_axis_helpers_match_full_noise() {
560        let mut rng = Xoroshiro::from_seed(98_765);
561        let splitter = rng.next_positional();
562        let noise = PerlinNoise::create(&splitter, -6, &[1.0, 0.0, 1.0, 1.0, 0.5]);
563        let samples = [
564            (0.0, 0.0),
565            (1.25, -30.75),
566            (-1000.0, 4096.5),
567            (33_554_431.5, -33_554_432.25),
568            (-0.000_000_1, 0.000_000_1),
569        ];
570
571        for &(a, b) in &samples {
572            assert_eq!(noise.get_value_xz(a, b), noise.get_value(a, 0.0, b));
573            assert_eq!(noise.get_value_xy(a, b), noise.get_value(a, b, 0.0));
574        }
575    }
576}