Resources Numerical accuracy Calculation policy

Rendered pictures

Draws still pictures and video frames of the model: the fast render, the path-traced render and the textures of the material library.

How the precision is kept

This code produces pixels. Geometry is handed to the graphics card in 32-bit buffers, light is summed in half-float or 32-bit float render targets, and the result is tone mapped to 8-bit PNG. No length, area or quantity is read from it; the one number it reports, the samples per pixel of an adaptive render, is recorded under its own policy.

Calculations under this policy

4 calculations in 4 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.

render/render-core.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Camera jitter per pass

32-bit, display only

Moves the camera by less than a pixel each pass so that the sum of passes is anti-aliased.

j_x = (h₂(i) − ½) × 2 ÷ w; rows 0 and 1 of the projection += j × row 3

Halton points in 64-bit shift the projection matrix; the passes are then added in a half-float render target and divided by the number of passes when shown.

  • render/render-core.js
  • lines 324 to 329
  • BimCraft 0.8.1
const a = halton(i + 1, 2), b = halton(i + 1, 3);
// camera: under a pixel; rows 0 and 1 of the projection += jitter × row 3 (works for both projections)
const jx = (a - 0.5) * 2 / w, jy = (b - 0.5) * 2 / h, e = P0.slice();
for (let k = 0; k < 4; k++) { e[k * 4] += jx * e[k * 4 + 3]; e[k * 4 + 1] += jy * e[k * 4 + 3]; }
cam.projectionMatrix.fromArray(e);
cam.projectionMatrixInverse.copy(cam.projectionMatrix).invert();
render/render-pt.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Section cut for the path tracer

32-bit, display only

Clips every triangle of the scene against the section planes before the picture is traced.

new corner = a + (b − a) × d_a ÷ (d_a − d_b)

Vertices are read from the drawn mesh, clipped in 64-bit and written back as Float32 attributes for the renderer. The cut triangles are used for this picture only and are not stored in the model.

  • render/render-pt.js
  • lines 40 to 83
  • BimCraft 0.8.1
/** Every mesh item baked to world space (drawRange and index applied), cut by the planes (keep n·p ≤ d); edges pass through. */
export function bakeItems(items, planes = []) {
  planes = planes || [];
  const out = [];
  const inside = (p, pl) => pl.n[0] * p[0] + pl.n[1] * p[1] + pl.n[2] * p[2] - pl.d;
  for (const it of items) {
    if (it.lines) { out.push(it); continue; }
    const g = it.geometry, P = g.getAttribute('position'), N = g.getAttribute('normal'), U = g.getAttribute('uv');
    const M = it.matrix ? new THREE.Matrix4().fromArray(it.matrix) : new THREE.Matrix4(), Nm = new THREE.Matrix3().getNormalMatrix(M);
    const idx = g.index ? g.index.array : null, start = g.drawRange.start || 0;
    const count = Math.min(idx ? idx.length : P.count, g.drawRange.count === Infinity ? Infinity : start + g.drawRange.count) - start;
    const v = new THREE.Vector3(), n = new THREE.Vector3();
    const vert = (i) => {
      v.fromBufferAttribute(P, i).applyMatrix4(M);
      n.fromBufferAttribute(N, i).applyMatrix3(Nm).normalize();
      return { p: v.toArray(), n: n.toArray(), uv: U ? [U.getX(i), U.getY(i)] : null };
    };
    const lerp = (a, b, f) => ({ p: a.p.map((x, k) => x + (b.p[k] - x) * f), n: a.n.map((x, k) => x + (b.n[k] - x) * f), uv: a.uv ? a.uv.map((x, k) => x + (b.uv[k] - x) * f) : null });
    const pos = [], nor = [], uv = [];
    for (let t = start; t + 2 < start + count; t += 3) {
      let poly = [0, 1, 2].map((k) => vert(idx ? idx[t + k] : t + k));
      for (const pl of planes) {
        const d = poly.map((q) => inside(q.p, pl));
        if (d.every((x) => x <= 0)) continue;
        const next = [];
        for (let i = 0; i < poly.length; i++) {
          const a = poly[i], b = poly[(i + 1) % poly.length], da = d[i], db = d[(i + 1) % poly.length];
          if (da <= 0) next.push(a);
          if ((da <= 0) !== (db <= 0)) next.push(lerp(a, b, da / (da - db)));
        }
        poly = next;
        if (poly.length < 3) break;
      }
      for (let i = 1; i + 1 < poly.length; i++) for (const q of [poly[0], poly[i], poly[i + 1]]) { pos.push(...q.p); nor.push(...q.n); uv.push(...(q.uv || [0, 0])); }
    }
    if (!pos.length) continue;
    const ng = new THREE.BufferGeometry();
    ng.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
    ng.setAttribute('normal', new THREE.Float32BufferAttribute(nor, 3));
    ng.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
    out.push({ ...it, geometry: ng, matrix: null, _own: true });
  }
  return out;
}
render/textures.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-render.mjs

Tiling fractal noise

32-bit, display only

The noise value behind the library's brick, wood, stone and concrete textures.

Σ ampₒ × noise(u fₒ, v fₒ) ÷ Σ ampₒ, f doubling and amp × gain per octave

Computed in 64-bit from 32-bit integer hashes, then stored as 8-bit color and 32-bit height and roughness pixels for the graphics card.

  • render/textures.js
  • lines 30 to 35
  • BimCraft 0.8.1
/** Fractal noise over the unit square (u, v in 0…1), tiling; `base` cells across at the first octave. 0…1. */
export function fbm(u, v, base, octaves, seed, gain = 0.5) {
  let s = 0, amp = 1, norm = 0, f = base;
  for (let o = 0; o < octaves; o++) { s += amp * vnoise(u * f, v * f, f, seed + o * 101); norm += amp; amp *= gain; f *= 2; }
  return s / norm;
}
render/three-mats.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Normal map bytes

32-bit, display only

Turns a baked material's normal layer into the texture three.js reads.

x = (byte − 128) ÷ 127; z = √(1 − x² − y²); out = (x ÷ 2 + ½) × 255

Bytes in, bytes out. The square root's argument is clamped at 0 and the result is stored in a Uint8ClampedArray, which rounds it.

  • render/three-mats.js
  • lines 66 to 67
  • BimCraft 0.8.1
const x = Math.max(-1, Math.min(1, (L.data[i * 4] - 128) / 127)), y = Math.max(-1, Math.min(1, (L.data[i * 4 + 1] - 128) / 127)), z = Math.sqrt(Math.max(0, 1 - x * x - y * y));
d[i * 4] = (x * 0.5 + 0.5) * 255; d[i * 4 + 1] = (y * 0.5 + 0.5) * 255; d[i * 4 + 2] = (z * 0.5 + 0.5) * 255; d[i * 4 + 3] = 255;

In detail

Method
Fast render: accumulation of jittered passes with three.js. Path tracing: a WGSL tracer where WebGPU is present, else three-gpu-pathtracer 0.0.26. Library textures: seeded value noise, the same pixels for the same id and size.
Number types
Float32 vertex buffers, half-float and 32-bit float render targets, Uint8 pixels; 64-bit numbers for the set-up arithmetic.
Limits
Colors and light levels in a picture are for looking at; they are not measured illuminance.

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