Search code examples
processingp5.jssimplexsimplex-noise

Issue with seamless 1-D SImplex noise


Typically when generating seamless Simplex noise the strategy is to go to 4-dimensions (a strategy which has worked well for me in the past when using 2-D Simplex), however I am trying to generate a seamless GIF loop using 1-D simplex noise (only interested in the noise specifying the Y-value in a line graph).

I'm curious if I'm either misunderstanding how to make things seamless in 1-D or if I possibly have an error in logic here. Basically, I'm generating a 2-D array, where the first dimension is the Z axis and the second dimension is a list of the points (x-y values) for that Z. I iterate over each z and simply plot each vertex in turn.

What I notice is that, when I hit my maximum Z-value, there is a clear jump indicating that I'm doing something wrong (not seamless).

I'm using the fast-simplex-noise library (I like it better than P5's built-in noise function) and specify it as so:

function setup() {
  let freq = 0.005;
  let octaves = 14;
  _noise = new FastSimplexNoise({ frequency: freq, octaves: octaves });

  // specify the points:
  points = [];
  
  step = 0;
  maxSteps = 150;
  let r = 1.0;
  
  for (let z = 0; z < maxSteps; z++) {
    let t = (1.0 * z) / maxSteps;
    
    points[z] = [];

    for (let x = o + 10; x < width - o - 10; x++) {
      let _n = _noise.get4DNoise(x, z, r*cos(TWO_PI*t), r*sin(TWO_PI*t));
      let _y = height/2 + 250*_n;
      points[i].push({ x: x, y: _y });
    }
  }
}

In the draw function I simply iterate over each vertex in the points list and keep track of the current z value per-draw iteration.


Solution

  • It sounds like you are expecting the change in noise values when jumping from z = maxSteps - 1 to z = 0 to be small. However this is not going to be the case when you specify z as the second parameter to get4DNoise because while the third and fourth dimensions will be quite close to each other for these two values of z (thanks to the use of sine and cosine), the second will differ by maxSteps - 1. This begs the question: why are you using 4D noise at all? You are trying to vary y randomly such that it is smoothly changing for changes in either z or x and also looping back around. In order to achieve this you simply need to sample noise values along a straight line in 3d space moving around a cylinder.

    Here's a visualization of your noise algorithm, and a very similar one that only uses 3d noise. Notice how the cylinder on the left has a seam, but the one on the right has no seam:

    const maxSteps = 100;
    const scaleFactor = 20;
    
    let texture1;
    let texture2;
    
    let _noise;
    let cam;
    
    function setup() {
      let size = Math.min(windowWidth, windowHeight);
      createCanvas(size, size, WEBGL);
      noStroke();
      cam = createCamera();
      cam.setPosition(0, 0, 500);
      cam.lookAt(0, 0, 0);
    
      _noise = new FastSimplexNoise({
        frequency: 0.005,
        octaves: 14
      });
    
      texture1 = makeTexture(true);
      texture2 = makeTexture(false);
    }
    
    function doubleClicked() {
      console.log(cam);
    }
    
    function makeTexture(use4d) {
      let points = [];
      // Using an r of 1.0 covers a very small and unchanging region of noise space
      let r = use4d ? 1.0 : maxSteps / 2;
    
      for (let z = 0; z < maxSteps; z++) {
        let t = z / maxSteps;
    
        points[z] = [];
    
        for (let x = 0; x < maxSteps; x++) {
          let _n =
            use4d ?
            _noise.get4DNoise(x, z, r * cos(TWO_PI * t), r * sin(TWO_PI * t)) :
            _noise.get3DNoise(x, r * cos(TWO_PI * t), r * sin(TWO_PI * t));
          let _y = 250 * _n;
          points[z].push({
            x: x,
            y: _y
          });
        }
      }
    
      let g = createGraphics(maxSteps, maxSteps);
    
      for (let z = 0; z < maxSteps; z++) {
        for (let x = 0; x < maxSteps; x++) {
          // x == points[z][x].x
          // Using z as x and x as y because of the texture coordinate layout for cylinders
          g.set(
            z, x,
            // Shifting y values upward because they tend to be small resulting in a dark texture
            map(points[z][x].y, 0, 250, 100, 300)
          );
        }
      }
    
      // required after set()?
      g.updatePixels();
    
      return g;
    }
    
    function draw() {
      background(255);
      orbitControl(2, 1, 0.1);
    
      push();
      translate(-150, 0, 0);
      texture(texture1);
      cylinder(100, 200);
      pop();
    
      push();
      translate(150, 0, 0);
      texture(texture2);
      cylinder(100, 200);
      pop();
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/fast-simplex-noise.js"></script>