Resources Numerical accuracy Calculation policy

USD export and render hand-off

Writes the open IFC models as an OpenUSD file and works out the transform that places the cameras, sun and lamps of a render hand-off in that file's world.

How the precision is kept

Mesh points are read from the 3D viewer's batches, which are Float32Array, moved by the model's viewer placement in 64-bit and stored again in a Float32Array for the file. They are in meters near the origin; each Site carries a transform of 64-bit numbers that takes them back to the IFC file's coordinates. Storey elevations are written as doubles.

Open item. Write the points from the IFC engine's 64-bit mesh positions instead of the viewer's 32-bit batches.

Calculations under this policy

6 calculations in 2 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.

usd-bimcraft.js BimCraft 0.8.1 32-bit in part 4 calculations Reviewed

Tests tools/usd/test-apps.mjs

Mesh points of an element

32-bit in part

The vertices of each element as the USD file stores them.

(x′, y′, z′) = P · (x, y, z, 1); stored as (x′, −z′, y′)

pos is the viewer batch's Float32Array; the placement is applied in 64-bit and the result is stored in the Float32Array pts. The exported points are therefore 32-bit values in meters near the viewer's origin; the Site transform carries the offset to the file's coordinates.

  • usd-bimcraft.js
  • lines 130 to 141
  • BimCraft 0.8.1
const pts = new Float32Array(nv * 3), nrm = new Float32Array(nt * 9), idx = new Int32Array(nt * 3), counts = new Int32Array(nt).fill(3);
const subsets = [];
let vo = 0, to = 0;
for (const r of parts) {
  const { bt } = r;
  const pos = bt.positions, nn = bt.normals, ix = bt.indices;
  for (let v = 0; v < r.vn; v++) {
    const s = (r.v0 + v) * 3;
    const x = pos[s], y = pos[s + 1], z = pos[s + 2];
    const wx = P[0] * x + P[4] * y + P[8] * z + P[12], wy = P[1] * x + P[5] * y + P[9] * z + P[13], wz = P[2] * x + P[6] * y + P[10] * z + P[14];
    pts[(vo + v) * 3] = wx; pts[(vo + v) * 3 + 1] = -wz; pts[(vo + v) * 3 + 2] = wy;
  }

Inverse of a placement

64-bit

The matrix that undoes a model's placement.

R⁻¹ = Rᵀ ÷ s², s² = m₀² + m₁² + m₂²; t⁻¹ = −R⁻¹ · t

Valid for a rotation with one uniform scale: the scale is taken from the first column only. A zero s² is replaced by 1.

  • usd-bimcraft.js
  • lines 33 to 42
  • BimCraft 0.8.1
function inv(m) {
  // rigid + uniform scale placements: transpose the rotation part, scale back, move back
  const s2 = m[0] * m[0] + m[1] * m[1] + m[2] * m[2] || 1;
  const r = [m[0] / s2, m[4] / s2, m[8] / s2, 0, m[1] / s2, m[5] / s2, m[9] / s2, 0, m[2] / s2, m[6] / s2, m[10] / s2, 0, 0, 0, 0, 1];
  const t = [m[12], m[13], m[14]];
  r[12] = -(r[0] * t[0] + r[4] * t[1] + r[8] * t[2]);
  r[13] = -(r[1] * t[0] + r[5] * t[1] + r[9] * t[2]);
  r[14] = -(r[2] * t[0] + r[6] * t[1] + r[10] * t[2]);
  return r;
}

Site transform back to the file's coordinates

64-bit

The transform on each Site that returns the exported points to the IFC file's own coordinates.

toFile = A · (P · C)⁻¹ · A⁻¹

Three 4 × 4 products and one inverse in 64-bit. The coordination matrix holds the offset that moved the model to the origin, so the large numbers stay in this matrix and out of the 32-bit points.

  • usd-bimcraft.js
  • lines 72 to 76
  • BimCraft 0.8.1
const P = matrixOf(e.mi) || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
let C = null;
try { C = [...api.GetCoordinationMatrix(mid)]; } catch { C = null; }
// scene = P·C·file (both Y up), so file = (P·C)⁻¹·scene; in Z up: A·(P·C)⁻¹·A⁻¹
const toFile = C && C.length === 16 ? mmul(A, mmul(inv(mmul(P, C)), AI)) : null;

Storey elevation in meters

64-bit

Each storey's elevation as an attribute of the USD file.

elevation in m = elevation × (mm per file unit ÷ 1000)

lengthToMetre is the unit factor divided by 1000, computed once; the elevation is multiplied by it and written as a double.

  • usd-bimcraft.js
  • line 91
  • BimCraft 0.8.1
if (Number.isFinite(s.elevation)) b.attr(p, 'ifc:elevation', 'double', s.elevation * lengthToMetre, { custom: true, uniform: true, meta: { customData: V('dictionary', new Map([['unit', V('string', 'm')]])) } });
handoff-bim.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests None found by the review

Inverse of a placement (hand-off)

64-bit

The matrix that undoes a model's placement, for the render hand-off.

R⁻¹ = Rᵀ ÷ s²; t⁻¹ = −R⁻¹ · t

The same formula as in usd-bimcraft.js: valid for a rotation with one uniform scale, the scale taken from the first column, a zero s² replaced by 1.

  • handoff-bim.js
  • lines 30 to 38
  • BimCraft 0.8.1
/** The inverse of a rigid placement with a uniform scale (what the viewer's model placements are): as usd-bimcraft.js does it. */
function inv(m) {
  const s2 = m[0] * m[0] + m[1] * m[1] + m[2] * m[2] || 1;
  const r = [m[0] / s2, m[4] / s2, m[8] / s2, 0, m[1] / s2, m[5] / s2, m[9] / s2, 0, m[2] / s2, m[6] / s2, m[10] / s2, 0, 0, 0, 0, 1];
  r[12] = -(r[0] * m[12] + r[4] * m[13] + r[8] * m[14]);
  r[13] = -(r[1] * m[12] + r[5] * m[13] + r[9] * m[14]);
  r[14] = -(r[2] * m[12] + r[6] * m[13] + r[10] * m[14]);
  return r;
}

Viewer scene to the USD stage

64-bit

The transform that takes camera, sun and lamp positions from the viewer's scene to the exported file's world.

toStage = A · (P · C)⁻¹; A alone when the model has no coordination matrix

Two 4 × 4 products and one inverse of 64-bit numbers. Nothing is rounded here.

  • handoff-bim.js
  • lines 40 to 52
  • BimCraft 0.8.1
/**
 * scene → composed stage world, for the model whose Site the render is of (the first visible one: the dialog's sun and site come from
 * the current model too). In Y up, scene = P·C·file, so file = (P·C)⁻¹·scene. bimUsd writes the viewer's points turned Z up (A) under a
 * Site whose own transform is A·(P·C)⁻¹·A⁻¹, so a point's composed position is that transform · A · scene = A·(P·C)⁻¹ · scene.
 * A file with no coordination matrix gets no Site transform: composed = A · scene.
 */
export function stageMatrix(entry, matrixOf = () => null) {
  let C = null;
  try { C = entry && entry.ifc && entry.ifc.api && entry.model ? [...entry.ifc.api.GetCoordinationMatrix(entry.model.modelID)] : null; } catch { C = null; }
  if (!C || C.length !== 16) return Y_TO_Z;
  const P = matrixOf(entry.mi) || IDENT;
  return mmul(Y_TO_Z, inv(mmul(P, C)));
}

In detail

Method
Point = P · v, turned from Y up to Z up as (x, −z, y). Site transform = A · (P · C)⁻¹ · A⁻¹, with the inverse taken as the transposed rotation ÷ s² and the translation moved back. Hand-off transform = A · (P · C)⁻¹.
Number types
Float32Array points and normals, Int32Array indexes; 16-number matrices of 64-bit numbers; elevations as 64-bit numbers.
Tolerances
The Site transform is left out when every entry is within 1e-12 of the identity.
Limits
The inverse assumes a rotation with one uniform scale; a placement with shear or unequal scale would not be inverted correctly. The geometry in the file has the precision of the viewer's 32-bit positions.

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