Frame the model
Place the camera with two angles, match the model's up axis, choose a projection, and control how tightly the subject fits.
Most renderers ask for a camera position, a target and an up vector. nanoraster asks for two angles: the camera sits on a sphere centred on the model's bounds, and the radius is solved so the subject fills the frame.
1. Place the camera with two angles
You choose the direction the model is seen from, not the distance.
import { renderImage } from 'nanoraster';const image = await renderImage(glb, { format: 'webp', phi: 60, theta: -45,});Rendering…
phi is the polar angle from the up axis (default 60); theta is the
right-handed azimuth around it (default -45). Because phi is measured from
the up axis it behaves like colatitude, not latitude: 0 looks straight down
the axis (top), 60 is the default three-quarter view, 90 is level with the
model (elevation) and 180 looks straight up (bottom). At phi: 0 only
theta spins the image.
Exact ranges for every field named here are in
RenderImageOptions.
2. Match the model's up axis
up does not rotate the model. It selects the world axis the angles are
measured against, so changing it reinterprets phi and theta rather than
transforming geometry.
const image = await renderImage(glb, { format: 'webp', up: 'x', phi: 60, theta: -45,});Rendering…
Y-up is the glTF convention and the default. CAD exports are often Z-up;
render one without saying so and the subject tips onto its side. The demo
gear's axis runs along X, so up: 'x' turns the same two angles into a view
looking down onto its face.
3. Choose a projection
const image = await renderImage(glb, { format: 'webp', projection: 'orthographic',});Rendering…
perspective is the default; nearer features look larger. Use orthographic
for canonical views where two parts of the model must be comparable at any
depth. It also makes the scale annotation depth-invariant.
4. Tighten or loosen the fit
margin is the fraction of the fitted extent left empty around the subject; the
camera radius is solved from the bounds and margin together.
const image = await renderImage(glb, { format: 'webp', margin: 0.02,});Rendering…
margin | Result |
|---|---|
0 | Subject touches the frame edge |
0.1 | Default; a comfortable border |
0.5 | Maximum; subject occupies the middle half |
Variations
Consistent framing across a set. In a
batch call, phi and theta are per view while up,
projection and margin are shared, so every tile of a contact sheet is
framed the same way.
Very wide or very tall outputs. Fitting accounts for the aspect ratio you
request, so a 1920×480 banner frames differently from a 512×512 square; set
width and height before tuning margin.
Notes
- You cannot dolly the camera. Removing distance from the API removes a
class of bad output: no camera inside the model, no subject reduced to a
speck. Adjust
marginif the subject looks too small. - Angles are not portable across up axes. A view tuned at Y-up needs different numbers at Z-up.
- The fit is deterministic. With identical bounds, angles and fitting
options —
margin,widthandheightall feed the fit — the renderer produces an identical camera, a precondition for reproducible output. - Degenerate bounds still render. A flat or single-point model has degenerate bounds; the fit falls back rather than dividing by zero, though the result is rarely useful.