---
name: procedural-pbr
description: >
  Bake tileable PBR maps in one Three.js file: a CPU height field per
  material, albedo written with it, normals and occlusion derived from the
  height, ORM packing, and vertex displacement. Use when adding or changing
  procedural snow, grass, sand, rock, dirt, leaves, mud, or another baked
  terrain material on WebGPU. The reference file is pbr.html beside this
  skill. If they disagree, the HTML wins.
---

# Procedural PBR maps

One HTML file. No bundler, no npm, no photo tiles. Three.js 0.186.1 from a CDN via an import map (`three.webgpu.js`, `three.tsl.js`, addons). `WebGPURenderer` only. Set `renderer._getFallback = null`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback.

The bake is CPU, in a worker (`<script id="baker" type="javascript/worker">`). The meshes are `MeshPhysicalNodeMaterial`. Maps are ordinary data textures. Do not replace the bake with a runtime noise graph unless the user asks. The point of the file is the baked maps.

`SIZE` is 512. The worker sets `N` from the posted size and `M = N - 1` (wrapping mask, so `N` stays a power of two).

Open it through a server. `file://` will not load the ES modules.

## Maps

Each generator fills:

- `H` float height, 0 to 1-ish
- `CR`, `CG`, `CB` albedo
- `RO` roughness, `SP` specular
- `FA` facet flag, used by snow

`finalize(hs, aoK, name)` then writes:

- albedo RGBA
- tangent-space normal from a central difference of `H`, except where `FA` is set (a small random tilt, colour unchanged)
- ORM: R occlusion, G roughness, B metalness (always 0), A specular
- displacement, `DN` by `DN`, box-filtered from `H`

`hs` is the height scale passed per material (snow 0.048, grass 0.115, sand 0.045, rock 0.105, dirt 0.090, leaves 0.100, mud 0.070). It scales both the normal and `dispRange`.

Occlusion samples eight directions at radii 2, 5, 11 and 23. `aoMap` reads `uv2`. Copy `uv` onto `uv2` on the base geometry before cloning, or AO samples texel 0.

## Scale

Blade, twig and leaf sizes were authored at 2048.

```javascript
const scale = N / 2048;
```

Counts go through `scale * scale`. Lengths and widths go through `scale`. Do not change the divisor when `SIZE` changes. At 512, `scale` is 0.25 and the picture stays the same with fewer texels. A divisor of 512 draws a few huge blades.

`DN` is 512. `step = N / DN` must be an integer greater than or equal to 1. 512, 1024 and 2048 are valid. A size under 512 needs `DN` lowered to match.

## Materials

Order, left to right: snow, grass, sand, rock, dirt, leaves, mud.

Snow and grass and leaves are `MeshPhysicalNodeMaterial` with sheen. Mud has a stronger clearcoat (the wet film). Snow's `nmul` is 0.40, mud's is 0.85, the others are 1. The normal slider multiplies `nmul`.

Grass and leaves are stamped with a height test after a noise undercoat. A new texel wins only when its height is greater. Do not replace that stamp with another `fbm` call.

Sand is a domain-warped sine. Mud flattens hollows into a water surface and drops roughness there.

## Meshes

One sphere and one cube per material, geometry cloned, material shared. UVs on the sphere are scaled (4 by 2) so a tile is about 1.6 world units.

`displace()` pushes the cloned positions along the normal from the height map, around 0.5. It caches the base positions on `geometry.userData.base`. The relief slider calls it again. Cubes use 0.4 times the sphere amount so edges do not split.

Three workers at a time (`bakeAll(3)`). Inline `Function` fallback if `Worker` from a blob is blocked. The load overlay stays until every material returns.

## If you add a material

1. A `genX` that fills `H` and calls `put()` for colour, roughness and specular.
2. A `case` in the worker with an `hs` and an `aoK`.
3. A recipe `{ name, x, nmul }` and a `MeshPhysicalNodeMaterial` branch if it needs clearcoat or sheen.
4. Leave `finalize()` as the only place that writes normals and occlusion.

Do not author a normal map by hand. The relief slider will move the vertices and the lighting will disagree.
