Writes a material's parameters, tile size and displacement to a MaterialX file, and reads them back from one.
How the precision is kept
Every number written to the file is rounded to six decimals. The tile size is written as its reciprocal rounded that way, so a tile of 3 m is written as 0.333333 and reads back as 3.000003 m. The displacement range is taken from a 32-bit height map and the lowest height is not written. Reading is 64-bit: values are parsed with Number and converted by exact factors of 1000 or by the light scale 0.02.
Open item. Write numbers with their full 64-bit digits instead of six decimals, and write the displacement offset alongside its scale.
Calculations under this policy
9 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.
shading/mtlx.jsBimCraft 0.8.1 32-bit in part8 calculationsReviewed
Tests bin/test-shading.mjs
A number as written to the file
64-bit
The text of every numeric value in the exported file.
text = round(x × 10⁶) ÷ 10⁶
The value kept in the file is rounded to six decimals with Math.round before it is turned into text, so at most six digits after the decimal point survive whatever the size of the number. Colors go through the AP1 to Rec.709 matrix and are clamped at 0 first.
The repeat of every texture in the file, from the material's tile size in meters.
uvtiling = round(10⁶ ÷ size) ÷ 10⁶
The reciprocal of each tile dimension is rounded to six decimals. A size whose reciprocal does not end within six decimals does not read back the same: 3 m is written as 0.333333, and 1 ÷ 0.333333 = 3.000003.
shading/mtlx.js
line 333
BimCraft 0.8.1
const M = uniq(name), size = bake.size, tile = `${f3(1 / size[0])}, ${f3(1 / size[1])}`, L = bake.layers, n = 512;
Shader parameters
64-bit
The constant inputs of the surface shader written to the file.
Standard Surface: emission = luminance × 0.02; thin film thickness × 1000
Values are 64-bit until they are written, then rounded to six decimals. For the Standard Surface flavor the emission is multiplied by the light scale 0.02 and the thin-film thickness by 1000 before that rounding.
shading/mtlx.js
lines 390 to 397
BimCraft 0.8.1
let v = p[k];
if (v === undefined) continue;
if (Array.isArray(v)) v = lin709(v);
if (cat === 'standard_surface') {
if (k === 'emission_luminance') v = v * LIGHT_SCALE;
if (k === 'thin_film_thickness') v = v * 1000;
}
ins.push(` <input name="${nm}" type="${Array.isArray(v) ? 'color3' : 'float'}" value="${Array.isArray(v) ? v3s(v) : f3(v)}" />`);
Displacement height range
32-bit in part
The lowest height and the span of the height map, which set the displacement texture's levels and the scale written to the file.
span = max h − min h; level = round(255 (h − min h) ÷ span); scale = displacement scale × span
The heights are read from the bake's Float32Array, so the lowest height and the span carry about 7 digits. The lowest height is kept on the bake object only and is not written to the file, and the texture holds 256 levels between the two.
shading/mtlx.js
lines 361 to 367
BimCraft 0.8.1
let lo = Infinity, hi = -Infinity;
for (const h of bake.height) { lo = Math.min(lo, h); hi = Math.max(hi, h); }
const rgb = new Uint8Array(n * n * 3), span = hi > lo ? hi - lo : 1, side = Math.round(Math.sqrt(bake.height.length)) || n;
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const v = bake.height[Math.min(side - 1, Math.floor(((n - 1 - y) * side) / n)) * side + Math.min(side - 1, Math.floor((x * side) / n))]; rgb[(y * n + x) * 3] = rgb[(y * n + x) * 3 + 1] = rgb[(y * n + x) * 3 + 2] = Math.round(((v - lo) / span) * 255); }
dispFile = png('height', rgb);
notes.push('height:' + M);
bake.dispRange = [lo, span];
Displacement scale written
32-bit in part
The scale input of the displacement shader in the exported file.
The span is the difference of two heights read from a Float32Array, multiplied by the material's displacement scale in 64-bit and rounded to six decimals. Only the scale is written: the displacement node gets no offset input.
The material's tile size in meters from the uvtiling shared by a file's images.
width = 1 ÷ tiling_u; height = 1 ÷ tiling_v
One 64-bit division each, used when every image has the same tiling (compared with ===) and no offset, and both are greater than zero. The result inherits whatever rounding the file's tiling had.
The single value given to a shader input the renderers and the file cannot vary across the surface.
mean = Σ f(uᵢ, vⱼ) ÷ 64², a 64 × 64 grid of texel centers
4,096 samples added in order in 64-bit and divided by the count. The grid is fixed at 64 × 64 whatever the bake size, so detail finer than a 64th of the tile is sampled, not integrated.
Constants as value attributes; texture layers as PNG files read by tiledimage nodes with uvtiling = 1 ÷ tile size; colors converted from AP1 to linear Rec.709 by a 3 × 3 matrix and clamped at 0. The file header says the reader and writer follow the MaterialX 1.38 and 1.39 specification.
Number types
64-bit numbers for parameters; Float32Array for the height map; 8-bit PNG layers.
Tolerances
Written values: Math.round(x × 1e6) ÷ 1e6. Height texture: 256 levels between the lowest and highest height. On reading, a shared uvtiling must match exactly (===) across images to become the tile size.
Checked by
bin/test-shading.mjs has a MaterialX section that imports shading/mtlx.js.
Limits
Six decimals is an absolute rounding: a parameter smaller than 0.0000005 is written as 0. The exported displacement has no offset, so heights are measured from the lowest point of the map.
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.