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 · 23 comments
Open

noise() is not Perlin noise #7430

cheind opened this issue Dec 17, 2024 · 23 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 :)

@davepagurek
Copy link
Contributor

The real Perlin noise is the friends you make along the way ;)

@postspectacular
Copy link

Just to give a little more historic perspective here:

I ported that Farbrausch code for processing.core back in summer 2003. Back then there:

  1. wasn't really that much easy-to-find literature or readily available sample code, though two of the most cited & helpful refs was this amazing Hugo Elias article and the noise treatment by the god father of computer graphics.
  2. Computers were much slower (and Java too), so for something like noise() to be generally useful (esp. the 2D and 3D versions), we wanted (needed!) to choose a performant implementation and so ended up with the Farbrausch version, which provided the best compromise (also was much faster than other impls I tried & ported back then)...
  3. I think it's fair to say that neither Ben, Casey nor I back then considered any of the finer nuances/differences between noise types too much (or at all 😉 )... I proposed a noise() function because I thought it'd be super useful for a lot of different applications and I'd already used it for some terrain & texture generation tasks in a few games I'd developed in Shockwave3D/Director and got some very interesting results from it...
  4. I'd have never thought it would stay around (largely unchanged) for that long...

Sorry for any inconvenience caused, heh! :)

@SableRaf
Copy link
Contributor

Thanks @postspectacular for the historical context! Grateful you took the time to share 😃

@SableRaf
Copy link
Contributor

SableRaf commented Dec 19, 2024

I talked to Casey about this earlier today. He didn't remember as much as Karsten, but he had a good suggestion regarding the documentation. Do mention at some point that there are different kinds of noise() functions—including Perlin, Simplex, etc.—and note that the specific implementation here is value noise, but overall focus more on what noise() actually is in relationship to a random() function, and less on any specific implementation. I like that.

@SableRaf
Copy link
Contributor

While we're going down rabbit holes, here's another one. The claim on the p5.js documentation that noise was "invented by Ken Perlin while animating the original Tron film in the 1980s" seemed suspicious, especially since the Wikipedia page for Perlin noise (likely source for that factoid) says Perlin invented noise() "after working on Disney's computer animated sci-fi motion picture Tron (1982)" (emphasis mine).

Besides, the 3D models in TRON don't have any textures 🤔

So I had a look at the source for that reference: In the beginning: The Pixel Stream Editor, in Chapter 4 of this SIGGRAPH 2002 Course Notes on Real-Time Shading Languages.

Here is what Ken Perlin writes (with added ellipses for concision):

Working on TRON was a blast, but on some level I was frustrated by the fact that everything looked machine-like (a typical scene is shown below). In fact, that machinelike aesthetic became the "look" associated with MAGI in the wake of TRON. So I got to work trying to help us break out of the "machine-look ghetto." (...)

Three geometric motorcycles speeding in a corridor. Still frame of the light cycle racing scene from the movie TRON (1982)

Unfortunately (or fortunately, depending on how you look at it) our Perkin-Elmer and Gould SEL computers, while extremely fast for the time, had very little RAM, so we couldn't fit detailed texture images into memory. I started to look for other approaches. (...)

The first thing I did in 1983 was to create a primitive space-filling signal that would give an impression of randomness. (...) I ended up developing a simple pseudo-random "noise" function that fills all of three dimensional space. A slice out of this stuff is pictured.

A circular slice of Perlin noise on a black background

How cool is that?

In summary, Perlin started to think seriously about procedural textures while working on Tron in 1981, precisely because they could not use regular textures. Only later—in 1983—did he develop the noise() function to address limitations of texturing and rendering systems at the time.

So in conclusion, it seems very probable that Ken Perlin did NOT invent noise() "while" working on TRON, but "after", as is correctly stated on the Wikipedia page for Perlin noise.

Thank you for watching my 2h long YouTube video essay reading my comment.

PS: On a side note, it's funny to see that Perlin noise is still being used in the exact way it was intended:

Noise itself doesn't do much except make a simple pseudo-random pattern. But it provides seasoning to help you make things irregular enough so that you can make them look more interesting.

@cheind
Copy link
Author

cheind commented Dec 20, 2024

For those interested, I reached out to Ken for historical clarification. In particular on the scalar d introduced in his original paper and mentioned in my reply above : #7430 (comment)

I keep everyone posted, once I receive a reply (or maybe he finds the time to post here).

@cheind
Copy link
Author

cheind commented Dec 29, 2024

Hey,

as noted in #7430 (comment), I kindly asked Ken Perlin to clarify the following point

[...] In studying your paper, An Image Synthesizer [2], I saw that you reference a scalar value d alongside the gradient at each lattice point and note that d is returned at integer lattice coordinates. This seems to suggest a different behavior than the general description on Wikipedia [3], which defines Perlin noise as a scalar-valued multivariate function with roots at integer lattice points. At first glance, this seems contradictory.

to which Ken replied (quoting with permission)

The goal of noise is to produce a pseudo-random signal that minimizes the spectral energy at scales different from the scale of the unit lattice. In practice I always set d = 0, because this minimizes the spectral energy at other scales.

Wishing everyone a smooth start into the new year.

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

7 participants