greenhouse · git:20261102.5a0e71a · 2026-11-02 · sha256 712ffbbd7a5ce0e2

greenhouse git:20261102.5a0e71aA

Immutable. This exact content never changes and is served at /api/v1/blob/712ffbbd7a5ce0e2.

---
name: greenhouse
description: "Build a believable procedural space or sci-fi greenhouse in Three.js: a ribbed pressurised glazing dome with airlock, catwalk and nutrient core, plus tiered hydroponic grow rows with crops, trays and LED bars. Use for a lunar or Mars habitat, an orbital farm, a research biodome, or any interior where food is grown under lamps."
---

# Greenhouse

Target: Three.js 0.187.0, WebGPU renderer (`three/webgpu`) and TSL node materials (`three/tsl`); WebGPU only, no WebGL fallback, no GLSL strings. Everything is generated by code; no model files or textures.

A greenhouse is a **working closed-loop machine that happens to be full of plants**, not a garden conservatory. Everything in it exists to keep a crop alive: the pressure envelope, the airlock, the drained floor, the nutrient core, the lamps, the rows, the service walkways. Build those first; the greenery is the result.

`scripts/greenhouse.js` registers two procedural models, **"Pressurised greenhouse dome"** (`greenhouse-dome`) and **"Hydroponic grow row"** (`grow-row`), and exports a bay layout and a whole-greenhouse helper. It is written against the procedural-models kernel shipped with the `pipes` skill (`procedural-models.js`); copy both files into the project together. Distribution pipework is **not** modelled here: use the `pipes` kit, and meet it at the feed points the layout returns.

## Vocabulary

Use these words consistently in code, object names and prose:

| Term | Meaning |
| --- | --- |
| **pressure envelope** | the glazed shell that holds the atmosphere: a ribbed ellipsoid, not a conservatory roof |
| **tension ring** | the base ring beam and plinth that take the envelope's outward thrust |
| **airlock / vestibule** | the two-door pressure threshold on the door axis; the dome's front door |
| **catwalk** | the perimeter service walkway at mid-height, with grating, rails and a ladder |
| **nutrient core** | the central tank and ring main that receives drain water and returns solution |
| **grow row** | one freestanding tiered rack; the repeated unit of production |
| **bay / ring** | an angular slice of the dome; rows sit tangentially in an inner and an outer ring |
| **feed** | the world point where a row's drip manifold meets a branch from `pipes` |

## When to use it

- a pressurised agricultural dome on a moon, Mars, an asteroid or a station
- a research biodome, a seed vault, a generation-ship farm, a terraforming camp
- an interior where the grow rows are the subject: a hydroponics hall, a vertical farm, a biotech lab
- a cutaway or diorama where the dome and the rows must read at the same scale

Not for: a domestic conservatory or a botanical garden (use architectural glazing and planting instead), a field or orchard (outdoor kits and `card-grass`), or a single potted plant. For the shell materials read the `glass` skill; for the lamps read `lighting`; for the pipes read `pipes`.

## What ships

| Export | What it is |
| --- | --- |
| `greenhouseDome` | model definition, id `greenhouse-dome` |
| `growRow` | model definition, id `grow-row` |
| `registerGreenhouse()` | registers both; returns their ids |
| `layoutRows(spec)` | rows in concentric bays; one entry per row with `spec`, `matrix`, `feed`, `ring`, `bay`, `angle` |
| `createGreenhouse(spec, opts)` | dome plus every row as one `THREE.Group`; `userData` carries `dome`, `rows`, `feeds`, `dispose()` |
| `greenhouseMaterials(def, opts)` | layer description to material; adds physical transmission glass and double-sided leaves |
| `FLOOR_Y` | top of the dome floor above the model origin, `0.55` m |

```js
import { registerModel, createModel, createField } from './procedural-models.js';
import { registerGreenhouse, createGreenhouse, layoutRows, greenhouseMaterials } from './greenhouse.js';

registerGreenhouse();
scene.add(createModel('greenhouse-dome', { diameter: 24, ribs: 24, bands: 6 }));
scene.add(createModel('grow-row', { length: 6, tiers: 4, crop: 'lettuce' }));

const whole = createGreenhouse({ diameter: 24, bays: 8, innerBays: 5 });
scene.add(whole);
```

## The dome

### Envelope

- A **ribbed ellipsoidal pressure envelope**, not a hemisphere: `rise` is the crown height as a fraction of the radius, default `0.8`. A 24 m dome rises 9.6 m. The shallow ellipse reads as a pressure vessel and leaves more usable height at the rim than a hemisphere.
- **Meridian ribs** every 15° (`ribs` 12–32, default 24), in `bands` rings (3–8, default 6). The glazing is **flat trapezoidal panels** between the ribs and ring beams, inset a few centimetres, with a visible gasket gap. Flat panels are what a real prefabricated glazing system uses; a continuous glass skin reads as a bubble.
- **Tension ring** at the base: a steel ring beam on a concrete plinth, with anchor nuts. The ring takes the outward thrust of the envelope, so it must be continuous and heavier than the ribs.
- **Crown compression ring and vent lantern**: a ring at the top, a short drum and a mushroom cap. The lantern is the pressure relief and heat exhaust; it is also the silhouette that says "habitat" rather than "observatory".
- **Floor**: a drained concrete slab 0.55 m above grade, with a perimeter drain channel and painted walkway lines. The slab is inset from the tension ring, leaving a service trough around the edge.
- Panel glazing can be `clear`, `frost` or `solar`. Clear is nearly colourless; frost has a rough, milky transmission; solar is green-grey and absorbs heat. Use one choice for the whole envelope.

### Openings and circulation

- One **airlock** on the door axis, a vestibule about 2.75 m square by 2.55 m high, with an outer and an inner door, a porthole, a status lamp and steps down to grade. A pressure envelope has no plain door: the airlock is the threshold, and it should be the first thing a viewer understands.
- The airlock is cut out of the lowest glazing bands, with a portal frame around the opening. Do not leave a glazing panel in front of the door.
- A **perimeter catwalk** at mid-height (about 4.8 m on the default dome), 1.05 m wide, on brackets off the shell, with grating, edge beams, toe boards, two handrails at 0.56 m and 1.08 m, posts every 1.5–2.5 m, and a ladder from the floor. The catwalk is how the lamps, vents and glazing are serviced.
- **Aisles**: the door axis is kept clear. `layoutRows` skips every bay within about half a bay of the door so the airlock opens onto a real aisle.
- Mark the walkway and the drain with painted lines, not geometry: thin prisms a few millimetres above the slab are enough.

### Core

A central **nutrient core**: a cylindrical tank about 2.2 m across, a domed lid, a level window, a control panel, a ring main around it and four risers. On top, a **light mast** with six high-bay fixtures aimed down at the inner beds. The core is the visual anchor under the crown; keep it below the catwalk so the dome reads as one volume.

### Dome rules

- The envelope is continuous: no gap between glazing and ring, no panel floating off a rib.
- Every rib lands on the tension ring; every ring beam lands on a rib.
- The airlock is on the ground, the catwalk is above head height, and neither blocks the other.
- The floor is the working level inside the ring. The airlock floor meets it, and the steps go down to grade outside; do not recess the working floor below the door.
- No interior wall or mezzanine that would cut the pressure envelope. If the programme needs rooms, put them in separate pressure vessels.

## The grow row

A freestanding **tiered hydroponic rack**, 2–12 m long, 0.6–1.2 m wide, 2–6 tiers. Defaults: 6 m, 0.9 m, 4 tiers, 0.65 m pitch.

- **Frame**: four corner posts on base plates, a crossbar at every tier and at the top, longitudinal shelf bars, and diagonal braces on the high-detail model. Painted steel, not aluminium extrusion.
- **Trays**: a U-channel profile with a 24 mm wall and 130 mm depth, one per tier, with a substrate slab 55 mm thick. The tray is a gutter, not a box: it has an open top and a lip.
- **Crop**: lettuce, herbs, strawberry, seedling or none. Plants are built from folded leaf ribbons, a stem and (strawberry, high detail) small fruit. Spacing is by crop:

  | Crop | Along row | Across row | Canopy height |
  | --- | --- | --- | --- |
  | Lettuce | 0.24 m | 0.24 m | 0.20 m |
  | Herbs | 0.18 m | 0.17 m | 0.17 m |
  | Strawberry | 0.26 m | 0.24 m | 0.16 m |
  | Seedlings | 0.12 m | 0.11 m | 0.08 m |

- **Light**: one or two LED bars above each tray, not one per plant. The bar sits 0.17 m below the next tray and 0.58 m above the top canopy; a pink-magenta diffuser with a dark housing. The lamp is emissive geometry, not a real light per bar.
- **Water**: a drip manifold at one end, a branch and a distribution line along each tray, emitters every 0.32 m at high detail, an isolation valve on the first branch, and a tank and pump under the bottom shelf (or a floor stub when `tank` is off). The manifold rises from the floor to above the top tray.
- **Clearance**: the tank fits under the first tier with the pump on top; the lowest tray is at 0.42 m so the tank and the floor can be cleaned. Nothing hangs below the base plates.

### Row rules

- The lamp is **above** the canopy and **below** the tray above: a plant must never grow into a fixture. If the crop is taller, raise the tier pitch, do not shrink the lamp gap.
- Trays overhang the frame by a few centimetres at most; a tray that overhangs the aisle is a head injury.
- Every row has a feed and a drain. A row that ends in mid-air is a diagram, not a machine.
- Rows are serviced from the aisles, so put the manifold and tank at the aisle end, not against a wall.
- Use `crop: 'none'` for a clean rack in a cutaway or for a row awaiting planting; empty trays still need substrate and lamps.

## Layout

`layoutRows` places rows tangentially in two concentric rings:

- inner ring at `0.35 R`, `innerBays` bays (default 5), `tiers - 1` tiers
- outer ring at `0.68 R`, `bays` bays (default 8), full tiers
- `rowsPerBay` rows per bay (default 2) at `rowPitch` centres (default 1.15 m): a double-row bed serviced from both sides
- row length = `clamp(chord × 0.78, 2.2, 10)`, rounded to 0.5 m, where `chord = 2 r sin(π / bays)`
- the bay containing the door is skipped, and crops alternate through `CROP_ORDER` when `crop: 'mixed'`

The default 24 m dome gives **22 rows**. Each row's `matrix` is a world transform with local +X along the row, +Y up and +Z radial; `feed` is the world point at the row's drip manifold, at `FLOOR_Y + 0.21`, ready to meet a branch.

```js
const rows = layoutRows({ diameter: 24, bays: 8, innerBays: 5, crop: 'mixed' });
for (const row of rows) {
  const m = createModel('grow-row', { ...row.spec, detail: 'high' }, { materials: greenhouseMaterials });
  m.position.setFromMatrixPosition(row.matrix);
  m.quaternion.setFromRotationMatrix(row.matrix);
  scene.add(m);
}
```

For many identical rows, build one `createField('grow-row', spec, matrices)` per crop and set `distances: [45, 160]`; call `field.update(camera)` whenever the camera moves. The field takes one spec for every instance, so group rows by crop and tier count rather than trying to vary them per instance.

## Reuse the pipes kit

The dome ships only short service pipes around the core. The **mains and branches belong to the `pipes` skill**: that kit already models flanged modules, supports, valves, lagging and end bends, and the `pipeRun` recipe in that skill stretches a whole number of modules along a line.

```js
import * as THREE from 'three/webgpu';
import { registerModel, createField } from './procedural-models.js';
import { pipeRack } from './pipe-rack.js';
import { layoutRows } from './greenhouse.js';

registerModel(pipeRack);
const rows = layoutRows({ diameter: 24 });

// The pipes module runs along local +Y and stands off along local +Z.
// Row local X is tangential, row local Z is radial; point +Y radially at the
// feed and +Z up, then lower the origin by the stand-off so the pipe centre
// lands at feed height.
const feedMatrices = rows.map(({ matrix, feed }) => new THREE.Matrix4()
  .makeBasis(
    new THREE.Vector3().setFromMatrixColumn(matrix, 0),
    new THREE.Vector3().setFromMatrixColumn(matrix, 2),
    new THREE.Vector3(0, 1, 0),
  )
  .setPosition(feed.x, feed.y - 0.3, feed.z));

const branches = createField('pipes', {
  pipes: 1, diameter: 0.15, length: 3, spacing: 1.8, stand: 0.3,
  supports: 'none', flanges: false, valve: false, bend: 'none', color: '#6C8E93',
}, feedMatrices);
scene.add(branches);
branches.update(camera);
```

Rules for the join:

- Run the branch along the aisle at the feed height, then a short bend into each row. Do not run a main across a row or through the airlock aisle.
- A 3 m pipe module is the shortest the `pipes` kit builds. Use it as the visible branch, place the feed at one end, and hide the last few centimetres behind the manifold.
- Mains follow the catwalk edge or the service trough, never diagonally across the floor.
- One colour per service everywhere: water, nutrient solution, drain, air, power. The dome's `Service pipes` layer and the `pipes` kit must agree.

## Materials and lighting

`greenhouseMaterials(def)` extends the kernel's `createMaterial`:

- `{ glass: true, color, rough, transmission, ior, thickness, attenuation?, attenuationDistance?, frost? }` → `MeshPhysicalNodeMaterial` with `transmission`, `thickness` and `DoubleSide`. Keep `transparent: false`: transmission is an opaque pass in three, and setting `transparent` reintroduces sorting artefacts.
- `{ leaf: true, color, ... }` → the normal painted material with `side: DoubleSide`. Leaves need double-sided shading or half the canopy disappears.
- Everything else passes through to the kernel (paint, metal, glow, coat).

Lighting:

- The LED diffusers and the high-bay lenses are **emissive geometry** (`glow` around 2.2). They read at any distance and cost nothing.
- Add **one or two real lights** for the pools on the floor and the core, not one per bar. A dome with 40 point lights will not render in a reasonable frame; see `lighting` for clustered or baked alternatives.
- Glass needs something to reflect: a sky, a starfield, an environment map. Without it, transmission reads as grey plastic.
- Shadows: the dome shell and the rows cast; the glazing is usually better as a shadow caster only at high detail, or not at all. The catwalk grating should not cast a solid slab shadow.

## Performance

Measured on the defaults, one model at each level:

| Model | high | medium | low |
| --- | --- | --- | --- |
| `greenhouse-dome` (24 m) | ~107k tris | ~36k | ~27k |
| `grow-row` (6 m, 4 tiers, lettuce) | ~47k | ~15k | ~12k |

- High detail is for a hero shot or a cutaway; medium is the working level for a scene; low is for rows seen from across the dome.
- The dome is one draw call per material layer (15 layers), the row one per layer (12). Instancing rows with `createField` is the only way to get hundreds.
- `createGreenhouse` shares one material cache across the dome and all rows. `userData.dispose()` frees the geometries and the materials; call it when the scene is torn down.
- Glass panels at high detail are separate quads; at low detail one lathe replaces them. Do not add more glazing subdivisions than the rib count can explain.

## Pitfalls

- **The model must sit on y = 0.** The dome's plinth and the row's base plates are the lowest geometry. If you edit a part, check the lowest vertex before shipping.
- **The row's origin is the floor**, not the first tray. Place rows at `FLOOR_Y + 0.01` in the dome, or on a slab of your own.
- **Do not pass layout parameters to the dome model.** `createGreenhouse` filters them for you; if you call `createModel('greenhouse-dome', spec)` yourself, pass only the dome parameters. Unknown keys warn.
- **Do not scale the model non-uniformly.** The glazing thickness, leaf normals and pipe radii all assume metres. Scale uniformly or rebuild with different parameters.
- **Do not clone hundreds of rows.** `clone()` shares geometry but each clone is still a group of meshes; use `createField`.
- **Do not add a flat green sheet as a crop.** The leaves are the crop; a green plane reads as AstroTurf.
- **Do not put a real light in every fixture**, and do not make the glass `transparent: true`.

## API

### `greenhouse-dome`

| Parameter | Range | Default | Meaning |
| --- | --- | --- | --- |
| `diameter` | 12–48 m | 24 | tension-ring diameter |
| `rise` | 0.55–1.0 R | 0.8 | crown height / radius |
| `ribs` | 12–32, step 4 | 24 | meridian ribs |
| `bands` | 3–8 | 6 | glazing bands |
| `glazing` | `clear` / `frost` / `solar` | `clear` | panel material |
| `airlock` | toggle | on | vestibule, doors, steps, porthole |
| `catwalk` | toggle | on | ring walkway, rails, ladder |
| `core` | toggle | on | nutrient core, ring main, light mast |
| `color` | colour | `#C6CBC4` | frame paint |
| `detail` | `high` / `medium` / `low` | `high` | level of detail |

### `grow-row`

| Parameter | Range | Default | Meaning |
| --- | --- | --- | --- |
| `length` | 2–12 m | 6 | rack length |
| `tiers` | 2–6 | 4 | tray levels |
| `width` | 0.6–1.2 m | 0.9 | tray width |
| `pitch` | 0.45–0.9 m | 0.65 | tier spacing |
| `crop` | `lettuce` / `herb` / `strawberry` / `seedling` / `none` | `lettuce` | planting |
| `lights` | toggle | on | LED bars |
| `tank` | toggle | on | tank and pump under the bottom shelf |
| `color` | colour | `#D8DAD3` | frame paint |
| `leafColor` | colour | `#4F8A3C` | crop colour |
| `detail` | `high` / `medium` / `low` | `high` | level of detail |

### `layoutRows(spec)`

Accepts `diameter`, `bays`, `innerBays`, `rowsPerBay`, `tiers`, `crop`, `rowPitch`, `color`, `leafColor`. Returns `[{ spec, matrix, feed, ring, bay, angle }]`; `spec` is ready for `grow-row`, `feed` is a `THREE.Vector3` in world space.

### `createGreenhouse(spec, opts)`

`spec` accepts the layout keys plus the dome keys (`rise`, `ribs`, `bands`, `glazing`, `airlock`, `catwalk`, `core`). `opts` accepts `detail` and anything `createModel` accepts. Returns a group with `userData.dome`, `userData.rows`, `userData.feeds` and `userData.dispose()`.

Research notes, precedents and the measurements behind these choices are in `references/space-greenhouse-systems.md`.