nanoraster
0.7.1

Place the camera

Capture an exact perspective or orthographic view with a portable position, target, up vector, field of view, zoom, and clipping contract.

Open Markdown

Use framing: 'fixed' when the capture must preserve a viewer pose or a remote request. Use framing: 'fit' when nanoraster should keep the whole model in frame.

Position, target, and roll

position, target, and up are three-number tuples in the caller world declared by the request's shared world. position and target establish the view direction. up establishes screen vertical, so changing it rolls the camera without moving the eye. Position and target must differ. A view collinear with up names no roll, so screen-up becomes world.forward, or world.up along forward. Omit world for canonical glTF +Y-up metres.

import type { RenderCamera, RenderWorld } from 'nanoraster';

const callerWorld = {
  up: '+z',
  forward: '-y',
  unit: 'meter',
} as const satisfies RenderWorld;

const inspectionCamera = {
  framing: 'fixed',
  position: [0.08, -0.08, 0.04],
  target: [0, 0, 0],
  up: [0, 0, 1],
  projection: { kind: 'perspective', verticalFieldOfView: 45, zoom: 1.2 },
  clipping: { near: 0.005, far: 1 },
} as const satisfies RenderCamera;

Here 0.08 is 80 mm along a Z-up metre caller axis. Pass callerWorld beside the camera. nanoraster performs the one conversion to canonical GLB space. Do not pre-convert in the caller.

Projection and zoom

The perspective field of view is vertical and in degrees. zoom is a positive magnification. Its definition does not depend on any rendering framework:

effective field of view = 2 × atan(tan(verticalFieldOfView / 2) / zoom)

Orthographic cameras replace field of view with verticalSpan, the visible caller-world height before zoom:

effective vertical span = verticalSpan / zoom

The output aspect is always width / height. There is no second aspect setting.

const planCamera = {
  framing: 'fixed',
  position: [0, 0.12, 0],
  target: [0, 0, 0],
  up: [0, 0, 1],
  projection: { kind: 'orthographic', verticalSpan: 0.08, zoom: 1 },
} as const satisfies RenderCamera;

Very narrow perspective

A tiny field of view is the wrong way to reach a parallel look. Framing distance grows as 1 / tan(fov / 2). A subject one height tall sits 57 heights away at 1 degree, and 573 heights away at 0.1. That distance collapses depth precision and makes clipping brittle. Ask for { kind: 'orthographic' }, which has no distance in it.

To hand a shot between the two projections without a visible jump, measure rather than guess. Render the view at both projections. Compare the frame corners, where perspective diverges most. Find the largest field of view whose worst-corner displacement from the orthographic projection stays under half a pixel, then switch.

Clipping

Omit clipping and nanoraster derives safe positive planes from subject bounds. Supply clipping to match another renderer: near must be positive and far greater. The renderer tightens unused range for depth precision without changing visible geometry. The renderer trims fat lines that cross the near plane before expansion.

Try two preset cameras

This request is also a serializable RPC payload. Pick a preset in the control panel, then edit its vectors and projection. The visible code and executed request change together.

import { directionFromOrbit, renderImages } from 'nanoraster';const images = await renderImages(glb, {  format: 'png',  views: [    {      id: 'three-quarter',      label: 'Three-quarter fit',      camera: {        framing: 'fit',        direction: directionFromOrbit({ azimuth: 45, elevation: 30 }),      },    },    {      id: 'close',      label: 'Close perspective',      camera: {        framing: 'fixed',        position: [0.08, 0.04, 0.08],        target: [0, 0, 0],        up: [0, 1, 0],        projection: { kind: 'perspective', zoom: 1.2 },        clipping: { near: 0.005, far: 1 },      },    },  ],});

Rendering…

advanced · direction XYZ

Each label names the preset in the resulting sheet. Format and annotate covers annotation behavior.

Capture a viewer

An adapter should freeze numeric camera state before asynchronous rendering:

const request = {
  format: 'png',
  width: 1600,
  height: 900,
  world: callerWorld,
  camera: inspectionCamera,
} as const;

const image = await renderImage(glb, request);

For Three.js, map camera.position, the controls target, vertical camera.fov, and camera.zoom into those fields. Screen up comes from the camera's world quaternion, not from camera.up. camera.up is the hint that lookAt solves against, and it holds no roll. For an orthographic camera, derive verticalSpan from (top - bottom) before zoom. Do that mapping in the consumer adapter. nanoraster's request contains no Three.js class, matrix, or serialization rule.

On this page