Place the camera
Capture an exact perspective or orthographic view with a portable position, target, up vector, field of view, zoom, and clipping contract.
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 / zoomThe 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.