Topographical map

Hello!

I am trying to figure out how to turn a perlin noise-type 3D terrain like this one:

int cols, rows;
int scl = 20; //scale
int w = 1200; //width, adjustable
int h = 900; //height, adjustable

float [][] z;

void setup() {
  size(1600, 1100, P3D);
  frameRate(60);

  cols = w / scl;
  rows = h / scl;
  z = new float[cols][rows];
  float yoff = 0; //small float offset for smooth changes in value
  for (int y = 0; y < rows; y++) {
    float xoff = 0;
    for (int x = 0; x < cols; x++) {
      z[x][y] = map(noise(xoff, yoff), 0, 1, -140, 140); //output of map(last numbers) controlled for height of mountains.
      xoff += 0.2;//change offsets to control smoothness.
    }
    yoff +=0.2;
  }
}

void draw() {
  background(0);
  stroke(255);
  fill(111, 222, 55);

  translate(width/2, height/2); //prepares to rotate everything relative to center of window
  rotateX(radians(45));             


  translate(-w/2, -h/2); //offsets beginning of coordinates so that all of them appear in the screen. 

  //TRIANGLE_STRIP: LAND
  for (int y = 0; y < rows-1; y++) {
    beginShape(TRIANGLE_STRIP);
    for (int x = 0; x < cols; x++) {
      vertex(x*scl, y*scl, z[x][y]);
      vertex(x*scl, (y+1)*scl, z[x][y+1]);
    }
    endShape();
  }
}

…into a topographical map, that is, a 2D map (or a 3D map looked at directly from above) where altitude is indicated by different colors, and by CONTOUR LINES. I am actually more concerned about the Countour Lines than the colors.

Anyway, any help is appreciated. Thanks.

1 Like

Hello, LordCacops.
For the purpose of a 2D map a 3D map may be a bit rough. I mean, in 2D you’d stick to a pixelwise resolution. If that is the case, the idea behind building the isolines is as follows:

go through a loop acrooss all your points (probably two nested loops across the rows and the columns) and check the left neighbour of the current point and the upper one. If those pass a threshold of the hight change in that point, paint it as belonging to the isoline.

I hope my explanation is not messy, anyway feel free to reask

You can get close to a contour effect in your data by requiring that each point fall within a discrete band of z values, e.g. -20, -10, 0, 10, 20… this become your pseudo-contours. Change this line:

  z[x][y] = int(map(noise(xoff, yoff), 0, 1, -7, 7))*20; //output of map(last numbers) controlled for height of mountains.

…and inspect the output with tweaked settings:

int cols, rows;
int scl = 10; //scale
int w = 600; //width, adjustable
int h = 600; //height, adjustable
float [][] z;

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

However, in an actual contour map you can’t “skip” a contour – going straight from 0 to 20. You always have to pass through the 10 line, no matter how close they are to each other. In your random perlin data, on the other hand, that isn’t true – some values increase by 10, others jump up by 30, so your triangle grid mesh has is no contour line through those faces.

You could solve this in a number of ways, but if you want to use perlin noise, that might mean trying to first discover contour lines in the noise by walking it, for example:

(wait a while to watch the sketch patterns emerge)

2 Likes

Hi @LordCacops,

There’s a thread about that exact same topic on the old Processing forum.

The suggested solution is to:

  • draw noise on a graphics buffer (noise is mapped against a domain ranging from 0/black to 255/white)
  • use the blobDetection library to detect the white spots
  • run that blob detection multiple times on the same image (your PGraphics object), increasing the threshold each time.

You’ll end-up with multiple concentric blobs whose faces will act as contour lines.
Here below an annotated example sketch (Python mode) based on your script that just does that:

add_library('blobDetection')
add_library('peasycam')

rows, cols = 100, 100    # number of rows and cols
scle = 7    # scale factor (how large is the output)
w, h = cols * scle, rows * scle    # width and height of the output map
factor = .05    # noise factor
levels = 40    # max number of contour lines
elevation = 200    # height domain of contour lines

                    
def setup():
    size(1400, 800, P3D)
    ortho(-width>>1, width>>1, -height>>1, height>>1)
    smooth(8)
    
    cam = PeasyCam(this, 1000)
    
    # Drawing noise on an off-screen graphics buffer
    pg = createGraphics(cols, rows, P2D)
    pg.beginDraw()
    pg.loadPixels()
    for i in xrange(rows * cols): 
        n = noise(i%cols * factor, i/cols * factor) * 255 # scaling up the noise value to match the color range
        pg.pixels[i] = color(n)
    pg.updatePixels()
    pg.endDraw()


    # BLOB DETECTION
    # -> looking for white spots on the graphics buffer and circling them
    # -> repeating this task as many times as required, increasing the threshold each time to get concentric circles
    blobs = []
    for l in range(levels):
        b = BlobDetection(pg.width, pg.height)
        b.setThreshold(l/float(levels))
        b.computeBlobs(pg.pixels)
        blobs.append(b)
        
    # RENDERING
    # -> accessing each edge of each blob and drawing a line between its 2 vertices
    # -> storing the whole as a PShape object
    global contours 
    contours = createShape()
    contours.beginShape(LINES)
    for i in xrange(levels):
        contours.stroke(i*8, 255 - i*10, 235 + i)
        for n in range(blobs[i].getBlobNb()):
            b = blobs[i].getBlob(n)
            if b is not None:              
                for edge in range(b.getEdgeNb()):
                    v1 = b.getEdgeVertexA(edge)
                    v2 = b.getEdgeVertexB(edge)
                    if v1 is not None and v2 is not None:
                        contours.vertex(v1.x*pg.width*scle, v1.y*pg.height*scle, i * (float(elevation)/levels))
                        contours.vertex(v2.x*pg.width*scle, v2.y*pg.height*scle, i * (float(elevation)/levels))         
    contours.endShape(CLOSE)
    

def draw():
    background('#000000')
    translate(-w>>1, -h>>1)
    
    shape(contours)


(Note that it is 3D orthographic projected contour map looked at from above)

Also see the dedicated Flickr gallery by Cedric Kiefer to get an idea of the various outputs you could come-up with using that technic.

3 Likes

Gentlemen, you gotta be kidding me :slight_smile:
The task is so simple it doesn’t require any libraries.

/**
 * Countour line example
 * for https://discourse.processing.org/t/topographical-map/13302/4
 * Press any key to toggle between viewing countour lines or hights (the lighter the higher)
 */
final static int W = 512;  // width of the hight map
final static int H = 512;  // height of the hight map
final static float k = 0.03;  //noise coefficient. play with it to get more or less «rough» land

final static int l = 10;  //show hight differense with this many contour lines

final static color c0 = #000000;  //color to draw background
final static color c1 = #008888;  //color to draw isolines

float[][] m;  // the hight map

boolean showHeightOrCountour = false;

void setup() {
  size(512, 512);
  m = new float[W][H];
  for (int j=0; j<H; j++) {
    for (int i=0; i<W; i++) {
      m[i][j] = noise((W+i)*k,(H+j)*k);
    }
  }
}

void draw() {
  loadPixels();
  for (int j=0; j<H; j++) {
    for (int i=0; i<W; i++) {
      if (!showHeightOrCountour) {
        pixels[W*j+i] = color(round(m[i][j]*l)*(255/l));  //show hight with l resolution
      } else {
        if (i>0 && i<W-1 && j>0 && j<H-1) {
          int h0 = round(m[i][j]*l);    //get and adjust height at this point
          int hw = round(m[i-1][j]*l);  //get west neighbour's height
          int hn = round(m[i][j-1]*l);  //get nothern neighbour's height
          if (h0!=hw || h0!=hn) {  //if any neighbour's height on a different «step» of hight… 
            pixels[W*j+i] = c1;    //…draw contour 
          } else {
            pixels[W*j+i] = c0;    //…otherwise draw background
          }
        } else {
          pixels[W*j+i] = c0;
        }
      }
    }
  }
  updatePixels();
  surface.setTitle("Drawing contour lines @ "+round(frameRate));
}

void keyPressed() {
  showHeightOrCountour = !showHeightOrCountour;
}

(I’ve been doing this back in late 1990’s on 8086 asm. it’s still working. if anybody interested, I’ll show some screens :slight_smile: )

3 Likes

Sweet ! I really like the simplicity and effectiveness of this method.

The OP could replace line 36 by something like the following to have the contours colored based on their corresponding noise value:

pixels[W*j+i] = color(m[i][j]*l, 255 - m[i][j]*l*4, m[i][j]*l*20); //…draw contour

One major difference though is that you’re not drawing lines but pixels. Depending on LordCacops’s goals this approach coud turn out to be rather unpractical.

For instance, what if one wants to draw contours lines in 3D ? You would have to sample the pixel-made contours (because too many pixels), find the adjacency relations and draw a line between the sampled pixels locations. This is cumbersome and would likely fail (connexions between 2 different but close contours that have the same noise value may happen).

Other things, what if you want to access the contours of a single “montain” ? Or write the altitude next to a specific contour at a specific location ? Or render a contour in a particular way that would require to know the spatial order of its vertices ? …

Anyhow, I really enjoy seing and discovering the various ways to tackle a problem with code and, yes, I would love to see some screens of your 8086 asm outputs.

1 Like

Hi solub, thank you for your comment.

Well, in my code I do draw pixels, not lines, but I think converting a 2d array representing a hightmap into a 3D mesh is pretty straightforward. and while doing it one might as well create the countour lines from those points I draw as pixels. within those for() loops their coord would be a PVector (i, j, m[j][j]) multiplied by some scale factor. I’ll leave it as an exersice for the readers :slight_smile:

And of course there are many tweaks to be applied like coloring the lines into different colors according to the hight etc. I see no problem with that either.

thanks again and stand by for the screens :slight_smile: coming soon! (I’ll post them from my home computer in a few hours)

These are both beautiful contour renderers, even though neither of them gives a full mesh. It might be a good time to ask the OP –

@LordCacops do you want just a drawing of contour lines? If you want the geometry, do you want just the contour geometry (rings), or do you want that as part of a connected mesh? Are there additional rendering requirements, like rendering faces or edges in different colors based on their heights?

If you want just the drawn lines, LuckSmith’s solution. If you want the contour geometry, solubs. If you want a full mesh, there are many ways, but here is one way:

Begin with solub’s solution.

  1. loop over your blobs, blob, edges

  2. instead of using createShape and contours.vertex, save your pairs of points in ArrayList<float[]> points.

     points.add(new float[]{v1.x*pg.width*scle, v1.y*pg.height*scle, i * (float(elevation)/levels)})
     points.add(new float[]{v2.x*pg.width*scle, v2.y*pg.height*scle, i * (float(elevation)/levels)})
    
  3. Once you have all your data saved, convert to float[][] pointsArray with points.toArray()

  4. use the mesh library to create a Delaunay diagram myDiagram. the output is a triangulation of the points, so it will have similar properties to your original TRIANGLE_STRIP – although points won’t be evenly distributed in x/y, but instead will follow contours.

  5. myDiagram.getLinks(pointsArray)

  6. loop over links and look up points to build / draw your edges

You can get 2D edges directly from the diagram, but links allows you to rejoin the edges with the 3D information in your original points list. mesh is a 2D library, and it might pass through 3D information, but I don’t believe it does (untested).

4 Likes

Hi guys, here’s the promised screenshots :blush: hope you’ll enjoy it!

4 Likes

For now I only want to have the contour lines appear as rings around the 3D mountains.

go through a loop acrooss all your points (probably two nested loops across the rows and the columns) and check the left neighbour of the current point and the upper one. If those pass a threshold of the hight change in that point, paint it as belonging to the isoline.

@LuckSmith I am doing something wrong, because instead of Isolines I am getting this interconnected mess.

int cols, rows;
int scl = 20; //scale
int w = 1200; //width, adjustable
int h = 900; //height, adjustable

float [][] z;

void setup() {
  size(1600, 1100, P3D);
  frameRate(60);

  cols = w / scl;
  rows = h / scl;
  z = new float[cols][rows];
  float yoff = 0; //small float offset for smooth changes in value
  for (int y = 0; y < rows; y++) {
    float xoff = 0;
    for (int x = 0; x < cols; x++) {
      z[x][y] = map(noise(xoff, yoff), 0, 1, -140, 140); //output of map(last numbers) controlled for height of mountains.
      xoff += 0.2;//change offsets to control smoothness.
    }
    yoff +=0.2;
  }
}

void draw() {
  background(0);

  translate(width/2, height/2); //prepares to rotate everything relative to center of window
  rotateX(radians(mouseY));             

  translate(-w/2, -h/2); //offsets beginning of coordinates so that all of them appear in the screen. 

  //TRIANGLE_STRIP: LAND
  for (int y = 0; y < rows-1; y++) {
    stroke(0, 255, 0);
    noFill();
    beginShape(TRIANGLE_STRIP);
    for (int x = 0; x < cols; x++) {
      vertex(x*scl, y*scl, z[x][y]);
      vertex(x*scl, (y+1)*scl, z[x][y+1]);
    }
    endShape();
  }

  //COUNTOUR LINES//defective
  stroke(255, 0, 0);
  noFill();      
  beginShape();
  for (int y = 0; y < rows-1; y++) {

    for (int x = 1; x < cols; x++) {
      if ((z[x-1][y] > z[x][y] - 20) && (z[x-1][y]  < z[x][y] + 20)) {
        if ((z[x][y+1] > z[x][y] - 20) && (z[x][y+1] < 120)) {
          vertex(x*scl, y*scl, z[x][y]);
        }
      }
    }
  }
  endShape(CLOSE);
}

I get that I need to end the shape once the threshold statement stops being true, but I wouldn’t know where to do that.

here’s the promised screenshots :blush: hope you’ll enjoy it!

Those are beautiful by the way. If I could get something like that I’d be ecstatic.

@solub How?

It turned out that converting my old algorythm into 3D is not as easy as I expected :slight_smile:
this is how far I’ve got so far:

/**
 * Contour lines example in 3D
 * I call it
 * Holy Mountain Peaks
 * (you'll see why ;)
 * (still messy, still needs corrections and improvements)
 * drag mouse to rotate the scene
 */
final static int W = 512;  // width of the hight map
final static int H = 512;  // height of the hight map
final static float k = 0.03;  //noise coefficient. play with it to get more or less «rough» land

final static int l = 10;  //show hight differense with this many contour lines

final static color c0 = #000000;  //color to draw background
final static color c1 = #ffcc88;  //color to draw contour lines
final static color c2 = #8844ff;  //contour to stroke mesh
final color c3 = color(128,0,255,128);  //contour to fill mesh

float hs = 8.0;   // horisontal scale
float vs = 64.0;  // vertical scale;

float[][] m;   // the hight map
int[][] ip;  // isopoints

float ax, ay;  //angles to turn the map in 3D

PShape mesh;

void setup() {
  size(1024, 512, P3D);
  /* initialize map with perlin noise */
  m = new float[W][H];
  for (int j=0; j<H; j++) {
    for (int i=0; i<W; i++) {
      m[i][j] = noise((W+i)*k,(H+j)*k);
    }
  }
  
  /* inspect our map to mark cells where the contour line vertices would be */
  ip = new int[W][H];
  for (int j=0; j<H; j++) {
    for (int i=0; i<W; i++) {
      if (i>0 && j>0 && i<W-1 && j<H-1) {
        int h0 = round(m[i][j]*l);    //get and adjust height at this point
        int hw = round(m[i-1][j]*l);  //get western neighbour's height
        int hn = round(m[i][j-1]*l);  //get nothern neighbour's height
        int he = round(m[i+1][j]*l);  //get eastern neighbour's height
        int hs = round(m[i][j+1]*l);  //get southern neighbour's height
        if (h0>hw || h0>hn || h0>he || h0>hs) {  //if this point is higher 
          ip[i][j] = h0;
        } else {
          ip[i][j] = -1;
        }
      } else {
        ip[i][j] = -1;
      }
    }
  }
  ax = 0;
  ay = 0;
  
  /* build our mesh */
  mesh = createShape();
  mesh.beginShape(TRIANGLES);
  mesh.strokeWeight(1);
  mesh.stroke(c2);
  mesh.fill(c3);
  for (int j=0; j<H-1; j++) {
    for (int i=0; i<W-1; i++) {
      mesh.vertex(hs*(W/2-i), -l*m[i][j]*vs, hs*(H/2-j));
      mesh.vertex(hs*(W/2-(i+1)), -l*m[i+1][j]*vs, hs*(H/2-j));
      mesh.vertex(hs*(W/2-(i+1)), -l*m[i+1][j+1]*vs, hs*(H/2-(j+1)));
      mesh.vertex(hs*(W/2-i), -l*m[i][j]*vs, hs*(H/2-j));
      mesh.vertex(hs*(W/2-(i+1)), -l*m[i+1][j+1]*vs, hs*(H/2-(j+1)));
      mesh.vertex(hs*(W/2-i), -l*m[i][j+1]*vs, hs*(H/2-(j+1)));
    }
  }
  mesh.endShape();
}

void draw() {
  background(c0);
  pushMatrix();
  translate(width/2, height+l*vs, -4*height);
  rotateX(ax);
  rotateY(ay);
  /* draw the mesh */
  shape(mesh);
  /* draw them contour lines */
  noFill();
  strokeWeight(2);
  stroke(c1);
  for (int j=0; j<H; j++) {
    for (int i=0; i<W; i++) {
      if (i>0 && i<W-1 && j>0 && j<H-1) {
        if (ip[i][j]>=0) {
          int h0 = ip[i][j];
          /* we'll inspect neighbourhood to east and south
           * and draw lines to those points if they are marked with the same height */
          if (ip[i+1][j]==h0) {  //*east
            line(hs*(W/2-i), -h0*vs, hs*(H/2-j), hs*(W/2-i-1), -h0*vs, hs*(H/2-j)); 
          }
          if (ip[i-1][j+1]==h0) {  //*south-west
            line(hs*(W/2-i), -h0*vs, hs*(H/2-j), hs*(W/2-i+1), -h0*vs, hs*(H/2-j-1)); 
          }
          if (ip[i][j+1]==h0) {  //*south
            line(hs*(W/2-i), -h0*vs, hs*(H/2-j), hs*(W/2-i), -h0*vs, hs*(H/2-j-1)); 
          }
          if (ip[i+1][j+1]==h0) {  //*south-east
            line(hs*(W/2-i), -h0*vs, hs*(H/2-j), hs*(W/2-i-1), -h0*vs, hs*(H/2-j-1)); 
          }
        }
      }
    }
  }
  popMatrix();
  surface.setTitle("Drawing contour lines @ "+round(frameRate));
}

void mouseDragged() {
  ax += (mouseY-pmouseY)/60.0;
  if (ax<0) {
    ax += TWO_PI;
  }
  if (ax>=TWO_PI) {
    ax -= TWO_PI;
  }
  ay += (mouseX-pmouseX)/60.0;
  if (ay<0) {
    ay += TWO_PI;
  }
  if (ay>=TWO_PI) {
    ay -= TWO_PI;
  }
}

I’m not on my computer right now (it’s dead), but I’ve done something similar in 2D using an algorithm called marching squares. It’s terrifyingly fast; the only tedious part is inserting 16 switch cases, but they should be available online.

If you want contour lines in 3D, a way to do it might be to take 2D cross-sections, then applying the 2D marching square algorithm to each layer. The main problem is that the algorithm works pixel-wise, but hopefully someone knows a way of nicely connecting consecutive points. The colour can then be based on the cross-section layer.

EDIT: Forgot to add, it will work as it is, and the benefit to this method is that you can adjust the “resolution” of the contour lines which can affect performance. Joining consecutive points will just allow you to have smoother curves using vertex() functions.

1 Like

Fun. There is a version of marching squares sometimes called “meandering triangles” – python version with demo here

example implementation in Java JPlotter here;

2 Likes

Here’s a rough sketch using the Marching Squares method I discussed earlier :smiley:. I mistakenly said that the algorithm works pixel-wise. It actually divides the terrain into tiles of fixed sizes, and the boundaries are drawn using lines. For the image below, the tile size used is 2x2, with the default P3D smoothing setting. If you can find a way to join the isolines such that you can use the vertex() function, then you could perhaps get even nicer looking curves. It looks pretty descent right now though. I couldn’t tilt it anymore because the isolines in the background start to overlap isolines in the foreground. Perhaps you could also extract the points and create triangle strips with fill to avoid this problem.

1 Like