---
name: threejs-window-mapping
description: Make windows in a Three.js or React Three Fiber scene look like glass with something behind it, using a flat quad per window and a parallax shader instead of modelled rooms. Bundles a tested, texture-free material (blinds, roller blinds, curtains, net curtains, blurry interior hints, night lighting, sky reflection) for both WebGLRenderer and WebGPURenderer/TSL. Use this whenever the user wants building or house windows to stop looking flat, black, like stickers or like plain blue rectangles; asks for interior mapping, parallax windows, fake interiors, a window shader, lit windows at night, curtains or blinds on a facade; mentions three-fenestra; or is building houses, streets, facades or a city in Three.js and windows come up at all, even if they never say "shader" or "interior mapping".
---

# Window mapping for Three.js

Windows are where cheap 3D buildings give themselves away. Modelling a room behind each
one is unaffordable, and real transparency brings sorting and fog problems. The fix games
have used since the late 2000s is a flat, opaque quad whose shader slides a few imaginary
layers sideways as the camera moves, so the eye reads depth that is not there.

The default goal of this skill is deliberately modest: **a nondescript, blurry hint of an
interior, mostly hidden by blinds or curtains.** Not a room. That is what most real
windows look like from the street, it needs no textures, and it never shows a sofa
repeating forty times across a facade.

## Choose the technique

| The user wants | Use | Where |
| --- | --- | --- |
| Windows that read as glass with *something* behind; blinds, curtains, blurry hints | **Layered parallax** (default) | `windowMaterial.tsl.js` |
| Recognisable furnished rooms, shopfronts, a hero building | Room-box interior mapping | `references/three-fenestra.md` (WebGL library) or `references/room-box.md` (add a room to the bundled material) |
| Background on why, or "how do games do this" | | `references/techniques.md` |

If the brief is ambiguous, build the default and show it. It is quick, and people usually
discover that the blurry hint is what they wanted once they see it.

## Workflow

### 1. Look at the project before writing anything

Three facts decide which file to use and how to wire it:

- **Renderer.** This copy is WebGPU only. Use `windowMaterial.tsl.js`. The GLSL twin
  patches `MeshStandardMaterial` through `onBeforeCompile`, which `WebGPURenderer`
  silently ignores, leaving a plain white quad. That file is not in this folder.
  Do not set `forceWebGL`.
- **Three version** (`package.json` or the import map). The GLSL file assumes WebGL2, so
  three r163 or later. Both files were tested on r172 and r186.
- **Scene scale.** All sizes in the material are in metres. If one scene unit is not a
  metre, set `unitsPerMeter` (100 for centimetres). Getting this wrong is the usual cause
  of "the blinds are one giant stripe" or "the slats are noise".

### 2. Copy the material file into the project

Copy the matching file from `assets/` into the project's source tree and import it. Do
not retype or "simplify" it; the maths was verified against real geometry and several
parts (see Gotchas) look removable but are not. Each file is self-contained and depends
only on `three`.

### 3. Give every window a quad whose UVs run 0 to 1

The shader works out the window's size and orientation from its UVs, so the one hard
requirement is that UVs span 0..1 across each window. A `THREE.PlaneGeometry` does.

| Situation | What to do |
| --- | --- |
| Scene is built in code | One `PlaneGeometry(width, height)` mesh per window, all sharing one material. Use an `InstancedMesh` once there are more than a few dozen. |
| Windows exist as boxes or coloured planes | Replace each with a plane of the same size and orientation, facing outward. |
| glTF with window meshes whose UVs point into a texture atlas | Do not reuse those UVs. Compute each window's bounding rectangle and place a fresh plane over it. |
| One big quad should become a whole glazed facade | Keep the quad, set `grid: [cols, rows]`. Each cell becomes a window and the frame width becomes the mullion between them. |
| Windows are merged into one geometry | Bake a per-vertex float attribute `windowId` (same value on a window's four vertices) and pass `idAttribute: true`. Without it every window in the merge looks identical. |

**Recess the quad.** If the wall has a real opening, set the quad 5–10 cm back inside it.
A reveal that catches light and shadow does more for realism than anything in the
shader, and it costs nothing.

**If the wall is a solid box** (common in simple scenes), there is nowhere to recess to.
Sit the quad 1–2 cm proud of the wall so it does not z-fight, and add a sill: a thin box
under each window, a little wider than it and projecting 6–8 cm. The sill's shadow line
does the job the reveal would have done. Flush windows with nothing around them look
painted on however good the material is.

### 4. Create one material and share it

```js
import { createWindowMaterial, setWindowNight } from './windowMaterial.tsl.js';

const windowMaterial = createWindowMaterial({
  dressing: { venetian: 2, roller: 2, curtains: 1, bare: 1 },   // weights; omitted kinds are off
  sheerChance: 0.4,
});
for (const w of windows) {
  const mesh = new THREE.Mesh(new THREE.PlaneGeometry(w.width, w.height), windowMaterial);
  mesh.position.copy(w.position);                          // recessed, or just proud of a solid wall
  mesh.lookAt(w.position.clone().add(w.outwardNormal));    // a PlaneGeometry faces +Z; point that outward
  scene.add(mesh);
}
```

Every window picks its own dressing, colours, blind height and room from a hash of its
world position, so one material gives a varied street. Do not clone the material per
window; that defeats batching and gains nothing. The quad receives shadows like any
standard material; leave `castShadow` off.

The draw is random, so a house with a dozen windows can come out lopsided: three roller
blinds in a row, or every light on one side. Change `seed` until it suits rather than
fighting the weights.

In React Three Fiber, create the material once with `useMemo` and pass it as
`<mesh material={windowMaterial}>`.

### 5. Tune to the brief

Start from the defaults and change few things. The options are documented at the top of
each asset file; these are the ones that matter most.

| To get | Change |
| --- | --- |
| Even less to see inside | `haze: 0.4`–`0.8` (milky glass), lower `ambient`, raise `sheerChance` |
| Blinds everywhere | `dressing: { venetian: 1 }`, or `{ venetian: 1, roller: 1 }` |
| Blinds mostly shut | `dropMin: 0.8` |
| A typical Irish or British street | `dressing: { roller: 2, curtains: 2, venetian: 1, bare: 1 }`, `sheerChance: 0.5` |
| An office block | `dressing: { venetian: 3, bare: 1 }`, `sheerChance: 0`, `panes: [1, 1]`, `coolChance: 0.7` |
| More visible depth | raise `interiorDepth` (3–4 m); the hint slides further per degree |
| Frame already modelled in the mesh | `frameWidth: 0, mullionWidth: 0` |
| Scene has `scene.environment` | `reflection: 0`–`0.3`, since real reflections now come from the environment map |
| A specific photo as the room | `interiorMap: texture`, `interiorMapBlur: 3`–`6`. Any photo works; it is sampled blurred and mirror-tiled |

The interior is emissive, so its brightness does not follow the scene's lights. If the
scene uses unusual light intensities or exposure, adjust `ambient` (day) and `lightLevel`
(night) until a bare window by day is clearly darker than the wall around it. A daytime
room that is as bright as the street is the most common way this effect looks wrong.

### 6. Day and night

```js
setWindowNight(windowMaterial, 1);                    // 0 = day, 1 = night; animate it for dusk
setWindowNight(windowMaterial, 1, { litRatio: 0.4 }); // fewer lights on
```

Lower the sun and ambient light yourself, but not to zero: walls and curtains still need
a little light to read as surfaces rather than cut-outs. A gentle bloom pass suits night
scenes. Live uniforms are on `material.userData.window` (for example `.uWinNight.value`).

### 7. Verify by looking

A shader that fails to compile draws nothing useful and says so only in the browser
console, and parallax cannot be judged from one still. Open `windows.html` through a
server and look from two angles. Drag right and the subject turns with the cursor.
Night is the button, or N. Check:

- No errors, and no black, white or magenta quads.
- Between the two angles, the room behind the glass has shifted noticeably and the
  blinds only slightly. Both shift the same way the camera moved.
- Neighbouring windows differ.
- Bare glass by day is darker than the wall.
- At night, lit rooms glow through closed blinds and curtains, not only through bare glass.
- At a shallow angle the interior fades towards a reflection rather than smearing.

WebGPU only. `renderer._getFallback = null` before `init()`. If `navigator.gpu` is
missing, or the backend is not WebGPU after `init()`, stop. Do not report the effect
as working from one still.

## How it works, briefly

Enough to modify it safely; `references/techniques.md` has the derivation.

Per fragment the shader builds the window's tangent frame from screen-space derivatives
of position and UV. That yields the window's real size in metres and `slope`, the
sideways travel per metre of depth behind the glass. Each layer is then sampled at
`pm + slope * depth`, where `pm` is the fragment's position on the window in metres:

| Layer | Depth | Content |
| --- | --- | --- |
| Frame and glazing bars | 0 | Procedural, lit by the scene |
| Dressing | `dressingDepth`, 8 cm | Venetian, roller, curtains or bare, plus an optional net curtain; lit by the scene |
| Hint, mid plane | 0.38 × `interiorDepth` | Dark masses rising from below |
| Hint, back plane | `interiorDepth`, 2.2 m | Smooth tonal noise with one soft light or dark patch |

The dressing goes to the material's diffuse colour, so sunlit facades get bright blinds.
The room goes to emissive, dimmed by the dressing in front of it and faded out at shallow
angles, where a fake sky reflection takes over. The whole surface is opaque.

To add a dressing style or change the hint, edit the clearly marked block in the asset
file. If the project might ever switch renderer, make the same edit in both files.

## Gotchas

- **Do not make the material transparent.** Opaque is the point; the interior is drawn
  on the surface. Transparency reintroduces the sorting problems this technique avoids.
- **Keep the slat filter.** Venetian slats are box-filtered analytically over the pixel
  footprint. Replacing that with a plain `fract()` stripe, or a simple distance fade,
  brings back diagonal moiré at mid distance. The same applies to any new periodic
  pattern you add: fade or filter it before it reaches two pixels per period.
- **Keep the grazing fade.** Past about 80 degrees the offset is clamped and the layers
  stop being geometrically right. The fade to reflection hides that, and matches real glass.
- **Net curtains are suppressed behind slatted blinds.** Folds seen through slats read as plaid.
- **Seeds come from world position**, quantised to 12.5 cm. Moving a window reshuffles
  it; animated or moving buildings should use `idAttribute` instead. Windows closer than
  12.5 cm to each other on every axis share a look.
- **Instanced meshes are seeded differently in the two files** (instance origin in GLSL,
  instance index in TSL), so the same scene gets a different shuffle after a renderer switch.
- **`grid` mode has no wall between cells.** It makes a curtain wall. For punched windows
  in masonry, use one quad per window.
- **`map` and `roughnessMap` on the returned material are ignored**; the shader writes
  those channels itself. Pass options to `createWindowMaterial` instead.
- **The quad is single-sided.** From indoors it is invisible. If the camera can go inside
  the building, give the interior its own glass.

## What is in this skill

- `windowMaterial.tsl.js`. WebGPURenderer version (TSL, `MeshStandardNodeMaterial`).
- `windows.html`. A small house. One shared material, sills on a solid box, Night or N.
- `references/techniques.md`. The ladder of techniques, lessons from shipped games, the maths.
- `references/three-fenestra.md`. The library route to photographic rooms, and its pitfalls.
- `references/room-box.md`. Verified room-box maths for adding a real room to the bundled material.
