24.7 C
New York
Tuesday, August 5, 2025

How does actual Perlin noise work?


Firstly, what you are describing is 2D noise (the inputs are x and y – a two-dimensional parameter area). The truth that you need to visualize the output on a 3rd axis would not make the noise itself three-dimensional. We might name a noise operate 3D if it took x, y, and z as inputs, and produced a fourth quantity as its output.

Second, the algorithm doesn’t “solid rays” from the corners of the grid. It chooses a pseudo-random gradient vector. This defines how the worth of the noise ought to change within the neighborhood of that nook.

On this diagram by Matthewslf from the Wikimedia Commons, you may see these gradients visualized as pink arrows radiating from every nook. The purple-to-green shaded sq. surrounding every nook reveals how the noise worth varies based on that nook’s gradient vector (ignoring the opposite corners for now).

See Understanding the “gradient” in Perlin Noise for extra on this.

We are able to subtract the coordinates of our pattern level from the coordinates of the nook to get a neighborhood offset from that nook. The dot product of this native offset with the gradient chosen for that nook offers us the output worth based on that nook.

// Integer x, y coordinate of the bottom-left nook 
// of the cell containing our pattern level.
int2 cell = int2(ground(samplePosition.xy));

// Offset of the pattern place from this bottom-left nook
// (in vary 0 <= x < 1, 0 <= y < 1).
float2 fraction = frac(samplePosition.xy);

// Noise worth based on every nook.
float bottomLeft  = dot(getGradient(cell.xy), fraction);
float bottomRight = dot(getGradient(cell.xy + float2(1, 0)), fraction - float2(1, 0));
float topLeft     = dot(getGradient(cell.xy + float2(0, 1)), fraction - float2(0, 1));
float topRight    = dot(getGradient(cell.xy + float2(1, 1)), fraction - float2(1, 1));

To make the noise mix easily between adjoining cells, we have to interpolate between these corners. For 2D noise, that will seem like this:

float2 weight = getInterpolationWeight(fraction);

float prime    = lerp(   topLeft,    topRight, weight.x);
float backside = lerp(bottomLeft, bottomRight, weight.x);
float center = lerp(    backside,         prime, weight.y);

return center; // Our closing, blended noise worth.

We would like the interpolation weight for every nook to be 1 after we’re sampling on the nook itself, and 0 when sampling from the alternative facet of our grid cell. We additionally need the primary and second derivatives of this weight operate to be zero on the edges of every cell, to keep away from a noticeable “crease” or “kink” within the noise worth, the place the floor immediately adjustments course or curvature if we visualize it like a heightfield. There are a lot of capabilities that may do that:

  • Perlin’s authentic proposal used a cubic Hermite curve (generally known as smoothstep):

    // Given an x, y offset inside a cell, within the vary 0-1 on every axis:
    float2 getCubicInterpolationWeight(float2 xy) {
        return xy * xy * (3.0f - 2.0f * xy);
    }
    
  • Fashionable implementations typically choose a quintic curve for a extra gradual mix alongside the cell borders, particularly if the cubic model results in artifacts within the given implementation.

    float2 getQuinticInterpolationWeight(float2 xy) {
        return xy * xy * xy * (xy * (xy * 6.0f - 15.0f) + 10.0f);
    }
    

You’ll be able to see an instance of utilizing these interpolation weights to linearly interpolate between 4 corners on this reply.

As for the way we choose the gradient vectors within the first place, that is additionally as much as the implementation. All that issues is that:

  • The choice process takes two integers x and y as inputs and returns a 2D gradient vector.
  • The gradient vectors are all of the similar size (conventionally, unit size, so no slope course is constantly steeper than others).
  • The gradient vectors are uniformly distributed over the enter area (so there isn’t any bias towards a selected x/y course).
  • The number of a gradient vector for a given nook is repeatable (each time you pattern the identical lattice level within the grid, you get the identical gradient for it).
  • The number of gradient vectors is not clearly correlated between close by factors (so we do not see repeating patterns). In follow, there will likely be some relationship between the factors, since we’re utilizing a pseudo-random algorithm, not a very random supply like cube or radioactive decay; we simply must make the algorithm jumble up the inputs sufficient to obfuscate that truth, so it is not visually distracting.

Perlin’s authentic scheme used a permutation desk to digest the x and y coordinates of the nook right into a pseudo-random index to make use of in wanting up into an array of gradient vectors. This suited CPUs of the time, however these days (and particularly on GPUs), it typically makes extra sense to only calculate the vector with some math. You should utilize any hash operate you want to rework the x and y coordinates of the nook into an index or parameter to lookup or generate your gradient vector.

As an example, in 2D, we may use one thing like this:

float2 getGradient(int2 nook) {
    // Use any hash operate you want to show x and y coordinates into one quantity:
    float hashed = hash(nook.xy);

    // Return a unit vector, in a course chosen by treating this
    // hashed worth as an angle in radians.
    return float2(cos(hashed), sin(hashed));
}

Placing all of it collectively, you may see (and play with) a pattern implementation in WebGL by Inigo Quilez, right here. His implementation makes use of the “Hugo Elias hash” like so:

float hash(int2 nook) {
   int n = nook.x + nook.y * 11111;

   n = (n << 13) ^ n;
   n = (n * (n * n * 15731 + 789221) + 1376312589) >> 16;

   return float(n);
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles