Resources Numerical accuracy Calculation policy

Scene handed to the renderer

Turns what the view shows into the renderer's geometry: positions, normals and texture coordinates per color, with material displacement on meshes.

How the precision is kept

Model points are read in 64-bit and written into 32-bit buffers for the renderer. Nothing computed here is written back to the model or shown as a number. Displacement moves mesh points along averaged normals by a height read from a 512 × 512 baked map.

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.

render-mc.js ModelCraft 0.8.1 32-bit, display only 3 calculations Reviewed

Tests bin/test-render.mjs

Render buffers of a definition

32-bit, display only

The triangles of a definition's shown faces as position, normal and texture-coordinate buffers.

Points are copied from the model's 64-bit vertices into Float32BufferAttribute arrays. The buffers are built for the render and disposed after it.

  • render-mc.js
  • lines 139 to 174
  • ModelCraft 0.8.1
/** One definition, one placement paint → [{look, geometry}] per colour, plus its edges. */
function buildDef(def, paint) {
  if (def.mesh) return buildMeshDef(def, paint);
  const by = new Map();
  const smooth = smoothNormals(def);
  for (const [fid, f] of def.faces) {
    if (f.hidden) continue;
    const n = faceNormal(def, f), k = keyOf(f, paint);
    let b = by.get(k);
    if (!b) { b = { pos: [], nor: [], uv: k.startsWith('mat:') || k.startsWith('img:') ? [] : null }; by.set(k, b); }
    const own = k.startsWith('img:') ? (f.color || paint) : null;   // the image's placement rides in the paint
    for (const tri of faceTris(def, f)) for (const v of tri) {
      const p = def.verts.get(v), q = smooth.get(fid + '|' + v) || n;
      b.pos.push(p[0], p[1], p[2]); b.nor.push(q[0], q[1], q[2]);
      if (b.uv) { const w = own ? imageUV(own, p) : planarUV(p, n); b.uv.push(w[0], w[1]); }   // image: its own placement; material: metres along the face
    }
  }
  const meshes = [];
  for (const [look, b] of by) {
    if (!b.pos.length) continue;
    const g = new THREE.BufferGeometry();
    g.setAttribute('position', new THREE.Float32BufferAttribute(b.pos, 3));
    g.setAttribute('normal', new THREE.Float32BufferAttribute(b.nor, 3));
    if (b.uv) g.setAttribute('uv', new THREE.Float32BufferAttribute(b.uv, 2));
    meshes.push({ look, geometry: g });
  }
  const ep = [];
  for (const e of def.edges.values()) {
    if (e.soft || e.hidden) continue;
    const a = def.verts.get(e.a), c = def.verts.get(e.b);
    ep.push(a[0], a[1], a[2], c[0], c[1], c[2]);
  }
  let edges = null;
  if (ep.length) { edges = new THREE.BufferGeometry(); edges.setAttribute('position', new THREE.Float32BufferAttribute(ep, 3)); }
  return { meshes, edges };
}

Height from a baked displacement map

32-bit, display only

The displacement height at a texture coordinate.

bilinear blend of the four texels round (u, v), wrapping at the edges, read at texel centers

The map is a 32-bit array of 512 × 512 values; the blend is computed in 64-bit and used only to move render points.

  • render-mc.js
  • lines 69 to 74
  • ModelCraft 0.8.1
function heightAt(D, u, v) {   // bilinear, wrapping, texel centres (as the kernels read layers)
  const fx = (u - Math.floor(u)) * D.n - 0.5, fy = (v - Math.floor(v)) * D.n - 0.5, x0 = Math.floor(fx), y0 = Math.floor(fy), tx = fx - x0, ty = fy - y0;
  let s = 0;
  for (let j = 0; j < 2; j++) for (let i = 0; i < 2; i++) s += (i ? tx : 1 - tx) * (j ? ty : 1 - ty) * D.h[(((y0 + j) % D.n + D.n) % D.n) * D.n + (((x0 + i) % D.n + D.n) % D.n)];
  return s;
}

Displaced mesh point

32-bit, display only

Moves a mesh point along its averaged normal by the material's height times its scale, for the render.

p′ = p + n̂ × height(u, v) × scale

The normal is the sum of the corner normals at the vertex divided by its length (a zero length is replaced by 1). The moved point goes into the 32-bit render buffer; the model's own points are not changed.

  • render-mc.js
  • lines 102 to 114
  • ModelCraft 0.8.1
if (D) {
  const l = Math.hypot(vn[v], vn[v + 1], vn[v + 2]) || 1, n = [vn[v] / l, vn[v + 1] / l, vn[v + 2] / l];
  let tu, tv;
  if (D.mapping === MAPPING.UV) { tu = w[0] / D.tile[0]; tv = w[1] / D.tile[1]; }
  else {
    // box: the vertex normal's main axis (Z is up, RENDER-SPEC § 6.6)
    const ax = Math.abs(n[1]) > Math.abs(n[0]) ? (Math.abs(n[2]) > Math.abs(n[1]) ? 2 : 1) : Math.abs(n[2]) > Math.abs(n[0]) ? 2 : 0;
    const [iu, iv] = ax === 2 ? [0, 1] : [3 - ax - 2, 2];
    tu = p[iu] / D.tile[0]; tv = p[iv] / D.tile[1];
  }
  const d = heightAt(D, tu, tv) * D.scale;
  p = [p[0] + n[0] * d, p[1] + n[1] * d, p[2] + n[2] * d];
}

In detail

Method
One three.js BufferGeometry per definition, placement paint and look; bilinear, wrapping lookup of the baked height map.
Number types
64-bit model points into Float32 buffer attributes; the baked height map is 32-bit.
Limits
For drawing only. The picture's geometry has 32-bit precision relative to each definition's own origin.

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