Resources Numerical accuracy Calculation policy

Import of OBJ, glTF and Collada models

The vertex positions and placements read from an OBJ, glTF or Collada file, turned to the model's Z-up meters.

How the precision is kept

Text formats (OBJ, Collada) are parsed to 64-bit numbers. glTF stores positions as 32-bit floats, so those coordinates arrive with the 7 digits the file holds and are widened to 64-bit. Axis changes are sign changes and swaps, which are exact; unit scale and node transforms are 64-bit matrix products. The points are then welded on a 1e-5 m grid (see Welding of imported geometry).

Calculations under this policy

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

m3d/io.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests None found by the review

OBJ vertex

64-bit

A vertex line of an OBJ file.

(x, y, z) → (x, −z, y)

The three fields are parsed with parseFloat and kept in 64-bit; the change from Y up to Z up is a swap and a sign change, which is exact. A value that is not finite, or 1e7 or more in size, stops the import.

  • m3d/io.js
  • lines 290 to 293
  • CadCraft 0.8.2
if (tag === 'v') {
  const p = [parseFloat(parts[1]), parseFloat(parts[2]), parseFloat(parts[3])];
  if (!p.every((x) => Number.isFinite(x) && Math.abs(x) < 1e7)) throw new Error(t('m3d.io.obj_vertex', { line: s.slice(0, 40) }));
  verts.push(yupToZup(p));

glTF accessor values

64-bit

The numbers of a glTF buffer, such as vertex positions.

Each component is read with the accessor's own type and stored in a Float64Array. glTF positions are 32-bit floats (getFloat32), so the imported coordinate has the digits the file held. The accessor's normalized flag is not read.

  • m3d/io.js
  • lines 385 to 397
  • CadCraft 0.8.2
const out = new Float64Array(count * nc);
if (a.bufferView === undefined) return { data: out, nc, count };
const v = views[a.bufferView];
if (!v || typeof v !== 'object') fail(t('m3d.io.a_buffer_view_is_missing'));
const buf = buffers[v.buffer];
if (!buf) fail(t('m3d.io.a_buffer_view_points_at_a'));
const vo = v.byteOffset || 0, vl = v.byteLength, ao = a.byteOffset || 0;
if (!Number.isInteger(vo) || !Number.isInteger(vl) || vo < 0 || vo + vl > buf.length) fail(t('m3d.io.a_buffer_view_runs_past_its'));
const stride = v.byteStride || size * nc;
if (!Number.isInteger(stride) || stride < size * nc) fail(t('m3d.io.a_buffer_view_stride_is_wrong'));
if (count && (!Number.isInteger(ao) || ao < 0 || ao + stride * (count - 1) + size * nc > vl)) fail(t('m3d.io.an_accessor_runs_past_its_buffer'));
const dv = new DataView(buf.buffer, buf.byteOffset + vo, vl);
for (let i = 0; i < count; i++) for (let c = 0; c < nc; c++) out[i * nc + c] = dv[getter](ao + i * stride + c * size, true);

glTF node transform

64-bit

The matrix of a glTF node from its matrix, or from its translation, rotation and scale.

M = T · R(q) · S

A 16-number matrix is used as given. Otherwise the rotation matrix is built from the quaternion's components as they are in the file; the quaternion is not normalized, so one that is not unit length gives a matrix that also scales.

  • m3d/io.js
  • lines 463 to 475
  • CadCraft 0.8.2
const nodeMatrix = (n) => {
  if (Array.isArray(n.matrix) && n.matrix.length === 16 && n.matrix.every(Number.isFinite)) return n.matrix.slice();
  const tr = Array.isArray(n.translation) ? n.translation : [0, 0, 0];
  const r = Array.isArray(n.rotation) ? n.rotation : [0, 0, 0, 1];
  const s = Array.isArray(n.scale) ? n.scale : [1, 1, 1];
  if (![...tr, ...r, ...s].every(Number.isFinite)) fail(t('m3d.io.a_node_transform_is_not_a'));
  const [x, y, z, w] = r;
  const R = [1 - 2 * (y * y + z * z), 2 * (x * y + z * w), 2 * (x * z - y * w), 0,
    2 * (x * y - z * w), 1 - 2 * (x * x + z * z), 2 * (y * z + x * w), 0,
    2 * (x * z + y * w), 2 * (y * z - x * w), 1 - 2 * (x * x + y * y), 0, 0, 0, 0, 1];
  const S = [s[0], 0, 0, 0, 0, s[1], 0, 0, 0, 0, s[2], 0, 0, 0, 0, 1];
  return mmul(translation(tr), mmul(R, S));
};

glTF vertex in the model

64-bit

A glTF vertex moved by its node transforms and turned to Z up.

p′ = M · p, then (x, y, z) → (x, −z, y)

One matrix times point in 64-bit with the accumulated node matrix, then the exact axis change.

  • m3d/io.js
  • line 451
  • CadCraft 0.8.2
const pt = (i) => yupToZup(apply(m, [P.data[i * 3], P.data[i * 3 + 1], P.data[i * 3 + 2]]));

Collada numbers

64-bit

How lists of numbers in a Collada file are read.

Each token is converted with Number(), which reads every digit of the text into a 64-bit value.

  • m3d/io.js
  • lines 1519 to 1526
  • CadCraft 0.8.2
const nums = (s) => {
  const str = String(s || '').trim();
  if (!str) return [];
  const a = str.split(/\s+/);
  budget -= a.length;
  if (budget < 0) throw new Error(t('m3d.io.this_collada_file_is_too_big'));
  return a.map(Number);
};

Collada unit and up axis

64-bit

The change from the file's unit and up axis to Z-up meters.

C = R_up · S(meter); node matrix → C · M · C⁻¹; point → C · p

The unit is parsed with parseFloat and used when it is above 0 and below 1e6, else 1. The up-axis turn has entries 0 and ±1. C⁻¹ comes from the general cofactor inverse, and each node matrix is multiplied on both sides, two matrix products per node.

  • m3d/io.js
  • lines 1531 to 1539
  • CadCraft 0.8.2
const meter = parseFloat(child(asset, 'unit')?.attrs.meter);
const s = meter > 0 && meter < 1e6 ? meter : 1;
const up = String(child(asset, 'up_axis')?.text || 'Y_UP').trim().toUpperCase();
report.up = ['X_UP', 'Y_UP', 'Z_UP'].includes(up) ? up : 'Y_UP';
report.meter = s;
const R = report.up === 'Z_UP' ? ident() : report.up === 'X_UP' ? [0, 0, 1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 1] : [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1];
const C = mmul(R, [s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1]);
const Ci = invert(C);
const conv = (m) => mmul(C, mmul(m, Ci));

Collada node transforms

64-bit

The matrix of a Collada node from its matrix, translate, rotate and scale elements.

rotate: angle in radians = degrees × π ÷ 180

The elements are multiplied in file order. A matrix is read row by row and reordered; a rotation takes its angle in degrees and converts it once, about the axis given (normalized in vec.js).

  • m3d/io.js
  • lines 1690 to 1693
  • CadCraft 0.8.2
if (c.name === 'matrix' && v.length === 16) m = mmul(m, [0, 1, 2, 3].flatMap((col) => [v[col], v[4 + col], v[8 + col], v[12 + col]]));
else if (c.name === 'translate' && v.length === 3) m = mmul(m, translation(v));
else if (c.name === 'rotate' && v.length === 4) { if (Math.hypot(v[0], v[1], v[2]) > 0) m = mmul(m, rotation(v.slice(0, 3), v[3] * Math.PI / 180)); }
else if (c.name === 'scale' && v.length === 3) m = mmul(m, [v[0], 0, 0, 0, 0, v[1], 0, 0, 0, 0, v[2], 0, 0, 0, 0, 1]);

Collada vertex in the model

64-bit

A Collada vertex turned to Z-up meters.

p′ = C · p

One matrix times point with the unit and up-axis matrix; a result that is not finite or is 1e7 or more in size stops the import.

  • m3d/io.js
  • lines 1597 to 1598
  • CadCraft 0.8.2
const p = apply(C, [data[off + i * stride], data[off + i * stride + 1], data[off + i * stride + 2]]);
if (!okPt(p)) fail(t('m3d.io.a_vertex_is_not_a_number'));

In detail

Method
Node transforms as 4×4 matrix products down the scene tree; glTF rotation from its quaternion; Collada unit and up axis as a similarity C applied as C · M · C⁻¹.
Number types
64-bit numbers; glTF accessors read with their own component type into a Float64Array.
Tolerances
A coordinate of 1e7 or more is refused. A Collada unit is used when 0 < meter < 1e6.
Limits
glTF positions carry 32-bit precision from the file. A glTF quaternion is used as given, without normalizing. Integer accessors marked normalized are read as plain integers.

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