Resources Numerical accuracy Calculation policy

Smooth shading and texture placement

The normals that make faces joined by soft edges shade as one surface, and where an imported texture lies on a face.

How the precision is kept

These values only change how faces look. Shading normals are 64-bit averages of face normals. A texture's placement is eight coefficients kept as text inside the face's paint, rounded to 7 significant digits.

Calculations under this policy

3 calculations 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/model.js CadCraft 0.8.2 32-bit, display only 3 calculations Reviewed

Tests bin/test-cadcraft-3d.mjs

Smooth shading normals

32-bit, display only

The normal at each corner of faces joined by soft edges.

n = normalize(Σ n_f × area_f) over the joined faces that meet at the corner

Unit face normals are weighted by the faces' outer-loop areas and summed in 64-bit, then normalized. They are used for shading and are written as the normals of exported files.

  • m3d/model.js
  • lines 804 to 834
  • CadCraft 0.8.2
/**
 * Smooth shading (0.4 M18): faces joined by soft edges are one smooth surface. → Map `${fid}|${v}` → the unit normal
 * (definition coordinates) at that corner: the area-weighted average over the faces of its surface that meet at v.
 * Faces with no soft edge are left out (they shade flat).
 */
export function smoothNormals(def) {
  const out = new Map();
  if (![...def.edges.values()].some((e) => e.soft)) return out;
  const ef = edgeFaces(def), up = new Map();
  const find = (x) => { while (up.get(x) !== x) { up.set(x, up.get(up.get(x))); x = up.get(x); } return x; };
  for (const fid of def.faces.keys()) up.set(fid, fid);
  const joined = new Set();
  for (const e of def.edges.values()) {
    if (!e.soft) continue;
    const fs = ef.get(key(e.a, e.b)) || [];
    for (let i = 1; i < fs.length; i++) { up.set(find(fs[i]), find(fs[0])); joined.add(fs[0]); joined.add(fs[i]); }
  }
  const acc = new Map();
  for (const fid of joined) {
    const f = def.faces.get(fid), n = faceNormal(def, f), a = polyArea3(f.loops[0].map((v) => def.verts.get(v))) || 1e-12, r = find(fid);
    for (const loop of f.loops) for (const v of loop) { const k = r + '|' + v; acc.set(k, add(acc.get(k) || [0, 0, 0], mul(n, a))); }
  }
  for (const fid of joined) {
    const f = def.faces.get(fid), r = find(fid), fn = faceNormal(def, f);
    for (const loop of f.loops) for (const v of loop) {
      const s = acc.get(r + '|' + v), L = len(s);
      out.set(fid + '|' + v, L > 1e-12 ? mul(s, 1 / L) : fn);
    }
  }
  return out;
}

Fitting a texture to a face

32-bit, display only

The map from a point on a face to its place on an imported image.

u = U · p + u₀, v = V · p + v₀, with U · n = 0 and V · n = 0

Two 4×4 systems are solved by Gaussian elimination with partial pivoting from the first three corners that give pivots of 1e-12 or more; the pivot test is absolute.

  • m3d/model.js
  • lines 1764 to 1785
  • CadCraft 0.8.2
/**
 * The image paint that puts texture coordinates `uvs` at the points `pts` of a flat face (normal n): the affine map
 * u = U·p + u0 with U·n = 0, solved from the first three points that make a triangle. → paint | null (degenerate).
 */
export function imagePaintFit(id, pts, uvs, n, avg = null) {
  for (let i = 0; i + 2 < pts.length; i++) for (let j = i + 1; j + 1 < pts.length; j++) for (let k = j + 1; k < pts.length; k++) {
    const A = [[...pts[i], 1], [...pts[j], 1], [...pts[k], 1], [n[0], n[1], n[2], 0]];
    const solve = (b) => {   // 4×4 Gaussian elimination with partial pivoting
      const M = A.map((r, q) => [...r, b[q]]);
      for (let c = 0; c < 4; c++) {
        let piv = c; for (let r = c + 1; r < 4; r++) if (Math.abs(M[r][c]) > Math.abs(M[piv][c])) piv = r;
        if (Math.abs(M[piv][c]) < 1e-12) return null;
        [M[c], M[piv]] = [M[piv], M[c]];
        for (let r = 0; r < 4; r++) if (r !== c) { const f = M[r][c] / M[c][c]; for (let q = c; q < 5; q++) M[r][q] -= f * M[c][q]; }
      }
      return M.map((r, q) => r[4] / r[q]);
    };
    const U = solve([uvs[i][0], uvs[j][0], uvs[k][0], 0]), V = solve([uvs[i][1], uvs[j][1], uvs[k][1], 0]);
    if (U && V && [...U, ...V].every(Number.isFinite)) return imagePaint(id, U, V, avg);
  }
  return null;
}

Texture coefficients as stored

32-bit, display only

The eight numbers of a texture's placement as they are kept in the face's paint.

text = x rounded to 7 significant digits

Each coefficient is rounded with toPrecision(7) before it is written into the paint string, so the stored placement carries 7 digits.

  • m3d/model.js
  • line 1756
  • CadCraft 0.8.2
const r7 = (x) => { const v = Number(x.toPrecision(7)); return Object.is(v, -0) ? 0 : v; };

In detail

Method
Area-weighted average of face normals over faces joined by soft edges; affine texture map solved from three corners by 4×4 Gaussian elimination with partial pivoting.
Number types
64-bit numbers; texture coefficients stored as text with 7 significant digits.
Tolerances
A pivot below 1e-12 in the texture solve rejects that choice of three corners. A summed normal shorter than 1e-12 falls back to the face's own normal.

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