Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2D, 3D, and 4D Perlin noise have incorrect scaling factors #359

Open
analog-hors opened this issue Dec 29, 2024 · 0 comments
Open

2D, 3D, and 4D Perlin noise have incorrect scaling factors #359

analog-hors opened this issue Dec 29, 2024 · 0 comments

Comments

@analog-hors
Copy link

All Perlin noise functions in noise have a scaling factor to map the raw output into [-1.0, 1.0]. The scaling factors are noted as being derived from the maximum value of N-d Perlin noise, which is calculated as sqrt(N) / 2.

This formula matches the calculations done on this Digital Freepen page, and is correct for "classic" Perlin noise. However, noise actually implements "improved" Perlin noise, which differs in 2 important ways:

  • Improved Perlin noise uses longer, non-unit gradient vectors.
  • Improved Perlin noise selects gradient vectors from a small predefined table, making some gradient configurations impossible.

As a result, noise's implementations of 2D, 3D, and 4D Perlin noise can produce values far outside of [-1.0, 1.0]. This can be easily observed once the output clamping is removed:

use noise::{NoiseFn, Perlin};

fn main() {
    let noise = Perlin::default();
    println!("2D: {}", noise.get([7.5, 8.5]));
    println!("3D: {}", noise.get([20.5, 31.5, 0.0]));
    println!("4D: {}", noise.get([8.5, 0.1, 2.8, 2.5]));
}
2D: 1.414213562373095
3D: 1.1547005383792515
4D: 1.11471461056

I wrote a script to greedily maximize the noise value, which I believe finds the true maximum values for each implementation:

use noise::core::perlin::{perlin_2d, perlin_3d, perlin_4d};
use noise::permutationtable::NoiseHasher;

#[derive(Debug, Clone)]
struct CustomHasher(Vec<u8>);

impl NoiseHasher for CustomHasher {
    fn hash(&self, to_hash: &[isize]) -> usize {
        let index = to_hash.iter().fold(0, |a, c| a * 2 + c);
        self.0[index as usize] as usize
    }
}

fn maximum_value(dims: u8, bits: u8, poll: impl Fn(&CustomHasher) -> f64) -> f64 {
    let mut max = CustomHasher(vec![0; 1 << dims]);
    for i in 0..max.0.len() {
        for gradient in 0..(1 << bits) {
            let mut new = max.clone();
            new.0[i] = gradient;
            if poll(&new) > poll(&max) {
                max = new;
            }
        }
    }

    poll(&max)
}

fn main() {
    println!("2D: {}", maximum_value(2, 2, |h| perlin_2d([0.5; 2].into(), h)));
    println!("3D: {}", maximum_value(3, 4, |h| perlin_3d([0.5; 3].into(), h)));
    println!("4D: {}", maximum_value(4, 5, |h| perlin_4d([0.5; 4].into(), h)));
}
2D: 1.414213562373095
3D: 1.1547005383792515
4D: 1.4375

It should be noted that the script assumes the maximum value can be found at the center of an n-cube, which I believe is true based on the Digital Freepen page. However, I do not really know if this still applies in Improved Perlin noise, as I am unable to follow along with the calculations. Still, if it is to be believed, the true scaling factors should be:

  • 2D: 1.0
  • 3D: 1.0
  • 4D: 16.0 / 23.0

As a side note, it should become increasingly harder to observe maximal values as the number of dimensions increases, as more and more gradient vectors are required to align. This may be a point against setting the scaling factor based on the absolute maximum, as it may result in a reduced range in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant