Resources Numerical accuracy Calculation policy

OBJ import and export

Reads and writes Wavefront OBJ files: points, faces, texture coordinates and normals as text, in meters with Y up.

How the precision is kept

On import each number in the text is parsed to a 64-bit number and stored as it is, with the axes swapped. On export every coordinate, texture coordinate and normal is printed with 6 decimals, so an exported point is rounded to 0.000001 m.

Open item. Print exported coordinates with the shortest text that reads back to the same 64-bit number instead of 6 decimals.

Calculations under this policy

6 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/obj.js ModelCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/modelcraft-test/meshfiles.mjs

Reading a vertex

64-bit

Parses the three coordinates of a 'v' line.

Each number is converted from text to the nearest 64-bit number and kept unrounded. A coordinate that is not finite or is 1e7 or more in size refuses the file.

  • io/obj.js
  • lines 114 to 117
  • ModelCraft 0.8.1
if (tag === 'v') {
  const x = +parts[1]; const y = +parts[2]; const z = +parts[3];
  if (![x, y, z].every((q) => Number.isFinite(q) && Math.abs(q) < 1e7)) throw fail(t('obj.why.vertex', { line: lineNo }));
  P.push(x, y, z);

Points into Z up

64-bit

Copies an object's points into the model's coordinates.

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

A swap and a sign change; the points then go into a Float64Array. No unit factor is applied: the file is read as meters.

  • io/obj.js
  • line 205
  • ModelCraft 0.8.1
if (k === undefined) { k = pts.length / 3; local.set(v, k); pts.push(obj.positions[v * 3], -obj.positions[v * 3 + 2], obj.positions[v * 3 + 1]); }

Sharp edges

64-bit

Marks an edge sharp where the file's normals break across it, where smoothing groups differ, or where the faces bend.

with normals: sharp unless n₁ · n₂ > 0.9995 at both ends; without normals or groups: sharp when the face normals' dot product < 0.99996

0.9995 is the cosine of about 1.8° and 0.99996 of about 0.5°. The corner normals compared are read from the mesh's 32-bit 'normal' layer.

  • io/obj.js
  • lines 249 to 263
  • ModelCraft 0.8.1
/** Sharp where the corner normals break across an edge; with no normals, between smoothing groups (0 = off: all sharp). */
export function markSharp(mesh, N, smoothOf = null) {
  const { hTw, hNx, hFl, hF } = mesh;
  const has = (a) => N && (N[a * 3] !== 0 || N[a * 3 + 1] !== 0 || N[a * 3 + 2] !== 0);
  const same = (a, b) => N[a * 3] * N[b * 3] + N[a * 3 + 1] * N[b * 3 + 1] + N[a * 3 + 2] * N[b * 3 + 2] > 0.9995;
  for (let h = 0; h < mesh.nh; h++) {
    const g = hTw[h];
    if (g < 0 || g < h) continue;
    let sharp;
    if (has(h) && has(g) && has(hNx[h]) && has(hNx[g])) sharp = !same(h, hNx[g]) || !same(hNx[h], g);
    else if (smoothOf) { const a = smoothOf[hF[h]]; const b = smoothOf[hF[g]]; sharp = !a || !b || a !== b; }
    else { const na = mesh.faceNormal(hF[h]); const nb = mesh.faceNormal(hF[g]); sharp = na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2] < 0.99996; }
    if (sharp) { hFl[h] |= FL_SHARP; hFl[g] |= FL_SHARP; }
  }
}

Number format on export

64-bit

Prints a number for the exported file.

text = the number rounded to 6 decimals, trailing zeros removed

toFixed(6) rounds every exported coordinate to 0.000001 m, and texture coordinates and normals to 6 decimals. This is a rounding of the exported value, not of a display.

  • io/obj.js
  • line 267
  • ModelCraft 0.8.1
const f6 = (x) => { const v = +x.toFixed(6); return Object.is(v, -0) ? '0' : String(v); };

Exported points

64-bit

Carries a mesh point to world coordinates and Y up for the file.

p′ = placement matrix × p; (x, y, z) → (x, z, −y)

The world point is 64-bit; it is rounded only when printed by f6. Faces of a mirrored placement (negative determinant) are written in reverse order.

  • io/obj.js
  • line 312
  • ModelCraft 0.8.1
const V = (v) => { if (!vmap.has(v)) { vmap.set(v, vBase + vs.length); vs.push(zy(apply(wm, [M.P[v * 3], M.P[v * 3 + 1], M.P[v * 3 + 2]]))); } return vmap.get(v); };

Texture coordinates with a map's scale and offset

64-bit

Applies a material's map scale and offset to a texture coordinate on import.

u′ = u × scale_u + offset_u; v′ = v × scale_v + offset_v

Computed in 64-bit and stored in the mesh's 32-bit 'uv' layer, which places a picture and is not a measured value.

  • io/obj.js
  • line 230
  • ModelCraft 0.8.1
if (UV && f.t[q] >= 0) { UV[h * 2] = obj.uvs[f.t[q] * 2] * sc[0] + of[0]; UV[h * 2 + 1] = obj.uvs[f.t[q] * 2 + 1] * sc[1] + of[1]; }

In detail

Method
Text parsed with the unary plus; (x, y, z) ↔ (x, −z, y); export in world coordinates through each placement's matrix.
Number types
64-bit numbers; Float64Array for imported points.
Tolerances
A vertex coordinate must be finite and below 1e7 in size. Edges are sharp where normals have a dot product of 0.9995 or less, or face normals under 0.99996.
Limits
Exported values are rounded to 6 decimals: a point keeps 1 micrometer, whatever its size.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach