Two-Dimensional Noise

Introduction

This is yet another copy and redo of a sketch from the Nature of Code. This time it's an extension of a prior post on making a one-dimensional noise graph.

Static Version (Using Set)

This first sketch is a static two-dimensional visualization of the p5 noise function, with the pixels of the canvas set to a level of gray (from black to white) based on the noise. I'll be doing the sketch again using a different method but this first one uses the (ambiguously named) set function that lets you set the grayscale or RGB-Alpha value for a single logical pixel (because different displays have different pixel-densities, there might be more "physical" pixels dedicated to each logical one, giving you better resolution). The set function is the simplest way to set a pixel, but, as noted in the p5 documentation, it adds overhead so there's another recommended way to do it if you need it to go faster (which is what the next section - Static Version Setting Pixels - is about). For static images (as this is) it's fast enough, though.

The Closure Function

This is the basic function that we pass to the p5 constructor to create our sketch.

function simple_sketch(p5) {
  const MAXIMUM_INTENSITY = 255;
  const NOISE_OFFSET_INCREMENT = 0.01;

We're only going to set the grayscale channel which has a maximum value of 255, thus the MAXIMUM_INTENSITY multiplied by noise will be what we use to figure out what value to set each pixel. The NOISE_OFFSET_INCREMENT is the amount that we're going to increase the x and y values as we step through the noise-space.

Set Up

First I'll define the p5 setup function. All it does is create the canvas that's the width of the div container. I'm using a variable STATIC_NOISE_GRAPH_DIV that I didn't show, but it just has the HTML div ID that will hold the sketch.

p5.setup = function() {
  p5.createCanvas(
    document.getElementById(STATIC_NOISE_GRAPH_DIV).offsetWidth,
    400);
} //end setup

Draw

The p5 draw function is where we do all the work of drawing the plot so I'll break it up a little bit to explain more of what's here.

p5.draw = function() {
  let intensity;
  let column_offset = 0.0;
  let row_offset;

Besides defining the function I'm declaring a variable, column_offset that holds the x-input value for the noise function, as well as row_offset which will hold the y-input.

I'm also creating a variable, intensity to hold the RGB setting we're going to use. It gets passed immediately to set once the value is created so it isn't really needed, I just created it for clarity.

Load Pixels

p5.loadPixels();

Behind the scenes the set function is actually manipulating a special data-structure called pixels, which I'll look at more in the next sketch. The important thing to note is that you have to load the display data into the array before you can use it (using loadPixels()), even when using the set function.

The For-Loops

for (let column = 0; column < p5.width; column++) {
  row_offset = 0.0;

  for (let row = 0; row < p5.height; row++) {

Since we're setting each of the pixels in the canvas I'll use two for-loops, traversing each column from top row to bottom row before moving on to the next column. I had previously followed the convention of using x for columns and y for rows, but I think calling them columns and rows will be a little clearer when we get to the next sketch.

Set the Intensity

let intensity = p5.noise(column_offset, row_offset) * MAXIMUM_INTENSITY;
p5.set(column, row, intensity);

The intensity for each pixel is the noise at the offset for that column-row pair multiplied by the MAXIMUM_INTENSITY (255) (since noise goes from \(0 \ldots 1\) and RGB-Alpha goes from \(0 \ldots 255\).) this gives us a fractional value of the MAXIMUM_INTENSITY. Once we get it we can set it at the matching column and row coordinate. The intensity is going to be a float, but as we'll see in the next sktech that won't matter, since the pixels array casts values to an integer, although I suppose if it were more important we might want to control it using floor, ceiling, or round. But since this is using noise I don't imagine we'd know the difference.

End the For-Loops

    row_offset += NOISE_OFFSET_INCREMENT;
  } //end row-for

  column_offset += NOISE_OFFSET_INCREMENT;
} // end column-for

At the end of each of the for-loops we add an offset value to change the input to the noise function by a little.

Update the Pixels and Stop the Loop

  p5.updatePixels();
  p5.noLoop();
} // end draw

When we called loadPixels we loaded the pixels array, then we updated the values in the array, but that alone won't update our canvas. To update our sketch we need to tell p5 to take our array values and apply them by calling updatePixels. Additionally, since we're looping over the same for-loop values over and over, the noise output isn't going to change so I'll call noLoop to stop the updating of the canvas.

Finally, I'll create the p5 instance with our sketch function, and we should be able to see the noise visualization.

new p5(simple_sketch, STATIC_NOISE_GRAPH_DIV);

And there you go. Now onto a version that sets the pixel array directly without using the set method.

Static Version Setting Pixels

This will essentially be the same sketch except instead of using the set function I'll set the values in the pixels array directly.

Some Constants

I'm not as familiar with javascript as I am with python so I was littering constant values all over the place trying to figure where the best place to put them would be. I finally decided to create these two objects to hold some constants that I'll use when updating the pixels array and when setting the slider up.

const PIXEL_ARRAY = {
  RED: 0,
  GREEN : 1,
  BLUE : 2,
  ALPHA : 3,
  CELLS_PER_PIXEL : 4,
  RGB_MAX : 255,
} // end PIXEL_ARRAY

The RED, GREEN, BLUE, and ALPHA values are to help locate their relative location in the array (more on that later), as is the CELLS_PER_PIXEL. I made RGB_MAX is so that maybe it's a little more obvious why there's a number 255 showing up in the code.

const SLIDER = {
  min: 0,
  max: 1,
  default_value: 0.01,
  step_size: 0,
} // end SLIDER_SETTINGS

These are the same values I used in the previous noise-sketches. A step_size of 0 just means that I'm not setting one so p5 can use whatever the default value is - the documentation says it's continuous but it seems to jump a bit when I use it.

Noise Plotter

The Noise Plotter class is going to draw the two-dimensional noise-visualization using the current slider value as the step-size to change the noise input.

The Noise Plotter Class

So, let's get started with the class definition.

class NoisePlotter {

There's nothing really being done in the constructor except storing the p5 and slider objects for later.

constructor(p5, slider) {
  this.slider = slider;
  this.p5 = p5
} // end constructur

The Draw Method

This is the workhorse that does all the plotting.

draw() {
  let intensity;
  let column_offset;
  let offset_increment = this.slider.value()
  let row_offset = 0;
  let pixel_index;

The variables:

  • intensity: This will hold the RGB value(s) that we set the pixels to based on noise
  • column_offset: The y-input for the noise function
  • offset_increment: How much to increase the noise function inputs (the offsets) in the loops
  • row_offset: x-input for the noise function
  • pixel_index: Starting index in the pixels array for our pixel

That last variable might take some explaining, so maybe here's a good spot to dump my understanding of how this works.

  this.p5.loadPixels();

  for (let y=0; y < this.p5.height; y++) {
    column_offset = 0;
    for (let x=0; x < this.p5.width; x++) {
      pixel_index = (x + y * this.p5.width) * PIXEL_ARRAY.CELLS_PER_PIXEL;
      intensity = (this.p5.noise(column_offset, row_offset)
                   * PIXEL_ARRAY.RGB_MAX);
      this.p5.pixels[pixel_index +
                     PIXEL_ARRAY.RED] = intensity;
      this.p5.pixels[pixel_index +
                     PIXEL_ARRAY.GREEN] = intensity;
      this.p5.pixels[pixel_index +
                     PIXEL_ARRAY.BLUE] = intensity;
      this.p5.pixels[pixel_index +
                     PIXEL_ARRAY.ALPHA] = PIXEL_ARRAY.RGB_MAX;
      column_offset += offset_increment;        
    } // end x for
    row_offset += offset_increment;
  }// end x for
  this.p5.updatePixels();
} // end draw

The Sketch

Note for later: You have to either set the background or the set the alpha channel in the pixel array. Leaving both out won't show anything.

The Closure Function

function static_pixels(p5) {
  const HEIGHT = 400;

  let plotter;
  let slider;

Once again, this is the sketch function that gets passed to a p5 constructor. I decided to create a class to handle the drawing of the visualization so the plotter variable is going to hold an instance of that. I'm also going to add a slider so that a user can change the amount the input to the noise changes, which is what the silder variable is for.

Set Up

p5.setup = function() {

Just the basic p5 setup function.

  • You Are My Density
    p5.pixelDensity(1);
    

    To draw the noise I'm going to set the values in the pixels array directly but that's actually not so straightforward as you might think. When we refer to a pixel, there's two things to consider - there's a logical pixel, which is what we referred to using the set function, and what most people probably think of when working with p5 - it's the (x, y) coordinate you've come to know and love, but that pixel doesn't necessarily map one-to-one with the physical pixels in a display. Because of this, the size of the pixels array and the number of cells within the array dedicated to each pixel depends on the display.

    The pixels documentation shows the proper way to set all the physical pixels, which requires you to check the pixelDensity and then for each logical pixel you would loop over the sub-pixels that represent it… maybe some other time. For now, setting pixelDensity(1) will turn off matching the pixel density of the user's display and let us just worry about the one logical pixel. I don't know if that means it wont' take advantage of a higher density display or not, but p5 is about making it easier to code visualizations, not high performance (to me, anyway) - and as we'll see, the for-loops we're using are already slow enough, adding two more nested loops will just make things even slower.

  • The Canvas
    p5.createCanvas(
      document.getElementById(STATIC_NOISE_PIXELS_DIV).offsetWidth,
      HEIGHT);
    

    This is the usual code I use, nothing fancy.

  • The Slider
    slider = p5.createSlider(SLIDER.min,
                             SLIDER.max,
                             SLIDER.default_value,
                             SLIDER.step_size);
    slider.style("width", "500px");
    

    This is also a pretty straight-forward slider (although I think that just dropping it in after the canvas like this isn't what you're supposed to do). The main difference is that I'm adding a callback:

    slider.input(() => p5.redraw());
    

    This uses javascript's crazy arrow function syntax (not that I think the idea behind it is crazy, but the weird looking syntax and the fact that there's so many ways to declare functions seems to make the language too complicated for the little advantage you get with all the variations).

    Since this is a mostly static drawing I'm going to turn off re-drawing the canvas, but this callback tells p5 that if the user changes the slider's value then it should re-draw the canvas. p5 also has a similar function called changed, but that doesn't trigger the callback until you let go of the mouse button, while input lets you see the changes as you drag the slider.

    Note: input and changed don't show up under the slider documentation but rather under the DOM category of the documentation so I don't know how anyone is supposed to know that they exist without searching forum posts. This seems to suggest that there might be other features of the p5 language that exist but aren't well documented so it's just luck if you figure out that they are there…

  • Text Setup
    p5.fill("white");
    p5.stroke("white");
    p5.textAlign(p5.CENTER);
    p5.textSize(32);
    p5.noStroke()
    

    This sets the values that I'll use to show what the current slider value is to the user. Since I'm setting the pixel array values directly and not calling any functions like stroke or fill to do the visualization, setting it here will stick for the life of the sketch.

  • A Noise Plotter
    plotter = new NoisePlotter(p5, slider);
    

    I thought that it was getting cluttered up enough that it would make sense to break the plotting of the noise into a class, since I findi it easier to work with an object-oriented approach.

  • No Loop
      p5.noLoop();
    } // end setup
    

    The last thing in the setup is turning off the re-drawing of the canvas. I'm still not clear on what the difference is between putting it here and in the draw function. It seems to work the same in both cases.

Draw

Now, our draw function.

p5.draw = function() {
  plotter.draw();
  // add a label to show the amount the noise changes
  p5.text(`Noise Change: ${slider.value().toFixed(3)}`,
          p5.width/2 , p5.height - 10);
} // end draw

Because I'm deferring most of the plotting to the NoisePlotter object it just calls its draw method and then sets the text to let the user know what the current slider setting is.

The P5 Instance

new p5(static_pixels, STATIC_NOISE_PIXELS_DIV);

And then we create the p5 object…

The Output

Moving Version

The Sketch

Note for later: Setting the canvas too wide slows the frame rate down a lot (since the x for-loop uses the width) so I needed to both shrink the canvas and add an extra div (above) to stick the slider into - because it was only showing up under the canvas before because there wasn't enough room for it to slide up alongside it.

Check the framerate in the browser's javascript console with

move_p5.frameRate();

Moving Noise Plotter

class MovingNoise {
  constructor({p5=undefined,
               slider=undefined,
               red=PIXEL_ARRAY.RGB_MAX,
               green=PIXEL_ARRAY.RGB_MAX,
               blue= PIXEL_ARRAY.RGB_MAX,
               y_start_offset=1000} = {}) {
    this.p5 = p5
    this.slider = slider;
    this.red_fraction = red/PIXEL_ARRAY.RGB_MAX;
    this.green_fraction = green/PIXEL_ARRAY.RGB_MAX;
    this.blue_fraction = blue/PIXEL_ARRAY.RGB_MAX;
    this.y_start_offset = y_start_offset;
    this.noise_start = 0;
  } // end constructur

  draw() {
    let offset_y = this.noise_start + this.y_start_offset;
    let offset_x;
    let pixel_index;
    let intensity;
    let increment = this.slider.value();

    this.p5.loadPixels();    

    for (let y=0; y < this.p5.height; y++) {
      offset_x = this.noise_start;
      for (let x=0; x < this.p5.width; x++) {
        pixel_index = (x + y * this.p5.width) * PIXEL_ARRAY.CELLS_PER_PIXEL;
        intensity = this.p5.noise(offset_x, offset_y) * PIXEL_ARRAY.RGB_MAX;
        this.p5.pixels[pixel_index + PIXEL_ARRAY.RED] = (intensity *
                                                  this.red_fraction);
        this.p5.pixels[pixel_index + PIXEL_ARRAY.GREEN] = (intensity *
                                                    this.green_fraction);
        this.p5.pixels[pixel_index + PIXEL_ARRAY.BLUE] = (intensity *
                                                   this.blue_fraction);
        this.p5.pixels[pixel_index + PIXEL_ARRAY.ALPHA] = PIXEL_ARRAY.RGB_MAX;
        offset_x += increment;        
      } // end x for
      offset_y += increment;
    }// end x for
    this.p5.updatePixels();
    this.noise_start += increment;
  } // end draw
} // end NoisePlotter

Sources