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.jsBimCraft 0.8.1 32-bit in part5 calculationsReviewed
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.
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.
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.