Mitigating Periodicity in Perlin Noise

I’m working on a Racing Eggs! animation, wherein four Egg objects race across the canvas from left to right. They each speed up and slow down independently of each other. When an Egg reaches the right edge of the canvas, it wraps around to appear again at the left edge. Each traverse of the canvas is considered a lap. The race continues indefinitely.

Each Egg object is labeled with its name (a greek letter, namely α, β, γ, and δ), and a number representing its current lap. Clicking the window toggles the animation on and off.


Question: Which Egg is currently in first place?
(answer at bottom of post)

The rate of progress of each Egg is governed by values from Perlin noise. These values are fetched from nearly parallel tracks in Perlin space.

This animation is purely for amusement, so the following concern is by no means of grave importance. However, it has me wondering whether Perlin noise is at all suitable for simulations that might be developed for research purposes.

For this animation, my worry is that the progress of the race is rendered cyclical due to the periodicity of Perlin noise, when the actual intention was to use that noise to confer randomness with a natural aspect to it. The same concern might apply to more serious research that involves performing statistical analysis to simulations of natural phenomena.

Here’s the p5.js code:

/* Racing Eggs Sketch */
// javagar
// posted July 12, 2021

let x_noise_distance;
let y_noise_distance;
let alpha;
let velocity_factor;

function setup() {
  let canvas = createCanvas(640, 320);
  canvas.parent('sketch_div');
  
  // distances of points in noise space
  // increment in x dimension per frame
  // higher -> more abrupt velocity changes
  x_noise_distance = 0.01;
  
  // separation in y space domension for each Egg
  // higher -> more independence of motion
  y_noise_distance = 1.0;  

  velocity_factor = 4.0;
  alpha = new Egg("α", 100, 55, 100, 60);
  beta = new Egg("β", 100, 125, 100, 60);
  gamma = new Egg("γ", 100, 195, 100, 60);
  delta = new Egg("δ", 100, 265, 100, 60);
  eggs = [alpha, beta, gamma, delta];
  background(255, 239, 223);
  for (let i = 0; i < eggs.length; i += 1) {
    
    // Use randomGaussian() to reduce effect of periodicity of Perlin noise ...
    // ... but does that solve the problem???
    eggs[i].move_by(noise(frameCount * x_noise_distance, i * y_noise_distance + x_noise_distance * randomGaussian()) * velocity_factor, 0);
    eggs[i].display();
  }
}

function draw() {
  if (anim) {
    background(255, 239, 223);
    for (let i = 0; i < eggs.length; i += 1) {
    // Use randomGaussian() to reduce effect of periodicity of Perlin noise ...
    // ... but does that solve the problem???
    eggs[i].move_by(noise(frameCount * x_noise_distance, i * y_noise_distance + x_noise_distance * randomGaussian()) * velocity_factor, 0);
    eggs[i].display();
    }
  }
}

class Egg {
  constructor(id, x, y, w, h) {
    this.id = id
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.lap = 1;
  }
  display() {
    push();
    textAlign(CENTER, CENTER);
    textSize(min(this.w, this.h) / 3);
    strokeWeight(2);
    fill(255, 255, 255);
    ellipse(this.x, this.y, this.w, this.h);
    strokeWeight(1);
    fill(255, 255, 191);
    ellipse(this.x, this.y, this.w - 20, this.h - 20);
    fill(0);
    text(this.id + ": " + this.lap, this.x, this.y);
    pop();
  }
  move_to(x, y) {
    this.x = x;
    this.y = y;
  }
  move_by(dx, dy) {
    this.x += dx;
    this.y += dy;
    if (this.x >= width) {
      this.x = this.x % width;
      this.lap += 1;
    }
  }
}

// Mouse clicks toggle animation off and on.
let anim = false;
document.addEventListener('click', toggleAnimation, true);
function toggleAnimation() {
  anim = !anim;
  if (anim) {
    loop();
  } else {
    noLoop();
  }
}

Here’s the HTML:

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <!-- PLEASE NO CHANGES BELOW THIS LINE (UNTIL I SAY SO) -->
  <script language="javascript" type="text/javascript" src="libraries/p5.min.js"></script>
  <script language="javascript" type="text/javascript" src="racing_eggs.js"></script>
  <!-- OK, YOU CAN MAKE CHANGES BELOW THIS LINE AGAIN -->

  <!-- This line removes any default padding and style.
       You might only need one of these values set. -->
  <style> body { padding: 0; margin: 10; background-color: #eeeeee; font-family: Verdana, Arial, Helvetica, sans-serif;} </style>
</head>

  <body>
    <h2>Racing Eggs!</h2>
    <div id="sketch_div">
    </div>
    <p>Click the page to start or stop the animation.</p>
  </body>
</html>

Note this in the draw() function:

    eggs[i].move_by(noise(frameCount * x_noise_distance, i * y_noise_distance + x_noise_distance * randomGaussian()) * velocity_factor, 0);

In that statement, a call to randomGaussian() is used to reduce, slightly, the periodicity that may result from following absolutely parallel tracks in Perlin space to fetch data. Is this sufficient? Can anyone suggest effective means of overcoming the problem of periodicity in Perlin space, or as suggested in the discussion Periodicity of perlin noise, should we give up, and use open simplex noise or something else, instead?

Answer to question regarding which Egg is winning the race in the illustration:
It is α (alpha), which is in lap 31, while the others are still in earlier laps.

EDITED 2x on July 12, 2021 to make revisions to the p5.js code.

Periodicity in Perlin noise tends to only be an issue when covering large areas of the noise space (for example if you were drawing cloud formations for a flight simulator and when you have a very large viewing distance). Over small-ish regions and if you stay away from the axes (where x or y is 0) you shouldn’t need to worry about it. You can take a look at this sketch to get an idea of what the space looks like: Perlin Noise Space Demo - OpenProcessing

Introducing randomGaussian() is going to give you a whole different kind of randomness. This is going to vary in a non-repeatable and not-smooth way.

1 Like

Thanks for the great Perlin Noise Space Demo.

So, the call to randomGaussian() is unnecessary, and would best be omitted. Prior to introduction of that fudge factor, this was the original version of the statement that calls the method for moving each Egg instance, and based on the above, it should suffice:

    eggs[i].move_by(noise(frameCount * x_noise_distance, i * y_noise_distance) * velocity_factor, 0);

Yup, it should work without the randomGaussian(). Basically you just need to tweak your x_noise_distance, y_noise_distance and the settings passed to noiseDetail to get results that work for your use case.

1 Like

Thanks for the helpful information and suggestions.

It works well with 5 octaves, a falloff factor of 0.5, and removal of the call to randomGaussian(). An adjustment to the code was also made in order to get away from the x axis in noise space. An increase in the canvas width looks better.

Following is the revised p5.js code:

// Racing Eggs!
// javagar
// Revised July 13, 2021

let x_noise_distance;
let y_noise_distance;
let alpha;
let velocity_factor;

function setup() {
  let canvas = createCanvas(800, 320);
  canvas.parent('sketch_div');
  
  // use 5 octaves
  noiseDetail(5, 0.5);
  
  // increment in x dimension per frame
  // higher -> more abrupt velocity changes
  x_noise_distance = 0.01;

  // separation in y space dimension for each Egg
  // higher -> more independence of motion
  y_noise_distance = 1.0;  

  velocity_factor = 4.0;
  alpha = new Egg("α", 100, 55, 100, 60);
  beta = new Egg("β", 100, 125, 100, 60);
  gamma = new Egg("γ", 100, 195, 100, 60);
  delta = new Egg("δ", 100, 265, 100, 60);
  eggs = [alpha, beta, gamma, delta];
  background(255, 239, 223);
  for (let i = 0; i < eggs.length; i += 1) {
    eggs[i].display();
  }
}

function draw() {
  if (anim) {
    background(255, 239, 223);
    for (let i = 0; i < eggs.length; i += 1) {
      eggs[i].move_by(noise(frameCount * x_noise_distance, (i + 1) * y_noise_distance) * velocity_factor, 0);
      eggs[i].display();
    }
  }
}

class Egg {
  constructor(id, x, y, w, h) {
    this.id = id
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.lap = 1;
  }
  display() {
    push();
    textAlign(CENTER, CENTER);
    textSize(min(this.w, this.h) / 3);
    strokeWeight(2);
    fill(255, 255, 255);
    ellipse(this.x, this.y, this.w, this.h);
    strokeWeight(1);
    fill(255, 255, 191);
    ellipse(this.x, this.y, this.w - 20, this.h - 20);
    fill(0);
    text(this.id + ": " + this.lap, this.x, this.y);
    pop();
  }
  move_to(x, y) {
    this.x = x;
    this.y = y;
  }
  move_by(dx, dy) {
    this.x += dx;
    this.y += dy;
    if (this.x >= width) {
      this.x = this.x % width;
      this.lap += 1;
    }
  }
}

// Mouse clicks toggle the animation on and off.
let anim = false;
document.addEventListener('click', toggleAnimation, true);
function toggleAnimation() {
  anim = !anim;
  if (anim) {
    loop();
  } else {
    noLoop();
  }
}