nanoraster
0.7.1

Work with raw pixels

Return the RGBA frame instead of an encoded file, for pixel diffs, video frames and texture uploads with no image decoder in the loop.

Open Markdown

format: 'raw' stops the render after readback and returns the frame instead of a file. Use it when the next step needs pixels rather than an image: a diff, a video frame, a texture.

1. Render the frame

import { renderImage } from 'nanoraster';

const { bytes, width, height } = await renderImage(glb, {
  format: 'raw',
  width: 640,
  height: 480,
});

bytes is straight-alpha (not premultiplied) sRGB RGBA8: exactly width * height * 4 bytes, row-major, top row first, no padding. Other libraries return premultiplied, BGRA, or bottom-up buffers. Check the layout before you pass one buffer to another library. Other options apply unchanged, and the renderer ignores quality. The RenderedImageFile has the name render.raw and the mimeType application/octet-stream.

Node has no image decoder: no ImageData, no createImageBitmap, no canvas. Without a multi-megabyte decoder dependency, an encoded file is where the pixels stop. Raw output has nothing to decode.

2. Diff two renders

import { renderImage } from 'nanoraster';

const options = { format: 'raw', width: 512, height: 512 } as const;
const baseline = await renderImage(baselineGlb, options);
const candidate = await renderImage(candidateGlb, options);

let changed = 0;
for (let index = 0; index < baseline.bytes.length; index += 1) {
  if (Math.abs(baseline.bytes[index] - candidate.bytes[index]) > 8) changed += 1;
}

Determinism makes the count meaningful. One host and one request give identical bytes. With no encoder in the loop, a non-zero count can only mean the model or the request changed. Across hosts, expect a few pixels of rasterisation difference and keep a tolerance. pixelmatch accepts the same two buffers when you want a diff image.

3. Render a turntable as one plan

import { directionFromOrbit, renderImages } from 'nanoraster';

const frames = await renderImages(glb, {
  format: 'raw',
  width: 480,
  height: 360,
  views: Array.from({ length: 36 }, (_, step) => ({
    id: `f${step}`,
    camera: {
      framing: 'fit',
      direction: directionFromOrbit({ azimuth: step * 10, elevation: 25 }),
    },
  })),
});

for (const { file } of frames) encoder.stdin.write(file.bytes);

The turn is around the caller world's up axis. If the request declares a world, pass it as the second argument of directionFromOrbit. One renderImages call uploads the GLB once and pipelines the views; a loop of single renders cannot. Pipe the frames, in order, into a video encoder that reads raw RGBA:

ffmpeg -f rawvideo -pixel_format rgba -video_size 480x360 -framerate 25 -i pipe:0 turntable.mp4

GIF encoders accept the same flat RGBA. A plan can mix formats, so one call can return a WebP poster beside the raw frames. Every view arrives at once, at width * height * 4 bytes each; 36 frames at 480×360 is 25 MB. For a long sequence, hold a renderer and write each batch out before you request the next, as Reuse the renderer shows.

4. Paint it in the browser

import { directionFromOrbit, renderImage } from 'nanoraster';const { bytes, width, height } = await renderImage(glb, {  format: 'raw',  camera: {    framing: 'fit',    direction: directionFromOrbit({ azimuth: 47, elevation: 28 }),    margin: 0.08,  },});// No copy and no decode: ImageData wraps the memory the renderer wrote.const frame = new ImageData(new Uint8ClampedArray(bytes.buffer), width, height);const canvas = document.querySelector('canvas');canvas.width = width;canvas.height = height;canvas.getContext('2d').putImageData(frame, 0, 0);

Rendering…

 

advanced · direction XYZ

A browser can decode images, so here raw saves time rather than a dependency. Painting through ImageData is about 2.5× faster per frame than decoding lossless WebP. The same buffer uploads unchanged as a WebGL or WebGPU texture and as a WebCodecs video frame. nanoraster renders stills and reads every frame back through the CPU; use a canvas renderer for live interaction.

Variations

Encode when the destination wants a file. Documents, wires, and viewers (PDFs, vision models, glTF textures, <img> and HTTP) take an encoded format; Format and annotate chooses one. When you re-encode a raw frame yourself, you give up the byte-identical output the built-in encoders guarantee.

Annotations. The renderer draws the axis indicator, the label, and the scale bar before readback, so they are in the buffer.

Workers. postMessage(bytes, [bytes.buffer]) transfers a frame without a copy. Render in the browser shows where the renderer belongs.

On this page