Resources Numerical accuracy Calculation policy

Alembic import

Reads meshes and their transforms from an Alembic cache and brings them in with Z up.

How the precision is kept

Numbers are read in the type the file stores: 64-bit floats stay 64-bit, 32-bit and 16-bit floats are held in a Float32Array, which keeps them exactly. Mesh points are then copied into a Float64Array with the axes swapped. The accuracy of a point is therefore the file's own, usually 32-bit. A cache with several samples is read at the one sample nearest the asked time.

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.

io/abc.js ModelCraft 0.8.1 64-bit 8 calculations Reviewed

Tests tools/modelcraft-test/meshfiles.mjs

One sample of a property

64-bit

Reads one stored sample of a scalar or array property into a typed array.

A 64-bit float property is read into a Float64Array; 32-bit and 16-bit floats into a Float32Array, which holds them without loss. Unsigned and signed 64-bit integers are read with getUint32 and getInt32 at the value's first four bytes, so only their low 32 bits are kept.

  • io/abc.js
  • lines 192 to 230
  • ModelCraft 0.8.1
const sample = (kids, h, i) => {
  const g = kids[h.index];
  if (!g || g.data) throw bad('a property is missing its data');
  const sk = og.group(g.pos);
  if (h.next === 0) return null;
  const k = stored(h, Math.min(i, h.next - 1));
  const di = h.kind === 'array' ? k * 2 : k;
  const de = sk[di];
  if (!de || !de.data) throw bad('a property sample is missing');
  const d = og.data(de.pos);
  if (d.length === 0) return null;
  if (d.length < 16) throw bad('a property sample is shorter than its hash');
  const body = d.subarray(16);
  if (h.pod === POD.string) return dec.decode(body).split('\0').slice(0, -1);
  if (h.pod === POD.wstring) return null;
  const size = POD_SIZE[h.pod];
  const n = Math.floor(body.length / size);
  numbers -= n;
  if (numbers < 0) throw new AbcError('too_big', t('abc.too_big'));
  const dv = new DataView(body.buffer, body.byteOffset, body.byteLength);
  const out = h.pod === POD.f64 ? new Float64Array(n) : h.pod === POD.f32 || h.pod === POD.f16 ? new Float32Array(n) : h.pod === POD.u32 || h.pod === POD.u64 ? new Uint32Array(n) : new Int32Array(n);
  for (let q = 0; q < n; q++) {
    const o = q * size;
    switch (h.pod) {
      case POD.bool: case POD.u8: out[q] = body[o]; break;
      case POD.i8: out[q] = dv.getInt8(o); break;
      case POD.u16: out[q] = dv.getUint16(o, true); break;
      case POD.i16: out[q] = dv.getInt16(o, true); break;
      case POD.u32: out[q] = dv.getUint32(o, true); break;
      case POD.i32: out[q] = dv.getInt32(o, true); break;
      case POD.u64: out[q] = dv.getUint32(o, true); break;
      case POD.i64: out[q] = dv.getInt32(o, true); break;
      case POD.f16: out[q] = half(dv.getUint16(o, true)); break;
      case POD.f32: out[q] = dv.getFloat32(o, true); break;
      default: out[q] = dv.getFloat64(o, true);
    }
  }
  return out;
};

16-bit float to a number

64-bit

Decodes a half-precision float.

subnormal: ± m × 2⁻²⁴; normal: ± (1 + m ÷ 1024) × 2^(e − 15)

Exact: every half-precision value is a 64-bit number, and the powers of two and the division by 1024 introduce no rounding.

  • io/abc.js
  • lines 348 to 353
  • ModelCraft 0.8.1
function half(h) {
  const s = h & 0x8000 ? -1 : 1;
  const e = (h >> 10) & 31;
  const m = h & 1023;
  return e === 0 ? s * m * 2 ** -24 : e === 31 ? (m ? NaN : s * Infinity) : s * (1 + m / 1024) * 2 ** (e - 15);
}

Which time sample is read

64-bit

The index of the stored sample nearest a given time.

time of sample i = times[i mod n] + floor(i ÷ n) × time per cycle; nearest by |time − t|

A linear scan over at most 1,000,000 samples comparing 64-bit times. Geometry is taken from that one sample; nothing is interpolated between samples.

  • io/abc.js
  • lines 177 to 190
  • ModelCraft 0.8.1
const sampleAt = (h, time) => {
  const n = h.next;
  if (n <= 1 || time === undefined || time === null) return 0;
  const T = times[h.ts];
  if (!T) return 0;
  // uniform and cyclic samplings repeat their times every cycle; an acyclic one (time per cycle = the largest double
  // ÷ 32) lists every time
  const acyclic = !(T.tpc < 1e300);
  const at = (i) => (acyclic ? T.times[Math.min(i, T.times.length - 1)] : T.times[i % T.times.length] + Math.floor(i / T.times.length) * T.tpc);
  let best = 0;
  let bd = Infinity;
  for (let i = 0; i < Math.min(n, 1e6); i++) { const dd = Math.abs(at(i) - time); if (dd < bd) { bd = dd; best = i; } }
  return best;
};

An object's transform

64-bit

Builds an object's matrix from its list of scale, translate, rotate and matrix operations.

M = op₁ × op₂ × … (the first operation outermost)

The operation values are used in the type the file stored them in and multiplied as 64-bit numbers. A matrix with any entry that is not finite is refused.

  • io/abc.js
  • lines 292 to 319
  • ModelCraft 0.8.1
const readXform = (node, g) => {
  const opsH = g.heads.find((h) => h.name === '.ops');
  const valsH = g.heads.find((h) => h.name === '.vals');
  const inhH = g.heads.find((h) => h.name === '.inherits');
  if (inhH) { const v = sample(g.kids, inhH, sampleAt(inhH, time)); node.inherits = !v || v[0] !== 0; }
  if (!opsH || !valsH) { node.matrix = ident(); return; }
  maxSamples = Math.max(maxSamples, valsH.next || 1);
  const ops = sample(g.kids, opsH, 0) || [];
  const vals = sample(g.kids, valsH, sampleAt(valsH, time)) || [];
  let m = ident();
  let c = 0;
  const take = (n) => { if (c + n > vals.length) throw bad('an xform has too few values for its operations'); const v = Array.from(vals.subarray(c, c + n)); c += n; return v; };
  for (const op of ops) {
    const type = op >> 4;
    let M;
    if (type === 0) { const [x, y, z] = take(3); M = [x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1]; }
    else if (type === 1) { const [x, y, z] = take(3); M = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1]; }
    else if (type === 2) { const [x, y, z, a] = take(4); M = axisAngle([x, y, z], a); }
    else if (type === 3) M = take(16);   // Imath rows (row vectors) = our columns (column vectors)
    else if (type === 4) M = axisAngle([1, 0, 0], take(1)[0]);
    else if (type === 5) M = axisAngle([0, 1, 0], take(1)[0]);
    else if (type === 6) M = axisAngle([0, 0, 1], take(1)[0]);
    else throw bad('an xform has an unknown operation');
    m = mmul(m, M);   // the first operation is the outermost
  }
  if (!m.every(Number.isFinite)) throw bad('an xform is not a number');
  node.matrix = m;
};

Rotation about an axis

64-bit

The matrix of a turn by an angle in degrees about an axis.

a = deg × π ÷ 180; Rodrigues' formula with the axis made unit

The axis is divided by its Math.hypot length; an axis of zero length gives the identity. A 90° turn has a cosine of about 6e-17 in its entries instead of 0.

  • io/abc.js
  • lines 355 to 365
  • ModelCraft 0.8.1
function axisAngle(axis, deg) {
  const l = Math.hypot(...axis);
  if (!(l > 0)) return ident();
  const [x, y, z] = axis.map((v) => v / l);
  const a = (deg * Math.PI) / 180;
  const c = Math.cos(a);
  const s = Math.sin(a);
  const k = 1 - c;
  // column-major, column vectors: a right-handed turn of `deg` about the axis
  return [c + x * x * k, y * x * k + z * s, z * x * k - y * s, 0, x * y * k - z * s, c + y * y * k, z * y * k + x * s, 0, x * z * k + y * s, y * z * k - x * s, c + z * z * k, 0, 0, 0, 0, 1];
}

Mesh points into Z up

64-bit

Copies the file's points into the model's coordinates.

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

A swap and a sign change into a Float64Array: nothing is rounded here. The values are those the file stored, 32-bit in most Alembic files.

  • io/abc.js
  • lines 417 to 418
  • ModelCraft 0.8.1
const pts = new Float64Array(nv * 3);
for (let i = 0; i < nv; i++) { pts[i * 3] = m.positions[i * 3]; pts[i * 3 + 1] = -m.positions[i * 3 + 2]; pts[i * 3 + 2] = m.positions[i * 3 + 1]; }

Node matrix into Z up

64-bit

Changes a node's matrix from Y up to Z up.

M′ = C × M × C⁻¹

C and its inverse hold only 0, 1 and −1, so the two products only move and negate entries of M.

  • io/abc.js
  • line 371
  • ModelCraft 0.8.1
const conj = (m) => mmul(C, mmul(m, CI));

Which way the faces turn

64-bit

Decides whether the file's faces must be turned round, by comparing each face's own normal with the file's normals.

Newell normal n = Σ (yₐ − y_b)(zₐ + z_b), …; agree when n · Σ file normals > 0

A yes-or-no decision from the sign of a 64-bit dot product, counted over at most the first 2,000 faces; faces whose dot product is exactly 0 are not counted. The faces are reversed unless more faces agree than disagree.

  • io/abc.js
  • lines 426 to 436
  • ModelCraft 0.8.1
// compare each face's own normal (counter-clockwise reading) with the file's normals
let agree = 0;
let disagree = 0;
for (let f = 0; f < nf && f < 2000; f++) {
  const n = newell(m.positions, m.indices, starts[f], m.counts[f]);
  let fx = 0; let fy = 0; let fz = 0;
  for (let c = starts[f]; c < starts[f + 1]; c++) { fx += Nf[c * 3]; fy += Nf[c * 3 + 1]; fz += Nf[c * 3 + 2]; }
  const d = n[0] * fx + n[1] * fy + n[2] * fz;
  if (d > 0) agree++; else if (d < 0) disagree++;
}
reverse = disagree >= agree;

In detail

Method
Ogawa container read by position; transform operations multiplied in file order; Y up to Z up by (x, y, z) → (x, −z, y).
Number types
Float64Array or Float32Array as the file's type says; Int32Array and Uint32Array for whole numbers; Float64Array for the mesh points handed to the model.
Tolerances
A mesh point must be below 1e8 in size. The winding is decided from the first 2,000 faces.
Limits
Positions stored as 32-bit floats carry about 7 digits. 64-bit whole numbers are read by their low 32 bits only. No interpolation between time samples: the nearest sample is used.

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