The vertex offsets stored for each shape key, the way a key is made from an edit or from imported positions, and the blended points of a mesh.
How the precision is kept
A mesh keeps its points in 64-bit numbers (a Float64Array), but every shape key layer, and the saved basis used while a key is edited, is a 32-bit attribute layer. Offsets and the restored basis therefore pass through 32-bit storage. The sum of the points and the weighted offsets is done in 64-bit numbers when the output is the mesh's own point array.
Open item. Create the shape.basis and shape key layers with type 'f64' (addAttr's type argument) so finishing an edit does not round the mesh points to 32-bit.
Calculations under this policy
5 calculations in 2 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.
anim/shapes.jsModelCraft 0.8.1 32-bit in part4 calculationsReviewed
Tests bin/test-animation.mjs
Start editing a key
32-bit in part
Saves the current points as the basis and shows the key at full strength.
point = basis + offset
The basis is copied into a layer made with addAttr's default type 'f32', so each saved basis coordinate is rounded to 32-bit. The edited point is basis (64-bit, before the copy) plus the 32-bit offset.
anim/shapes.js
lines 47 to 61
ModelCraft 0.8.1
/** Start editing key `id`: every vertex there now is marked with the key's number (a vertex added later reads 0). */
export function beginEdit(mesh, id) {
if (mesh.hasAttr(BASIS) || !mesh.hasAttr(PREFIX + id)) return false;
mesh.addAttr(BASIS, 'vertex', 3);
mesh.addAttr(EDITING, 'vertex', 1);
const P = mesh.P, off = mesh.attrArray(PREFIX + id), b = [0, 0, 0], k = [+id.slice(1)];
for (let v = 0; v < mesh.nv; v++) {
if (mesh.vertDead(v)) continue;
b[0] = P[v * 3]; b[1] = P[v * 3 + 1]; b[2] = P[v * 3 + 2];
mesh.setAttr(BASIS, v, b);
mesh.setAttr(EDITING, v, k);
mesh.setPos(v, b[0] + off[v * 3], b[1] + off[v * 3 + 1], b[2] + off[v * 3 + 2]);
}
return true;
}
Finish editing a key
32-bit in part
Turns the edited points into the key's offsets and puts the mesh back to its basis.
offset = edited − basis; point = basis
The basis is read from the 32-bit layer and written back as the mesh point, so after the edit every vertex that was in the edit sits at its 32-bit rounded basis coordinate rather than the original 64-bit one. The offset is a 64-bit difference of the edited point and that 32-bit basis, then stored in a 32-bit layer.
anim/shapes.js
lines 62 to 78
ModelCraft 0.8.1
/** End an edit: the key = the points now − the basis; the points go back to the basis (vertices added meanwhile keep their point, offset 0). */
export function finishEdit(mesh) {
const id = editing(mesh);
if (!id) { if (mesh.hasAttr(BASIS)) { mesh.removeAttr(BASIS); mesh.removeAttr(EDITING); } return null; }
const B = mesh.attrArray(BASIS), E = mesh.attrArray(EDITING), P = mesh.P, d = [0, 0, 0];
const name = PREFIX + id;
for (let v = 0; v < mesh.nv; v++) {
if (mesh.vertDead(v)) continue;
if (!(E[v] > 0)) { d[0] = d[1] = d[2] = 0; mesh.setAttr(name, v, d); continue; } // made during the edit: no basis of its own
const bx = B[v * 3], by = B[v * 3 + 1], bz = B[v * 3 + 2];
d[0] = P[v * 3] - bx; d[1] = P[v * 3 + 1] - by; d[2] = P[v * 3 + 2] - bz;
mesh.setAttr(name, v, d);
mesh.setPos(v, bx, by, bz);
}
mesh.removeAttr(BASIS); mesh.removeAttr(EDITING);
return id;
}
Key from imported positions
32-bit in part
Makes a key that moves the basis to a list of positions (glTF morph targets, USD blend shapes).
offset = position − basis point
The positions are documented as a Float32Array, and the offset is stored in a 32-bit layer. The subtraction itself is 64-bit.
The offsets are read from 32-bit layers. The sum is accumulated in place in the output array, in key order: in 64-bit when the deformer's own point array is used, in 32-bit when blendShapes allocates its default Float32Array.
anim/shapes.js
lines 97 to 110
ModelCraft 0.8.1
/** Every vertex's point with the keys at `weights` (keys at 0 cost nothing). `out` is reused when it is big enough. */
export function blendShapes(mesh, weights, out) {
const n = mesh.nv * 3, P = mesh.P;
const o = out && out.length >= n ? out : new Float32Array(n);
o.set(P.subarray(0, n));
if (!weights) return o;
for (const [id, w] of weights) {
if (!w) continue;
const off = mesh.attrArray(PREFIX + id);
if (!off) continue;
for (let i = 0; i < n; i++) o[i] += w * off[i];
}
return o;
}
anim/deform.jsModelCraft 0.8.1 32-bit in part1 calculationReviewed
Tests bin/test-animation.mjs
Deformed copy of a mesh
32-bit in part
The mesh moved by its shape keys and then its skin, which the view, picking and exports read.
points = blend of keys; then skin applied to the same array
The copy's points are the 64-bit array of the cloned mesh. The key offsets added into it are 32-bit values. The skin step is skinPoints in rig.js, not read here.
anim/deform.js
lines 54 to 80
ModelCraft 0.8.1
function deform(def, base) {
if (base !== def.mesh || !deforms(def)) return null; // a curve object's made mesh has no shape keys or skin
const s = state.get(def.id);
let c = cache.get(def.id);
const key = base.uid + ':' + base.v + ':' + base.live + ':' + base.nv;
if (!c || c.key !== key) { c = { key, mesh: base.clone(), stamp: -1 }; cache.set(def.id, c); }
if (c.stamp !== s.stamp) {
const D = c.mesh, P = D.P;
blendShapes(base, s.weights, P); // straight into the copy's points
if (s.show) {
// painting weights (AN.2): at rest, each face coloured by the bone's weight there (blue none … red all), on this copy
// only; topo goes up so the view builds its colours again
const W = base.attrArray('weight.' + s.show), cs = [];
for (let f = 0; f < base.nf; f++) {
if (base.faceDead(f)) continue;
base.faceVerts(f, cs);
let w = 0;
for (const v of cs) w += W ? W[v] : 0;
D.setFaceMat(f, D.paintIndex(RAMP[Math.round((w / (cs.length || 1)) * (RAMP.length - 1))]));
}
D.topo++;
} else if (s.skin) s.skin.apply(base, P);
D.live++;
c.stamp = s.stamp;
}
return c.mesh;
}
In detail
Method
Key offset = edited point − basis point; blended point = basis + Σ weight × offset over the keys with a weight other than 0.
Number types
mesh.P is a Float64Array (mesh.js, first base column). Attribute layers added with addAttr default to type 'f32' and are Float32Array columns. blendShapes allocates a Float32Array only when no output array is given; the deformer passes the 64-bit point array.
Tolerances
None. A weight of exactly 0 skips a key.
Limits
Imported morph target positions arrive as a Float32Array (see setFromPositions), so those keys cannot be more exact than 32-bit.
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.