Cameras

The render is taken from your Three.js camera, exactly where it is when you call render(). Perspective and orthographic cameras both work.

Supported cameras

Three.jsIn the render
PerspectiveCameraThe same view. fov, aspect, near, far and zoom are all used.
OrthographicCameraThe same view, from left, right, top, bottom and zoom.

A camera that sits inside a moving rig, such as an orbit pivot or a turntable, is rendered from where you actually see it.

Which camera gets used

In order:

  1. The camera you pass to render(), for that one call.
  2. The camera you passed to attach().
  3. In React Three Fiber, the live camera. useBakery3() picks it up for you, also after drei's controls swap it.
// Render another viewpoint without moving what the user is looking at
await bakery3.render({ camera: detailCamera });

Several cameras at once

Pass cameras to render the same scene from each of them in one call. The scene is uploaded once, however many cameras you list.

const shots = await bakery3.render({
  cameras: ['hero', 'front', 'detail'],
  quality: 'studio',
  maxCost: 2,                       // the ceiling for the whole set
});

const [hero, front, detail] = await shots.result();
gallery.hero.src = hero.url;

You can name a camera in three ways, and mix them in one list:

Given asMeans
'hero'The camera in your scene whose name is hero. In R3F, <PerspectiveCamera name="hero" />.
a CameraThat camera, where it is now. Its name is its id. Without a name it is camera-1, camera-2, by its place in the list.
{ id, position, target, fov }A shot you have no camera object for. It looks from position at target, with +Y up.

You get a set of pictures back instead of one job. shots.result() waits for all of them and returns them in the order you listed the cameras. Each has the camera it was taken from, its url, its size and its cost. maxCost is the limit for the whole set, and one idempotencyKey covers the whole set too.

If a picture is refused when it is sent, result() rejects with RenderSetRefused instead of returning a shorter list. The pictures that were accepted still render.

import { RenderSetRefused } from '@oeave/bakery3';

try {
  const [hero, front, detail] = await shots.result();
} catch (error) {
  if (!(error instanceof RenderSetRefused)) throw error;
  error.refusals;   // [{ camera: 'detail', error: { code: 'RenderBudgetExceeded', … } }]
  for await (const image of shots.images()) show(image.camera, image.url);
}

To render every variant of a product from every camera, give the same list to renderVariants(). See Render every variant.

Match the image size to your viewport

The image has your camera's shape unless you say otherwise, so a 16:9 viewport renders 16:9, framed as you see it. Leave out width and height for the quality's long edge, or give one side and the other follows.

await bakery3.render();                  // 2048 × 1152 from a 16:9 camera
await bakery3.render({ height: 1080 });  // 1920 × 1080

Give both sides in another shape and the picture keeps the camera's height of view, so it shows more or less at the sides. With cameras, every picture in the set has the first camera's shape.

Depth of field

Mark the camera with an f-number and the point to keep sharp. The render blurs what is nearer and farther than that point.

camera.userData.bakery3 = {
  focus: { fStop: 2, target: product.position },
};

await bakery3.render();

A smaller fStop blurs more: 4 is a light blur, 2 a clear one, 1.2 a strong one. It behaves like a full-frame camera at your camera's fov, so a narrower fov blurs more at the same number.

target is a point in world space, as an array or a Vector3. In a video the point stays sharp while the camera moves. To focus at a fixed distance instead, give distance in meters in place of target.

Your Three.js viewport stays sharp, because Three.js has no depth of field of its own. Remove the focus mark to render everything sharp again.

Animate the camera

You can animate the camera's position, lookAt and fov, and orbit it around your product. See Render video.

Frame a product so it fits

Place the camera from the product's bounding box instead of a hand-tuned position. The framing then still works when the model is swapped for a bigger one.

import { Box3, Vector3 } from 'three';

const box = new Box3().setFromObject(product);
const center = box.getCenter(new Vector3());
const radius = box.getSize(new Vector3()).length() / 2;

const distance = radius / Math.sin((camera.fov * Math.PI) / 360);
const direction = new Vector3(0.6, 0.4, 1).normalize();

camera.position.copy(center).addScaledVector(direction, distance);
camera.lookAt(center);
camera.updateMatrixWorld();

await bakery3.render();