Resources Numerical accuracy Calculation policy

Model for rendering (glTF export)

Writes what the 3D view shows as a glTF 2.0 binary file for rendering programs: meshes, materials, lamps and the site's location.

How the precision is kept

Vertex positions, normals and texture coordinates are written as 32-bit floats, the only floating type glTF allows for them, so a coordinate in the file carries about 7 digits. Each batch's placement is written beside it as a 4 × 4 matrix in the file's JSON, in full 64-bit text. Lamp values are rounded: intensity to 3 decimals, position to 6 decimals of a meter.

Open item. Say in the export that vertex positions are 32-bit, and keep each batch's vertices near its own origin with the offset in the node's 64-bit matrix.

Calculations under this policy

5 calculations in 1 source file. 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.

glb-bim.js BimCraft 0.8.1 32-bit in part 5 calculations Reviewed

Tests tools/bimcraft-test/extra2-test.mjs

Vertices of one batch

32-bit in part

The positions, normals and texture coordinates of the triangles a batch shows.

normal = (x, y, z) ÷ √(x² + y² + z²); uv = planar meters ÷ tile size

Positions are copied into a Float32Array and normals and texture coordinates are computed and stored in Float32Array too. Whatever precision the scene's geometry had, the exported coordinates are 32-bit. A normal shorter than 1e-9 is written as straight up instead of being divided.

  • glb-bim.js
  • lines 31 to 57
  • BimCraft 0.8.1
/** The used part of one batch: its triangles' vertices only, re-numbered. → {pos, nor, uv, idx} | null */
export function batchPart(geometry, uvScale = null) {
  const P = geometry.getAttribute('position')?.array, N = geometry.getAttribute('normal')?.array, U = geometry.getAttribute('uv')?.array;
  if (!P) return null;
  const all = geometry.index ? geometry.index.array : null;
  const nv = P.length / 3, dr = geometry.drawRange || { start: 0, count: Infinity };
  const start = dr.start || 0, end = Math.min(all ? all.length : nv, start + (Number.isFinite(dr.count) ? dr.count : Infinity));
  const n = Math.floor((end - start) / 3) * 3;
  if (n <= 0) return null;
  const map = new Int32Array(nv).fill(-1), used = [];
  const idx = new Uint32Array(n);
  for (let i = 0; i < n; i++) {
    const v = all ? all[start + i] : start + i;
    if (map[v] < 0) { map[v] = used.length; used.push(v); }
    idx[i] = map[v];
  }
  const pos = new Float32Array(used.length * 3), nor = N ? new Float32Array(used.length * 3) : null, uv = U && uvScale ? new Float32Array(used.length * 2) : null;
  used.forEach((v, k) => {
    pos[k * 3] = P[v * 3]; pos[k * 3 + 1] = P[v * 3 + 1]; pos[k * 3 + 2] = P[v * 3 + 2];
    if (nor) {   // unit length (the glTF rule); a degenerate normal points up
      const x = N[v * 3], y = N[v * 3 + 1], z = N[v * 3 + 2], l = Math.hypot(x, y, z);
      if (l > 1e-9) { nor[k * 3] = x / l; nor[k * 3 + 1] = y / l; nor[k * 3 + 2] = z / l; } else nor[k * 3 + 1] = 1;
    }
    if (uv) { uv[k * 2] = U[v * 2] / uvScale[0]; uv[k * 2 + 1] = U[v * 2 + 1] / uvScale[1]; }
  });
  return { pos, nor, uv, idx };
}

Bounds of a mesh

32-bit in part

The smallest and largest coordinate of a mesh, which glTF requires beside its positions.

The minimum and maximum are read back from the 32-bit position array, so they are exactly the values a reader will find in the file. They are 32-bit values written as JSON numbers.

  • glb-bim.js
  • lines 85 to 87
  • BimCraft 0.8.1
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < arr.length; i += 3) for (let k = 0; k < 3; k++) { if (arr[i + k] < lo[k]) lo[k] = arr[i + k]; if (arr[i + k] > hi[k]) hi[k] = arr[i + k]; }
a.min = lo; a.max = hi;

Placement of a batch

64-bit

The world matrix written on each node.

The matrix is written into the file's JSON as 64-bit numbers, unrounded, except that entries smaller than 1e-15 in size become 0. It is left out when every entry is within 1e-12 of the identity.

  • glb-bim.js
  • lines 151 to 152
  • BimCraft 0.8.1
const m = it.matrix;
if (m && m.some((v, i) => Math.abs(v - (i % 5 === 0 ? 1 : 0)) > 1e-12)) node.matrix = m.map((v) => (Math.abs(v) < 1e-15 ? 0 : v));

Lamps

64-bit

Each lamp as a glTF light: its intensity, cone and position.

candela = lumens ÷ 4π; outer cone = angle × π ÷ 360, held between 0.02 and π ÷ 2; inner = 0.8 × outer

The intensity is rounded to 3 decimals of a candela, the cone angles to 5 decimals of a radian and the position to 6 decimals of a meter. Lumens ÷ 4π is the intensity of a source that shines evenly in all directions; a spot is written with the same figure. A lamp with no lumens is written as 800.

  • glb-bim.js
  • lines 164 to 167
  • BimCraft 0.8.1
const light = { name: String(L.name || 'Lamp'), type: spot ? 'spot' : 'point', color: hexLinear(/^#[0-9a-f]{6}$/i.test(L.color || '') ? L.color : '#ffe7c4'), intensity: round((Number(L.lumens) || 800) / (4 * Math.PI), 3) };
if (spot) { const outer = Math.min(Math.PI / 2, Math.max(0.02, ((Number(L.angle) || 60) * Math.PI) / 360)); light.spot = { innerConeAngle: round(outer * 0.8, 5), outerConeAngle: round(outer, 5) }; }
lights.push(light);
const node = { name: light.name, translation: L.p.map((v) => round(v)), extensions: { KHR_lights_punctual: { light: lights.length - 1 } } };

Direction of a spot

64-bit

The rotation that points a spot light along its direction.

q = (a × d, 1 + a · d) ÷ |…|, a = (0, 0, −1)

The half-way quaternion from −Z to the direction, normalized with Math.hypot and then rounded to 7 decimals, so the written quaternion is unit length to about 1e-7. When a · d is below −0.999999 (straight back) the result is written as half a turn about Y.

  • glb-bim.js
  • lines 59 to 67
  • BimCraft 0.8.1
/** Rotation turning the node's −Z (a glTF spot light's direction) onto `dir` (unit). → quaternion [x, y, z, w] */
export function quatFromMinusZ(dir) {
  const a = [0, 0, -1], d = dir;
  const c = a[0] * d[0] + a[1] * d[1] + a[2] * d[2];
  if (c < -0.999999) return [0, 1, 0, 0];   // straight back: half a turn about Y
  const x = a[1] * d[2] - a[2] * d[1], y = a[2] * d[0] - a[0] * d[2], z = a[0] * d[1] - a[1] * d[0];
  const w = 1 + c, l = Math.hypot(x, y, z, w);
  return [x / l, y / l, z / l, w / l].map((v) => round(v, 7));
}

In detail

Method
Written from the Khronos glTF 2.0 specification with KHR_lights_punctual and KHR_materials_transmission (the file's header). One node per shown batch with its world matrix; only the vertices its triangles use are written, re-numbered.
Number types
Float32Array for positions, normals and texture coordinates; Uint32Array for indexes; 64-bit numbers as JSON text for matrices, lamps and the site.
Tolerances
A matrix is written only when it differs from the identity by more than 1e-12 in some entry, and entries smaller than 1e-15 are written as 0. A normal shorter than 1e-9 is replaced by straight up.
Limits
Units are meters. 32-bit values are spaced about 0.06 mm apart at 1 km from the origin of a batch's coordinates and about 8 mm apart at 100 km. Edges are not exported.

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