Turns an FBX file's axes and unit into Z up and meters, and builds each object's matrix from its translation, pivots, rotations and scaling.
How the precision is kept
The file's unit (centimeters per unit) is multiplied by 0.01 to get meters and put, with the axis signs, into one matrix A; points become A × p and node matrices A × M × A⁻¹. Object matrices are products of eleven 4×4 matrices in 64-bit numbers. Rotations are Euler angles in degrees, converted with π ÷ 180.
Open item. Compare the axis determinant with the unit factor cubed instead of the fixed 1e-12, so a small file unit does not switch off the axis conversion.
Calculations under this policy
7 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.
The matrix that takes the file's coordinates to Z up and meters.
k = UnitScaleFactor × 0.01; columns = ± k along the model axis each file axis maps to
k is the file's unit factor times 0.01, which is not exactly representable in binary. The fallback test |det| < 1e-12 is absolute while the determinant equals ± k³, so for k below about 1e-4 a valid set of axes is replaced by a plain scale.
io/fbx-import.js
lines 146 to 157
ModelCraft 0.8.1
/** The file's axes and unit → ModelCraft (Z up, metres): up → +Z, front → −Y, coord → +X. */
export function axisMatrix(s) {
const e = (i, sign) => { const v = [0, 0, 0]; if (i >= 0 && i <= 2) v[i] = sign < 0 ? -1 : 1; return v; };
// rows of the map: model X = coord axis, model Y = −front axis, model Z = up axis (each a unit vector in file space)
const X = e(s.coord, s.coordSign); const Y = e(s.front, s.frontSign).map((v) => -v); const Z = e(s.up, s.upSign);
const k = (s.unit > 0 && Number.isFinite(s.unit) ? s.unit : 1) * 0.01;
// column-major: column j = the model coordinates of file axis j
const m = ident();
for (let j = 0; j < 3; j++) { m[j * 4] = X[j] * k; m[j * 4 + 1] = Y[j] * k; m[j * 4 + 2] = Z[j] * k; }
if (Math.abs(det(m)) < 1e-12) return S([k, k, k]); // contradictory axes: just the unit
return m;
}
Node matrices into the model's axes
64-bit
Carries a node's matrix from the file's axes and unit into the model's.
M′ = A × M × A⁻¹
A⁻¹ comes from invert in vec.js. The unit factor in A meets its reciprocal in A⁻¹ on the rotation part, where their 64-bit product can differ from 1 in the last bit; the translation is scaled by the unit factor.
io/fbx-import.js
lines 165 to 167
ModelCraft 0.8.1
const A = axisMatrix(scene.settings);
const AI = invert(A);
const conj = (m) => mmul(A, mmul(m, AI));
Euler angles to a matrix
64-bit
The rotation matrix of three angles in degrees in the file's rotation order.
XYZ order: R = Rz × Ry × Rx, angles × π ÷ 180
Three single-axis matrices multiplied in 64-bit. Angles such as 90° give a cosine of about 6e-17 instead of 0. An unknown order number falls back to XYZ.
io/fbx-import.js
lines 122 to 127
ModelCraft 0.8.1
/** Euler angles (degrees) in a RotationOrder: "XYZ" turns about X first, then Y, then Z. */
function euler(v, order = 0) {
const [x, y, z] = [Rx(v[0]), Ry(v[1]), Rz(v[2])];
const seq = [[z, y, x], [y, z, x], [x, z, y], [z, x, y], [y, x, z], [x, y, z]][order] || [z, y, x];
return mmul(seq[0], mmul(seq[1], seq[2]));
}
An object's own matrix
64-bit
Builds a model's local matrix from its translation, offsets, pivots, pre-, own and post-rotation, and scaling.
M = T × Roff × Rp × Rpre × R × Rpost⁻¹ × Rp⁻¹ × Soff × Sp × S × Sp⁻¹
Eleven 4×4 matrices multiplied left to right in 64-bit; the post-rotation is inverted with the general inverse. Missing or non-finite properties take their defaults.
A lamp's position, direction and brightness from an FBX light.
lumens = Intensity × 8; direction = the node's −Y, made unit
FBX intensity has no unit; the factor 8 is the file's stated convention (100 gives 800 lumens), not a physical conversion. Position and direction go through the 64-bit world matrix.
io/fbx-import.js
lines 355 to 367
ModelCraft 0.8.1
function addLight(model, attr, M, report, skip) {
const p = attr.props;
const type = num(p, 'LightType', 0);
if (type === 1) { skip(t('fbx.note.directional')); return; }
if (type > 2) { skip(t('fbx.note.area_light')); return; }
if (!model.addLight) return;
const c = vec3(p, 'Color', [1, 1, 1]);
const hex = '#' + c.map((x) => Math.round(Math.min(1, Math.max(0, x)) * 255).toString(16).padStart(2, '0')).join('');
const dir = norm(applyDir(M, [0, -1, 0])); // FBX spot lights shine along their own −Y
model.addLight({ kind: type === 2 ? 'spot' : 'point', p: apply(M, [0, 0, 0]), dir: len(dir) ? dir : [0, 0, -1], color: hex,
lumens: num(p, 'Intensity', 100) * 8, angle: num(p, 'OuterAngle', num(p, 'Cone angle', 45)), name: attr.name || '' });
report.lights++;
}
Cameras
64-bit
A saved view's eye, target and field of view from an FBX camera.
Axes whose matrix has |determinant| < 1e-12 are treated as contradictory and only the unit is applied; the test is absolute, and the determinant is the unit factor cubed. A geometric transform within 1e-12 of the identity is ignored.
Limits
The centimeter factor 0.01 is not an exact binary number, so a coordinate can differ in its last bit from the same value divided by 100. With a factor to meters below about 1e-4 (a file unit smaller than about 0.1 mm) the determinant test drops the axis conversion.
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.