Resources Numerical accuracy Calculation policy

Material graphs baked to textures

Turns a node material, a decal or a texture bake (ambient occlusion, curvature, normals, lighting) into the pictures the renderers read.

How the precision is kept

The results are texture pixels: 8-bit layers quantized as ⌊255 x + ½⌋, 32-bit height maps, and 32-bit mesh buffers for decals. Node maths runs in 64-bit in the reference path and in the shader's 32-bit floats on the GPU path. The numbers a MaterialX file carries from a bake are recorded under the MaterialX policy.

Calculations under this policy

4 calculations in 4 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.

shading/bake.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-shading.mjs

Values to texture bytes

32-bit, display only

Stores a value from 0 to 1, or a normal component from −1 to 1, as one byte of a texture layer.

byte = ⌊255 x + ½⌋; normal byte = ⌊127 x + 128.5⌋

Each texel keeps 8 bits per channel; values at or beyond the ends are clamped to 0 and 255. A normal component of 0 maps to exactly 128.

  • shading/bake.js
  • lines 30 to 32
  • BimCraft 0.8.1
const q8 = (x) => (x <= 0 ? 0 : x >= 1 ? 255 : Math.floor(x * 255 + 0.5));
/** A normal component −1…1 → a byte, 0 → exactly 128 (RENDER-SPEC § 6.6). */
const qn = (x) => Math.min(255, Math.max(0, Math.floor(127 * x + 128.5)));
shading/lib.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-shading.mjs

Tiling value noise

32-bit, display only

The smooth noise that procedural textures are built from.

a + (b − a) f_x + (c − a) f_y + (a − b − c + d) f_x f_y, f = t² (3 − 2t)

The four lattice values are 24-bit hashes of whole numbers, the same on the CPU and the GPU; the blend is 64-bit here and 32-bit in the WGSL copy of the same function.

  • shading/lib.js
  • lines 23 to 28
  • BimCraft 0.8.1
export function vnoise(x, y, px, py, seed) {
  const x0 = Math.floor(x), y0 = Math.floor(y), fx = smooth(x - x0), fy = smooth(y - y0);
  const xa = wrap(x0, px), xb = wrap(x0 + 1, px), ya = wrap(y0, py), yb = wrap(y0 + 1, py);
  const a = hash01(xa, ya, seed), b = hash01(xb, ya, seed), c = hash01(xa, yb, seed), d = hash01(xb, yb, seed);
  return a + (b - a) * fx + (c - a) * fy + (a - b - c + d) * fx * fy;
}
shading/texbake-job.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-shading.mjs

Ambient occlusion of a texel

32-bit, display only

The share of directions above a surface point that are not blocked nearby.

open ÷ rays, directions (√u₁ cos 2πu₂, √u₁ sin 2πu₂, √(1 − u₁)) about the normal

A count of unblocked rays divided by the number of rays, so it takes one of rays + 1 values. Texel positions and normals arrive in Float32Array and the result is stored in one, then as a byte of a picture.

  • shading/texbake-job.js
  • lines 47 to 59
  • BimCraft 0.8.1
/** The unoccluded fraction over `rays` cosine-weighted directions above (p, n), up to `dist`; texel k. */
function openness(occ, px, py, pz, nx, ny, nz, rays, dist, k, seed, eps) {
  frame(nx, ny, nz);
  O[0] = px + nx * eps; O[1] = py + ny * eps; O[2] = pz + nz * eps;
  let open = 0;
  for (let r = 0; r < rays; r++) {
    const u1 = hash(k, r, seed), u2 = hash(k, r, seed + 7919);
    const rr = Math.sqrt(u1), ph = 2 * Math.PI * u2, x = rr * Math.cos(ph), y = rr * Math.sin(ph), z = Math.sqrt(Math.max(0, 1 - u1));
    D[0] = F[0] * x + F[3] * y + nx * z; D[1] = F[1] * x + F[4] * y + ny * z; D[2] = F[2] * x + F[5] * y + nz * z;
    if (!anyHit(occ.tree, occ.tris, O, D, dist)) open++;
  }
  return open / rays;
}
shading/decal.js BimCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-shading.mjs

Decal geometry

32-bit, display only

The triangles under a decal, clipped to its box and lifted off the surface.

corner = o + x u + y v + z n + 0.0005 × face normal

Clipping runs in Float64Array scratch space; the positions, normals and UVs are returned as Float32Array for drawing. Faces turned more than the decal's angle from its normal are left out by comparing a dot product with a cosine.

  • shading/decal.js
  • lines 98 to 136
  • BimCraft 0.8.1
export function decalGeometry(D, tris) {
  const n = D.n, u = D.u, v = cross(n, u), hw = D.size[0] / 2, hh = D.size[1] / 2, cosMax = Math.cos((D.angle * Math.PI) / 180);
  const pos = [], nor = [], uv = [];
  const nt = Math.floor(tris.length / 9);
  for (let i = 0; i < nt; i++) {
    const o = i * 9;
    // the triangle's normal must face the projector
    const e1 = [tris[o + 3] - tris[o], tris[o + 4] - tris[o + 1], tris[o + 5] - tris[o + 2]], e2 = [tris[o + 6] - tris[o], tris[o + 7] - tris[o + 1], tris[o + 8] - tris[o + 2]];
    const fn = unit(cross(e1, e2));
    if (!fn || dot(fn, n) < cosMax) continue;
    // into decal space; a quick box reject before clipping
    let out = 0;
    for (let c = 0; c < 3; c++) {
      const q = [tris[o + c * 3] - D.o[0], tris[o + c * 3 + 1] - D.o[1], tris[o + c * 3 + 2] - D.o[2]];
      A[c * 3] = dot(q, u); A[c * 3 + 1] = dot(q, v); A[c * 3 + 2] = dot(q, n);
    }
    for (let ax = 0; ax < 3 && !out; ax++) {
      const lim = ax === 0 ? hw : ax === 1 ? hh : D.depth;
      if (A[ax] > lim && A[3 + ax] > lim && A[6 + ax] > lim) out = 1;
      if (A[ax] < -lim && A[3 + ax] < -lim && A[6 + ax] < -lim) out = 1;
    }
    if (out) continue;
    let k = 3;
    k = clipAxis(A, k, B, 0, 1, hw); if (k < 3) continue;
    k = clipAxis(B, k, A, 0, -1, hw); if (k < 3) continue;
    k = clipAxis(A, k, B, 1, 1, hh); if (k < 3) continue;
    k = clipAxis(B, k, A, 1, -1, hh); if (k < 3) continue;
    k = clipAxis(A, k, B, 2, 1, D.depth); if (k < 3) continue;
    k = clipAxis(B, k, A, 2, -1, D.depth); if (k < 3) continue;
    // a fan, back in the definition's coordinates, lifted along the face's normal
    for (let j = 1; j + 1 < k; j++) for (const c of [0, j, j + 1]) {
      const x = A[c * 3], y = A[c * 3 + 1], z = A[c * 3 + 2];
      for (let a = 0; a < 3; a++) pos.push(D.o[a] + u[a] * x + v[a] * y + n[a] * z + fn[a] * LIFT);
      nor.push(fn[0], fn[1], fn[2]);
      uv.push(x + hw, y + hh);   // metres from the lower-left corner: the material's tile is the decal's size
    }
  }
  return { pos: new Float32Array(pos), nor: new Float32Array(nor), uv: new Float32Array(uv), n: pos.length / 9 };
}

In detail

Method
Graphs evaluated per texel over one tile; lattice noise from 32-bit integer hashes so both paths start from the same bits; ambient occlusion by cosine-weighted rays against a bounding-volume hierarchy; decals by clipping triangles to the decal's box.
Number types
Uint8 texture layers, Float32Array heights and bake values, Float32Array decal geometry; 64-bit numbers in the JavaScript evaluation.
Limits
Texture values are appearance data. A decal floats 0.5 mm off its surface by design.

Approach: Display only

Computed to draw the screen, often in 32-bit on the graphics processor. No number a person reads, saves or exports comes from it.

Every policy that takes this approach