# Light a room

> Make an interior look warm and solid. Sun through a window that follows the time of day, a beam of light in dusty air, window glass that darkens at night, lamps that come on at dusk, ambient occlusion, soft shadows, tone mapping, and shadow-only walls and roofs.

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.

```tsx title="DayRoom.tsx"
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 own `keys`).
- **`keys`** replaces the day's colours (an array of `DaylightKey`, see [Lights reference](https://matthiasbrandolin.dev/apricot/reference/lights#daylight)). A room's light is warmer than outdoors; the room builder in the Apricot playground uses its own keys.
- **`background={false}`** keeps your own `background` instead 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 round `bounds`, takes the sun's colour, and shows only while the sun is on the window's side of the wall. `intensity` is its brightness at the sun's brightest.
- Without `sun`, give **`to`**: the middle of where it lands. Use this for a fixed beam or a spotlight's cone on a stage.
- **`from`** is the middle of the opening, **`size`** its width and height, **`facing`** the 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.

```tsx
<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`:

```tsx title="NightSign.tsx"
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](https://matthiasbrandolin.dev/apricot/concepts/performance).

## Related

- [Lighting, environments and shadows](https://matthiasbrandolin.dev/apricot/guides/lighting).
- [Lights reference](https://matthiasbrandolin.dev/apricot/reference/lights): `Lamp`, `LightBeam`, `WindowPane`, `Daylight`.
- [Placing things by hand](https://matthiasbrandolin.dev/apricot/guides/placement).
- [Room builder recipe](https://matthiasbrandolin.dev/apricot/recipes/room-builder).
