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 plumbing differs per host.
Pipeline
A render moves CPU → GPU → CPU: parse and fit on the CPU,
upload–rasterise–read back on the GPU, encode 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. An out-of-profile material is
not rejected — it degrades instead, as the material model
describes.
Fit and upload compute a camera that frames the model's bounds from your
phi, theta, up, projection and margin, then move the geometry to the
device once; see Frame the model.
Rasterise draws the model under the studio preset or the rig you supply, tone maps with the ACES filmic curve, then draws authored edge lines. Failures here are GPU-class.
Encode compresses the pixels that came back. A request it cannot
represent, such as a transparent JPEG, fails as encode. Because the failure
code names the stage that rejected the work, the
retry decision is mechanical rather than a
guess. With format: 'raw' this stage is skipped and the frame is returned 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 for parse and fit, setup for device
acquisition and upload, then per-view render, overlay for the annotation
pass, and encode.
Both hosts, Node.js native and browser WebGPU, wrap the same Rust core and codecs. Only GPU rasterisation differs, so bytes may differ by a shade (see Determinism) but 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, whose lifetime is the process or worker. A views plan is the
work: every requested image in one value, so the core can schedule the next
view's GPU pass while the previous one encodes, which a loop over single
renders can never grant.
A renderer keeps the device, shader, pipelines and the last output size's render targets between calls; parsing, camera fitting and the geometry upload happen per call. The one-shot functions run through one shared renderer per process: created on the first call, run in call order, never disposed, and trimmed of render targets larger than 2048² after each call 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 are used as evidence: a CI artifact that proves a part changed, a screenshot an agent compares across a tool loop. Evidence that varies for reasons unrelated to the subject invites false conclusions, so nanoraster removes inputs rather than seeding them. Lighting is the studio preset unless the request carries a rig; tone map and gamma are fixed; camera distance is derived from the model's bounds; angles, format, size, background and annotations are yours.
What is guaranteed:
- Repeating a request on the same host produces identical bytes, in the same process or a different one.
- Lighting is identical for every render that states the same
lighting, or none. - The camera fit is identical for identical bounds and angles.
- 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 as well as the options,
and compare cross-host perceptually rather than by hash. Any
lossless format works as a baseline, but 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 factor-only glTF 2.0 metallic-roughness materials. A material is three numbers and a colour, not an image. That restriction is the renderer's supported profile, and it lets a render be reproduced from the model file alone: a texture-backed material would make the output depend on image decoding and mip generation, which the host supplies rather than the GLB, so two hosts could disagree about the same file.
metallicFactor selects between two behaviours rather than blending two looks.
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 only the analytic environment
gives it a body colour. roughnessFactor spreads the specular lobe: low values
give a tight highlight, high values smear 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; passing an sRGB triple 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.
A texture-backed material is ignored rather than rejected: it falls back to its
factors, so the render succeeds and looks flatter than the authoring tool
showed. Metals read as grey at small sizes, because only an analytic
environment is there to reflect; prefer a dielectric for thumbnails. Authored
edge lines are drawn in their own pass, unaffected by the 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.