Resources Numerical accuracy Calculation policy

Polygon mesh: shading normals

The normal at each face corner that makes a mesh shade smooth or faceted on screen.

How the precision is kept

Face normals are computed in 64-bit and averaged round each vertex, then written to a Float32Array of unit vectors for drawing. They are shading data and take no part in a measurement.

Calculations under this policy

1 calculation in 1 source file. 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.

m3d/mesh.js CadCraft 0.8.2 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Normal per corner

32-bit, display only

A unit normal for every corner of every face.

n = normalize(Σ N_f) over the faces of a smooth group

The unnormalized Newell vectors (length twice the face area) are summed, so larger faces weigh more; the sum is normalized and stored in a Float32Array. A custom 'normal' layer is copied instead when the mesh has one.

  • m3d/mesh.js
  • lines 573 to 615
  • CadCraft 0.8.2
/**
 * A unit normal per corner (Float32Array, 3 per half-edge slot): the 'normal' layer when there is one, else the faces
 * round each vertex averaged by area, split where an edge is sharp (FL_SHARP) — or every corner its face's own normal
 * when `flat`.
 */
cornerNormals({ flat = false } = {}) {
  const out = new Float32Array(this.nh * 3);
  const custom = this.attrs.get('normal');
  const fn = new Float64Array(this.nf * 3), t = [0, 0, 0];
  for (let f = 0; f < this.nf; f++) { if (this.faceDead(f)) continue; this.faceNormalRaw(f, t); fn[f * 3] = t[0]; fn[f * 3 + 1] = t[1]; fn[f * 3 + 2] = t[2]; }
  const hF = this.hF, hFl = this.hFl;
  if (custom && custom.domain === 'corner' && custom.size === 3) {
    for (let h = 0; h < this.nh; h++) { if (hFl[h] & FL_DEAD) continue; for (let k = 0; k < 3; k++) out[h * 3 + k] = custom.col.a[h * 3 + k]; }
    return out;
  }
  const unit = (o, x, y, z) => { const l = Math.hypot(x, y, z) || 1; out[o] = x / l; out[o + 1] = y / l; out[o + 2] = z / l; };
  if (flat) {
    for (let h = 0; h < this.nh; h++) { if (hFl[h] & FL_DEAD) continue; const f = hF[h] * 3; unit(h * 3, fn[f], fn[f + 1], fn[f + 2]); }
    return out;
  }
  for (let v = 0; v < this.nv; v++) {
    if (this.vertDead(v)) continue;
    const n = this._walk(v), r = this._ring;
    if (!n) continue;
    // start a group just after a sharp edge when the fan is closed, so no group wraps round the start
    let s = 0;
    const closed = this.hTw[r[n - 1]] >= 0;
    if (closed) for (let i = 0; i < n; i++) if (hFl[r[i]] & FL_SHARP) { s = (i + 1) % n; break; }
    let i0 = 0;
    while (i0 < n) {
      let x = 0, y = 0, z = 0, i1 = i0;
      for (;;) {
        const h = r[(s + i1) % n], f = hF[h] * 3;
        x += fn[f]; y += fn[f + 1]; z += fn[f + 2];
        i1++;
        if (i1 >= n || hFl[h] & FL_SHARP) break;
      }
      for (let i = i0; i < i1; i++) unit(r[(s + i) % n] * 3, x, y, z);
      i0 = i1;
    }
  }
  return out;
}

In detail

Method
Area-weighted average of the Newell normals of the faces round a vertex, split at sharp edges.
Number types
Float64Array for the face normals; Float32Array for the result.

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