---
name: procedural-rock
description: >
  Sculpt a procedural rock in one Three.js file: a welded icosphere,
  ridged cracks, planar cuts, a per-vertex cavity attribute, and a shared
  TSL material for crevices, worn edges, moss and dust. Use when building
  or changing a hero rock, pebbles, or the dry grass around them on WebGPU.
  The reference file is rock.html beside this skill. If they disagree, the
  HTML wins.
---

# Procedural rock

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

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

## Mesh

`icosphere(detail)` welds `IcosahedronGeometry` vertices (position key rounded to 1e-4) before displacement. Do not skip the weld. Unwelded vertices facet the normals.

`buildRock(seed, detail, pebble)`:

1. Anisotropic scale on x, y, z. The rock is never a ball.
2. Large FBM for the silhouette, a tighter FBM for medium forms, ridged noise for cracks (`pow(ridged, 4)` subtracted), a little high-frequency grain.
3. Random planar cuts. Hero: 5 to 8. Pebble: 3. A vertex past the plane is pulled back along the plane normal by `t * k`.
4. Soft-squash the underside (lowest 16% of the height, compressed to 0.3) so it sits in the ground.
5. `computeVertexNormals()`, then `bakeCavity()`.

Subdivision: hero detail 5, pebble detail 3. The study asked for 64 and 10. An icosphere multiplies faces by four per level (`20 * 4^detail`). Those numbers do not finish. Do not raise detail past 6 on the hero.

## Cavity

`bakeCavity` writes a float attribute named `cavity`.

- For each vertex, average neighbour positions.
- `dot(average - position, normal) / meanEdge * 3`, clamped to [-1, 1].
- Positive: concave crevice. Negative: convex edge.
- One Laplacian smooth: `0.4 * cav + 0.6 * neighbour average`.

The material reads `attribute('cavity')`. Renaming the attribute without renaming the node breaks the crevice darkening.

## Material

One `MeshStandardNodeMaterial` from `rockMaterial()`, shared by the hero and every pebble. Do not clone a new material per rock.

- Body: two greys plus a warm patch from `mx_noise_float(positionWorld)`, then fine grain.
- Crevices: `smoothstep` on positive cavity, mixed toward a dark brown.
- Worn edges: `smoothstep` on negated cavity, mixed toward a light stone.
- Moss: up-facing `normalWorld.y`, patchy, multiplied down on convex edges.
- Dust: low `positionWorld.y`.
- Roughness: high, slightly lower on convex edges.
- `metalness` 0.

Grass is a separate `MeshStandardNodeMaterial`. `positionNode` bends by `positionLocal.y` squared, phased by `instanceIndex`. Tips bleach from `positionLocal.y`.

## Orbit

Match `THREE.Spherical`:

```
x = target.x + dist * sin(phi) * sin(theta)
y = target.y + dist * cos(phi)
z = target.z + dist * sin(phi) * cos(theta)
```

Pointer move right decreases theta (`theta -= dx`). That turns the rock with the cursor. Swapping sin and cos on x and z reverses left and right. Phi is clamped. Idle drift is `theta += 0.0018` after 3 seconds.

## Dressing

Hero scale 1.5, dropped so `boundingBox.min.y` sits just under the ground. 14 pebbles in an annulus. Grass: `ConeGeometry` translated so the base is at y 0, `InstancedMesh`, clumps from two random offsets (triangular falloff). Both cast and receive shadows.

`New rock` calls `buildScene` with a new seed and disposes the previous group geometries.
