TRIANGLE_STRIP lighting problem

I want to add some simple lighting to my 3D terrain that I’m generating using beginShape with TRIANGLE_STRIP like this:

for (int i = 0; i < count - 1; i++) {
  beginShape(TRIANGLE_STRIP);
  for (int j = 0; j < count; j++) {
    vertex(j,i, data[i][j]);
    vertex(j, i + 1, data[i + 1][j]);
  }
  endShape();
}

(I simplified the code a lot. The real deal has a lot of scaling and offsetting going on in the vertex calls.)
The issue is, this generates striped shading, and I suspect that it is possible to make it so that it doesn’t look that way, so that it’s smooth in both directions. I just don’t know how.

Hi

What about this ??

That’s the same issue. It contains stripes, which I wanna get rid of.

1 Like

Hello @Tsskyx,

I was able to achieve this effect without lights() and coloring the vertices:

for (int i = 0; i < gs - 1; i++) 
    {
    beginShape(TRIANGLE_STRIP);  // Or use QUAD_STRIP
    for (int j = 0; j < gs; j++) 
      {
      if (fv) fill(255-data[i][j]);
      vertex(j*gsp, i*gsp, data[i][j]);
      if(fv) fill(255-data[i + 1][j]);
      vertex(j*gsp, (i + 1)*gsp, data[i + 1][j]);
      }
    endShape(); 
    }    

I have yet to try experimenting with normal() and lights():
https://processing.org/reference/normal_.html

Busy days for me so may not be following up anytime soon…

:)

That’s a nice workaround. I didn’t know you could assign fill() to individual vertices. Still, I wanna try it with normal(). I’m not sure how to do that though, there seem to be multiple issues with this at once.

Hi
I am not sure what you mean but look at this

After implementing the main algorithm from that post, I have obtained the following:

beginShape(QUADS);
for (int i = 0; i < count - 1; i++) {
  for (int j = 0; j < count - 1; j++) {
    vertex(j + 0, i + 0, data[i + 0][j + 0]);
    vertex(j + 1, i + 0, data[i + 0][j + 1]);
    vertex(j + 1, i + 1, data[i + 1][j + 1]);
    vertex(j + 0, i + 1, data[i + 1][j + 0]);
  }
}
endShape();

Once again, I’m leaving out all the scaling and perspective shifting details.

The result now looks like this:


In my opinion, this looks much better, because the stripes are gone.

However, I am still interested in polishing the lighting, so that the shadows are completely smooth, as they were in each individual stripe from before. If I’m not mistaken, this would require me to set the normal vector for each vertex during each loop iteration, but with neighbor tile averaging of some kind. Has someone created an algorithm for this already?

1 Like

Here’s the full code of my program.

int count = 200;
float noiseScale = 2;
boolean showMesh = false;
boolean trueLights = true;

float[][] data;
float angle_x, angle_z, draw_x, draw_y, draw_z;
boolean L, R, F, B, U, D;
int[][] coords = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
int zoomFactor;

void setup() {
  size(800, 800, P3D);
  noStroke();
  noiseDetail(round(log(200) / log(2)), 0.5);
  data = new float[count][count];
  reset();
  reseed();
}

void reset() {
  zoomFactor = 0;
  angle_x = angle_z = draw_x = draw_y = draw_z = 0;
}

void reseed() {
  noiseSeed((int)random(MAX_INT));
  for (int i = 0; i < count; i++)
    for (int j = 0; j < count; j++)
      data[i][j] = noise((float)i / count * noiseScale, (float)j / count * noiseScale);
}

void draw() {
  background(0);

  if (trueLights) lights();

  if (showMesh)
    stroke(0);
  else
    noStroke();

  translate(width / 2, height / 2);
  rotateX(angle_x);
  rotateZ(angle_z);
  translate(draw_x, draw_y, draw_z);
  scale((float)height / (count - 1) * pow(1.1, zoomFactor));
  translate(-(count - 1) * 0.5, -(count - 1) * 0.5, 0);

  beginShape(QUADS);
  for (int i = 0; i < count - 1; i++) {
    for (int j = 0; j < count - 1; j++) {
      for (int k = 0; k < 4; k++) {
        int I = i + coords[k][0];
        int J = j + coords[k][1];
        if (trueLights)
          fill(255);
        else
          fill(255 * data[I][J]);
        vertex(J, I, (data[I][J] - 0.5) * (count - 1) * 0.5);
      }
    }
  }
  endShape();
}

void mouseDragged() {
  int dx = mouseX - pmouseX, dy = mouseY - pmouseY;
  switch (mouseButton) {
  case LEFT:
    angle_z -= dx * 0.005;
    angle_x -= dy * 0.005;
    break;
  case RIGHT:
    draw_x += dx * +cos(angle_z) + dy * sin(angle_z) * cos(angle_x);
    draw_y += dx * -sin(angle_z) + dy * cos(angle_z) * cos(angle_x);
    draw_z += dy * -sin(angle_x);
    break;
  }
}

void mouseWheel(MouseEvent event) {
  zoomFactor -= event.getCount();
}

void keyPressed() {
  switch (key) {
  case ' ':
    reseed();
    break;
  case 'r':
    reset();
    break;
  case 'm':
    showMesh = !showMesh;
    break;
  case 'l':
    trueLights = !trueLights;
    break;
  }
}
1 Like

A simple hack that is probably good enough: create vectors between the points on either side of a given vertex, one in the x direction and the other in y, take their cross product and normalize it. Use it as your normal vector. Along the edges, use the vector from your vertex to the existing neighbor.

If you want to do more math, you could fit parabolas through the three points in each directions, compute the derivatives, and take the cross product. But I doubt you could tell the difference between that and the linear approximation especially when your data source is noise() to begin with.

2 Likes

@scudly After much trial and error and testing of different lighting algorithms, I have determined that yours is one of the best. Here’s my code, it features 4 different types of lighting modes (cycle through them with ‘c’):

import peasy.*;
PeasyCam cam;

final int points = 200;
final float camMin = .1;
final float camMax = 50;
final float tileScale = 10;
final float noiseScale = 2;
final float noiseFalloff = 0.5;
final float zScale = 1. / noiseScale;
final int noiseOctave = 8;

boolean showNormals = false;
boolean trueLights = true;
boolean showMesh = false;
boolean flatten = false;
int lightingStyle = 0;

float[][] hmap = new float[points][points];
PVector[][] P = new PVector[points][points];
PVector[][][] N = new PVector[points - 1][points - 1][4];

void setup() {
  size(800, 800, P3D);
  cam = new PeasyCam(this, tileScale * sqrt(3) * height / 2);
  cam.setMinimumDistance(camMin * height);
  cam.setMaximumDistance(camMax * height);
  perspective(TAU / 6, (float)width / height, camMin * height, camMax * height);
  noStroke();
  noiseDetail(noiseOctave, noiseFalloff);

  for (int i = 0; i < points; i++)
    for (int j = 0; j < points; j++)
      P[i][j] = new PVector();
  for (int i = 0; i < points - 1; i++)
    for (int j = 0; j < points - 1; j++)
      for (int k = 0; k < 4; k++)
        N[i][j][k] = new PVector();
  updateMap();
  setLights();
}

void updateMap() {
  noiseSeed((int)random(MAX_INT));
  for (int i = 0; i < points; i++) {
    for (int j = 0; j < points; j++) {
      hmap[i][j] = noise(i * noiseScale / points, j * noiseScale / points);
      P[i][j].set(j - 0.5 * (points - 1), i - 0.5 * (points - 1), (hmap[i][j] - 0.5) * points * zScale);
      P[i][j].mult(tileScale * height / (points - 1));
    }
  }
}

void setLights() {
  int $ = points - 1;
  switch (lightingStyle) {
  case 0: // normals of two triangles (processing default)
    for (int i = 0; i < points - 1; i++) {
      for (int j = 0; j < points - 1; j++) {
        PVector s1 = PVector.sub(P[i][j], P[i + 0][j + 1]);
        PVector s2 = PVector.sub(P[i][j], P[i + 1][j + 1]);
        PVector s3 = PVector.sub(P[i][j], P[i + 1][j + 0]);
        PVector n1 = PVector.cross(s1, s2, null).normalize();
        PVector n2 = PVector.cross(s2, s3, null).normalize();
        N[i][j][0].set(n2);
        N[i][j][1].set(n1);
        N[i][j][2].set(n2);
        N[i][j][3].set(n2);
      }
    }
    break;
  case 1: // cross product of tile diagonals (no gradient)
    for (int i = 0; i < points - 1; i++) {
      for (int j = 0; j < points - 1; j++) {
        PVector n = PVector.cross(PVector.sub(P[i][j], P[i + 1][j + 1]), PVector.sub(P[i][j + 1], P[i + 1][j]), null).normalize();
        for (int k = 0; k < 4; k++)
          N[i][j][k].set(n);
      }
    }
    break;
  case 2: // 4-neighbor normal (smooth gradient)
    for (int i = 0; i < points; i++) {
      for (int j = 0; j < points; j++) {
        PVector n = PVector.cross(PVector.sub(P[i][max(j - 1, 0)], P[i][min(j + 1, points - 1)]), PVector.sub(P[max(i - 1, 0)][j], P[min(i + 1, points - 1)][j]), null).normalize();
        if (i != $ && j != $) N[i - 0][j - 0][0].set(n);
        if (i != $ && j != 0) N[i - 0][j - 1][1].set(n);
        if (i != 0 && j != 0) N[i - 1][j - 1][2].set(n);
        if (i != 0 && j != $) N[i - 1][j - 0][3].set(n);
      }
    }
    break;
  case 3: // 6-neighbor normal (even smoother gradient)
    for (int i = 0; i < points; i++) {
      for (int j = 0; j < points; j++) {
        int iL = max(i - 1, 0), iH = min(i + 1, $), jL = max(j - 1, 0), jH = min(j + 1, $);
        PVector A = P[iL][jL], U = P[iL][j], R = P[i][jH], B = P[iH][jH], D = P[iH][j] ,L = P[i][jL];
        PVector nXY = PVector.cross(PVector.sub(R, L), PVector.sub(D, U), null).normalize();
        PVector nXZ = PVector.cross(PVector.sub(R, L), PVector.sub(B, A), null).normalize();
        PVector nYZ = PVector.cross(PVector.sub(B, A), PVector.sub(D, U), null).normalize();
        PVector sum = new PVector().add(nXY).add(nXZ).add(nYZ);
        if (i != $ && j != $) N[i - 0][j - 0][0].set(sum);
        if (i != $ && j != 0) N[i - 0][j - 1][1].set(sum);
        if (i != 0 && j != 0) N[i - 1][j - 1][2].set(sum);
        if (i != 0 && j != $) N[i - 1][j - 0][3].set(sum);
      }
    }
    break;
  }
}

void draw() {
  background(32);
  
  if (showMesh)
    stroke(0);
  else
    noStroke();
  if (trueLights) lights();
  
  beginShape(QUADS);
  for (int i = 0; i < points - 1; i++) {
    for (int j = 0; j < points - 1; j++) {
      for (int k = 0; k < 4; k++) {
        if (trueLights) {
          fill(255);
          normal(N[i][j][k].x, N[i][j][k].y, N[i][j][k].z);
        } else fill(255 * get(hmap, i, j, k));
        vertex(get(P, i, j, k).x, get(P, i, j, k).y, flatten ? 0 : get(P, i, j, k).z);
      }
    }
  }
  endShape();

  if (showNormals) {
    float tileSize = tileScale * (float)height / (points - 1);
    color[] colors = {color(255, 0, 0), color(255, 255, 0), color(0, 255, 0), color(0, 0, 255)};
    for (int i = 0; i < points - 1; i++) {
      for (int j = 0; j < points - 1; j++) {
        for (int k = 0; k < 4; k++) {
          stroke(colors[k]);
          PVector p = get(P, i, j, k), n = N[i][j][k];
          line(p.x, p.y, p.z, p.x + tileSize * n.x, p.y + tileSize * n.y, p.z + tileSize * n.z);
        }
      }
    }
  }
}

void keyPressed() {
  switch (key) {
  case ' ':
    updateMap();
    setLights();
    break;
  case 'm':
    showMesh = !showMesh;
    break;
  case 'l':
    trueLights = !trueLights;
    break;
  case 'c':
    lightingStyle = (lightingStyle + 1) % 4;
    setLights();
    break;
  case 'f':
    flatten = !flatten;
    break;
  case 'n':
    showNormals = !showNormals;
    break;
  }
}

final int[][] coords = {{0, 0}, {0, 1}, {1, 1}, {1, 0}};

float get(float[][] v, int i, int j, int k) {
  return v[i + coords[k][0]][j + coords[k][1]];
}

PVector get(PVector[][] v, int i, int j, int k) {
  return v[i + coords[k][0]][j + coords[k][1]];
}
3 Likes