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.
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;
},
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.