Outdoor pieces for islands, beaches, gardens, docks and dioramas:
| Piece | Use |
|---|---|
<Slab> |
Flat ground in any shape: an island, a lawn, a deck round a pool (with holes) |
<Terrain> |
A tile of ground of any height, cut square at its edges like a slice of the earth: a beach running into the sea, a hill |
<Water> |
A water surface moved by waves: the sea, a pool, a pond |
useWaves |
Waves shared by the water and what floats on it |
useShore |
How far each point of the water is from land: pale shallows, deep sea, foam along the shore |
smoothOutline |
Rounds a few corner points into a smooth shore |
<Placement areas zones snaps> |
Where things may stand, float or snap into place |
Shapes on the ground are outlines: arrays of [x, z] corners in metres, in order round the shape (Outline).
An island in the sea#
import {
Box,
CameraRig,
type LayoutItem,
Placeable,
Placement,
type PlacementArea,
Scene,
Slab,
smoothOutline,
Sphere,
useLayout,
useShore,
useWaves,
Water,
} from "@apricot/react";
import { useMemo } from "react";
const SIZE = [12, 10] as const;
const EVERYWHERE = [
[-6, -5],
[6, -5],
[6, 5],
[-6, 5],
] as const;
// A few points, rounded into a shore.
const ISLAND = smoothOutline([
[-2.4, -1],
[0, -1.8],
[2.2, -0.9],
[2, 1.3],
[-0.4, 1.8],
[-2.6, 0.7],
]);
const SAND = 0.2; // the island's top, above the sea at 0
interface Thing extends LayoutItem {
kind: "lounger" | "duck";
}
const START: Thing[] = [
{ id: "lounger", kind: "lounger", position: [0, SAND, 0], turn: 0 },
{ id: "duck", kind: "duck", position: [3.5, 0, 2], turn: 30 },
];
export function IslandScreen() {
const sea = useWaves({ height: 0.03, length: 1.6, speed: 0.6 });
const shore = useShore([ISLAND], [-6, -5, 6, 5], { reach: 2.5 });
const layout = useLayout(START, { save: "island" });
// Later areas lie on earlier ones: the island on the sea.
const areas = useMemo<PlacementArea[]>(
() => [
{ id: "sea", outline: EVERYWHERE, water: sea },
{ id: "island", outline: ISLAND, level: SAND },
],
[sea],
);
return (
<Scene style={{ flex: 1 }} look="soft" background="#d5ecf3" shadows>
<Water size={SIZE} waves={sea} shore={shore} clarity={0.3} />
<Slab outline={ISLAND} height={0.5} position={[0, SAND - 0.5, 0]} color="#f3d9a4" />
<Placement areas={areas} collide="warn" onChange={layout.place}>
{layout.loaded &&
layout.items.map((t) => (
<Placeable key={t.id} id={t.id} position={t.position} turn={t.turn} on={t.kind === "duck" ? "water" : "floor"}>
{t.kind === "duck" ? (
<Sphere radius={0.15} position={[0, 0.1, 0]} color="#ffd23f" />
) : (
<Box size={[0.6, 0.15, 1.6]} position={[0, 0.075, 0]} color="#f58fa8" />
)}
</Placeable>
))}
</Placement>
<CameraRig subject={null} view={{ azimuth: 30, elevation: 40, distance: 16 }} orbit={{ turn: 60, elevation: [12, 70] }} />
</Scene>
);
}- The water lies level at its position's height (0 here) and covers
size(width along x, depth along z), centred on its position. - The slab stands up from its position by
height, so its top is atposition.y + height:SANDhere. - The lounger stands on the island; it can't go into the sea. The duck has
on="water": it floats in the sea, bobbing and tilting on the same waves, and can't go ashore. - With
collide="warn", a thing dragged over the wrong ground passes over it with a red marker and goes back if let go there. With"block", it slides along the shore.
Water#
| Prop | Default | Meaning |
|---|---|---|
size |
required | [width, depth] in metres |
waves |
still water | Waves from useWaves, shared with what floats |
shore |
none | Distance to land from useShore: shallows, deep water and foam by the shore |
shallow, deep, beach |
turquoise, azure, pale aqua | Colours over the shallows, out at sea, and where it laps the sand |
foam |
white | Foam colour along the shore, or false |
caustics |
0.6 |
Light dancing on the bottom of the shallows, 0–1 (with a shore) |
glow |
0.35 |
How much it glows with its own colour, so it stays vivid in any light |
clarity |
0 |
How much you see through it, 0 (opaque) to 1 |
sparkle |
0.5 |
A pale glint on the crests, 0–1 |
spacing |
0.25 |
Metres between its points: finer carries shorter waves |
position |
[0, 0, 0] |
Where its middle is; it is moved, never turned |
Without shore, the colour goes from shallow looking down into it to deep at a slant. While the water has waves, the scene keeps rendering.
useWaves(options)#
| Option | Default | Meaning |
|---|---|---|
height |
0.06 |
Crest height above the middle, metres. 0 is still water |
length |
2.5 |
Metres from one crest to the next |
speed |
0.8 |
Metres per second |
direction |
0 |
Degrees round the vertical: 0 travels towards +x |
The waves are made once and change live when the options change (no shader rebuild). Pass the same waves to <Water waves> and to a water area of <Placement>, so floating things ride the waves you see.
useShore(land, bounds, options)#
Measures, once, how far each point of bounds ([minX, minZ, maxX, maxZ]) is from land.
landis an array of outlines ([ISLAND]), or a test(x, z) => boolean(true on land). Keep a test function stable (define it outside the component): a new function measures again.reach(default 4) is how far out, in metres, the water deepens from the shore to the open sea.size(default 128) is the field's resolution.
Slab#
| Prop | Default | Meaning |
|---|---|---|
outline |
required | The shape on the ground |
height |
0.2 |
How thick, standing up from its position |
holes |
none | Outlines cut through it (a pool in a deck) |
bevel |
0.03 |
Rounding of its edges, metres |
Plus everything a shape takes: material, color, transforms, onPress, shadows.
smoothOutline(points, segments = 96) turns a few corners into a smooth closed curve of segments points: the easiest way to draw a natural shore.
Terrain#
<Terrain> is ground whose height you give as a function. It is cut square at its edges, with sides that can show layers and, where it dips under water, the sea's depth seen through the cut: a diorama tile.
import { Scene, Terrain, useShore, useWaves, Water } from "@apricot/react";
// Sand that slopes from x = -4 down into the sea past x = 1. Keep these functions outside the component.
const groundAt = (x: number, z: number) => 0.25 - 0.12 * (x + 1) + 0.04 * Math.sin(z * 1.5);
const sandAt = (_x: number, _z: number, y: number) => (y > 0.04 ? "#f3d9a4" : "#d9b77e");
const layerAt = (y: number) => (y > -0.15 ? "#e2c48e" : "#a9825a");
const isLand = (x: number, z: number) => groundAt(x, z) > 0;
export function Beach() {
const sea = useWaves({ height: 0.012, length: 1.1, speed: 0.5, direction: 20 });
const shore = useShore(isLand, [-4, -3, 4, 3], { reach: 2.2 });
return (
<Scene style={{ flex: 1 }} look="soft" background="#d5ecf3" shadows camera={{ position: [0, 6, 11], fov: 35 }}>
<Terrain size={[8, 6]} height={groundAt} color={sandAt} sides={layerAt} water={{ level: 0 }} />
<Water size={[8, 6]} waves={sea} shore={shore} clarity={0.3} />
</Scene>
);
}| Prop | Default | Meaning |
|---|---|---|
size |
required | [width, depth] in metres, centred on its position |
height |
required | (x, z) => y: the ground's height at each point, metres |
color |
white | (x, z, y) => colour: the top's colour point by point (sand to grass by height) |
sides |
the top's edge colour | (y) => colour: layers in the cut |
spacing |
0.1 |
Metres between its points |
base |
0.2 below its lowest point | How far down its sides reach |
roughness |
0.9 |
0 polished to 1 matte |
water |
none | { level, top, deep, fade, clarity }: water in it up to level, its depth showing in the cut. Pair it with a <Water> of the same size at that level |
Where things may go#
<Placement> takes three outdoor options (see Placing things by hand for the rest):
| Option | Each item | Meaning |
|---|---|---|
areas |
{ id, outline, level, water } |
Ground (or water) things stand in. A thing stays wholly inside one area, at its level, sliding along its edge. Later areas lie on earlier ones: list the sea first, then the island, then a deck on the island. Areas with water (waves, or true) take only on="water" things; the others take the rest |
zones |
{ id, outline } |
Places nothing may stand: rocks, a flower bed, round a palm. A thing touching one doesn't fit |
snaps |
{ at: [x, z], turn, radius, accepts } |
Spots that pull a thing within radius (default 0.3 m) into place and turn it to turn degrees. accepts(id) says which things it takes |
<Placement
areas={[
{ id: "sea", outline: everywhere, water: sea },
{ id: "sand", outline: drySand, level: 0.16 },
{ id: "deck", outline: deck, level: 0.26 },
]}
zones={[{ id: "rocks", outline: rocks }]}
snaps={[{ at: [2, 1.75], turn: 0, accepts: (id) => id.startsWith("lounger") }]}
// The boat only in the sea; canPlace is told the area and the zones touched.
canPlace={(id, place, { area }) => !id.startsWith("boat") || area === "sea"}
collide="warn"
>
…
</Placement>Floating things (on="water") bob and tilt with the waves at rest and cast no shadow while they move, so the shadow map isn't redrawn every frame.
Performance#
- Waves keep the scene rendering. Keep the water's
spacingas coarse as the waves allow, and put still things (the ground, the scenery, the<Placement>) in a<Bundle>. useShoreis measured once (an exact distance transform: a 160 × 160 field takes about 16 ms on a desktop). Change its inputs rarely.- Instance repeated scenery (palms, rocks, fence posts) with
<Instances>.
Related#
- Ground and water reference.
- Placing things by hand.
- Lighting:
<Daylight>for the sun over the beach.