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.
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;
}
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;
}
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.