# Nearby short grass

Copy `grass-detail.js` into `dist/`. It has no imports and adds no downloaded assets. It reads the existing `assets/environment/terrain-surface-mask.png` and matching JSON once.

```js
import {createGrassDetail} from './grass-detail.js?v=5';
const grassDetail = await createGrassDetail({
  THREE, scene, renderer,
  height: terrainHeight,
  buildingAt,
  surfaceHeight: architecture.surfaceHeight
});
// Once per rendered frame; time is seconds:
grassDetail.update(player.p, elapsed);
```

Call `grassDetail.setEnabled(false)` for an optional performance setting; `dispose()` releases its owned geometry/material and removes the mesh. Pass the measured terrain callback, not `groundAt`, because the latter can return roofs or elevated walkable structures.

## Behavior

A single instanced mesh holds at most 2,300 tufts, each with twelve original tapered blade triangles. Individual blades are 7–13 mm wide and 4.2–12 cm tall before distance fade. Tuft roots cover a small 62 cm patch and follow the local measured terrain tangent. Maximum geometry is 27,600 triangles, with roughly 172 KiB of instance storage and a 2.5 MiB compact CPU mask. No wind textures, alpha blending, transparent cards or cast shadows are used.

The deterministic placement grid is stable in world coordinates. It refreshes only after at least three meters of movement. The pool extends 30 m; blades contract smoothly into their roots from 19 to 26 m so boundary refreshes remain outside the visible range. Grass hides when the observer is more than 16 m above the terrain or below it. Wind moves blade tips by up to roughly 5 mm. A small transmission term uses the scene's first directional light.

## Placement evidence

The existing actual-data mask defines R as mapped grass/meadow/scrub/wood, G as mapped hard surfaces/road corridors and B as rock. The mask is attributed to OpenStreetMap contributors under ODbL in its existing metadata and project credits. This module adds no new land-cover assumptions outside that mask.

Grass requires R >= 210, G <= 12 and B <= 12 throughout a conservative 3×3-pixel neighborhood (pixels are approximately 1.12×1.11 m). Additional building checks cover the center and tuft corners. Passing `architecture.surfaceHeight` additionally rejects actual rendered paving. Slopes steeper than a 0.62 rise/run and terrain outside the expected island height range are rejected. Roots embed 2 mm into the measured tangent plane; terrain and collision meshes remain unchanged.

Raster placement is conservative, not a surveyed inventory of individual plants. No tufts are placed on unknown cover. Mapped scrub/wood grass areas can receive the same short ground detail where the building/paving/slope checks permit it.

## Validation

`GRASS-DETAIL-VALIDATION.json` uses the actual 2048×1280 mask, recovered OSM building data, terrain triangle sampler and rendered paving sampler. All populated points passed mask, building, paving, slope, height and finite-buffer checks. Root sample error was zero. Static and one-meter updates did not rebuild; flight and disabled states hid the mesh.

- El Morro lawn: 1,782 tufts, sample refresh 8.84 ms.
- El Morro south lawn: 2,074 tufts, sample refresh 4.19 ms.
- El Morro entrance: 1,131 tufts, sample refresh 3.07 ms.
- Plaza de Armas, cemetery and open water: zero tufts.

These are local Node generation timings, not browser frame-rate measurements. Node syntax and shader-injection checks passed; WebGL compilation and final appearance require integration QA. No Site checkout or publication was changed.
