Resources Numerical accuracy Calculation policy

USD transforms and placement

Works out where every object of a USD scene sits: each object's own transform, its place in the world, the change to meters with Z up, and the placement of instanced copies.

How the precision is kept

Matrices are plain arrays of sixteen 64-bit numbers and are multiplied, inverted and written as 64-bit (the matrix4d type). Rotations come from Math.cos and Math.sin of angles converted from degrees, so a quarter turn carries a cosine of about 6e-17 instead of 0.

Open item. Build quarter-turn rotations (such as the Y-up to Z-up turn) from exact 0 and ±1 entries instead of Math.cos and Math.sin of 90°.

Calculations under this policy

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

usd/geom.js CadCraft 0.8.2 64-bit 9 calculations Reviewed

Tests tools/usd/test.mjs, tools/usd/test-apps.mjs, bin/test-agents.mjs

Matrix product

64-bit

Combines two transforms into one.

o[c][r] = Σₖ a[k][r] × b[c][k]

Sixteen sums of four 64-bit products each, written out in a fixed order.

  • usd/geom.js
  • lines 22 to 29
  • CadCraft 0.8.2
/** a·b in column-vector terms (apply b first): column-major arrays. */
export function mul(a, b) {
  const o = new Array(16);
  for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) {
    o[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
  }
  return o;
}

Matrix inverse

64-bit

The transform that undoes another.

inverse = cofactor matrix ÷ determinant

Each cofactor is a sum of six triple products, which can cancel for a matrix with a large translation and small scale. The only guard is a determinant that is exactly 0 or not finite, in which case the identity is returned; a nearly singular matrix is inverted as it is.

  • usd/geom.js
  • lines 30 to 52
  • CadCraft 0.8.2
export function invert(m) {
  const a = m, inv = new Array(16);
  inv[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
  inv[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
  inv[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
  inv[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
  inv[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
  inv[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
  inv[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
  inv[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
  inv[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
  inv[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
  inv[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
  inv[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
  inv[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
  inv[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
  inv[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
  inv[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
  const det = a[0] * inv[0] + a[1] * inv[4] + a[2] * inv[8] + a[3] * inv[12];
  if (!det || !Number.isFinite(det)) return IDENTITY.slice();
  for (let i = 0; i < 16; i++) inv[i] /= det;
  return inv;
}

Transforming a point

64-bit

Applies a transform to a point.

p′ = (M × [x, y, z, 1]) ÷ w

Three sums of four terms and a division by w; a w of 0 is replaced by 1.

  • usd/geom.js
  • lines 53 to 57
  • CadCraft 0.8.2
export function transformPoint(m, p) {
  const x = p[0], y = p[1], z = p[2];
  const w = m[3] * x + m[7] * y + m[11] * z + m[15] || 1;
  return [(m[0] * x + m[4] * y + m[8] * z + m[12]) / w, (m[1] * x + m[5] * y + m[9] * z + m[13]) / w, (m[2] * x + m[6] * y + m[10] * z + m[14]) / w];
}

Rotation about an axis

64-bit

The matrix of a rotation by an angle in degrees about X, Y or Z.

a = deg × π ÷ 180; entries cos a, ± sin a

The angle is converted with Math.PI and the entries come from Math.cos and Math.sin. For 90° the cosine is about 6.1e-17, not 0, so a quarter turn is not an exact permutation of the axes.

  • usd/geom.js
  • lines 60 to 65
  • CadCraft 0.8.2
function rot(axis, deg) {
  const a = (deg * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a);
  if (axis === 'X') return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
  if (axis === 'Y') return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
  return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
}

Quaternion to rotation matrix

64-bit

The rotation matrix of an orientation stored as a quaternion.

q ÷ |q|; R₀₀ = 1 − 2(y² + z²), R₀₁ = 2(xy + wz), …

The quaternion is normalized with Math.hypot first, so a slightly non-unit input still gives a rotation. A zero quaternion is divided by 1 instead of 0.

  • usd/geom.js
  • lines 66 to 77
  • CadCraft 0.8.2
/** [w, x, y, z] → rotation matrix (normalised). */
export function quatMatrix(q) {
  let [w, x, y, z] = q;
  const n = Math.hypot(w, x, y, z) || 1;
  w /= n; x /= n; y /= n; z /= n;
  return [
    1 - 2 * (y * y + z * z), 2 * (x * y + w * z), 2 * (x * z - w * y), 0,
    2 * (x * y - w * z), 1 - 2 * (x * x + z * z), 2 * (y * z + w * x), 0,
    2 * (x * z + w * y), 2 * (y * z - w * x), 1 - 2 * (x * x + y * y), 0,
    0, 0, 0, 1,
  ];
}

Value at a time

64-bit

An animated value at a given time, between its stored samples.

v = a + (b − a) × (t − t₀) ÷ (t₁ − t₀)

Linear interpolation in 64-bit for numbers and plain arrays. When the samples are Float32Array the result is written to a new Float32Array, so it is rounded to 32-bit again. Quaternions, tokens and booleans are held, not interpolated.

  • usd/geom.js
  • lines 81 to 101
  • CadCraft 0.8.2
/** An attribute's value at `time` (undefined time → the default, else the first sample). */
export function valueAt(prop, time) {
  if (!prop) return null;
  const ts = prop.timeSamples;
  if (!ts || !ts.length) return prop.value;
  if (time === undefined || time === null) return prop.value || ts[0][1];
  if (time <= ts[0][0]) return ts[0][1];
  if (time >= ts[ts.length - 1][0]) return ts[ts.length - 1][1];
  let i = 1;
  while (ts[i][0] < time) i++;
  const [t0, a] = ts[i - 1], [t1, b] = ts[i];
  if (!a || !b || a.t !== b.t || typeof a.v === 'string' || a.t.startsWith('quat') || a.t.includes('token') || a.t === 'bool') return a;
  const u = (time - t0) / (t1 - t0);
  if (typeof a.v === 'number') return { t: a.t, v: a.v + (b.v - a.v) * u };
  if (a.v && b.v && a.v.length === b.v.length) {
    const out = a.v instanceof Float32Array ? new Float32Array(a.v.length) : new Array(a.v.length);
    for (let k = 0; k < a.v.length; k++) out[k] = a.v[k] + (b.v[k] - a.v[k]) * u;
    return { t: a.t, v: out };
  }
  return a;
}

An object's own transform

64-bit

Builds the transform of one object from its list of translate, rotate, scale and matrix operations.

M = op₁ × op₂ × … in the listed order; an inverted operation uses its inverse

Each operation becomes a 64-bit matrix and the product is accumulated in order. Three-axis rotations are three single-axis matrices multiplied together.

  • usd/geom.js
  • lines 108 to 141
  • CadCraft 0.8.2
/** The prim's own transform from xformOpOrder. */
export function localMatrix(prim, time) {
  const orderV = valueAt(prim.props.get('xformOpOrder'));
  if (!orderV) return { m: IDENTITY.slice(), reset: false };
  let m = IDENTITY.slice();
  let reset = false;
  for (const raw of orderV.v) {
    if (raw === '!resetXformStack!') { reset = true; m = IDENTITY.slice(); continue; }
    const inverse = raw.startsWith('!invert!');
    const name = inverse ? raw.slice(8) : raw;
    const v = valueAt(prim.props.get(name), time);
    if (!v) continue;
    const kind = name.split(':')[1] || '';
    let op = null;
    const x = v.v;
    if (kind === 'translate') op = translate(x);
    else if (kind === 'scale') op = scale(x);
    else if (kind === 'transform') op = Array.from(x);
    else if (kind === 'orient') op = quatMatrix(x);
    else if (kind === 'rotateX' || kind === 'rotateY' || kind === 'rotateZ') op = rot(kind.slice(-1), x);
    else {
      const mm = ROT3.exec(kind);
      if (mm) {
        const axes = mm[1];
        const ang = { X: x[0], Y: x[1], Z: x[2] };
        // rotateABC: A first, then B, then C
        op = mul(rot(axes[2], ang[axes[2]]), mul(rot(axes[1], ang[axes[1]]), rot(axes[0], ang[axes[0]])));
      }
    }
    if (!op) continue;
    m = mul(m, inverse ? invert(op) : op);
  }
  return { m, reset };
}

World transform

64-bit

The transform of an object in the scene, through all its parents.

W = M_root × … × M_parent × M_own

The local matrices are multiplied from the outermost parent inward, one 64-bit product per level, stopping at a parent that resets the stack.

  • usd/geom.js
  • lines 142 to 152
  • CadCraft 0.8.2
export function worldMatrix(prim, time) {
  const chain = [];
  for (let p = prim; p && p.path !== '/'; p = p.parent) {
    const { m, reset } = localMatrix(p, time);
    chain.push(m);
    if (reset) break;
  }
  let w = IDENTITY.slice();
  for (let i = chain.length - 1; i >= 0; i--) w = mul(w, chain[i]);
  return w;
}

Scene units and up axis

64-bit

The transform from the scene's own units and up axis to meters with Z up.

scale by metersPerUnit; Y-up scenes: rotate 90° about X

The unit factor is used as stored and defaults to 0.01 when missing or not positive. The Y-up turn is built with Math.cos and Math.sin of 90°, so the matrix has entries of about 6.1e-17 × scale where an exact turn has 0.

  • usd/geom.js
  • lines 153 to 158
  • CadCraft 0.8.2
/** Stage space → metres, Z up. */
export function stageFrame(stage) {
  const s = stage.meta.metersPerUnit > 0 ? stage.meta.metersPerUnit : 0.01;
  const m = scale([s, s, s]);
  return stage.meta.upAxis === 'Y' ? mul(rot('X', 90), m) : m;
}
usd/scene.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests tools/usd/test.mjs, tools/usd/test-apps.mjs

Placement of instanced copies

64-bit

The transform of each copy of a point instancer from its position, orientation and scale.

M = translate(p) × rotate(q) × scale(s)

The matrix arithmetic is 64-bit. The positions, orientations and scales are used as the file holds them, which for the usual float-typed attributes is 32-bit.

  • usd/scene.js
  • lines 112 to 137
  • CadCraft 0.8.2
function pointInstances(prim, node, time, build, protos) {
  const rel = prim.props.get('prototypes');
  const targets = rel ? rel.targets : [];
  const idx = valueAt(prim.props.get('protoIndices'), time)?.v || [];
  const pos = valueAt(prim.props.get('positions'), time)?.v || [];
  const ori = valueAt(prim.props.get('orientations'), time)?.v || valueAt(prim.props.get('orientationsf'), time)?.v || null;
  const sc = valueAt(prim.props.get('scales'), time)?.v || null;
  const keys = targets.map((t) => {
    const key = 'pi|' + t;
    if (!protos.has(key)) {
      const p = findPrim(prim, t);
      const proto = { path: key, name: p ? p.name : 'Prototype', displayName: p ? p.name : 'Prototype', type: 'Xform', kind: '', matrix: IDENT(), visible: true, children: [], mesh: null, proto: null, data: {}, material: null };
      if (p) { const k = build(p, true); if (k) { k.matrix = IDENT(); proto.children.push(k); } }
      protos.set(key, proto);
    }
    return key;
  });
  for (let i = 0; i < idx.length; i++) {
    const k = keys[idx[i]];
    if (!k || pos.length < (i + 1) * 3) continue;
    let m = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2], 1];
    if (ori && ori.length >= (i + 1) * 4) { const r = quatMatrix([ori[i * 4], ori[i * 4 + 1], ori[i * 4 + 2], ori[i * 4 + 3]]); r[12] = pos[i * 3]; r[13] = pos[i * 3 + 1]; r[14] = pos[i * 3 + 2]; m = r; }
    if (sc && sc.length >= (i + 1) * 3) m = mul(m, [sc[i * 3], 0, 0, 0, 0, sc[i * 3 + 1], 0, 0, 0, 0, sc[i * 3 + 2], 0, 0, 0, 0, 1]);
    node.children.push({ path: prim.path + '#' + i, name: 'instance_' + i, displayName: 'instance ' + i, type: 'Xform', kind: '', matrix: m, visible: true, children: [], mesh: null, proto: k, data: {}, material: null });
  }
}
usd/build.js CadCraft 0.8.2 64-bit 1 calculation Reviewed

Tests tools/usd/test.mjs, tools/usd/test-apps.mjs

Writing an object's transform

64-bit

Stores an object's placement in the exported scene.

The sixteen numbers are written with the type matrix4d, which is 64-bit. The matrix is left out only when every entry equals the identity exactly.

  • usd/build.js
  • lines 110 to 115
  • CadCraft 0.8.2
/** Transform ops: a 16-number matrix (column-major = USD's flat row-major) as one xformOp:transform. */
setMatrix(p, m) {
  if (!m || isIdentity(m)) return;
  this.attr(p, 'xformOp:transform', 'matrix4d', Array.from(m));
  this.attr(p, 'xformOpOrder', 'token[]', ['xformOp:transform'], { uniform: true });
}

In detail

Method
4 × 4 matrix products in the order of the scene's transform operations; inverse by cofactors divided by the determinant; unit quaternion to rotation matrix; linear interpolation between time samples.
Number types
64-bit numbers in plain arrays. Matrices are written with the 64-bit type matrix4d.
Tolerances
A matrix is left out of the file only when it equals the identity exactly. The inverse falls back to the identity when the determinant is 0 or not finite.
Limits
The inverse has no check for a nearly singular matrix. Scenes without a unit default to 0.01 m per unit.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach