Resources Numerical accuracy Calculation policy

Rendered picture: sample sums and color tables

Adds up the light samples of a CPU render into the picture and applies .cube color tables to it.

How the precision is kept

Draws pictures only. Tile results, the accumulation buffer and the color tables are Float32Array; samples are added in tile order so the picture is the same whatever the number of workers. A color table written to a .cube file is printed with seven significant digits.

Calculations under this policy

5 calculations in 2 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.

compute/cpu-render.js BimCraft 0.8.1 32-bit, display only 2 calculations Reviewed

Tests bin/test-compute.mjs

Adding tile results to the picture

32-bit, display only

The sums of red, green, blue and sample count of every pixel after a pass.

out[pixel] += tile[pixel]

32-bit sums in a Float32Array, added tile by tile in a fixed order so the result does not depend on which worker rendered which tile.

  • compute/cpu-render.js
  • lines 63 to 70
  • BimCraft 0.8.1
// add in tile order: the same sums whatever ran where
for (let i = 0; i < tiles; i++) {
  const [x0, y0, w, h] = tileRect(i), part = cur.parts.get(i);
  for (let y = 0; y < h; y++) {
    const src = y * w * 4, dst = ((y0 + y) * W + x0) * 4;
    for (let k = 0; k < w * 4; k++) out[dst + k] += part[src + k];
  }
}

Mean radiance of each pixel

32-bit, display only

The picture as the average of its samples.

color = Σ ÷ n

Each 32-bit sum is divided by the 32-bit sample count; a count of 0 is replaced by 1. The result is stored in a Float32Array.

  • compute/cpu-render.js
  • lines 91 to 94
  • BimCraft 0.8.1
image(out = new Float32Array(W * H * 3)) {
  for (let i = 0; i < W * H; i++) { const n = accum[i * 4 + 3] || 1; out[i * 3] = accum[i * 4] / n; out[i * 3 + 1] = accum[i * 4 + 1] / n; out[i * 3 + 2] = accum[i * 4 + 2] / n; }
  return out;
},
exr/cube.js BimCraft 0.8.1 32-bit, display only 3 calculations Reviewed

Tests bin/test-render.mjs

Color through a .cube table

32-bit, display only

A color after the table's 1D and 3D look-ups.

x = clamp((r − lo) ÷ (hi − lo)); trilinear: c = c₀ + (c₁ − c₀) × d along each axis in turn

The table entries are 32-bit; positions and weights are computed in 64-bit. A NaN position is set to 0 and the index is capped so the upper neighbor stays inside the table.

  • exr/cube.js
  • lines 119 to 178
  • BimCraft 0.8.1
export function applyCube(lut, r, g, b, out = [0, 0, 0], opts = {}) {
  const { domainMin: lo, domainMax: hi } = lut;
  let x = clamp01((r - lo[0]) / (hi[0] - lo[0]));
  let y = clamp01((g - lo[1]) / (hi[1] - lo[1]));
  let z = clamp01((b - lo[2]) / (hi[2] - lo[2]));
  if (Number.isNaN(x)) x = 0;
  if (Number.isNaN(y)) y = 0;
  if (Number.isNaN(z)) z = 0;
  if (lut.lut1d) {
    const t = lut.lut1d;
    const n = lut.size1d - 1;
    const one = (v, c) => {
      const f = v * n;
      const i = Math.min(n - 1, Math.floor(f));
      const u = f - i;
      return t[i * 3 + c] * (1 - u) + t[(i + 1) * 3 + c] * u;
    };
    x = one(x, 0); y = one(y, 1); z = one(z, 2);
    if (!lut.lut3d) { out[0] = x; out[1] = y; out[2] = z; return out; }
    // the shaper's output is the 3D table's input, in the same domain
    x = clamp01(x); y = clamp01(y); z = clamp01(z);
  }
  const t = lut.lut3d;
  const n = lut.size3d;
  const m = n - 1;
  const fx = x * m; const fy = y * m; const fz = z * m;
  const ix = Math.min(m - 1, Math.floor(fx)); const iy = Math.min(m - 1, Math.floor(fy)); const iz = Math.min(m - 1, Math.floor(fz));
  const dx = fx - ix; const dy = fy - iy; const dz = fz - iz;
  const at = (i, j, k) => ((k * n + j) * n + i) * 3;
  for (let c = 0; c < 3; c++) {
    const c000 = t[at(ix, iy, iz) + c];
    const c111 = t[at(ix + 1, iy + 1, iz + 1) + c];
    if (opts.tetrahedral) {
      let v;
      if (dx > dy) {
        if (dy > dz) v = c000 + dx * (t[at(ix + 1, iy, iz) + c] - c000) + dy * (t[at(ix + 1, iy + 1, iz) + c] - t[at(ix + 1, iy, iz) + c]) + dz * (c111 - t[at(ix + 1, iy + 1, iz) + c]);
        else if (dx > dz) v = c000 + dx * (t[at(ix + 1, iy, iz) + c] - c000) + dz * (t[at(ix + 1, iy, iz + 1) + c] - t[at(ix + 1, iy, iz) + c]) + dy * (c111 - t[at(ix + 1, iy, iz + 1) + c]);
        else v = c000 + dz * (t[at(ix, iy, iz + 1) + c] - c000) + dx * (t[at(ix + 1, iy, iz + 1) + c] - t[at(ix, iy, iz + 1) + c]) + dy * (c111 - t[at(ix + 1, iy, iz + 1) + c]);
      } else if (dz > dy) v = c000 + dz * (t[at(ix, iy, iz + 1) + c] - c000) + dy * (t[at(ix, iy + 1, iz + 1) + c] - t[at(ix, iy, iz + 1) + c]) + dx * (c111 - t[at(ix, iy + 1, iz + 1) + c]);
      else if (dz > dx) v = c000 + dy * (t[at(ix, iy + 1, iz) + c] - c000) + dz * (t[at(ix, iy + 1, iz + 1) + c] - t[at(ix, iy + 1, iz) + c]) + dx * (c111 - t[at(ix, iy + 1, iz + 1) + c]);
      else v = c000 + dy * (t[at(ix, iy + 1, iz) + c] - c000) + dx * (t[at(ix + 1, iy + 1, iz) + c] - t[at(ix, iy + 1, iz) + c]) + dz * (c111 - t[at(ix + 1, iy + 1, iz) + c]);
      out[c] = v;
      continue;
    }
    const c100 = t[at(ix + 1, iy, iz) + c];
    const c010 = t[at(ix, iy + 1, iz) + c];
    const c110 = t[at(ix + 1, iy + 1, iz) + c];
    const c001 = t[at(ix, iy, iz + 1) + c];
    const c101 = t[at(ix + 1, iy, iz + 1) + c];
    const c011 = t[at(ix, iy + 1, iz + 1) + c];
    const c00 = c000 + (c100 - c000) * dx;
    const c10 = c010 + (c110 - c010) * dx;
    const c01 = c001 + (c101 - c001) * dx;
    const c11 = c011 + (c111 - c011) * dx;
    const c0 = c00 + (c10 - c00) * dy;
    const c1 = c01 + (c11 - c01) * dy;
    out[c] = c0 + (c1 - c0) * dz;
  }
  return out;
}

Table rows read from a .cube file

32-bit, display only

The entries of a color table as they are held in memory.

Each row's three numbers are converted with Number() and stored in a Float32Array, so the table is held in 32-bit.

  • exr/cube.js
  • lines 65 to 70
  • BimCraft 0.8.1
  rows = new Float32Array(need * 3);
}
const v = s.split(/\s+/).map(num);
if (v.length !== 3 || v.some(Number.isNaN)) throw dmg('ccexr.cube.line', { line: i + 1 });
if (have >= need) throw dmg('ccexr.cube.count', { have: have + 1, want: need });
rows[have * 3] = v[0]; rows[have * 3 + 1] = v[1]; rows[have * 3 + 2] = v[2];

Numbers written to a .cube file

32-bit, display only

A table entry as text in a saved color table.

text = Number(v.toPrecision(7))

Every value is written with seven significant digits, about what a 32-bit number holds. Zero and negative zero are written as 0.

  • exr/cube.js
  • lines 84 to 88
  • BimCraft 0.8.1
const fmt = (v) => {
  if (Object.is(v, -0) || v === 0) return '0';
  const s = v.toPrecision(7);
  return String(Number(s));
};

In detail

Method
Pixel = Σ samples ÷ n. Color table: trilinear interpolation between the eight surrounding entries, or tetrahedral on request; a 1D table is applied first when the file has one. The reader follows the Cube LUT Specification version 1.0, as the file's header says.
Number types
Float32Array buffers and tables; interpolation weights are 64-bit numbers.
Limits
Inputs outside the table's domain are clamped to it.

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