Writes the points of every mesh into a USD file and reads them back when a USD file is opened.
How the precision is kept
Mesh points are written with the attribute type point3f[] and the bounding extent with float3[], and both are converted to 32-bit numbers when the value is made. A coordinate in the file therefore keeps about 7 significant digits. The object's placement matrix is 64-bit, so points that are stored close to their object's origin lose little.
Open item. Write mesh points and extent with the 64-bit type point3d[] (or keep points local to each object and document the range) so saved coordinates keep 15 digits.
Calculations under this policy
7 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.
usd/build.jsCadCraft 0.8.2 32-bit in part3 calculationsReviewed
Tests tools/usd/test.mjs, tools/usd/test-apps.mjs
Attribute value to its storage type
32-bit in part
Converts a list of numbers to the number type its USD attribute type calls for.
An array whose type is float or half based is converted with Float32Array.from, which rounds every number to 32-bit; a double based array becomes a Float64Array and keeps its value. This is where a mesh coordinate loses its digits beyond about 7.
usd/build.js
lines 59 to 69
CadCraft 0.8.2
/** A JS value as a Value of the attribute type (role names allowed: 'point3f[]', 'color3f'). */
export function valueOf(typeName, x) {
const info = typeInfo(typeName);
if (!info) throw new Error('unknown type ' + typeName); // i18n-ignore: programmer error
const { base, array, def } = info;
if (!array) return V(base, def.n > 1 ? Array.from(x) : x);
if (def.k === 'f32' || def.k === 'f16') return V(base + '[]', x instanceof Float32Array ? x : Float32Array.from(x));
if (def.k === 'f64') return V(base + '[]', x instanceof Float64Array ? x : Float64Array.from(x));
if (def.k === 'i32') return V(base + '[]', x instanceof Int32Array ? x : Int32Array.from(x));
return V(base + '[]', Array.from(x));
}
Mesh points and extent
32-bit in part
Stores the points of a mesh and its bounding box in the scene.
extent = [min x, min y, min z, max x, max y, max z] of the points
The points are written with the type point3f[] and the extent with float3[], so both pass through Float32Array. The minimum and maximum are found in 64-bit and then rounded to 32-bit; rounding keeps order, so the stored extent is the box of the stored points.
usd/build.js
lines 133 to 141
CadCraft 0.8.2
const pts = geo.points;
let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < pts.length; i += 3) for (let c = 0; c < 3; c++) { lo[c] = Math.min(lo[c], pts[i + c]); hi[c] = Math.max(hi[c], pts[i + c]); }
if (!pts.length) { lo = [0, 0, 0]; hi = [0, 0, 0]; }
this.attr(p, 'extent', 'float3[]', [...lo, ...hi]);
this.attr(p, 'faceVertexCounts', 'int[]', geo.counts);
this.attr(p, 'faceVertexIndices', 'int[]', geo.indices);
if (geo.holes && geo.holes.length) this.attr(p, 'holeIndices', 'int[]', geo.holes);
this.attr(p, 'points', 'point3f[]', pts);
Lamp intensity
32-bit, display only
Converts a lamp's lumens to the intensity a renderer reads.
intensity = lumens ÷ (683 × area × π)
The intensity is a rendering input and is written as a 32-bit float. The lumens a person entered are also written unchanged as a 64-bit custom attribute. The sphere area is floored at 1e-6.
usd/usdc-write.jsCadCraft 0.8.2 32-bit in part2 calculationsReviewed
Tests tools/usd/test.mjs
Single values in the binary file
64-bit
Writes one number, tuple or matrix into the binary USD file.
A 64-bit value is stored in 32 bits only when Math.fround(x) is exactly x (checked with Object.is), which loses nothing; otherwise all 8 bytes are written. A matrix is stored in short form only when its diagonal is small whole numbers and every other entry is exactly 0. Values of an int type are truncated with x | 0.
usd/usdc-write.js
lines 129 to 165
CadCraft 0.8.2
const packScalar = (base, d, x) => {
const e = d.e;
if (base === 'pathExpression') return rep(e, at(null, () => w.u32(str(x))));
if (d.n === 1) {
switch (d.k) {
case 'bool': return rep(e, x ? 1 : 0, { inline: true });
case 'u8': return rep(e, x & 0xff, { inline: true });
case 'i32': return rep(e, (x | 0) >>> 0, { inline: true });
case 'u32': return rep(e, x >>> 0, { inline: true });
case 'f16': return rep(e, floatToHalf(x), { inline: true });
case 'f32': { F.setFloat32(0, x, true); return rep(e, F.getUint32(0, true), { inline: true }); }
case 'str': return rep(e, str(x), { inline: true });
case 'tok': case 'asset': return rep(e, tok(x), { inline: true });
case 'f64':
if (base !== 'timecode' && Object.is(Math.fround(x), x) && Number.isFinite(x)) { F.setFloat32(0, x, true); return rep(e, F.getUint32(0, true), { inline: true }); }
return rep(e, at(null, () => w.f64(x)));
case 'i64': case 'u64': {
const n = typeof x === 'bigint' ? x : BigInt(x);
if (d.k === 'i64' && n >= -2147483648n && n <= 2147483647n) return rep(e, Number(BigInt.asUintN(32, n)), { inline: true });
if (d.k === 'u64' && n >= 0n && n <= 4294967295n) return rep(e, Number(n), { inline: true });
return rep(e, at(null, () => (d.k === 'i64' ? w.i64(n) : w.u64(n))));
}
default: break;
}
}
const a = Array.from(x);
if (d.mat) {
const m = d.mat;
let ok = true;
for (let r = 0; r < m && ok; r++) for (let c = 0; c < m; c++) { const y = a[r * m + c]; if (r === c ? !isI8(y) : !Object.is(y, 0)) { ok = false; break; } }
if (ok) { let pl = 0; for (let k = 0; k < m; k++) pl |= (a[k * m + k] & 0xff) << (8 * k); return rep(e, pl >>> 0, { inline: true }); }
return rep(e, at(null, () => { for (const y of a) w.f64(y); }));
}
if (d.quat) return rep(e, at(null, () => { for (const y of quatToMemory(a)) writeScalar(d.k, y); }));
if (a.every(isI8)) { let pl = 0; for (let k = 0; k < d.n; k++) pl |= (a[k] & 0xff) << (8 * k); return rep(e, pl >>> 0, { inline: true }); }
return rep(e, at(null, () => { for (const y of a) writeScalar(d.k, y); }));
};
Arrays in the binary file
32-bit in part
Writes an array of numbers into the binary USD file.
A float-typed array is written as the bytes of a Float32Array and a double-typed array as the bytes of a Float64Array, with no further change. Mesh points are float-typed, so the file holds their 32-bit values.
usd/usdc-write.js
lines 167 to 190
CadCraft 0.8.2
const packArray = (base, d, v) => {
const e = d.e;
const len = v.length;
if (!len) return rep(e, 0, { array: true });
const n = len / d.n;
const k = d.k;
if ((k === 'i32' || k === 'u32' || k === 'i64' || k === 'u64') && n >= 16) {
const wide = k === 'i64' || k === 'u64';
return rep(e, at(null, () => {
w.u64(n);
const packed = encodeInts(wide ? Array.from(v, (x) => BigInt(x)) : v, { wide });
w.u64(packed.length); w.bytes(packed);
}), { array: true, compressed: true });
}
return rep(e, at(null, () => {
w.u64(n);
if (k === 'str' || k === 'asset' || k === 'pathexpr') { for (const s of v) w.u32(str(s)); return; }
if (k === 'tok') { for (const s of v) w.u32(tok(s)); return; }
if (d.quat) { for (let i = 0; i < len; i += 4) for (const y of quatToMemory([v[i], v[i + 1], v[i + 2], v[i + 3]])) writeScalar(k, y); return; }
if (k === 'f32') { w.bytes(new Uint8Array(Float32Array.from(v).buffer)); return; }
if (k === 'f64') { w.bytes(new Uint8Array(Float64Array.from(v).buffer)); return; }
for (let i = 0; i < len; i++) writeScalar(k, v[i]);
}), { array: true });
};
usd/geom.jsCadCraft 0.8.2 32-bit in part2 calculationsReviewed
The points, faces and attached values of a mesh read from a USD scene.
The points are copied into a Float64Array with Float64Array.from, which keeps exactly what the file holds: 32-bit values for the usual point3f[] attribute, 64-bit for point3d[]. Face counts and indices are checked against the number of points before the mesh is accepted.
usd/geom.js
lines 184 to 223
CadCraft 0.8.2
export function meshOf(prim, time) {
if (prim.typeName !== 'Mesh') {
// a built-in shape: its own topology, and the prim's constant primvars (a display colour still applies)
const m = shapeMesh(prim, time);
if (m) for (const name of prim.props.keys()) {
if (!name.startsWith('primvars:') || name.endsWith(':indices')) continue;
const pv = primvar(prim, name, time);
if (pv && pv.interp === 'constant') m.primvars.set(name.slice(9), pv);
}
return m;
}
const pts = valueAt(prim.props.get('points'), time);
const counts = valueAt(prim.props.get('faceVertexCounts'), time);
const idx = valueAt(prim.props.get('faceVertexIndices'), time);
if (!pts || !counts || !idx) return null;
const points = Float64Array.from(pts.v);
const nPts = points.length / 3;
const ci = Int32Array.from(counts.v), ii = Int32Array.from(idx.v);
// drop a mesh whose faces don't add up or point outside its points (untrusted: never index past an array)
let total = 0;
for (const c of ci) { if (c < 0) return null; total += c; }
if (total !== ii.length) return null;
for (const k of ii) if (k < 0 || k >= nPts) return null;
const primvars = new Map();
for (const name of prim.props.keys()) {
if (name.startsWith('primvars:') && !name.endsWith(':indices')) { const pv = primvar(prim, name, time); if (pv) primvars.set(name.slice(9), pv); }
}
const nrm = primvar(prim, 'normals', time);
if (nrm && !primvars.has('normals')) primvars.set('normals', nrm);
const holes = valueAt(prim.props.get('holeIndices'), time);
const ci2 = valueAt(prim.props.get('creaseIndices'), time), cl = valueAt(prim.props.get('creaseLengths'), time), cs = valueAt(prim.props.get('creaseSharpnesses'), time);
return {
points, counts: ci, indices: ii, holes: holes ? Array.from(holes.v) : [],
leftHanded: tokenOf(prim, 'orientation', 'rightHanded') === 'leftHanded',
subdivision: tokenOf(prim, 'subdivisionScheme', 'catmullClark'),
creases: ci2 && cl ? { indices: Array.from(ci2.v), lengths: Array.from(cl.v), sharpness: cs ? Array.from(cs.v) : [] } : null,
doubleSided: valueAt(prim.props.get('doubleSided'))?.v === true,
primvars,
};
}
Built-in shapes as meshes
64-bit
A cube, sphere, cylinder, cone, capsule or plane of a USD scene as a mesh.
ring point = (r·cos θ, r·sin θ, z), θ = 2π·j ÷ 24
Points are computed in 64-bit from their own angles and kept in a Float64Array. Round shapes have 24 segments around (a sphere has 12 rings), so the mesh lies inside the true surface.
usd/geom.js
lines 226 to 283
CadCraft 0.8.2
/** The built-in shapes as meshes (fallback sizes from the schema). */
export function shapeMesh(prim, time) {
const t = prim.typeName;
const P = [], C = [], I = [];
const quad = (a, b, c, d) => { C.push(4); I.push(a, b, c, d); };
const tri = (a, b, c) => { C.push(3); I.push(a, b, c); };
const v = (x, y, z) => { P.push(x, y, z); return P.length / 3 - 1; };
if (t === 'Cube') {
const s = num(prim, 'size', time, 2) / 2;
for (const [x, y, z] of [[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1], [-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]]) v(x * s, y * s, z * s);
quad(0, 3, 2, 1); quad(4, 5, 6, 7); quad(0, 1, 5, 4); quad(1, 2, 6, 5); quad(2, 3, 7, 6); quad(3, 0, 4, 7);
return out(P, C, I);
}
if (t === 'Sphere') {
const r = num(prim, 'radius', time, 1), rings = SEG / 2;
const top = v(0, 0, r), bot = v(0, 0, -r);
const ring = [];
for (let i = 1; i < rings; i++) {
const ph = (Math.PI * i) / rings, row = [];
for (let j = 0; j < SEG; j++) { const th = (2 * Math.PI * j) / SEG; row.push(v(r * Math.sin(ph) * Math.cos(th), r * Math.sin(ph) * Math.sin(th), r * Math.cos(ph))); }
ring.push(row);
}
for (let j = 0; j < SEG; j++) {
const k = (j + 1) % SEG;
tri(top, ring[0][j], ring[0][k]);
for (let i = 0; i < ring.length - 1; i++) quad(ring[i][j], ring[i + 1][j], ring[i + 1][k], ring[i][k]);
tri(bot, ring[ring.length - 1][k], ring[ring.length - 1][j]);
}
return axisTurn(out(P, C, I), 'Z');
}
if (t === 'Cylinder' || t === 'Cone' || t === 'Capsule') {
const h = num(prim, 'height', time, t === 'Capsule' ? 1 : 2), r = num(prim, 'radius', time, t === 'Capsule' ? 0.5 : 1);
const axis = tokenOf(prim, 'axis', 'Z');
const z0 = -h / 2, z1 = h / 2;
const ringAt = (z, rr) => { const row = []; for (let j = 0; j < SEG; j++) { const th = (2 * Math.PI * j) / SEG; row.push(v(rr * Math.cos(th), rr * Math.sin(th), z)); } return row; };
if (t === 'Cone') {
const b = ringAt(z0, r), apex = v(0, 0, z1), c0 = v(0, 0, z0);
for (let j = 0; j < SEG; j++) { const k = (j + 1) % SEG; tri(b[j], b[k], apex); tri(c0, b[k], b[j]); }
} else if (t === 'Cylinder') {
const b = ringAt(z0, r), a = ringAt(z1, r), c0 = v(0, 0, z0), c1 = v(0, 0, z1);
for (let j = 0; j < SEG; j++) { const k = (j + 1) % SEG; quad(b[j], b[k], a[k], a[j]); tri(c0, b[k], b[j]); tri(c1, a[j], a[k]); }
} else {
const rings = [];
const half = SEG / 4;
for (let i = half; i >= 0; i--) { const ph = (Math.PI / 2) * (i / half); rings.push(ringAt(z0 - r * Math.sin(ph), r * Math.cos(ph))); }
for (let i = 0; i <= half; i++) { const ph = (Math.PI / 2) * (i / half); rings.push(ringAt(z1 + r * Math.sin(ph), r * Math.cos(ph))); }
for (let i = 0; i < rings.length - 1; i++) for (let j = 0; j < SEG; j++) { const k = (j + 1) % SEG; quad(rings[i][j], rings[i][k], rings[i + 1][k], rings[i + 1][j]); }
}
return axisTurn(out(P, C, I), axis);
}
if (t === 'Plane') {
const w = num(prim, 'width', time, 2) / 2, l = num(prim, 'length', time, 2) / 2;
v(-w, -l, 0); v(w, -l, 0); v(w, l, 0); v(-w, l, 0);
quad(0, 1, 2, 3);
return axisTurn(out(P, C, I), tokenOf(prim, 'axis', 'Z'));
}
return null;
}
In detail
Method
Attribute values are converted to the storage type of their USD type name; float-typed arrays become Float32Array and are written as raw 32-bit bytes in the binary format.
Number types
Float32Array for points, extent, normals, texture coordinates and colors; 64-bit for matrices and double-typed attributes; Float64Array after reading.
Limits
At 1000 units from an object's origin, neighboring 32-bit values are about 0.00006 units apart. Built-in shapes are meshed with 24 segments around.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.