Finds where a ray meets the triangles of a scene, used for walking through a model and for dropping scattered objects onto surfaces.
How the precision is kept
The triangles are stored as Float32Array records (vertex plus two edge vectors) and the tree boxes are rounded outward to 32-bit. The ray and the intersection arithmetic are 64-bit JavaScript numbers, so a hit distance t is computed in 64-bit from vertices that were already rounded to 32 bits.
Open item. Where a hit height or distance is stored or reported (walk floor height, scatter placement), take it from the 64-bit source mesh or re-test the found triangle in 64-bit.
Calculations under this policy
6 calculations in 3 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/bvh-core.jsBimCraft 0.8.1 32-bit in part3 calculationsReviewed
Tests bin/test-compute.mjs
Round a box edge outward to 32-bit
32-bit in part
A 32-bit number that is never above (down) or never below (up) the 64-bit value.
Writes the value into a one-element Float32Array, which rounds to nearest, and if that is above x steps the bit pattern one place toward minus infinity. It keeps the stored 32-bit box around the 64-bit geometry, but the stored edge itself is a 32-bit value. f32up is the mirror image.
compute/bvh-core.js
lines 35 to 42
BimCraft 0.8.1
export function f32down(x) {
F[0] = x;
if (F[0] <= x || !Number.isFinite(x)) return F[0];
// F[0] > x: step one ulp toward −∞
if (F[0] === 0) return -1.401298464324817e-45;
U[0] += F[0] > 0 ? -1 : 1;
return F[0];
}
Ray and triangle
32-bit in part
Whether a ray meets a triangle and at what distance t and barycentric u, v.
t = (e2 · (s × e1)) ÷ (e1 · (d × e2))
The vertex and edge vectors come from the Float32Array record, everything else is 64-bit. It returns false for |det| < 1e-12 and has no tolerance on u and v, so a ray through a shared edge depends on rounding.
compute/bvh-core.js
lines 310 to 329
BimCraft 0.8.1
/** Möller–Trumbore, double-sided; tris are RENDER-SPEC § 6.2 records (in original triangle order). */
export function intersectTri(tris, i, ox, oy, oz, dx, dy, dz, tmin, tmax, out) {
const k = i * 12;
const e1x = tris[k + 4], e1y = tris[k + 5], e1z = tris[k + 6];
const e2x = tris[k + 8], e2y = tris[k + 9], e2z = tris[k + 10];
const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x;
const det = e1x * px + e1y * py + e1z * pz;
if (det > -1e-12 && det < 1e-12) return false;
const inv = 1 / det;
const sx = ox - tris[k], sy = oy - tris[k + 1], sz = oz - tris[k + 2];
const u = (sx * px + sy * py + sz * pz) * inv;
if (u < 0 || u > 1) return false;
const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
const v = (dx * qx + dy * qy + dz * qz) * inv;
if (v < 0 || u + v > 1) return false;
const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
if (!(t > tmin && t < tmax)) return false;
if (out) { out.t = t; out.u = u; out.v = v; }
return true;
}
Ray and box slab test
64-bit
The distance at which a ray enters a box, or Infinity.
t = (box − o) ÷ d, max of the near, min of the far
Computed in 64-bit from box limits that are 32-bit values. The exit distance is multiplied by 1 + 4e-7 (relative), which the comment gives as the reason: 32-bit boxes, 64-bit rays, never miss a grazing hit.
compute/bvh-core.js
lines 333 to 342
BimCraft 0.8.1
function slab(bmin, bmax, k, ox, oy, oz, ix, iy, iz, tmax) {
let t0 = (bmin[k] - ox) * ix, t1 = (bmax[k] - ox) * ix;
let lo = t0 < t1 ? t0 : t1, hi = t0 < t1 ? t1 : t0;
t0 = (bmin[k + 1] - oy) * iy; t1 = (bmax[k + 1] - oy) * iy;
lo = Math.max(lo, t0 < t1 ? t0 : t1); hi = Math.min(hi, t0 < t1 ? t1 : t0);
t0 = (bmin[k + 2] - oz) * iz; t1 = (bmax[k + 2] - oz) * iz;
lo = Math.max(lo, t0 < t1 ? t0 : t1); hi = Math.min(hi, t0 < t1 ? t1 : t0);
hi *= 1 + 4e-7; // f32 boxes, f64 rays: never miss a grazing hit
return lo <= hi && hi >= 0 && lo <= tmax ? Math.max(lo, 0) : Infinity;
}
compute/bvh-scene.jsBimCraft 0.8.1 32-bit in part2 calculationsReviewed
Tests bin/test-compute.mjs
Instance inverse matrix
32-bit in part
The world-to-object matrix of an instance as 12 numbers.
M⁻¹ = adj(M) ÷ det(M), translation = −M⁻¹ t
The cofactor inverse is computed in 64-bit and returns null if |det| < 1e-300 or det is not finite. buildScene and moveInstances then store the 12 numbers in a Float32Array (inst), so the matrix used for tracing carries 32-bit rounding.
compute/bvh-scene.js
lines 38 to 56
BimCraft 0.8.1
/** Inverse of an affine column-major 4×4 as three rows (12 numbers: r0.xyz t0, r1.xyz t1, r2.xyz t2). */
export function invAffineRows(m) {
const a = m[0], b = m[4], c = m[8], d = m[1], e = m[5], f = m[9], g = m[2], h = m[6], i = m[10];
const A = e * i - f * h, B = -(d * i - f * g), C = d * h - e * g;
const det = a * A + b * B + c * C;
if (!Number.isFinite(det) || Math.abs(det) < 1e-300) return null;
const s = 1 / det;
const r = [
A * s, -(b * i - c * h) * s, (b * f - c * e) * s,
B * s, (a * i - c * g) * s, -(a * f - c * d) * s,
C * s, -(a * h - b * g) * s, (a * e - b * d) * s,
];
const tx = m[12], ty = m[13], tz = m[14];
return [
r[0], r[1], r[2], -(r[0] * tx + r[1] * ty + r[2] * tz),
r[3], r[4], r[5], -(r[3] * tx + r[4] * ty + r[5] * tz),
r[6], r[7], r[8], -(r[6] * tx + r[7] * ty + r[8] * tz),
];
}
Ray into object space of an instance
32-bit in part
The ray origin and direction in an instance frame, and the triangle test that follows.
The 32-bit matrix rows are multiplied with the 64-bit ray in 64-bit arithmetic, then traverseWide and intersectTri test the 32-bit triangles. The reported t is in the units of the ray, because the direction is transformed without renormalizing.
rt/rt-scatter-ui.jsBimCraft 0.8.1 32-bit in part1 calculationReviewed
TestsNone found by the review
Triangle records from world triangles
32-bit in part
The 12-number triangle records the scatter ray tests use.
e1 = v1 − v0; e2 = v2 − v0
The edge vectors are subtracted from the input numbers in 64-bit and written into a Float32Array, so both the vertex and the edges are rounded to 32-bit. The scatter placement then tests rays against these records.
rt/rt-scatter-ui.js
lines 20 to 25
BimCraft 0.8.1
const tris12 = (t9) => { // world triangles (9 floats) → RENDER-SPEC § 6.2 records (12 floats: v0, e1, e2)
const n = t9.length / 9, out = new Float32Array(n * 12);
for (let i = 0; i < n; i++) for (let k = 0; k < 3; k++) {
out[i * 12 + k] = t9[i * 9 + k]; out[i * 12 + 4 + k] = t9[i * 9 + 3 + k] - t9[i * 9 + k]; out[i * 12 + 8 + k] = t9[i * 9 + 6 + k] - t9[i * 9 + k];
}
return out;
In detail
Method
Moller-Trumbore ray and triangle test (double-sided), slab test against boxes, two-level tree with a world-to-object matrix per instance.
Number types
tris, nodes and the instance matrix rows are Float32Array; rays, hit distances, u and v are 64-bit numbers; instance boxes and the TLAS build input are Float64Array.
Tolerances
intersectTri rejects a parallel ray when |det| < 1e-12 (absolute, not scaled to the triangle or the ray); u and v have no tolerance (u < 0, u > 1, v < 0, u + v > 1 reject); slab widens the exit distance by a factor 1 + 4e-7 (relative) so 32-bit boxes never cause a miss; a zero ray direction component uses 1e30 as its inverse.
Limits
Vertex positions have about 7 significant digits, so a hit height or distance in a model far from the origin can be off by the 32-bit step of the coordinates. The 1e-12 determinant limit is absolute, so very small triangles can be skipped.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.