# The Specify house model standard — `specify-house-model/1`

How a 3D model of a type house has to be built so that it works in Specify's configurator: a
customer on a housebuilder's website chooses the size, the garage, the roof and the facade, and sees
that exact house, from outside and from inside, on whatever device they are holding.

**Who this is for.** First, the 3D modeller Specify commissions to build a house — you do not need
to know anything about Specify, and everything you must do is in [Part A](#part-a--what-to-deliver).
Second, Specify's own engineers, who build the conformance check against the numbered rules
(`HM-1`, `HM-2`, …) and the reasoning in [Part B](#part-b--why-it-is-this-way).

**Status: version 1, 2026-09-24.** Decided by TG (`T-260924-G7LB`). No model has been built to it
yet. The numeric budgets in [§ A5](#a5-quality-tiers) are a first calibration and **will be tuned
against the first commissioned delivery** — that is expected, not a sign the standard is wrong.

---

## Part A — What to deliver

### A1. The package

One folder per house. `<house>` is the house's key (see [§ A3](#a3-names)), e.g. `villa-140`.

```
villa-140/
├── house.json                      the manifest — what can be configured (§ A2)
├── villa-140.high.glb              three quality tiers of the same house (§ A5)
├── villa-140.standard.glb
├── villa-140.low.glb
├── plans/
│   ├── villa-140.plan.size-120.svg one floor plan per layout variant (§ A8)
│   ├── villa-140.plan.size-140.svg
│   └── villa-140.plan.size-160.svg
└── source/
    ├── villa-140.blend             the editable source (§ A9)
    ├── textures/
    └── README.txt
```

- `HM-1` The package contains `house.json`, exactly three `.glb` files named
  `<house>.high.glb`, `<house>.standard.glb` and `<house>.low.glb`, a `plans/` folder and a
  `source/` folder.

### A2. The manifest — `house.json`

A small file that says what a customer can configure. It is the first thing Specify reads, and the
models are checked against it. The format is defined exactly by
[house.schema.json](house.schema.json); an example:

```json
{
  "standard": "specify-house-model/1",
  "house": "villa-140",
  "name": "Villa 140",
  "groups": [
    { "key": "size",   "kind": "geometry", "layout": true,
      "default": "140", "options": ["120", "140", "160"] },
    { "key": "garage", "kind": "geometry",
      "default": "no",  "options": ["no", "yes"], "empty": ["no"] },
    { "key": "roof",   "kind": "geometry",
      "default": "gable", "options": ["gable", "hip"] },
    { "key": "facade", "kind": "material",
      "default": "brick", "options": ["brick", "wood", "render"] }
  ],
  "rooms": {
    "entrance": "Entrance", "living": "Living room", "kitchen": "Kitchen",
    "bedroom-1": "Bedroom 1", "bedroom-2": "Bedroom 2", "bathroom": "Bathroom"
  },
  "views": { "front": "Front", "garden": "Garden side" }
}
```

A **group** is one thing the customer chooses. There are two kinds:

- **`geometry`** — the choice changes the shape of the house (size, garage, roof shape). Each
  option is a set of parts shown or hidden (§ A4).
- **`material`** — the choice changes a surface's finish (facade, roof covering, window frames).
  Each option is a material variant (§ A6).

- `HM-2` `house.json` validates against `house.schema.json`, and `standard` is exactly
  `specify-house-model/1`.
- `HM-3` Every group has a `default` that is one of its `options`.
- `HM-4` An option listed in `empty` is allowed to show no parts at all (e.g. `garage: no`). Every
  other geometry option must show at least one part in every combination (§ A4).
- `HM-5` At least one geometry group is marked `"layout": true` — the groups that change the floor
  plan. Usually this is only `size`.
- `HM-6` The number of geometry combinations — the product of the option counts of every geometry
  group — is at most **64**.

### A3. Names

Every key — the house, a group, an option, a room, a view, a material slot — uses only **lower-case
letters, digits, hyphens and underscores**, and starts with a letter or digit: `villa-140`, `size`,
`140`, `bedroom-1`, `window-frame`.

- `HM-7` Every key matches `^[a-z0-9][a-z0-9_-]*$`.

These are the same characters Specify's configurator uses for its own row and option keys, **on
purpose**: a group called `size` with an option `140` connects to a configurator row `size` with an
option `140` automatically. If the housebuilder's configurator already exists, use its keys.

### A4. Scene structure and variant parts

The scene is one root node named `house`, with two children, `exterior` and `interior`. Everything
else goes underneath one of them.

**A part that only appears for certain choices is tagged in its node name**:

| Node name | Shown when |
|---|---|
| `var:garage=yes` | the garage is `yes` |
| `var:size=160` | the size is `160` |
| `var:size=160;roof=hip` | the size is `160` **and** the roof is `hip` |
| *(anything without `var:`)* | always |

Put the tag on a **group node** (an Empty in Blender) and place the meshes inside it; they inherit
its visibility. A node inside a hidden node is hidden too.

- `HM-8` The root node is named `house` and has exactly the children `exterior` and `interior`.
- `HM-9` A tagged node's name is `var:` followed by one or more `group=option` conditions joined by
  `;`, where every group is a `geometry` group in `house.json` and every option is one of its
  options. A condition names each group at most once.
- `HM-10` For **every** combination of geometry options (up to 64, § A2), each geometry group's
  chosen option shows at least one part — unless it is listed in `empty`. This is what catches the
  missing `var:size=160;roof=hip` roof.
- `HM-11` Node names are unique, **except** that a trailing `.001`, `.002` … (which Blender adds to
  duplicates) is ignored — so two nodes may both be `var:garage=yes` if one is in `exterior` and one
  in `interior`.

### A5. Quality tiers

You deliver the **same house three times, at three levels of detail**. Specify's viewer picks the
one the customer's device can carry.

| Tier | For | Triangles, whole house, every variant present | Largest texture | Texture images | Delivered `.glb` at most |
|---|---|---|---|---|---|
| **High** | desktop computers with a good graphics card | 1,500,000 | 4096 × 4096 | 64 | 250 MB |
| **Standard** | ordinary laptops, recent phones | 300,000 | 2048 × 2048 | 32 | 60 MB |
| **Low** | older phones, slow connections, the wireframe view | 60,000 | 1024 × 1024 | 16 | 12 MB |

Counts are for the file as delivered, with **every variant part present** — the worst case, not a
typical configuration. Specify compresses geometry and textures for serving, so you may deliver
uncompressed.

**The three tiers must have the same structure.** Detail differs; names do not.

- `HM-12` Each tier is within its budget on every column above.
- `HM-13` All three tiers have identical sets of `var:`, `room:` and `view:` node names, identical
  material slot names and identical material variant names.
- `HM-14` **Low tier: clean hard edges.** The wireframe view draws the Low tier's edges, so walls,
  roofs and openings must read as clean architectural lines — no stray diagonals across flat faces
  where they would show as edges, no decimation noise.

### A6. Materials

Physically based materials only, in glTF's standard metallic-roughness model. Each material is
named for the surface it covers:

- `HM-15` Every material is named `mat:<slot>`, e.g. `mat:facade`, `mat:roof`, `mat:window-frame`,
  `mat:glass`, `mat:floor`, `mat:wall-interior`.

**Material choices use the glTF extension `KHR_materials_variants`**, which Blender's glTF exporter
supports: the same mesh carries one material per variant. Each variant is named `group=option`,
matching a `material` group in `house.json` — e.g. variants `facade=brick`, `facade=wood`,
`facade=render`.

- `HM-16` Every `material` group in `house.json` has one variant per option, named `group=option`,
  and the default option's material is also the mesh's own material.
- `HM-17` Textures are PNG, JPEG or WebP, embedded in the `.glb`, with power-of-two dimensions.
- `HM-18` Ambient occlusion, if baked, goes in the material's `occlusionTexture` (it may use a
  second UV set, `TEXCOORD_1`). Do not bake lighting into the base colour: the viewer lights the
  house, and baked-in light and shadow would double.

### A7. Interior layout

The interior is the **layout, not the furnishing**: walls with their real thickness, floors,
interior doors (the opening and the door leaf), windows (frame and glass), and stairs where there is
more than one storey. No loose furniture, no appliances, no decoration.

Each room is a node named `room:<key>`, containing that room's floor. Room names shown to customers
come from the `rooms` map in `house.json`.

- `HM-19` Every room in `house.json` has a `room:<key>` node under `interior`, and every
  `room:<key>` node is listed in `house.json`.
- `HM-20` A room's floor lies at its storey's floor level and is a single closed surface, so
  Specify can compute its area.
- `HM-21` No furniture, appliances or decorative objects. (Built-in kitchens and bathrooms may be
  added as a later extension of the same model — not in this version.)

### A8. Floor plans

One plan per **layout variant** — one per combination of the groups marked `"layout": true`, which
is usually one per size. A roof or cladding choice does not change the plan, so it does not need its
own.

- `HM-22` One plan per layout combination, named
  `<house>.plan.<group>-<option>[.<group>-<option>…].svg`, e.g. `villa-140.plan.size-160.svg`.
- `HM-23` SVG is preferred. PDF, or PNG at least 2400 pixels on its long side, are accepted.
- `HM-24` Each plan shows every room with its name and floor area in m², the overall dimensions, a
  north arrow and a scale bar, and matches the model's interior layout for that variant.

### A9. Source files

The editable source, so the model can be changed later without building it again.

- `HM-25` `source/` contains the working file — **Blender `.blend` (4.2 LTS or newer) is
  preferred**; another tool's native file plus an FBX export is accepted — with all textures, and a
  `README.txt` naming the tool and its version.

### A10. Coordinates and scale

- `HM-26` Real-world scale in **metres**. **+Y is up.** The ground-floor finished floor level is at
  **Y = 0**. The house is centred on its footprint in X and Z, and **the front entrance faces +Z**.
  (This is glTF's own convention, so nothing is rotated or rescaled when the house loads.)
- `HM-27` Transforms are applied: no node has a negative or non-uniform scale.

### A11. Viewpoints (optional, recommended)

Named camera positions the viewer can start from or move to, as empty nodes: `view:front`,
`view:garden`. Position the empty where the eye should be and point it at the house.

- `HM-28` Every `view:<key>` node is listed in `views` in `house.json`, and the reverse.

### A12. What must not be in the file

- `HM-29` **No external references.** Every buffer and texture is embedded in the `.glb`; no
  `uri` points to a file or a web address.
- `HM-30` No animations, skins or morph targets.
- `HM-31` No cameras and no lights — the viewer provides both. Use `view:` nodes for camera
  positions.
- `HM-32` No unused materials, textures or meshes, and no hidden geometry.
- `HM-33` Only these glTF extensions may be used: `KHR_materials_variants`,
  `KHR_texture_transform`, `KHR_materials_emissive_strength`, `KHR_materials_transmission`,
  `KHR_materials_ior`, `KHR_mesh_quantization`, `EXT_meshopt_compression`,
  `KHR_draco_mesh_compression`, `KHR_texture_basisu`.
- `HM-34` Each `.glb` states the standard it conforms to in `asset.extras.standard` =
  `"specify-house-model/1"`, and passes the Khronos glTF-Validator with no errors.
- `HM-35` No logos, text or branding that the housebuilder does not own.

### A13. Before you deliver

Run your `.glb` files through the **Khronos glTF-Validator**
(<https://github.khronos.org/glTF-Validator/>) and fix every error. Specify then runs the same
validator plus every `HM-` rule above, and returns a list naming each rule a delivery breaks. A
delivery is accepted when that list is empty.

---

## Part B — Why it is this way

### Why glTF, and one format

glTF 2.0 is the Khronos Group's open standard for delivering 3D — often called "the JPEG of 3D". It
is what Three.js (Specify's viewer), Blender, Sketchfab and both Apple's and Google's AR pipelines
read and write, it carries physically based materials and a named part hierarchy in one compact
binary file, and its extensions cover exactly what a configurable house needs. The alternatives were
worse for this purpose: FBX is proprietary and not web-native; OBJ carries no real materials and no
hierarchy; IFC is a building-information format for construction, excellent for architects and
wrong for a real-time viewer in a browser.

### Why variants are built in, not generated

TG decided on 2026-09-24 that a house's dimensions change as **discrete variants the modeller
builds** — 120, 140 or 160 m², garage or not — rather than as geometry generated at runtime. That
is how type houses are actually sold, it keeps every house a customer can see one somebody designed
and checked, and it keeps Specify out of the business of generating buildings. Tagging parts in
node names keeps it inside plain glTF, so any tool that exports glTF can produce a conforming house.

### Why the keys match the configurator

Specify once built a feature that asked every merchant to hand-draw regions on their photographs,
and removed it the evening it shipped (`d2d37887`; `docs/architecture/computed-product-imagery.md`
keeps the record): work handed to every merchant is not a product. Using the configurator's own key
characters means the model binds to the configuration by name (`T-260924-E6WT`), and a person only
reviews the result.

### Why three tiers — and what "game quality" can mean in a browser

The ambition ranges from "Assassin's Creed Odyssey" down to "simple wireframes". A game like that
runs on dedicated graphics hardware with tens of gigabytes of assets; a browser on a good desktop can
come genuinely close for **one** house, and a phone cannot, and nobody should download the desktop
house to see it on a phone. So a house is delivered at three levels and the viewer picks. Specify's
product pages are held to strict performance rules, and the tiers are what make 3D compatible with
them. The wireframe view is a way of drawing the Low tier, not a fourth model to pay for.

**The largest single lever on the High tier's look is baked global illumination, and this version
deliberately leaves it out** (`HM-18`). glTF has no standard slot for a lightmap; supporting one means
choosing a convention the viewer understands. That is a decision for version 2, once the viewer
exists and the first house shows how far realtime lighting gets.

### Why no external references

A model that loads a texture from somebody else's server reports every customer's IP address and
browser to that server. Specify already refuses the same thing in merchant stylesheets
(`frontend/src/lib/cms/scopeCss.ts`) for the same reason.

### What version 1 deliberately leaves out

Furnished interiors (a later extension of the same model); baked global illumination (above);
opening doors and any animation; AR-specific files (AR is deferred — `T-260924-X4DF` — and glTF
converts to Apple's USDZ when it is picked up); the building plot and landscaping; and continuous,
drag-to-resize dimensions.

### Versioning

A change that would make a conforming house stop conforming is a new version,
`specify-house-model/2`, and a house states the version it was built to (`HM-2`, `HM-34`). A
calibration of the budgets in § A5 after the first delivery is expected and will be recorded here.

## The reference house

`reference/reference-house/` — and `reference/reference-house.zip`, the same folder as one download —
is a complete package built to this standard: a worked example to open beside the rules, and the
fixture Specify's own chain is tested against. It is generated by a script, so it is plain (one
storey, rooms in two rows): it shows what conforming means, not what a good house looks like.

**Its surfaces show § A6 in use** (`T-261010-89DB`):

- Six textures — brick, wood cladding, roof tiles, the door, an oak floor and interior plaster —
  each a base-colour map and a tangent-space normal map in glTF's convention (+Y up), JPEG,
  embedded (`HM-17`, `HM-29`). They are 1024 × 1024 in the High tier and 512 × 512 in Standard and
  Low: far inside § A5's budgets, which a commissioned house is expected to use much more of.
- The UVs carry the real-world scale. A face's U runs along the horizontal in its plane and its V up
  the plane, divided by the texture's real tile size (a brick tile is 2.08 m × 1.95 m: eight bricks
  by twenty-six courses), so a brick is a brick's size on every wall and the courses run level
  round a corner. One repeating sampler; no `KHR_texture_transform`.
- Nothing is lit or shadowed in the base colour (`HM-18`): the viewer lights the house.
- The facade is three materials, all named `mat:facade`, one per variant (`HM-16`): brick, wood
  cladding, and a render that is the plaster texture under a warmer tint and a stronger
  `normalTexture.scale`.
- The window frames are a second material group, `windows`, built the same way (`T-261011-MYST`):
  three materials named `mat:window-frame`, one per variant, without a texture. Each is drawn as the
  material its option names (aluminium cladding in dark grey, oiled wood, white PVC), not as a colour
  a housebuilder sells. The two groups share the file's one variant list, and a mesh carries mappings
  for its own group's variants only, so a facade choice never reaches a frame. The front door's frame
  is `mat:door-frame` and the roof's painted woodwork `mat:trim`, both without a texture or a variant.
- `source/textures/` holds the full-size textures, as a delivery's source does (`HM-25`).

**Regenerating it** (Specify's engineers), from `apps/cms-service`:

```bash
npx tsx scripts/generate-reference-textures.ts [name ...]   # only to change a texture
npx tsx scripts/generate-reference-house.ts                 # after any change to the generator, a texture or this standard
```

The textures are procedural — drawn by the first script into
`src/houseModel/reference/textures/` — and **committed**: the second script embeds them byte for
byte and never encodes an image, because JPEG encoding differs between library versions and the
package's files are compared byte for byte with a fresh build (`referenceHouse.test.ts`). Commit
both scripts' output together. A pilot store that already shows the reference house gets the new
files the next time it is composed (`composeHouse`, `tools/outreach/src/pilot.js`).

## Related

- [house.schema.json](house.schema.json) — the manifest's exact format.
- `T-260924-V2NR` — the conformance check that implements the `HM-` rules.
- `T-260924-S4HX` — a reference house built to this standard, sent to the modeller as a worked
  example. It is generated in code (`apps/cms-service/src/houseModel/reference/`), so a new option
  is a change to the generator and a regeneration, not a modeller's job. Its groups are size (`100`,
  `120`), garage, roof (`gable`, `hip`, `mono-pitch` — the last since `T-261008-KPRH`), facade
  (`brick`, `wood`, `render` — likewise) and windows (`wood-aluminium-triple`, `wood-triple`,
  `pvc-triple` — the keys of the housebuilding configurator's own windows row, as § A3 asks, since
  `T-261011-MYST`). Its surfaces and how it is regenerated are [above](#the-reference-house).
- `T-260924-E6WT` — how a house binds to the configurator.
- `T-260924-F3PK` — the exterior viewer.
- `docs/contracts/house-visibility/` — how Specify's viewers read the `var:` tags of § A4 off a
  loaded model, what they do with a tag that breaks `HM-9`, and the cases both are held to.
- `docs/contracts/house-choices/` — how the storefront and the admin turn a configuration's answers
  into one option per group through the binding, and the cases both are held to.
