Resources Numerical accuracy Calculation policy

Polygon mesh: attribute layers (UV, color, crease, weights)

The extra values a mesh keeps per vertex, corner, face or edge, such as texture coordinates, colors, edge crease and skin weights.

How the precision is kept

An attribute layer is a typed array chosen by the layer's type: 'f32' is a Float32Array and 'i32' an Int32Array. There is no 64-bit layer type, so every number stored in a layer is rounded to 32 bits (about 7 significant digits) when it is written, and that is what is read back, blended and saved. Vertex positions are not a layer; they are 64-bit.

Open item. Add a 64-bit layer type for values that shape geometry (crease, weights), or state the 7-digit limit where such values are shown.

Calculations under this policy

4 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/mesh.js CadCraft 0.8.2 32-bit in part 4 calculations Reviewed

Tests None found by the review

Adding a layer

32-bit in part

Creates a layer of 1 to 16 numbers per element.

The layer's array is a Float32Array for type 'f32' and an Int32Array for 'i32'; no other types are accepted. The fill value is stored through the same array.

  • m3d/mesh.js
  • lines 449 to 462
  • CadCraft 0.8.2
/** Add an attribute layer (filled with `fill`); → its info. Throws MeshError 'bad' for a bad name/domain/size/type. */
addAttr(name, domain, size = 1, type = 'f32', fill = 0) {
  if (typeof name !== 'string' || !NAME_RE.test(name) || !DOMAINS.includes(domain) || !(Number.isInteger(size) && size >= 1 && size <= 16) || !['f32', 'i32'].includes(type)) throw new MeshError('bad');
  if (this.attrs.has(name)) throw new MeshError('exists');
  if (this.attrs.size >= LIMITS.attrs) throw new MeshError('limit');
  const dom = DOM_OF[domain];
  const col = new Col(this.cols.length, dom, size, type === 'f32' ? Float32Array : Int32Array, this.cap[dom]);
  if (fill) col.a.fill(fill);
  this.cols.push(col);
  const L = { name, domain, size, type, col };
  this.attrs.set(name, L);
  if (this._rec) this._rec.ops.push({ at: this._rec.len, kind: 'attrAdd', layer: L });
  return this.attrInfo(name);
}

Storing a value in a layer

32-bit in part

Writes the values of one element of a layer.

Each value is converted with unary plus and anything that is not a number, or is zero, becomes 0. It is then stored in the layer's array, which rounds it to 32 bits. The stored 32-bit value is compared with the 64-bit input to decide whether to write, so an input that 32 bits cannot hold exactly is written again each time.

  • m3d/mesh.js
  • lines 482 to 489
  • CadCraft 0.8.2
/** Set element el of a layer to `values` (an edge's value goes on both halves). */
setAttr(name, el, values) {
  const L = this.attrs.get(name);
  if (!L) throw new MeshError('missing');
  const put = (x) => { for (let k = 0; k < L.size; k++) { const val = +values[k] || 0; if (L.col.a[x * L.size + k] !== val) this._w(L.col.id, x * L.size + k, val); } };
  put(el);
  if (L.domain === 'edge' && this.hTw[el] >= 0) put(this.hTw[el]);
}

Blending corner values onto new faces

32-bit in part

The values new face corners get when faces are cut or joined, as a weighted mix of old corners.

out = Σ wᵢ × valueᵢ

The source values are read from the 32-bit layer, weighted and summed in a Float64Array, and the sum is stored back into the 32-bit layer. The weights are not checked to add up to 1.

  • m3d/mesh.js
  • lines 329 to 335
  • CadCraft 0.8.2
const blend = (src, L) => {
  const out = new Float64Array(L.size);
  if (src === null || src === undefined) return out;
  const list = Array.isArray(src) ? src : [[src, 1]];
  for (const [c, w] of list) for (let k = 0; k < L.size; k++) out[k] += L.col.a[c * L.size + k] * w;
  return out;
};

Layers in the mesh file

32-bit in part

The layer data as it is written into the file.

Each layer is copied into a new array of its own type (Float32Array for 'f32') and written as base64 of its bytes, so the file holds the same 32-bit values as memory.

  • m3d/mesh.js
  • lines 780 to 785
  • CadCraft 0.8.2
const T = L.type === 'f32' ? Float32Array : Int32Array, s = L.size;
let out;
if (L.domain === 'vertex') { out = new T(nv * s); for (let v = 0; v < this.nv; v++) if (vmap[v] >= 0) for (let j = 0; j < s; j++) out[vmap[v] * s + j] = L.col.a[v * s + j]; }
else if (L.domain === 'face') { out = new T(faces.length * s); faces.forEach((f, i) => { for (let j = 0; j < s; j++) out[i * s + j] = L.col.a[f * s + j]; }); }
else { out = new T(nc * s); for (let c = 0; c < nc; c++) for (let j = 0; j < s; j++) out[c * s + j] = L.col.a[corners[c] * s + j]; }
attrs.push({ name: L.name, domain: L.domain, size: s, type: L.type, data: b64(out) });

In detail

Method
Typed array per layer; weighted sum of source corners when faces are replaced.
Number types
Float32Array or Int32Array per layer; blends are summed in a Float64Array and then stored in the layer.
Limits
A value typed as 0.3 is stored as the nearest 32-bit number. The file header names the layers in use by convention: uv, normal, color, crease and weight layers.

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.

Every policy that takes this approach