@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]];
}