Ground mesh with elevation bands
32-bit, display onlyThe triangles, normals and colors of a ground surface, cut at the elevation band levels.
position = (x + dx, y + dy, (z − origin z) × exaggeration); normal = normalized sum of face normals
Normals are summed in a Float64Array; positions are computed in 64-bit relative to the scene origin and then stored in Float32Array. Triangles that span a band level are clipped at the true elevation of the level.
export function surfaceMesh(s, O, exag, bands, single) {
const { X, Y, Z, tris } = s;
const nV = X.length;
const dx = s.ox - O.x, dy = s.oy - O.y;
// smooth normals: area-weighted face normals, faces turned to face up
const N = new Float64Array(nV * 3);
for (let t = 0; t < tris.length; t += 3) {
const a = tris[t], b = tris[t + 1], c = tris[t + 2];
const ux = X[b] - X[a], uy = Y[b] - Y[a], uz = (Z[b] - Z[a]) * exag, vx = X[c] - X[a], vy = Y[c] - Y[a], vz = (Z[c] - Z[a]) * exag;
let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
if (nz < 0) { nx = -nx; ny = -ny; nz = -nz; }
for (const v of [a, b, c]) { N[3 * v] += nx; N[3 * v + 1] += ny; N[3 * v + 2] += nz; }
}
for (let v = 0; v < nV; v++) { const l = Math.hypot(N[3 * v], N[3 * v + 1], N[3 * v + 2]) || 1; N[3 * v] /= l; N[3 * v + 1] /= l; N[3 * v + 2] /= l; if (l === 1 && !N[3 * v + 2]) N[3 * v + 2] = 1; }
const pos = [], nor = [], col = [], idx = [];
const upCCW = (i0, i1, i2) => { const ux = pos[3 * i1] - pos[3 * i0], uy = pos[3 * i1 + 1] - pos[3 * i0 + 1], vx = pos[3 * i2] - pos[3 * i0], vy = pos[3 * i2 + 1] - pos[3 * i0 + 1]; return ux * vy - uy * vx >= 0; };
const tri = (i0, i1, i2) => { if (upCCW(i0, i1, i2)) idx.push(i0, i1, i2); else idx.push(i0, i2, i1); };
const push = (x, y, z, nx, ny, nz, c) => { pos.push(x + dx, y + dy, (z - O.z) * exag); nor.push(nx, ny, nz); col.push(c[0], c[1], c[2]); return pos.length / 3 - 1; };
if (!bands) {
const map = new Int32Array(nV).fill(-1);
for (let t = 0; t < tris.length; t += 3) {
const ids = [0, 1, 2].map((k) => { const v = tris[t + k]; if (map[v] < 0) map[v] = push(X[v], Y[v], Z[v], N[3 * v], N[3 * v + 1], N[3 * v + 2], single); return map[v]; });
tri(ids[0], ids[1], ids[2]);
}
} else {
const { levels, colors } = bands;
const band = (z) => { let b = 0; while (b < levels.length && z > levels[b]) b++; return b; };
const shared = new Map();
for (let t = 0; t < tris.length; t += 3) {
const vs = [tris[t], tris[t + 1], tris[t + 2]];
const bs = vs.map((v) => band(Z[v]));
const lo = Math.min(...bs), hi = Math.max(...bs);
if (lo === hi) {
const ids = vs.map((v) => { const k = v * 128 + lo; let i = shared.get(k); if (i === undefined) { i = push(X[v], Y[v], Z[v], N[3 * v], N[3 * v + 1], N[3 * v + 2], colors[lo]); shared.set(k, i); } return i; });
tri(ids[0], ids[1], ids[2]);
continue;
}
const poly0 = vs.map((v) => [X[v], Y[v], Z[v], N[3 * v], N[3 * v + 1], N[3 * v + 2]]);
for (let b = lo; b <= hi; b++) {
let poly = poly0;
if (b > 0) poly = clipZ(poly, levels[b - 1], 1);
if (b < levels.length) poly = clipZ(poly, levels[b], -1);
if (poly.length < 3) continue;
const ids = poly.map((q) => { const l = Math.hypot(q[3], q[4], q[5]) || 1; return push(q[0], q[1], q[2], q[3] / l, q[4] / l, q[5] / l, colors[b]); });
for (let k = 1; k + 1 < ids.length; k++) tri(ids[0], ids[k], ids[k + 1]);
}
}
}
return { positions: new Float32Array(pos), normals: new Float32Array(nor), colors: new Float32Array(col), indices: new Uint32Array(idx) };
}