# Scenes

> What a Scene is, how it starts, when it renders and when it sleeps, which props are fixed at mount, and how to use it as a full screen, a card or a transparent overlay.

A `<Scene>` is a React Native view with a 3D world inside. It owns a camera, lights (from its `look`), a renderer and a frame loop. Everything 3D you render goes inside one.

```tsx title="ProductCard.tsx"
import { Box, Scene } from "@apricot/react";
import { View } from "react-native";

export function ProductCard() {
  return (
    <View style={{ height: 320, borderRadius: 24, overflow: "hidden" }}>
      <Scene style={{ flex: 1 }} look="studio">
        <Box size={1.5} radius={0.2} material="gold" />
      </Scene>
    </View>
  );
}
```

## Size and layout

A scene is sized by its `style`, like a `View`: `flex: 1`, a fixed `height`, or `StyleSheet.absoluteFill`. It follows layout changes (rotation, split screen, a sheet animating in) and resizes its canvas. A scene with no size draws nothing.

You can put several scenes on one screen; each has its own camera and loop. Prefer one scene with several objects when they share a camera.

## Lifecycle

1. **Mount.** The scene is created and waits for its canvas to be laid out.
2. **Attach.** The GPU starts (`onCreated` fires with the `ApricotScene`). Children mount now, so `useFrame` and `useScene` work in them.
3. **Warm-up.** Shaders for everything in the scene compile in the background. Meanwhile frames show only the background (for up to 4 s), so the app never freezes on a long compile.
4. **Ready.** The first frame with content draws and `onReady` fires.
5. **Unmount.** The scene, its materials and geometry are released. Models and textures are reference-counted and freed when their last user unmounts.

### Loading and the first frame

Expect a short wait before content appears: the GPU starts, then shaders compile. On a mid-range phone that takes about a second. Show your own view meanwhile with `loading`:

```tsx title="LoadingScene.tsx"
import { Box, Scene } from "@apricot/react";
import { ActivityIndicator } from "react-native";

export function LoadingScene() {
  return (
    <Scene
      style={{ flex: 1 }}
      loading={<ActivityIndicator style={{ flex: 1 }} />}
      onReady={() => console.log("scene shown")}
      onError={(error) => console.warn("3D failed to start", error)}
    >
      <Box />
    </Scene>
  );
}
```

`loading` covers the scene until its first content frame. `<Label>` views stay hidden until then too.

## Props fixed at mount

These options are read **once**, when the scene mounts. Changing them later has no effect:

`look`, `camera`, `fog`, `environment`, `transparent`, `background`, `shadows`, `maxPixelRatio`, `linger`.

To change them at runtime:

| To change | Use |
| --- | --- |
| Reflections | `<Environment name>` (fades between environments) |
| Time of day, sun and sky | `<Daylight time>` (with `look="none"`) |
| Camera position or framing | `<CameraRig view>`, `<FitCamera>`, `<FollowCamera>`, or the camera through `useScene().camera` |
| Lights | Light components such as `<DirectionalLight intensity>` |
| Anything else in the list | Remount the scene with a new `key` |

`frameloop` is the exception: it can change at any time.

## When the scene renders

By default (`frameloop="demand"`) a scene renders **only when something changes**, then sleeps. A sleeping scene costs no CPU or GPU time. These things wake it:

- a prop changing (a new `position`, `color`, `view`…);
- a shared value bound to a prop changing (a gesture, a spring, a sensor);
- a `useFrame` callback returning `true` (still animating);
- a behavior that is moving (`<Spin>` with a non-zero speed, a driving `<Vehicle>`, a falling `<Pile>`);
- a tap, a drag on `<CameraRig orbit>`, a resize;
- `useScene().invalidate()`.

After the last change it keeps rendering for `linger` seconds (default 0.25), so a burst of changes such as a drag looks continuous.

Use `frameloop="always"` only for scenes that never stop moving and do not report it themselves.

> [!TIP]
> Add `debug` to a scene while developing. The HUD shows fps, render time and present time, and stops updating when the scene sleeps, which is how you can see that an idle scene is free. Its **Share** button exports a full performance report.

## Three ways to place a scene

### Full screen

```tsx title="FullScreen.tsx"
import { Scene, Sphere } from "@apricot/react";

export function FullScreen() {
  return (
    <Scene style={{ flex: 1 }}>
      <Sphere material="chrome" />
    </Scene>
  );
}
```

### Behind your UI

Put the scene in `StyleSheet.absoluteFill` and your views above it. Use `<CameraRig inset>` so the subject is framed in the part of the screen your UI does not cover.

```tsx title="WithPanel.tsx"
import { Box, CameraRig, Scene } from "@apricot/react";
import { StyleSheet, Text, View } from "react-native";

export function WithPanel() {
  return (
    <View style={{ flex: 1 }}>
      <Scene style={StyleSheet.absoluteFill} look="studio">
        <Box size={1.5} radius={0.2} material="car-paint">
          <CameraRig view="three-quarter" inset={{ bottom: 220 }} />
        </Box>
      </Scene>
      <View style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: 220, padding: 24 }}>
        <Text>Your panel</Text>
      </View>
    </View>
  );
}
```

### Over your UI (overlay)

A transparent scene draws only its objects, and `pointerEvents="none"` lets every touch reach the screen below. This is how objects fall over an existing screen ([Falling objects](https://matthiasbrandolin.dev/apricot/guides/falling-objects)).

```tsx title="Overlay.tsx"
import { Apple } from "@apricot/assets";
import { Pile, Scene } from "@apricot/react";
import type { ReactNode } from "react";
import { StyleSheet, View } from "react-native";

export function Overlay({ children }: { children: ReactNode }) {
  return (
    <View style={{ flex: 1 }}>
      {children}
      <Scene transparent pointerEvents="none" style={StyleSheet.absoluteFill} camera={{ position: [0, 0, 20], fov: 30 }}>
        <Pile count={40}>
          <Apple />
        </Pile>
      </Scene>
    </View>
  );
}
```

## Background colours are exact

`background` and fog colours show exactly the colour you give, so a scene can sit seamlessly inside your UI. (Tone mapping would normally turn `#ffffff` light grey; Apricot compensates.) Without `background`, the scene uses its look's colour: `soft` is `#f6efe6`, `studio` is `#eceff3`, `flat` is `#ffffff`.

## The scene object

`useScene()` (inside the scene) and `onCreated` / `onReady` give you the `ApricotScene`:

| Member | What it is |
| --- | --- |
| `invalidate()` | Requests a frame (needed after changing objects outside `useFrame`). |
| `camera` | The three.js `PerspectiveCamera`. |
| `scene` | The three.js `Scene` (the root of your objects). |
| `size` | Layout size in points, `{ width, height }`. |
| `onResize(listener)` | Called after every resize; returns a remover. |
| `ready`, `onReady(listener)` | Whether the first content frame has drawn; subscribe to it. |
| `report(extra?)` | A JSON performance report (frame times, slowest frames, GPU details). |
| `threeRenderer` | The three.js renderer, once attached (escape hatch). |

## Related

- [Scene reference](https://matthiasbrandolin.dev/apricot/reference/scene): every prop with its default.
- [Animation and shared values](https://matthiasbrandolin.dev/apricot/concepts/animation): how motion reaches the renderer.
- [Performance](https://matthiasbrandolin.dev/apricot/concepts/performance): keeping scenes fast.
