nanoraster
0.4.1

Light the subject

Replace the studio preset with a rig of your own, render from the environment alone, or fix a light to the model instead of the camera.

Open Markdown

Every render is lit by the studio preset unless the request says otherwise. lighting takes the preset name or an explicit rig; a rig replaces the studio lights and inherits every value it leaves out.

1. Replace the studio lights

import { renderImage } from 'nanoraster';const image = await renderImage(glb, {  format: 'webp',  lighting: {    lights: [      { direction: [-0.5, 0.6, 0.6], color: [3, 2.9, 2.7] },      { direction: [0.6, -0.2, 0.4], color: [0.7, 0.8, 1] },    ],    ambient: 0.02,    exposure: 1,  },});

Rendering…

lights replaces the three studio lights outright — it never adds to them. direction points from the surface toward the light, in view space by default (+x right, +y up, +z toward the viewer), so the rig travels with the camera and every view is lit the same way; any non-zero vector is normalised. color is linear RGB radiance, unitless, up to eight lights. ambient lifts the shadow side; exposure scales the rig before the ACES tone map.

The studio preset, for reference (ambient: 0.02, analytic environment):

Lightdirectioncolor
Key[-0.45, 0.61, 0.63][2.09, 2.09, 2.09]
Fill[0.45, -0.61, -0.63][1.45, 1.42, 1.38]
Headlamp[0.03, 0.74, 0.67][0.68, 0.66, 0.62]

2. Render from the environment alone

const image = await renderImage(glb, {  format: 'webp',  lighting: {    lights: [],    ambient: 0,    environment: 'studio',    exposure: 1.4,  },});

Rendering…

An empty lights array with ambient: 0 leaves the analytic environment as the only source (a rig inherits the studio ambient unless it says otherwise). Metals, which have no diffuse lobe, read well this way; dielectrics go soft and flat. environment: 'none' removes the specular reflection and the diffuse irradiance together and leaves only the lights you supplied — with none, the subject is black.

3. Fix a light to the model

const images = await renderImages(glb, {  format: 'webp',  lighting: {    lights: [{ direction: [1, 0.4, 0], color: [3, 2.9, 2.7] }],    space: 'world',  },  views: [    { id: 'front', phi: 90, theta: 0 },    { id: 'back', phi: 90, theta: 180 },  ],});

Rendering…

space: 'world' authors the directions in glTF world coordinates, so the rig stays attached to the model while the camera orbits it. That deliberately breaks what makes a view sheet comparable: under the default 'view', front and back differ only in geometry; under 'world' the back is in shadow because the light never moved. Choose it when the light belongs to the subject, not for contact sheets.

4. Keep the preset pinned

const image = await renderImage(glb, {
  format: 'webp',
  lighting: 'studio',
});

Omitting lighting, passing 'studio' and spelling out the studio values produce the same bytes. Pin it when the request, not the default, should say how the image was lit; determinism covers either.

Variations

Lighting is shared across a batch. renderImages takes one rig for the whole call; a model-fixed light is space: 'world', as above.

Annotations ignore the rig. The axis indicator, label and scale bar are composited after shading; authored edge lines are drawn unlit.

Metals depend on the environment. A fully metallic subject has no diffuse term, so under environment: 'none' it renders nearly black; see Material model.

Cost scales with light count. Eight lights cost roughly eight times the direct-lighting work of one — invisible at thumbnail sizes, not at 4096².

Every bound is exported. renderImageMaxLights, renderImageLightColorRange, renderImageAmbientRange and renderImageExposureRange are what the validator applies.

On this page