Resources Numerical accuracy Calculation policy

3D site view scene

Builds the triangles and lines the 3D view draws: the ground with its elevation bands, roads, hardscape, plants, tracks, pipes and structures.

How the precision is kept

Every position is taken relative to a local origin in 64-bit and then stored in Float32Array for the graphics card, with the elevation multiplied by the view's vertical exaggeration. The local origin keeps eastings and northings in the millions from losing digits in 32-bit. These arrays draw the screen; the same arrays are also what the 3D export and the 3D pointer readout use, which is recorded under the 3D site export policy.

Calculations under this policy

4 calculations in 3 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

site3d.js CivilCraft 0.8.1 32-bit, display only 2 calculations Reviewed

Tests bin/test-civilcraft-3d.mjs, bin/test-civilcraft-drape.mjs

Ground mesh with elevation bands

32-bit, display only

The 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.

  • site3d.js
  • lines 565 to 614
  • CivilCraft 0.8.1
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) };
}

Map picture coordinates on the ground

32-bit, display only

Where each ground vertex falls in the map picture draped over it.

u = (x + origin x − min x) ÷ width; v = (y + origin y − min y) ÷ height

Reads the 32-bit local positions, adds the origin back in 64-bit and stores the result in a Float32Array for the graphics card.

  • site3d.js
  • lines 558 to 563
  • CivilCraft 0.8.1
/** 0.4 V3: texture coordinates for a drape over the box { minX, minY, maxX, maxY } (project coordinates): u east, v north. */
export function drapeUVs(positions, O, box) {
  const w = Math.max(box.maxX - box.minX, 1e-9), h = Math.max(box.maxY - box.minY, 1e-9), out = new Float32Array((positions.length / 3) * 2);
  for (let i = 0, j = 0; i < positions.length; i += 3, j += 2) { out[j] = (positions[i] + O.x - box.minX) / w; out[j + 1] = (positions[i + 1] + O.y - box.minY) / h; }
  return out;
}
site3d-landscape.js CivilCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-civilcraft-3d.mjs

Road body mesh

32-bit, display only

The lanes, shoulders, curbs and walks of a corridor as triangles between neighboring sections.

Corridor section points are made local to the scene origin in 64-bit and joined into two triangles per strip; the mesh builder stores them in Float32Array for drawing.

  • site3d-landscape.js
  • lines 122 to 142
  • CivilCraft 0.8.1
export function roadMesh(B, corridors, L, exag, C) {
  let n = 0;
  for (const c of corridors || []) {
    const secs = c.sections || [];
    if (secs.length < 2) continue;
    B.owner({ kind: 'alignment', id: c.id, name: c.name });
    for (let i = 0; i + 1 < secs.length; i++) for (const side of ['L', 'R']) {
      const A = sidePoints(secs[i], side), Bs = sidePoints(secs[i + 1], side);
      const byCode = new Map(Bs.map((q, k) => [q.code, k]));
      for (let k = 1; k < A.length; k++) {
        const part = partOf(A[k].code), kb = byCode.get(A[k].code), kb0 = byCode.get(A[k - 1].code);
        if (!part || kb === undefined || kb0 === undefined) continue;
        const p0 = L(A[k - 1].x, A[k - 1].y, A[k - 1].z), p1 = L(A[k].x, A[k].y, A[k].z), q0 = L(Bs[kb0].x, Bs[kb0].y, Bs[kb0].z), q1 = L(Bs[kb].x, Bs[kb].y, Bs[kb].z);
        const col = rgbOf(C[part]), standing = /^(TOC|BC)/.test(A[k].code);
        face(B, p0, p1, q1, col, !standing); face(B, p0, q1, q0, col, !standing);
        n++;
      }
    }
  }
  return n;
}
site3d-rail.js CivilCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-civilcraft-rail.mjs

Rail mesh

32-bit, display only

Each rail as a swept I-shaped outline along the track's sections.

The outline is placed at each section in 64-bit, with its depth below the rail head kept at true size while the track elevation is exaggerated; the triangles are stored in Float32Array for drawing.

  • site3d-rail.js
  • lines 45 to 64
  • CivilCraft 0.8.1
export function railMesh(B, tracks, L, C) {
  let n = 0;
  const col = rgbOf(C.rail || RAIL_COLORS.rail);
  for (const tr of tracks || []) {
    const S = tr.sections, out = railOutline(tr.rail, tr.units), nT = S[0].track.shape.tracks.length;
    B.owner({ kind: 'alignment', id: tr.id, name: tr.name });
    for (let k = 0; k < nT; k++) for (let j = 0; j < 2; j++) {
      // true size: the head on the (exaggerated) track, the section's depth below it unexaggerated
      const ring = (sec) => { const r = sec.track.shape.tracks[k].rails[j]; return out.map(([o, dz]) => { const q = world(sec, r.o + o, r.z), v = L(q.x, q.y, q.z); v[2] += dz; return v; }); };
      let A = ring(S[0]);
      for (let i = 1; i < S.length; i++) {
        const Bq = ring(S[i]);
        for (let v = 0; v < out.length; v++) { const w = (v + 1) % out.length; face(B, A[v], Bq[w], Bq[v], col); face(B, A[v], A[w], Bq[w], col); }
        A = Bq;
      }
      n++;
    }
  }
  return n;
}

In detail

Method
Indexed triangle meshes with area-weighted vertex normals; triangles cut at the band levels so band edges sit at the levels; texture coordinates over the map box.
Number types
Float64Array while normals are summed; Float32Array for positions, normals, colors and texture coordinates.
Tolerances
The drape box width and height are floored at 1e-9.

Approach: Display only

Computed to draw the screen, often in 32-bit on the graphics processor. No number a person reads, saves or exports comes from it.

Every policy that takes this approach