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.
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#
- Mount. The scene is created and waits for its canvas to be laid out.
- Attach. The GPU starts (
onCreatedfires with theApricotScene). Children mount now, souseFrameanduseScenework in them. - 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.
- Ready. The first frame with content draws and
onReadyfires. - 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:
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
useFramecallback returningtrue(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.
Three ways to place a scene#
Full screen#
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.
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).
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: every prop with its default.
- Animation and shared values: how motion reaches the renderer.
- Performance: keeping scenes fast.