Resources Numerical accuracy Calculation policy

Path tracer kernels

Follows light paths through the packed scene to work out each pixel of a high-quality render.

How the precision is kept

The reference tracer on the processor works in 64-bit and adds samples into a Float64Array; the graphics-card kernels are the same steps in 32-bit. Both read the 32-bit packed scene and produce only a picture.

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/pt-ref.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-render.mjs

Adding samples to a pixel

32-bit, display only

The sum of sample colors and the sample count of every pixel in the reference tracer.

Sums are kept in a Float64Array. A sample with a channel that is not finite is counted but not added; negative channels are clamped to 0.

  • render/pt-ref.js
  • lines 580 to 600
  • BimCraft 0.8.1
function render({ spp = 1, s0 = 0, accum = null, aux = null, x0 = 0, y0 = 0, x1 = w, y1 = h, active = null } = {}) {   // active (RN.7, pt-adapt.js): 0 = this pixel gets no sample
  accum = accum || new Float64Array(w * h * 4);
  if (STRIDE) aux = aux || new Float64Array(w * h * STRIDE * 4);
  for (let s = s0; s < s0 + spp; s++) {
    for (let py = y0; py < y1; py++) for (let px = x0; px < x1; px++) {
      if (active && !active[py * w + px]) continue;
      const c = radiance(px, py, s), o = (py * w + px) * 4;
      const fine = Number.isFinite(c[0]) && Number.isFinite(c[1]) && Number.isFinite(c[2]);
      if (fine) { accum[o] += Math.max(0, c[0]); accum[o + 1] += Math.max(0, c[1]); accum[o + 2] += Math.max(0, c[2]); }
      accum[o + 3] += 1;
      if (STRIDE && fine) {
        const b = (py * w + px) * STRIDE * 4;
        for (let i = 0; i < STRIDE * 4; i++) {
          if (lit && i >= EL.IDS * 4 && i < EL.IDS * 4 + 4) { if (s === 0) aux[b + i] = AX[i]; continue; }
          aux[b + i] += Number.isFinite(AX[i]) ? AX[i] : 0;
        }
      }
    }
  }
  return STRIDE ? Object.assign(accum, { aux }) : accum;
}
render/pt-ggx.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-render.mjs

Fresnel reflectance of glass

32-bit, display only

The share of light a clear surface reflects at an angle.

F = (r∥² + r⊥²) ÷ 2

64-bit. The cosine is clamped to [−1, 1], and total internal reflection returns 1 before the square root is taken.

  • render/pt-ggx.js
  • lines 29 to 37
  • BimCraft 0.8.1
export function frDielectric(cosI, eta) {
  let c = Math.min(1, Math.max(-1, cosI));
  if (c < 0) { eta = 1 / eta; c = -c; }
  const s2 = 1 - c * c, t2 = s2 / (eta * eta);
  if (t2 >= 1) return 1;
  const ct = Math.sqrt(1 - t2);
  const rpa = (eta * c - ct) / (eta * c + ct), rpe = (c - eta * ct) / (c + eta * ct);
  return (rpa * rpa + rpe * rpe) / 2;
}
render/pt-sampler.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-render.mjs

Sample position

32-bit, display only

The random-like number in [0, 1) used for one dimension of one sample of one pixel.

Worked out in 32-bit unsigned integer arithmetic with Math.imul and shifts, so the processor and the graphics card get the same bits. The result is the top 24 bits divided by 2²⁴ (u32ToUnit in pt-spec.js).

  • render/pt-sampler.js
  • lines 76 to 88
  • BimCraft 0.8.1
/**
 * The sample for pixel (px, py), sample index s, dimension d (RENDER-SPEC § 4). d < BN_LIMIT: an additive recurrence (R2 for
 * the 2-D dimensions, the golden ratio for the 1-D ones: alphaFor) rotated by blue noise, in 32-bit fixed point; else a PCG hash.
 */
export function sample(mask, px, py, s, d) {
  if (d < DIMS.BN_LIMIT) {
    const q = d >> 2, ox = (13 * q) & 63, oy = (29 * q) & 63;
    const bn = mask[((((py + oy) & 63) * 64 + ((px + ox) & 63)) << 2) + (d & 3)];
    const off = ((bn << 24) | (hash4(px, py, d, 0x5bd1e995) & 0xffffff)) >>> 0;
    return u32ToUnit((Math.imul(s >>> 0, alphaFor(d)) + off) >>> 0);
  }
  return u32ToUnit(hash4(px, py, s, d));
}
render/pt-adapt.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-render.mjs

Adaptive sampling test

32-bit, display only

Decides when a pixel has had enough samples.

v = max(0, (S₂ − S₁² ÷ J) ÷ (J − 1)); stop when √(v ÷ J) ≤ noise × (μ + 0.02)

The running sums are in a Float64Array. S₂ − S₁² ÷ J subtracts nearly equal numbers when the passes agree, and the result is floored at 0 before the square root.

  • render/pt-adapt.js
  • lines 21 to 44
  • BimCraft 0.8.1
export function makeAdaptive(w, h, { noise = 0.02, min = 16 } = {}) {
  const n = w * h, active = new Uint8Array(n).fill(1), S = new Float64Array(n * 4);   // S1, S2, Y before, N before
  const j0 = minPasses(min);
  let count = n;
  return {
    active, noise, min, w, h,
    get count() { return count; },
    update(accum) {
      for (let i = 0; i < n; i++) {
        if (!active[i]) continue;
        const o = i * 4, N = accum[o + 3], Y = 0.2722 * accum[o] + 0.6741 * accum[o + 1] + 0.0537 * accum[o + 2], dn = N - S[o + 3];
        if (!(dn > 0)) continue;
        const m = (Y - S[o + 2]) / dn;
        S[o] += m; S[o + 1] += m * m; S[o + 2] = Y; S[o + 3] = N;
        const J = Math.round(N / ADAPT.PASS);
        if (J >= j0) {
          const mu = S[o] / J, v = Math.max(0, (S[o + 1] - (S[o] * S[o]) / J) / (J - 1)), se = Math.sqrt(v / J);
          if (se <= noise * (mu + ADAPT.FLOOR)) { active[i] = 0; count--; }
        }
      }
      return count;
    },
  };
}

In detail

Method
Path tracing with next-event estimation and multiple importance sampling, the OpenPBR surface model, a blue-noise rotated low-discrepancy sampler in 32-bit fixed point, and adaptive sampling by the standard error of pass means. Sources are cited in each file's header.
Number types
64-bit numbers and Float64Array in the reference tracer; 32-bit floats on the graphics card; 32-bit unsigned integer arithmetic for sample positions.
Tolerances
Adaptive sampling stops a pixel when the standard error is at most noise × (mean + 0.02), after at least 4 passes of 4 samples.

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