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.
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();
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;
}
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;
}
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.