Interiors need light that comes from somewhere: a window, a lamp. Apricot has pieces for each, all driven by <Daylight> so a single time sets the whole mood:
| Piece | Does |
|---|---|
<Daylight time> |
Sun (moon at night) and sky light for an hour of the day, easing between hours |
<LightBeam sun> |
A beam of sunlight through the window in dusty air, following the sun |
<WindowPane> |
The glass: bright by day, deep blue at night |
<Lamp on="dark"> |
A light from a thing (a table lamp, a candle) that comes on at dusk |
shadowOnly |
A roof or wall that is never drawn but keeps the sun out |
<Scene occlusion> |
Soft darkening in corners and under furniture |
<Scene toneMapping="neutral"> |
Colours closer to as written: pastels stay rich |
A room through the day#
A cut-away room with a window in its right wall. Buttons change the hour: the beam swings round with the sun, the glass darkens, the lamp comes on at dusk.
import {
Box,
CameraRig,
Cylinder,
Daylight,
Lamp,
LightBeam,
Merged,
Scene,
WindowPane,
} from "@apricot/react";
import { useState } from "react";
import { Button, StyleSheet, View } from "react-native";
const W = 2; // half the room's width (x)
const D = 1.5; // half its depth (z)
const H = 2.6; // its height
const WINDOW = { y: 1.4, z: 0, width: 1, height: 1.2 };
const T = 0.1; // wall thickness
function Shell() {
const top = WINDOW.y + WINDOW.height / 2;
const bottom = WINDOW.y - WINDOW.height / 2;
return (
<Merged>
{/* Floor and the back wall. */}
<Box size={[2 * W, 0.1, 2 * D]} position={[0, -0.05, 0]} color="#ebc79c" />
<Box size={[2 * W, H, T]} position={[0, H / 2, -D - T / 2]} color="#faf1e8" />
{/* The right wall, round the window. */}
<Box size={[T, bottom, 2 * D]} position={[W + T / 2, bottom / 2, 0]} color="#faf1e8" />
<Box size={[T, H - top, 2 * D]} position={[W + T / 2, (H + top) / 2, 0]} color="#faf1e8" />
<Box size={[T, WINDOW.height, D - WINDOW.width / 2]} position={[W + T / 2, WINDOW.y, -(D + WINDOW.width / 2) / 2]} color="#faf1e8" />
<Box size={[T, WINDOW.height, D - WINDOW.width / 2]} position={[W + T / 2, WINDOW.y, (D + WINDOW.width / 2) / 2]} color="#faf1e8" />
</Merged>
);
}
export function DayRoom() {
const [hour, setHour] = useState(16.5);
return (
<View style={{ flex: 1 }}>
<Scene
style={StyleSheet.absoluteFill}
look="none"
toneMapping="neutral"
shadows={{ softness: 3, darkness: 0.94 }}
occlusion={{ radius: 0.5, strength: 1 }}
lamps={1}
>
{/* The compass turned half round: the window (in the +x wall) faces west, towards the afternoon sun.
Indoors, the sun needs to be much stronger than outdoors. */}
<Daylight time={hour} north={Math.PI} strength={{ sun: 4 }} />
<Shell />
{/* Never drawn, but they keep the sun out: it comes in through the window only. */}
<Box size={[2 * W + 0.4, T, 2 * D + 0.4]} position={[0, H + T / 2, 0]} shadowOnly />
<Box size={[2 * W + 0.4, H, T]} position={[0, H / 2, D + T / 2]} shadowOnly />
<Box size={[T, H, 2 * D + 0.4]} position={[-W - T / 2, H / 2, 0]} shadowOnly />
<WindowPane size={[WINDOW.width, WINDOW.height]} position={[W + T / 2, WINDOW.y, WINDOW.z]} rotation={[0, Math.PI / 2, 0]} />
<LightBeam
sun
from={[W, WINDOW.y, WINDOW.z]}
size={[WINDOW.width, WINDOW.height]}
facing={[-1, 0, 0]} // into the room
bounds={[-W, -D, W, D]} // the walls it may land on
intensity={5}
/>
{/* A table lamp in the corner, on from dusk. */}
<Merged position={[-W + 0.4, 0, -D + 0.4]}>
<Cylinder radius={0.25} height={0.5} position={[0, 0.25, 0]} color="#e5c595" />
<Cylinder radius={0.04} height={0.35} position={[0, 0.67, 0]} color="#8d5c3d" />
</Merged>
<Lamp position={[-W + 0.4, 0.9, -D + 0.4]} on="dark" glow={0.12} />
<CameraRig subject={null} view={{ azimuth: -36, elevation: 32, distance: 9, target: [0, 1, 0] }} orbit={{ turn: 40, elevation: [12, 70] }} />
</Scene>
<View style={{ position: "absolute", bottom: 40, left: 0, right: 0, flexDirection: "row", justifyContent: "center" }}>
<Button title="Morning" onPress={() => setHour(8.5)} />
<Button title="Afternoon" onPress={() => setHour(16.5)} />
<Button title="Sunset" onPress={() => setHour(18.6)} />
<Button title="Night" onPress={() => setHour(22)} />
</View>
</View>
);
}Daylight indoors#
<Daylight> sets the sun's direction, colour and brightness and the sky's light for time (hours, 0–24). Plain values ease over transition seconds (default 1.5); a shared value is followed exactly (a slider).
north(radians) turns the compass. By default the sun rises at +x (east) and sets at −x. Turn it so the window faces the light you want.strength={{ sun, sky }}multiplies the sun's and sky's brightness. An interior lit through a window wants a much stronger sun than an outdoor scene ({ sun: 4 }, or a stronger key in your ownkeys).keysreplaces the day's colours (an array ofDaylightKey, see Lights reference). A room's light is warmer than outdoors; the room builder in the Apricot playground uses its own keys.background={false}keeps your ownbackgroundinstead of the sky colour.
Use look="none" with <Daylight>, so the look's lights don't add to it.
The beam through the window: <LightBeam>#
A soft, see-through volume of light, brightest where it comes in and fading along its length and at its edges.
- With
sun, it comes in along the sun's (or moon's) rays, lands on the floor or on a wall roundbounds, takes the sun's colour, and shows only while the sun is on the window's side of the wall.intensityis its brightness at the sun's brightest. - Without
sun, giveto: the middle of where it lands. Use this for a fixed beam or a spotlight's cone on a stage. fromis the middle of the opening,sizeits width and height,facingthe way it faces into the room (a window in a wall facing −x is[-1, 0, 0]). The beam keeps the opening's shape all the way.
The beam is light in the air only. The bright patch on the floor comes from the sun itself (<Daylight>, or a <DirectionalLight> through the window) with shadows on, so walls and roofs block it everywhere else.
The glass: <WindowPane>#
A thin pane of glass, a centimetre thick along its own z: rotate it into the wall. It glows warm by day (day) and turns deep blue at night (night), following <Daylight>. It casts no shadow, so light comes through.
Lamps#
<Lamp> is a point light that comes from a thing: put it inside a lamp's model or <Placeable> and it moves with it.
<Placeable id="lamp" on="surface" position={[0.3, 0.45, 0.2]} turn={0}>
<Model src={require("./assets/table-lamp.glb")} ground />
<Lamp position={[0, 0.36, 0]} on="dark" glow={0.2} />
</Placeable>| Prop | Default | Meaning |
|---|---|---|
position |
[0, 0, 0] |
Where the bulb is, in its parent's space |
on |
true |
true, false, or "dark": on from dusk with <Daylight> (always on without one). Switching fades |
transition |
0.6 |
Seconds to fade on or off |
color |
a warm bulb (#ffc98a) |
|
intensity |
1.5 |
Brightness when on, in candela |
distance |
3 |
How far it reaches, metres |
glow |
0 |
Radius of a soft glow round the bulb while on, metres |
<Scene lamps={n}> decides how many lamps may light the scene at once (default 0: lamps only glow). The lights are made at the start, dark until used, so switching, adding or carrying lamps never builds shaders again. Past n, the brightest win and the others only glow. Each one costs a little on everything lit: three cost the GPU about 7% in the playground's room. Lamps cast no shadows.
For many lamps (a street, a restaurant), use <Scene lighting="dynamic" lamps={16}>: lights reach the shaders as arrays, so each lamp costs only while it is on. The arrays make every light a little more expensive (about 15% more GPU than three fixed lamps), so it pays with many lamps, not a few.
Follow the time of day yourself: useDaylight#
useDaylight() returns a function that reads the light of the moment (null without a <Daylight>). Call it in useFrame:
import { Box, type SceneObject, useDaylight, useFrame } from "@apricot/react";
import { useRef } from "react";
/** A sign that shows only after dark. */
export function NightSign() {
const daylight = useDaylight();
const sign = useRef<SceneObject>(null);
useFrame(() => {
const dark = daylight()?.dark ?? 0;
if (sign.current) sign.current.visible = dark > 0.5;
return false;
});
return <Box ref={sign} size={[1, 0.3, 0.05]} position={[0, 2.2, -1.4]} material="light" color="#ff6b9a" />;
}The value (DaylightNow) has hour, dark (0 by day to 1 at night, rising over the hour before sunset), level (the sun's brightness, 0–1), direct, sunDirection, and the current colours and intensities.
Making it look solid#
| Option | Effect | Cost |
|---|---|---|
occlusion |
Soft darkening where surfaces meet or come close: corners, under a sofa, between cushions. true or { radius, strength, resolution, samples } |
A quick extra pass, worked out at half resolution |
toneMapping="neutral" |
Colours closer to as written. "agx" (default) is softer and more forgiving; "filmic" more contrasty; "none" raw |
Free |
exposure |
Brightens (above 1) or darkens (below 1) the whole picture | Free |
shadows={{ softness }} |
Softer edges (texels, default 2) | Small |
shadows={{ blur: true, softness: 8 }} |
Wide, blurred edges, as under the soft light of a room or an overcast sky | More than softness alone |
shadowOnly on a shape |
Not drawn, only casting: a roof over a cut-away room, a tree off screen whose shadow falls in | Its shadow only |
Performance#
A furnished room is many shapes. Keep it cheap on mid-range Android phones:
- Build each piece with
<Merged>(one draw call per piece). - Wrap the room in
<Bundle>: still things are recorded once and replayed. Put see-through things (the<LightBeam>, glass) inside the bundle too: bundles are drawn after everything else. - Turn on
<Scene debug>: the HUD shows GPU time per frame next to the CPU's, so you know which to work on.
See Performance.
Related#
- Lighting, environments and shadows.
- Lights reference:
Lamp,LightBeam,WindowPane,Daylight. - Placing things by hand.
- Room builder recipe.