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