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

noise() is not Perlin noise #7430

Open
cheind opened this issue Dec 17, 2024 · 16 comments
Open

noise() is not Perlin noise #7430

cheind opened this issue Dec 17, 2024 · 16 comments

Comments

@cheind
Copy link

cheind commented Dec 17, 2024

noise() is not Perlin noise

Hi,

I've been browsing the documentation of the noise function

[...] Ken Perlin invented noise() while animating the original Tron film in the 1980s

and continued to study its implementation. From a first sight, I believe that the implementation found in p5.js deviates from Perlin noise in two characteristics:

Perlin noise defines gradients at integer grid locations

The noise value associated with a grid locations is given by the dot-product between the stored gradient and the offset vector. If I read your implementation correctly, you are directly assigning a random value to each integer location that is later interpolated.

Perlin noise is zero at all integer grid locations

if the dot-product becomes zero, either because of orthogonality between the gradient direction and the offset vector or because the offset vector is zero (at integer locations), the resulting noise value becomes zero. Hence, at integer locations the resulting noise should be zero. See https://en.wikipedia.org/wiki/Perlin_noise. In p5.js noise(x=0,0,0) will not return a zero noise value in general.

Hence, noise() provides smooth noise, but not Perlin noise. I don't believe that's an issue for the intended audience, but in case one relies on the above Perlin characteristics to hold true, you should mentioned the deviations in the docs.

Copy link

welcome bot commented Dec 17, 2024

Welcome! 👋 Thanks for opening your first issue here! And to ensure the community is able to respond to your issue, please make sure to fill out the inputs in the issue forms. Thank you!

@subhraneel2005
Copy link

Hi, thank you pointing out the differences between the current implementation and traditional Perlin noise.

I tried to fix it, and this is what i did, please review them:

  • modified the function to define gradients at integer grid locations, as Perlin originally intended.
  • ensured that the noise value is zero at integer points that aligning with the expected behavior

My Solution 👇

p5.prototype.noise = function(x, y = 0, z = 0) {  
  if (perlin == null) {  
    perlin = new Array(PERLIN_SIZE + 1);  
    for (let i = 0; i < PERLIN_SIZE + 1; i++) {  
      // Assigned random gradient vectors instead of random values  
      let angle = Math.random() * Math.PI * 2;  
      perlin[i] = { x: Math.cos(angle), y: Math.sin(angle) }; // 2D gradient  
    }  
  }  

  if (x < 0) x = -x;  
  if (y < 0) y = -y;  
  if (z < 0) z = -z;  

  let xi = Math.floor(x),  
      yi = Math.floor(y),  
      zi = Math.floor(z);  
  let xf = x - xi;  
  let yf = y - yi;  
  let zf = z - zi;  

  let r = 0;  
  let ampl = 1.0; // Started with full amplitude  

  for (let o = 0; o < perlin_octaves; o++) {  
    let of = xi + (yi << PERLIN_YWRAPB) + (zi << PERLIN_ZWRAPB);  

    // Calculated dot product with gradients  
    let n1 = dotProduct(perlin[of & PERLIN_SIZE], xf, yf);  
    let n2 = dotProduct(perlin[(of + PERLIN_YWRAP) & PERLIN_SIZE], xf, yf);  
    n1 += scaled_cosine(yf) * (n2 - n1);  

    let n3 = dotProduct(perlin[(of + PERLIN_ZWRAP) & PERLIN_SIZE], xf, yf);  
    let n4 = dotProduct(perlin[(of + PERLIN_YWRAP + PERLIN_ZWRAP) & PERLIN_SIZE], xf, yf);  
    n3 += scaled_cosine(yf) * (n4 - n3);  

    n1 += scaled_cosine(zf) * (n3 - n1);  

    r += n1 * ampl;  
    ampl *= perlin_amp_falloff;  
    
    // Updated xi, yi, zi for the next octave  
    xi <<= 1;  
    xf *= 2;  
    yi <<= 1;  
    yf *= 2;  
    zi <<= 1;  
    zf *= 2;  

    // Wraped around logic  
    if (xf >= 1.0) {  
      xi++;  
      xf--;  
    }  
    if (yf >= 1.0) {  
      yi++;  
      yf--;  
    }  
    if (zf >= 1.0) {  
      zi++;  
      zf--;  
    }  
  }  

  return r;  
};

@cheind
Copy link
Author

cheind commented Dec 17, 2024

@subhraneel2005 that was quick :) I haven't reviewed your code (yet). I want to emphasize that transitioning to Perlin noise in p5.js could have unintended side effects. One concern is the zero-noise-at-grid-locations issue: if you generate a noise image by sampling positions that fall on integer grid locations (e.g., exact pixel locations), the noise values will be all zeros.

From a first glance, your implementation generates gradient vectors that are always 2-dimensional. From a scientific perspective, gradients should have the same number of dimensions as the surrounding space. Also, in https://mrl.cs.nyu.edu/~perlin/paper445.pdf the gradients aren't chosen to be random, but rather pre-defined to avoid artefacts.

@limzykenneth
Copy link
Member

The implementation in p5.js comes directly from Processing and I believe the intention is for a more useful noise function that is random based. It is fine to edit the documentation to indicate this minor difference but I don't think we will be changing the implementation.

@subhraneel2005
Copy link

subhraneel2005 commented Dec 17, 2024

thanks for the insights

@subhraneel2005
Copy link

The implementation in p5.js comes directly from Processing and I believe the intention is for a more useful noise function that is random based. It is fine to edit the documentation to indicate this minor difference but I don't think we will be changing the implementation.

alright then, i will not change any code implementaion. thanks for letting me know :)

@davepagurek
Copy link
Contributor

It's true that the p5 noise implementation is not quite Perlin noise, and is based on the Processing noise implementation (which I hear is based on demoscene code from 2001, possibly for code size or performance constraints that were more important when it was first added?)

Anyway, this topic has definitely come up before, but there's balance we have to find with how complex we keep the reference. Especially in explaining what the deviation is -- talking about dot products with vector offsets seems maybe a little too much technical detail for the p5.js reference. One option might be to use actual Perlin noise, although it has been brought up that, at this point, being able to use the same noise function that has been in Processing from the start is also kinda important. We've also thought about having different noise modes, where a library could possibly implement e.g. simplex noise.

@limzykenneth @ksen0 let me know if you have any thoughts on this one!

@davepagurek
Copy link
Contributor

oh haha I see you added a comment in the mean time while Github was having issues

@cheind
Copy link
Author

cheind commented Dec 17, 2024

@davepagurek, @limzykenneth Agreed. I think that changing the implementation is an overkill here - but I believe the deviations should be mentioned in case anyone is using p5.js for scientific visualizations. Probably one can shorten this to something along the lines:

The noise() function generates smooth noise similar to Perlin noise but differs in two key ways: it is not a gradient-based noise function, and its zero-noise iso-contours do not fall on integer grid-locations in general.

Since processing and p5js is widely spread, this inaccuracy gets wide-spread. For example Kahn academy is teaching Perlin noise: https://www.khanacademy.org/computing/computer-programming/programming-natural-simulations/programming-noise/a/perlin-noise which is not Perlin noise :)

@davepagurek
Copy link
Contributor

I think I'm still in the camp that mentioning iso-contours is beyond the reading level of the target audience for the reference, which is more aimed at a grade school reading level, so my inclination would be to mention that p5's noise is "inspired by Perlin noise". It would be good to have the technical details somewhere though. One idea: maybe we could put a README.md in the src/math directory with a paragraph with the background and info in this thread, and then link to that from the reference?

@cheind
Copy link
Author

cheind commented Dec 17, 2024

@davepagurek you may need to sift through examples as well. Its called Perlin noise at quite a few locations.

@SableRaf
Copy link
Contributor

SableRaf commented Dec 18, 2024

Fascinating! I checked and the Processing documentation for noise() leans even heavier on the history of Perlin noise, and it goes out of its way to state that it is the original Perlin noise and not Simplex noise:

There have been debates over the accuracy of the implementation of noise in Processing. For clarification, it's an implementation of "classic Perlin noise" from 1983, and not the newer "simplex noise" method from 2001.

Are you telling me it's neither? 😅 (edit: some context for the above)

As it turns out, both Processing and p5.js use a form of value noise:

I agree with @limzykenneth that we should keep the implementation as it is, especially given how central noise() is to generative art practices and aesthetics. However, while it's important to simplify things for beginners, the documentation should avoid making false claims, especially when it doesn't really add value for learners.

I'd suggest revising the documentation to include something like:

The noise() function is similar to "Perlin noise," a popular technique for for generating smooth, random-like patterns, invented by Ken Perlin while animating the original Tron film in the 1980s. Note that noise() is not an exact implementation of Perlin noise, but belongs to a category called "value noise." which has slightly different properties.

What do you think?

Digressions

The original inspiration was the demo "Art" by the German demoscene group Farbrausch. The source code is available in a file deceptively called perlin.cpp. Just for fun, I asked ChatGPT o1 about the differences between Farbrauch's implementation and the original Perlin noise here.

Interestingly, the Wikipedia page for value noise notes:

Value noise (...) is conceptually different from, and often confused with gradient noise, examples of which are Perlin noise and Simplex noise.

@cheind
Copy link
Author

cheind commented Dec 18, 2024

@SableRaf , I believe noise() is a value noise generator, since the generated noise stems from random values assigned to grid locations (whereas gradient noise would assign random gradients to grid locations). From my understanding, simplex noise is a variant of gradient noise, just with better runtime properties in higher dims: naive impl requires O(2**dims) dots/interpolations, and simplex noise reduces this to O(dims**2).

As previously mentioned, I agree that re-writing the implementation solely to align with the documentation would be misguided. Gradient noise possesses unique properties that might surprise your user base. However, it also offers distinct advantages over value noise. Notably, gradient noise places greater emphasis on higher frequencies compared to lower frequencies, which helps to reduce the prevalence of flat areas in the noise landscape. Side note, the hash function used in the current implementation may additionally limit the randomness of the output (see #7431).

As @davepagurek suggested, providing a link to this discussion or a page with detailed clarifications would be a good starting point for readers seeking more information.

@SableRaf
Copy link
Contributor

For added context, this has been discussed within Processing in the past: Perlin noise documentation #51

A noiseMode() function was suggested, but that was ruled out, likely to keep the core libraries simple.

@nickmcintyre
Copy link
Member

Good catch @cheind! Yeah, I probably went too far with connecting the reference to an important bit of history -- agreed that I should have just corrected the inaccuracy.

@SableRaf's suggested revision strikes a nice balance. I believe there's one minor typo:

...called "value noise," which...

@cheind
Copy link
Author

cheind commented Dec 18, 2024

@nickmcintyre these things happen all the time :) Right now, I'm sifting through the 1985 original paper to double-check and indeed he introduces a random value d alongside the gradient at each lattice location. He mentions that this value is returned for integer locations. Its not clear to me what role d plays for non-lattice locations. However, he also metions that quote

The author has developed a number of surprisingly different
implementations of the Noise() function. Some real tradeoffs are
involved between time, storage space, algorithmic complexity,
and adherence to the three defining statistical constraints.
Because of space limitations, we will describe only the simplest
such technique.

So who knows what's truly Perlin noise after all :)

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

No branches or pull requests

6 participants