Perlin Noise Flow Fields in Processing - Part II

In this part of the tutorial we’ll tackle drawing the flow lines that go through the flow field.

  1. Selecting a starting Point
  2. Flow Line Algorithm
  3. Drawing the Flow-Field
  4. Choosing Hyper-Parameters

Selecting a starting Point

As Tyler Hobbs discusses in his essay, there a number of choices that we have to consider now. For example, how we decide the starting points of our flow lines. For now we’ll just choose them at random. We can do so by selecting a random GridAngle from our array, like so:

int x = (int)random(grid.size());
y = (int)random(grid.size());
GridAngle ga = grid.get(x).get(y);

Alternatively we could also iterate over each grid angle and draw a respective flow line.

Flow Line Algorithm

Then from that starting point the basic strategy will be to draw a line that follows the direction of the angles that fall directly underneath it. Similarly to how a river digs it’s way through soil and earth. We do this incrementally, from our starting point we search for the nearest point in the grid and get it’s angle, then extend our line in the direction of that angle and repeat.

beginShape();
PVector vec = ga.v.copy()
curveVertex(vec.x, vec.y);

Again, we’ll make use of our good old friend the curveVertex() function to help us draw our flow lines. We take the angle from the starting point we selected, and store it in a new variable called vec, we’ll extend our line from here.

The next part, I’ll be honest with you, is quite inefficient. And can be improved in multiple ways, but to keep it simple we’ll first do it this way and then maybe improve it later. The inefficiency comes from finding the point that is closest to the current end of our line, the brute force way (which we’re doing) is going through all points in the grid and finding the one that has the smallest distance to end of the flow line. For this process we’ll need a couple of parameters:

// Used to check for the closest point in the grid
float minDist = Float.MAX_VALUE;
float dist = 0.0;

// indices of closest point in the grid
int nearestX = 0;
int nearestY = 0;

// stores closest point in grid
GridAngle tempGA;

// stylistic parameters controlling the line length
int lineLength = 50;
float segmentLength = 2;

These’ll make more sense in a second. We’ll start by creating three loops, the first one specifying how many times we would like to extend our flowline (hence determining the overall length of it) and then a nested loop that goes over all the GridAngles in our ArrayList:

for (int n = 0; n<lineLength; n++) {
   for (int x = 0; x<grid.size(); x++) {
     for (int y = 0; y<grid.get(0).size(); y++) {
     }
   }
 }

Next we want to find the closest point, that’s where our previously declared variables come into play:

for (int n = 0; n<lineLength; n++) {
    for (int x = 0; x<grid.size(); x++) {
      for (int y = 0; y<grid.get(0).size(); y++) {
        tempGA = grid.get(x).get(y);
        dist = (float)dist(vec.x, vec.y, tempGA.x, tempGA.y);

        if (dist < minDist) {
          minDist = dist;
          nearestX = x;
          nearestY = y;
        }
      }
    }
  }

Our nested loop will essentially go over all the points in the grid storing them in tempGA, then calculating the distance between that point and our start point / previous point that extended the flow line. If that distance is smaller than what is previously stored in the minDist variable, we’ll overwrite that and also store the indices of that point in the array. This is quite expensive but allows us to find the closest point to the end of the line.

Once we’ve found that point we need to reset minDist to some large value and extend our flowline. Float.MAX_VALUE essentially sets the variable minDist to the largest possible value that fits into a float, we could equivalently set it to some number like 99999.9, the only thing that matters here is that it’s larger than the most distant point in the grid:

minDist = Float.MAX_VALUE;
float angle = grid.get(nearestX).get(nearestY).angle;

vec = new PVector(vec.x + segmentLength*cos(angle),
                 vec.y + segmentLength*sin(angle));

curveVertex(vec.x, vec.y);

Here it’s super important that you overwrite the vec variable, since it’ll have to serve as the starting point for the next iteration in the loop. And that’s pretty much it, once you’re out of the three loops, you just have to call endShape(); and it should have drawn a flowline.

As you probably can see, there’s a lot of room for improvement, but first let’s draw the Flow-Field and see what it looks like!

Drawing the Flow-Field

Everything discussed above can be neatly combined as a function:

// stylistic parameters controlling the line length
int lineLength = 50;
float segmentLength = 2;

void drawFlowLine() {
  //random starting point
  GridAngle ga = grid.get((int)random(grid.size())).get((int)random(grid.size()));
  
  beginShape();
  PVector vec = ga.v;
  curveVertex(vec.x, vec.y);

  // Used to check for the closest point in the grid
  float minDist = Float.MAX_VALUE;
  float dist = 0.0;

  // indices of closest point in the grid
  int nearestX = 0;
  int nearestY = 0;

  // stores closest point in grid
  GridAngle tempGA;

  for (int n = 0; n<lineLength; n++) {
    for (int x = 0; x<grid.size(); x++) {
      for (int y = 0; y<grid.get(0).size(); y++) {
        tempGA = grid.get(x).get(y);
        dist = (float)dist(vec.x, vec.y, tempGA.x, tempGA.y);

        if (dist<minDist) {
          minDist = dist;
          nearestX = x;
          nearestY = y;
        }
      }
    }

    minDist = Float.MAX_VALUE;
    float angle = grid.get(nearestX).get(nearestY).angle;

    vec = new PVector(vec.x + segmentLength*cos(angle),
                      vec.y + segmentLength*sin(angle));
    curveVertex(vec.x, vec.y);
  }
  endShape();
}

Now to actually draw the Flow Field we need to call this function in the draw loop, which is going to end up looking something like this:

final int numLines = 200; //number of flow lines we'll draw
void draw() {
  background(220);
  // We don't need to draw the grid anymore
  /*
  for (int x = 0; x<grid.size(); x++) {
    for (int y = 0; y<grid.get(0).size(); y++) {
      grid.get(x).get(y).display();
    }
  }
  */
  
  for (int n = 0; n < numLines; n++) {
    drawFlowLine();
  }
  
  noLoop(); // noneed to loop
}

And we’ll obtain a flow field like this:

This doesn’t look very nice, we can do much better by playing a little bit with the parameters and generating some more flow fields!

Choosing Hyper-Parameters

One last thing we want to do, to make it look nice, is span the grid of angles such that it begins outside of the canvas and ends outside of the canvas. For this we can simply modify the loops in the createGrid function as such:

void createGrid() {
  for (int x = -xOff; x<width+xOff; x+=spacing) {
    for (int y = -yOff; y<width+yOff; y+=spacing) {}
  }
}

Next, let’s crank up the number of flowlines with the numLine parameter to 500, 1000, 2000 and 5000 respectively:

The lines are too squiggly for my taste, we can fix that by reducing the rez parameter that controls overall angles of the grid given by the Perlin Noise. Reducing it to 0.003 and 0.001 makes it look a lot less jagged:

Also reducing the the spacing between the grid angles will make it look a lot more dense. A spacing of 10 and 5 respectively (note that this makes it way slower since there’s more grid angles it has to check):

We could also control the length by which the line gets extended each iteration. A length of 5 and 1 respectively:

And that’s just some of the results you can get with the code we wrote. Feel free to mess around, modify, deconstruct the code in any way, shape or form you like, and send your results my way! Hope you enjoyed! And maybe share it with a friend, it helps a lot!

The entire code:

final ArrayList<ArrayList<GridAngle>> grid = new ArrayList<ArrayList<GridAngle>>();

final int xOff = 100;
final int yOff = 100;
final int spacing = 12;
final float rez = 0.0005;
final int numLines = 5000;
final int lineLength = 100;
final float segmentLength = 2;

class GridAngle {
  int x, y, r;
  float angle;

  PVector v;

  GridAngle(int x_, int y_, int r_, float angle_) {
    x = x_;
    y = y_;

    angle = angle_;
    r = 1;
    v = new PVector(x + r * cos(angle),
                    y + r * sin(angle));
  }

  void display() {
    strokeWeight(2);
    line(x, y, v.x, v.y);
  }
}


void drawFlowLine() {
  GridAngle fa = grid.get((int)random(grid.size())).get((int)random(grid.size()));
  
  beginShape();
  //PVector vec = new PVector(fa.x + 1 *cos(fa.angle), 
  //  fa.y + 1 * sin(fa.angle));

  PVector vec = fa.v;

  curveVertex(vec.x, vec.y);

  float minDist = Float.MAX_VALUE;
  float dist = 0.0;
  int nearestX = 0;
  int nearestY = 0;
  GridAngle tempFA;

  for (int n = 0; n<lineLength; n++) {
    for (int x = 0; x<grid.size(); x++) {
      for (int y = 0; y<grid.get(0).size(); y++) {
        tempFA = grid.get(x).get(y);
        dist = (float)dist(vec.x, vec.y, tempFA.x, tempFA.y);

        if (dist<minDist) {
          minDist = dist;
          nearestX = x;
          nearestY = y;
        }
      }
    }

    minDist = Float.MAX_VALUE;
    float angle = grid.get(nearestX).get(nearestY).angle;

    vec = new PVector(vec.x + segmentLength*cos(angle),
                      vec.y + segmentLength*sin(angle));
    curveVertex(vec.x, vec.y);

  }
  endShape();
}


void createGrid() {
  for (int x = -xOff; x<width+xOff; x+=spacing) {
    ArrayList<GridAngle> row  = new ArrayList<GridAngle>();
    for (int y = -yOff; y<width+yOff; y+=spacing) {
      float angle = map(noise(x*rez, y*rez), 0.0, 1.0, 0.0, TAU);

      row.add(new GridAngle(x, y, spacing/2, angle));
    }
    grid.add(row);
  }
}

void setup(){
  size(600,600);
  noFill();
  createGrid();
}

void draw() {
  background(220);
  for (int x = 0; x<grid.size(); x++) {
    for (int y = 0; y<grid.get(0).size(); y++) {
      grid.get(x).get(y).display();
    }
  }
  
  for (int n = 0; n < numLines; n++) {
    drawFlowLine();
  }

  noLoop();
}

If you enjoyed reading this article, consider following me on Twitter!

For new blog posts and updates, consider joining my mailing list!