---
name: procedural-models
description: >
  Stairs and a modular truss in one Three.js file. Parameters rebuild the
  mesh, wear is a TSL uniform, and the page is WebGPU only. Use when
  changing the models, the camera or the panel. The reference file is
  models.html beside this skill. If a handoff disagrees, the HTML wins.
---

# Stairs and truss

One HTML file. No textures, no GLB, no separate module. Three.js 0.186.1. `three` and `three/webgpu` both point at `three.webgpu.js`. `three/tsl` at `three.tsl.js`. `three/addons/` stays, because the page imports `three/addons/tsl/display/SSAONode.js`. `?noao` skips that import.

`WebGPURenderer` only. `antialias` true. Set `renderer._getFallback = null` before `init()`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop. No WebGL fallback and no `forceWebGL`. Do not put the `?webgl` retry back.

Open it through a server. `file://` will not load the ES modules. `php -S localhost:8000` or `npx serve`.

## Models

Units are metres. Y is up. Both models sit on `y = 0`.

`stairs` climbs along +X. The foot is at the origin. Width is centred on Z = 0. `truss` is centred on the origin. Width runs along X, depth along Z, height along Y.

`detail` is `high`, `medium` or `low`. High keeps bolts, grating and bevels. Medium drops fasteners. Low keeps the silhouette. A slider sets `dirtyBuild`, and the next frame rebuilds the geometry. Do not swap in a loaded mesh.

Wear is a vertex attribute, 1 on the bevel strips and 0 elsewhere, driven by one TSL uniform.

The panel prints a `createModel` call. That string is the recipe. This page does not import a library file.

## Camera

Elevation is up from the horizon. `x = dist * cos(el) * sin(az)`, `y = dist * sin(el)`, `z = dist * cos(el) * cos(az)`. Drag right does `az -= dx`. When spin is on, `goal.az` increases. Do not swap sin and cos on x and z. If you flip the drag, flip the spin too.

## Frame

`html` and `body` are `height: 100%`, so the page fills a 16:9 iframe. Under 880px the stage row is `minmax(0, 56%)`. Do not put a 320px minimum back on that row.
