Creating variation in grid with random(); & image

I’m interested in morphing the grid with randomness while also having an image interact with the grid. How do I go about designing varying cells and joining them together as a grid image? Can I use the random(); with (TRIANGLE_STRIP),(QUAD_STRIP) to create a variation or would line() be more successful in getting to following results pictured?

f29b0ec3811fbc1e82188a290247f10f6e903ac0_2_484x500



I have tried the following code, but I can’t seem to get the visual results above.

// a class to representing a point on our grid.
// This should/would/could be done better by defining it as faces (of a triangle mesh).
// but would it keep as simple as possible 
class GridPoint {
  float x, y, v;
  public GridPoint(float px, float py, float pv) {
    x = px;  // x-pos of the grid point
    y = py;  // y-pos of the grid point
    v = pv;  // height of the grid point
  }
}

//helper variables
float halfWidth, halfHeight;
boolean mode = false;
// storage of out grid
int rows, cols;
GridPoint[][] grid;
// image used for texture and heightmap
PImage img;


void setup() {
  size(980, 980, P3D); //OPENGL?
  noLoop();
  frameRate(1);
  smooth(8);
  halfWidth  = width/2.;
  halfHeight = height/2.;
  initGrid(11.);
  // use textureMode IMAGE as the grid is not normalized
  textureMode(IMAGE);
}

void initGrid(float scl) {
  // load the image, used for texture and heightmap.
  // usually more than one picture will be used. One for heightmap, one for normalmap and one with the texture.
  // for simplicity it has the same size than our grid to not make the code more math intensive.
  // to get this work our image is somehow a bit blurred (like noise is), otherwise the mesh isn't smooth enough
  img = loadImage("gradient888.jpg");
  img.loadPixels();
  img.resize(980,980);
  // initialize our gridpoints
  rows = floor(height/scl);
  cols = floor(width/scl);
  grid= new GridPoint[rows][cols];
  for (int y = 0; y < rows; y++) {
    for (int x = 0; x < cols; x++) {
      int sx = floor(x*scl);
      int sy = floor(y*scl);
      // set x and y coordinates (center origin) and the height of the current point.
      // height is the normalized brightness value [0..1] of the pixel color from the image.
      // Subtract 0.5 to align it to center [-0.5..0.5]
      // scaling it by 50, so the range is [50.0*(-0.5)..50.0*(0.5)]
      grid[y][x] = new GridPoint(-halfWidth+sx+scl/2, -halfHeight+sy+scl/2, 200.*(brightness(img.get(sx, sy))/255-1.0));
    }
  }
}

void draw() 
{
    background(0);
  
  // set the origin to the center of the screen, and move it 400px far away on z-axis
  translate(halfWidth, halfHeight, 0.05);  // zoom in (lower numbers) zoom out (higher numbers around 200)
  

  // switch between mesh and texured mode
  //if (frameCount % int(TAU*100.) == 0) {
  //  mode = !mode;
  //}

  if (mode) 
  {
    //if textured mode no strokes and a light from viewpoint to object
    noStroke();
    directionalLight(255, 255, 255, 0, 0, -1);
  } else {
    // if mesh mode only show strokes
    noFill();
    stroke(250);
    strokeWeight(1.5);
    smooth(8);
    
  }

  // apply the rotation after lighting, otherwhile the light gets also rotated, which we not want.
  // tipping 30degree back on X-axis
  //rotateX(radians(30));
  // tipping left/right a bit per frame from -PI/4 - +PI/4 on Y-axis
  //rotateY(sin(frameCount/100.)*QUARTER_PI);

  // build the surface/terrain by triangle strips to display it
  for (int y = 0; y < rows-1; y ++) 
  {
    //beginShape (TRIANGLE_STRIP); // or (TRIANGLE_STRIP),(QUAD_STRIP), 
    int selector = int(random(4));
  
  if (selector == 0) {
    beginShape(TRIANGLE_STRIP);
   } else if (selector == 1) {
    beginShape(QUAD_STRIP);
   } else if  (selector == 2) {
    beginShape(LINES);
   } else if ( selector == 3) {
    beginShape(QUAD_STRIP);
   } else if ( selector == 4) {
    beginShape(QUAD_STRIP);
   } 
   
    if (mode) {
      // on textured mode set the texture
      texture(img);
    }
    // common triangle strips
    for (int x = 0; x < cols; x++) {
      GridPoint ca = grid[y][x];
      GridPoint cb = grid[y+1][x];
      vertex(ca.x, ca.y, ca.v, ca.x+halfWidth, ca.y+halfHeight);
   
      if (mode) {
        // on textured mode we need to set the normal vector, so the light knows how to behave on hittng the surface
        // usually there are better calculation than this cheap hack
        // comment it out to see the difference if normals are messed up
        PVector na = new PVector(ca.x-cb.x, ca.y-cb.y, 0).sub(new PVector(ca.x-cb.x, ca.y-cb.y, 1)).normalize();
        normal(na.x, na.y, na.z);
      }
      vertex(cb.x, cb.y, cb.v, cb.x+halfWidth, cb.y+halfHeight);
      if (mode) {
        // on textured mode we need to set the normal vector, so the light knows how to behave on hittng the surface
        // usually there are better calculation than this cheap hack
        // comment it out to see the difference if normals are messed up
        PVector nb = new PVector(cb.x-ca.x, cb.y-ca.y, 0).sub(new PVector(cb.x-ca.x, cb.y-ca.y, 1)).normalize();
        normal(nb.x, nb.y, nb.z);
        }
      }
    endShape();
    }

This is my result with my current code

Hi @asymmetric,

It’s unclear for me what this really means. By morphing you mean having an animation or just have a random displacement effect on the grid?

Also which interaction do you want with the image? Is it the diagonal orientation that will depend on the pixel color? (either 0 or 1)

Here is an implementation where I use line() because I find it easier:

int borderGap = 60;
int resolution = 10;
int maxHeight = 70;

class Grid {
  PVector[][] points;

  Grid(int resolution, int borderGap) {
    points = new PVector[resolution][resolution];

    float spaceBetweenPoints = (float) (width - (2 * borderGap)) / (resolution - 1);

    // Construct points
    for (int i = 0; i < resolution; i++) {
      float x = borderGap + i * spaceBetweenPoints;
      for (int j = 0; j < resolution; j++) {
        float y = borderGap + j * spaceBetweenPoints;
        float pointHeight = random(maxHeight);
        points[i][j] = new PVector(x, y, pointHeight);
      }
    }
  }

  void diagonal(int i, int j) {
    // Choose a random direction
    boolean randDiag = random(1) < 0.5;

    // Alternate the i index
    lineBetween(
      points[i + (randDiag ? 1 : 0)][j], 
      points[i + (randDiag ? 0 : 1)][j + 1]
      );
  }

  void displayHorizontalLines() {
    stroke(255);
    strokeWeight(1);

    for (int j = 0; j < resolution; j++) {
      for (int i = 0; i < resolution - 1; i++) {
        lineBetween(points[i][j], points[i + 1][j]);

        if (j < resolution - 1) {
          diagonal(i, j);
        }
      }
    }
  }

  void displayVerticalLines() {
    stroke(255);
    strokeWeight(1);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution - 1; j++) {
        lineBetween(points[i][j], points[i][j + 1]);
      }
    }
  }

  void displayPoints() {
    strokeWeight(8);
    stroke(200, 50, 200);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution; j++) {
        PVector P = points[i][j];
        point(P.x, P.y, P.z);
      }
    }
  }

  void display() {
    displayHorizontalLines();
    displayVerticalLines();
    displayPoints();
  }
}

Grid grid;

void setup() {
  size(500, 500, P3D);

  grid = new Grid(resolution, borderGap);
}

void lineBetween(PVector A, PVector B) {
  line(A.x, A.y, A.z, B.x, B.y, B.z);
}

void draw() {
  background(0);
  
  grid.display();

  noLoop();
}

Currently this implementation does not store the diagonal data in the Grid which means that if you were to animate it, it would recompute the diagonal orientation every time and flicker. You might want to store it for each face. (a boolean value or 0 / 1)

Here is another implementation using beginShape(TRIANGLE_STRIP) and storing diagonal data. It was more tricky to figure out :innocent:

int borderGap = 60;
int resolution = 10;
int maxHeight = 70;

void vertexP(PVector P) {
  vertex(P.x, P.y, P.z);
}

/**
 * Give vertices in this order:
 *
 *  A -- B
 *  |    |
 *  D -- C
 */
void triangulatedFace(PVector A, PVector B, PVector C, PVector D, boolean flipDiagonal) {
  // B -> A -> C -> D
  if (flipDiagonal) {
    vertexP(B);
    vertexP(A);
    vertexP(C);
    vertexP(D);
  } else {
    // A -> D -> B -> C
    vertexP(A);
    vertexP(D);
    vertexP(B);
    vertexP(C);
  }
}

class Grid {
  PVector[][] points;
  boolean[][] diagonals;

  Grid(int resolution, int borderGap) {
    points = new PVector[resolution][resolution];
    diagonals = new boolean[resolution][resolution];

    float spaceBetweenPoints = (float) (width - (2 * borderGap)) / (resolution - 1);

    // Construct points
    for (int i = 0; i < resolution; i++) {
      float x = borderGap + i * spaceBetweenPoints;
      for (int j = 0; j < resolution; j++) {
        float y = borderGap + j * spaceBetweenPoints;
        float pointHeight = random(maxHeight);
        points[i][j] = new PVector(x, y, pointHeight);
        
        // Store diagonal info, with 1/2 flip
        diagonals[i][j] = random(1) < 0.5;
      }
    }
  }

  void displayPoints() {
    strokeWeight(8);
    stroke(200, 50, 200);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution; j++) {
        PVector P = points[i][j];
        point(P.x, P.y, P.z);
      }
    }
  }
  
  void displayTriangleStrips() {
    stroke(255);
    strokeWeight(1);
    noFill();

    for (int j = 0; j < resolution - 1; j++) {
      for (int i = 0; i < resolution - 1; i++) {
        beginShape(TRIANGLE_STRIP);
        
        triangulatedFace(
          points[i][j], 
          points[i + 1][j], 
          points[i + 1][j + 1], 
          points[i][j + 1], 
          diagonals[i][j]
        );
        
        endShape();
      }
    }
  }

  void display() {
    displayTriangleStrips();
    displayPoints();
  }
}

Grid grid;

void setup() {
  size(500, 500, P3D);

  grid = new Grid(resolution, borderGap);
}

void draw() {
  background(0);

  grid.display();

  noLoop();
}

→ With a resolution of 200 for the grid, the frameRate was around 14 fps using line() and 12 using TRIANGLE_STRIP. Maybe my second implementation is not optimal thought…

Then you can influence your grid with an image…

Hope it helps! :wink:

2 Likes

Thank you @Joseph!! How do I go about connecting my image to the above code?

This is your next assignment ahah! :wink:

If you are not familiar with the concepts I used above and image manipulation, make sure to follow the Processing tutorials, read the reference and document yourself

  • How do I load an image using Processing?
  • How do I read pixels?
  • How do I drive each vertex height from the pixels of an image?
1 Like

Okay, Joseph! Here I go… I may have more questions today!

My understanding is that I need to include the following code to create the height map with the variation grid design that you provided. With that being said, I am having trouble connecting the code provided to the following vertex height/ image reading.

//helper variables
float halfWidth, halfHeight;
boolean mode = false;
// storage of out grid
int rows, cols;
GridPoint[][] grid;
// image used for texture and heightmap
PImage img;


void setup() {
  size(980, 980, P3D); 
  noLoop();
  frameRate(1);
  smooth(8);
  halfWidth  = width/2.;
  halfHeight = height/2.;
  initGrid(11.);
  // use textureMode IMAGE as the grid is not normalized
  //textureMode(IMAGE);
}

void initGrid(float scl) {
  // load the image, used for texture and heightmap.
  // usually more than one picture will be used. One for heightmap, one for normalmap and one    //with the texture.
  // for simplicity it has the same size than our grid to not make the code more math intensive.
  // to get this work our image is somehow a bit blurred (like noise is), otherwise the mesh //isn't smooth enough
  img = loadImage("gradient888.jpg");
  img.loadPixels();
  img.resize(980,980);
  // initialize our gridpoints
  rows = floor(height/scl);
  cols = floor(width/scl);
  grid= new GridPoint[rows][cols];
  for (int y = 0; y < rows; y++) {
    for (int x = 0; x < cols; x++) {
      int sx = floor(x*scl);
      int sy = floor(y*scl);
      // set x and y coordinates (center origin) and the height of the current point.
      // height is the normalized brightness value [0..1] of the pixel color from the image.
      // Subtract 0.5 to align it to center [-0.5..0.5]
      // scaling it by 50, so the range is [50.0*(-0.5)..50.0*(0.5)]
      grid[y][x] = new GridPoint(-halfWidth+sx+scl/2, -halfHeight+sy+scl/2, 200.*(brightness(img.get(sx, sy))/255-1.0));
    }
  }
}

After inserting my function void initGrid(float scl), what do I need to update? GridPoint no longer is the name of the class, so I will replace that with Grid?

Hi @josephh ! I’m trying to figure out how to include an image to interact with the grid you created, but I don’t know where to start… Can you help me?

This is what I have so far!

int borderGap = 5;
int resolution = 50;
int maxHeight = 0;

class Grid {
  PVector[][] points;

  Grid(int resolution, int borderGap) {
    points = new PVector[resolution][resolution];

    float spaceBetweenPoints = (float) (width - (2 * borderGap)) / (resolution - 1);

    // Construct points
    for (int i = 0; i < resolution; i++) {
      float x = borderGap + i * spaceBetweenPoints;
      for (int j = 0; j < resolution; j++) {
        float y = borderGap + j * spaceBetweenPoints;
        float pointHeight = random(maxHeight);
        points[i][j] = new PVector(x, y, pointHeight);
      }
    }
  }

  void diagonal(int i, int j) {
    // Choose a random direction
    boolean randDiag = random(1) < 0.5;

    // Alternate the i index
    lineBetween(
      points[i + (randDiag ? 1 : 0)][j], 
      points[i + (randDiag ? 0 : 1)][j + 1]
      );
  }

  void displayHorizontalLines() {
    stroke(255);
    strokeWeight(2);

    for (int j = 0; j < resolution; j++) {
      for (int i = 0; i < resolution - 1; i++) {
        lineBetween(points[i][j], points[i + 1][j]);

        if (j < resolution - 1) {
          diagonal(i, j);
        }
      }
    }
  }

  void displayVerticalLines() {
    stroke(255);
    strokeWeight(1);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution - 1; j++) {
        lineBetween(points[i][j], points[i][j + 1]);
      }
    }
  }

  void displayPoints() {
    strokeWeight(1);
    stroke(#0BFF00);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution; j++) {
        PVector P = points[i][j];
        point(P.x, P.y, P.z);
      }
    }
  }

  void display() {
    displayHorizontalLines();
    displayVerticalLines();
    displayPoints();
  }
}

   void initGrid(float scl) {
  // load the image, used for texture and heightmap.
  // usually more than one picture will be used. One for heightmap, one for normalmap and one    //with the texture.
  // for simplicity it has the same size than our grid to not make the code more math intensive.
  // to get this work our image is somehow a bit blurred (like noise is), otherwise the mesh //isn't smooth enough
  img = loadImage("gradient888.jpg");
  img.loadPixels();
  img.resize(1000,1000);
  // initialize our gridpoints
  rows = floor(height/scl);
  cols = floor(width/scl);
  grid= new Grid[rows][cols];
  for (int y = 0; y < rows; y++) {
    for (int x = 0; x < cols; x++) {
      int sx = floor(x*scl);
      int sy = floor(y*scl);
      // set x and y coordinates (center origin) and the height of the current point.
      // height is the normalized brightness value [0..1] of the pixel color from the image.
      // Subtract 0.5 to align it to center [-0.5..0.5]
      // scaling it by 50, so the range is [50.0*(-0.5)..50.0*(0.5)]
      grid[y][x] = new GridPoint(-halfWidth+sx+scl/2, -halfHeight+sy+scl/2, 200.*(brightness(img.get(sx, sy))/255-1.0));
    }
  }
}

  

Grid grid;

void setup() {
  size(1000, 1000, P3D);

  grid = new Grid(resolution, borderGap);
}

void lineBetween(PVector A, PVector B) {
  line(A.x, A.y, A.z, B.x, B.y, B.z);
}

void draw() {
  background(0);
  
  grid.display();

  noLoop();
}

Sorry for the delay! :wink:

Try to solve this:

  • In the code I posted, where are grid points initialized? What are the values given to the x, y, z coordinates? Where is the height value computed? How do you replace that with the brightness of your image?

  • It’s not just a matter of adding your function to the Grid class that will work out of the box. Try to understand how classes and methods work.

The height is calculated by


int maxHeight = 70; /// Height brightness 

And the X,Y, Z


// x, y, z 
void vertexP(PVector P) {
  vertex(P.x, P.y, P.z);
}

Are the following instance variables X and Y?


int borderGap = 60; // zoom in or out 
int resolution = 10; // grid cell and row size

I’m still feeling confused… Im thankful for your hep and guidance!

No, these are global variables meaning they are accessible everywhere in the code (inside functions, loops… and more generally any blocks).

int globalVariable = 0;

void example() {
  int localVariableToTheExampleFunction = 1;
  println(globalVariable); // Accessible here
}

example(); // Prints 0
println(localVariableToTheExampleFunction); // Error: not defined in this scope

This is the notion of scope in computer science.

This is a common practice to put global variables at the top of your program so modifying and reading them is easy. In this case, I am just declaring a variable that controls the maximum height that points can have using the z coordinate (pointing from you from the window point of view).

You need to understand how object oriented programming works (using classes with the class) keyword. See a previous message I did on the subject:


To summarize, I am using a Grid class that describes the grid you want to display. It has a points attribute which is a 2D array of PVector (which is itself a class holding up to three coordinates x, y, z).

This is perfect since we want to handle 3D points organized in a grid (rows and columns).

A class has a constructor, it’s a special “method” with some parameters used to initialize the object (in this case the Grid):

class Grid {
  Grid(int resolution, int borderGap) {
    // Initialize the grid here (mainly point coordinates)
  }
}

Try to read the code and understand what each part does before trying to copy paste code at some places. :wink:

1 Like

I think I need an example for how to connect an image height map to the code above…

Learning how to code is a process, either you don’t want to learn or you want me to give you the full solution to your issue (assignment?).

I don’t want to be rude but I gave you enough resources for you to look at them and understand how the different pieces fit together. So go for it! :wink:

If you still don’t understand the above code, try to start with a simpler example and build up to a more complex state.

1 Like

I really do want to learn but I’m hitting a wall interconnecting the image heighmap aspect. I completely understand where you are coming from, but I tend to learn from examples where my misunderstandings are. I just completed an OOP JavaScript class but I’m getting lost in how to connect the variables or the renaming of the variables…

Is this where you initialized the Grid Points?

// Construct points
    for (int i = 0; i < resolution; i++) {
      float x = borderGap + i * spaceBetweenPoints;
      for (int j = 0; j < resolution; j++) {
        float y = borderGap + j * spaceBetweenPoints;
        float pointHeight = random(maxHeight);
        points[i][j] = new PVector(x, y, pointHeight);
        
        // Store diagonal info, with 1/2 flip
        diagonals[i][j] = random(1) < 0.5;
      }
    }
  }

Yes, so you need to use the image pixel information (it can be the brightness or something else) and put that into the height of each point

2 Likes

That’s the part that I get confused with…

      points[I][j] = new PVector(-resolution+x, -resolution+y, 200.*(brightness(img.get(x, y))/ borderGap));

This is my code right now…

//GLOBAL VARIABLES
PImage img;

int borderGap = 5;
int resolution = 70;
int maxHeight = 0;

Grid grid;

void vertexP(PVector P) {
  vertex(P.x, P.y, P.z);
}


void setup() {
  size(1000, 1000, P3D);

  grid = new Grid(resolution, borderGap);
  
  smooth(8);
}

void draw() {
  background(0);

  grid.display();

  noLoop();
}



/**
 * Give vertices in this order:
 *
 *  A -- B
 *  |    |
 *  D -- C
 */
void triangulatedFace(PVector A, PVector B, PVector C, PVector D, boolean flipDiagonal) {
  // B -> A -> C -> D
  if (flipDiagonal) {
    vertexP(B);
    vertexP(A);
    vertexP(C);
    vertexP(D);
  } else {
    // A -> D -> B -> C
    vertexP(A);
    vertexP(D);
    vertexP(B);
    vertexP(C);
  }
}

//CLASS
class Grid {
  //INSTANCE VARIABLES
  PVector[][] points;
  boolean[][] diagonals;

  //CONSTRUCTOR 
  Grid(int resolution, int borderGap) {
    points = new PVector[resolution][resolution];
    diagonals = new boolean[resolution][resolution];

    float spaceBetweenPoints = (float) (width - (2 * borderGap)) / (resolution - 1);

    img = loadImage("b.png");
    img.loadPixels();
    img.resize(1000,1000);
    // Construct points
    for (int i = 0; i < resolution; i++) {
      float x = borderGap + i * spaceBetweenPoints;
      for (int j = 0; j < resolution; j++) {
        float y = borderGap + j * spaceBetweenPoints;
        float pointHeight = random(maxHeight);
        points[i][j] = new PVector(x, y, pointHeight);
        
        // Store diagonal info, with 1/2 flip
        diagonals[i][j] = random(1) < 0.5;
        

      }
    }
  }

  void displayPoints() {
    
    strokeWeight(2);
    stroke(#0BFF00);

    for (int i = 0; i < resolution; i++) {
      for (int j = 0; j < resolution; j++) {
        PVector P = points[i][j];
        point(P.x, P.y, P.z);
      }
    }
  }
  
  void displayTriangleStrips() {
    stroke(255);
    strokeWeight(1);
    noFill();

    for (int j = 0; j < resolution - 1; j++) {
      for (int i = 0; i < resolution - 1; i++) {
        beginShape(TRIANGLE_STRIP);
        
        triangulatedFace(
          points[i][j], 
          points[i + 1][j], 
          points[i + 1][j + 1], 
          points[i][j + 1], 
          diagonals[i][j]
        );
        
        endShape();
      }
    }
  }

  void display() {
    displayTriangleStrips();
    displayPoints();
  }
}

Here try to understand what each part does:

  • What does img.get(x, y) returns?
  • What does brightness(...) returns?

In my code I have:

float pointHeight = random(maxHeight);
points[i][j] = new PVector(x, y, pointHeight);

So the height of each point is a random value between 0 and maxHeight. This is not what you want so you should remove that. You want the height to depend on the brightness of the pixel on your image.

Hopefully that makes sense :wink:

1 Like