Resources Numerical accuracy Calculation policy

USD save, open and import

Opens ModelCraft's own USD files back to the exact model, takes in changes another program made, and imports any other USD scene in meters with Z up.

How the precision is kept

A file ModelCraft saved carries each definition twice: its exact JSON with 64-bit points, and native USD points written as floats for other programs. On opening, the exact data are used unless the native points no longer equal the model's points rounded to 32-bit; then the definition is rebuilt from the native points, which have the precision the file stores. Other scenes are transformed in 64-bit through the stage's frame.

Calculations under this policy

6 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.

usd-modelcraft.js ModelCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/modelcraft-test/usd.mjs

Did another program change the points

Exact

Decides whether a definition's native USD points still match the model's own.

changed if any fround(own coordinate) ≠ file coordinate, or the counts differ

Each of the model's 64-bit coordinates is rounded to 32-bit with Math.fround and compared for exact equality with the file's value, with no tolerance. An unchanged file therefore opens from the exact 64-bit data, and any change of one 32-bit step is seen.

  • usd-modelcraft.js
  • lines 200 to 215
  • ModelCraft 0.8.1
/** The native points no longer the definition's own (as floats)? */
function geometryChanged(g, dj) {
  const pv = valueAt(g.props.get('points'));
  if (!pv) return true;
  const pts = pv.v;
  let mine;
  if (dj.kind === 'mesh' && dj.mesh) {
    try { mine = Mesh.fromJSON(dj.mesh).toArrays().p; } catch { return true; }
  } else {
    mine = [];
    for (const v of dj.verts || []) mine.push(v[1], v[2], v[3]);
  }
  if (mine.length !== pts.length) return true;
  for (let i = 0; i < pts.length; i++) if (Math.fround(mine[i]) !== pts[i]) return true;
  return false;
}

Mesh from the native points

64-bit

Rebuilds a definition that was changed elsewhere from the file's native points and faces.

The file's points are copied into a Float64Array without any arithmetic. They are the native points that geometryChanged compares with 32-bit values, so the rebuilt mesh has the file's 32-bit precision, not the model's.

  • usd-modelcraft.js
  • lines 216 to 227
  • ModelCraft 0.8.1
function meshFromPrim(g) {
  const pv = valueAt(g.props.get('points')), c = valueAt(g.props.get('faceVertexCounts')), x = valueAt(g.props.get('faceVertexIndices'));
  if (!pv || !c || !x) return null;
  const p = pv.v, n = p.length / 3;
  for (const k of x.v) if (k < 0 || k >= n) return null;
  let s = 0;
  for (const k of c.v) s += k;
  if (s !== x.v.length) return null;
  const m = new Mesh();
  m._bulk(Float64Array.from(p), n, Int32Array.from(c.v), Int32Array.from(x.v));
  return m;
}

Was a placement moved elsewhere

64-bit

Decides whether a placement's matrix in the file differs from the model's and takes the file's if so.

moved if any |mₖ(file) − mₖ(own)| > 10⁻⁷ × max(1, |mₖ(own)|)

The tolerance is relative for entries above 1 and absolute (1e-7) below. A placement moved by less keeps its own matrix; one moved by more takes all 16 entries from the file.

  • usd-modelcraft.js
  • lines 184 to 185
  • ModelCraft 0.8.1
const m = localMatrix(c).m;
if (!Array.isArray(i.m) || m.some((x, k) => Math.abs(x - i.m[k]) > 1e-7 * Math.max(1, Math.abs(i.m[k])))) { i.m = m; report.moved++; }

Placements through the stage's frame

64-bit

Brings a placement matrix from the file's units and up axis into meters with Z up.

M′ = F · M · F⁻¹

The frame is inverted once and each placement is multiplied on both sides in 64-bit. The inverse comes from the USD library's invert.

  • usd-modelcraft.js
  • lines 241 to 242
  • ModelCraft 0.8.1
const F = scene.frame, FI = invert(F);
const conj = (m) => mul(F, mul(m, FI));

Imported mesh points

64-bit

The points of an imported mesh in meters with Z up.

p′ = F · p (rotation, scale and translation of the frame)

Each point is transformed with nine products and three sums into a Float64Array. The source points are as precise as the file stores them.

  • usd-modelcraft.js
  • lines 333 to 339
  • ModelCraft 0.8.1
const p = new Float64Array(P.length);
for (let i = 0; i < n; i++) {
  const x = P[i * 3], y = P[i * 3 + 1], z = P[i * 3 + 2];
  p[i * 3] = F[0] * x + F[4] * y + F[8] * z + F[12];
  p[i * 3 + 1] = F[1] * x + F[5] * y + F[9] * z + F[13];
  p[i * 3 + 2] = F[2] * x + F[6] * y + F[10] * z + F[14];
}

Lamp output from a USD light

64-bit

The lumens of an imported lamp when the file does not carry ModelCraft's own value.

area = width × height × scale² or 4π·(radius × scale)²; lumens = intensity × 2^exposure × 683 × max(area, 10⁻⁶) × π

The scale is the length of the first column of the light's matrix, so a light scaled differently along its axes uses the first axis only. The area has a floor of 1e-6. A lumens value ModelCraft wrote itself is used unchanged when it is a finite number.

  • usd-modelcraft.js
  • lines 306 to 308
  • ModelCraft 0.8.1
const scale = Math.hypot(W[0], W[1], W[2]) || 1;
const area = lt.kind === 'area' ? lt.width * lt.height * scale * scale : 4 * Math.PI * (lt.radius * scale) ** 2;
const lumens = Number.isFinite(lt.data['cadcraft:lumens']) ? lt.data['cadcraft:lumens'] : lt.intensity * 2 ** lt.exposure * 683 * Math.max(area, 1e-6) * Math.PI;

In detail

Method
Exact comparison of Math.fround(own point) with the file's point; placement matrices compared with a relative tolerance; import through the frame F as F·p and F·M·F⁻¹.
Number types
64-bit numbers and Float64Array for imported points; the native USD points being compared are 32-bit.
Tolerances
A placement counts as moved when any matrix entry differs by more than 1e-7 × max(1, |entry|).
Limits
A definition changed in another program opens from 32-bit points. A placement moved by less than the tolerance keeps its old matrix.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach