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.
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.
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.
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;
}
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.
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.