Resources Numerical accuracy Calculation policy

Ray hits against 32-bit triangle records

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.js BimCraft 0.8.1 32-bit in part 3 calculations Reviewed

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.js BimCraft 0.8.1 32-bit in part 2 calculations Reviewed

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.

  • compute/bvh-scene.js
  • lines 162 to 167
  • BimCraft 0.8.1
local.ox = I[k] * r.ox + I[k + 1] * r.oy + I[k + 2] * r.oz + I[k + 3];
local.oy = I[k + 4] * r.ox + I[k + 5] * r.oy + I[k + 6] * r.oz + I[k + 7];
local.oz = I[k + 8] * r.ox + I[k + 9] * r.oy + I[k + 10] * r.oz + I[k + 11];
local.dx = I[k] * r.dx + I[k + 1] * r.dy + I[k + 2] * r.dz;
local.dy = I[k + 4] * r.dx + I[k + 5] * r.dy + I[k + 6] * r.dz;
local.dz = I[k + 8] * r.dx + I[k + 9] * r.dy + I[k + 10] * r.dz;
rt/rt-scatter-ui.js BimCraft 0.8.1 32-bit in part 1 calculation Reviewed

Tests None 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.

Every policy that takes this approach