How it works
The four render stages, what reproducibility does and does not guarantee, and the factor-only material profile.
nanoraster is a thin JavaScript surface over a Rust render core. Everything that decides a pixel's value happens in that core. Only the host integration differs.
Pipeline
A render moves CPU → GPU → CPU. Parse and fit run on the CPU. Upload,
rasterise, and read-back run on the GPU. Encode runs on the CPU. Failures carry
the stage name (parse, gpu, encode).
Parse reads the GLB and rejects malformed or structurally unsupported
input. A failure here is classified parse. The parser does not reject an
out-of-profile material. It degrades instead, as the
material model describes.
Resolve and upload settles the camera from eligible referenced indexed geometry, primitive visibility, and any section caps. It then moves geometry to the device once. See Frame the model, Place the camera, Choose visible geometry, and Cut section views.
Rasterise clips and draws surfaces, section caps, cap outlines, and authored lines, then applies the requested annotations. Surface lighting uses the studio preset or supplied rig. Section caps are unlit evidence. Failures here are GPU-class.
Encode compresses the read-back pixels. A request the encoder cannot
represent, such as a transparent JPEG, fails as encode. The failure code
names the stage that rejected the work, so the
retry decision is mechanical, not a guess.
With format: 'raw' the renderer skips this stage and returns the frame as
read back. See Work with raw pixels.
renderImages parses and uploads once, then fits,
rasterises, and encodes once per view. RenderTimings
fields map onto these stages. parse covers parse and fit, capBuild shared
presentation work, and upload source geometry. Each view then
reports render, overlay for the annotation pass, and encode. setup
measures the complete acquisition, presentation, and upload interval.
Both hosts, Node.js native and browser WebGPU, wrap the same Rust core and codecs. Only GPU rasterisation differs, so bytes can differ by a shade (see Determinism). Any structural difference between hosts is a bug.
Device lifetime
State crosses the boundary as handles. Work crosses as plans. A
Renderer is the state: the GPU device, shader, pipelines, and
render targets. Its lifetime is the process or worker. A views plan is the
work: every requested image in one value. The core can then schedule the next
view's GPU pass while the previous view encodes. A loop over single renders
cannot grant that overlap.
A renderer keeps the device, shader, pipelines, and render targets for the last output size between calls. Parsing, camera fitting, and geometry upload happen per call. The one-shot functions run through one shared renderer per process. The first call creates it, calls run in call order, and it is never disposed. After each call it drops render targets larger than 2048², so one large render does not hold that memory.
Reuse changes no pixels: it skips re-creation, not arithmetic, so a warm renderer's output is byte-identical to the one-shot functions on the same adapter. If the device is lost between calls, the renderer rebuilds it on the next call. The loss surfaces once, as a GPU-class failure on the call that hit it.
Determinism
Renders serve as evidence: a CI artifact that proves a part changed, or a screenshot an agent compares across a tool loop. Evidence that varies for reasons unrelated to the subject invites false conclusions. nanoraster therefore removes inputs rather than seeds them.
Lighting is the studio preset unless the request carries a rig. Tone map and gamma are fixed. Fitted placement derives from referenced indexed geometry. Camera angles, format, size, background, and annotations are yours.
What is guaranteed:
- Repeating a request on the same host produces identical bytes, in the same or a different process.
- Lighting is identical for every render that states the same
lighting, or none. - The camera fit is identical for identical referenced geometry and camera inputs.
- Input failures reproduce exactly.
What is not guaranteed:
- Identical bytes across different GPUs or drivers: rasterisation differs at the margins.
- Identical bytes between the native and wasm paths: same core, different device and adapter.
- GPU failures reproducing: device loss is by definition transient.
- Byte stability across nanoraster versions: a render fix changes pixels, so treat a version bump as a baseline reset.
For byte-exact comparison in CI, pin the runner image and the options.
Compare cross-host results perceptually rather than by hash. Any
lossless format works as a baseline. A
byte-locked baseline also locks the encoder, and a version bump can change
lossless WebP bytes as well as pixels.
format: 'raw' removes the encoder from the
comparison, so a diff can only report the render.
Material model
nanoraster renders glTF 2.0 metallic-roughness materials, embedded PNG/JPEG/WebP maps and the physical material extensions listed in the compatibility guide. Image decoding and mip generation run in the shared Rust core on every host. A fixed model, camera and lighting rig therefore has the same material inputs on native and browser backends.
metallicFactor blends dielectric and metallic responses.
At 0 the surface has a diffuse colour and a white specular highlight. At 1
there is no diffuse term: the body colour is reflected environment, tinted by
the base colour. A metal has no diffuse lobe, so the prefiltered room environment
gives it a body colour. roughnessFactor spreads the specular lobe: low values
give a tight highlight, and high values spread it into a broad sheen.
A rough dielectric produces the soft, evenly shaded look usually associated with a neutral matcap:
{ "pbrMetallicRoughness": { "baseColorFactor": [0.2622506575, 0.3277780981, 0.4072402119, 1], "metallicFactor": 0, "roughnessFactor": 0.85 }}Rendering…
The base colour above is the linear-space equivalent of sRGB #8C9BAB. Most
glTF exporters convert an sRGB value for you. An sRGB triple passed directly
gives a washed-out surface. Keep metallicFactor at 0, use roughnessFactor
between 0.8 and 1, and replace the colour with the tint you want. This is
an approximation, not pixel parity. A matcap maps view-space normals into an
image, while PBR derives the result from material, surface, camera, and
lighting.
The studio uses a neutral room environment, an integrated GGX lookup table,
multiple-scattering compensation and PBR Neutral tone mapping. Glass samples
retained HDR scene mip levels with bicubic filtering. Unlit materials bypass
lighting but still receive exposure and tone mapping. Authored edge lines
remain in their own pass, unaffected by material factors.
With lighting held constant, a difference
between two renders of the same model is a material difference and nothing
else.
Handle render failures
Classify a rejection, decide whether retrying can help, and keep the last good image when a GPU fault is transient.
API reference
Every public export of nanoraster — the two render calls, the renderer handle, the request and result types, the failure codes, and the validation constants.