Builtin Library
readme.md
hair system
Strands grown over a mesh's surface, lit as fibres.
Hair built from cards reads as sheets because it is sheets — a handful of wide quads carrying a hair texture, with a silhouette made of straight edges. A groom instead grows one ribbon per strand out of the mesh it belongs to, so the silhouette is made of strands.
The shading is the other half. A strand is a cylinder, not a patch of surface: it reflects into a cone around its own axis rather than about a normal, so it carries two offset bands of specular where a surface carries one round spot. The second band is the light that went into the strand, reflected off its far wall and came back out, which is why it is broader and warmer than the first.
Using it
Put Hair on an entity that draws a mesh — author it once and it persists with
the scene:
entity(headId).component.add("Hair", {
strands = 1200,
length = 0.09,
regionMin = 0.6, -- only the top of the head
})
Every field is live: edit one in the inspector and the strands regrow from it.
For code that grows hair at runtime — a character spawner, a creature generator — reach for the module directly:
local hair = require("@builtin::systems.hair.hair")
hair.grow(headId, { strands = 1200, length = 0.09, regionMin = 0.6 })
hair.stats(headId) -- strands, vertices, triangles
hair.groom(headId) -- what it was grown with
hair.clear(headId) -- remove the strands and their material
hair.list() -- every entity carrying a groom
hair.budget() -- what one groom may spend
What the knobs do
| field | effect |
|---|---|
strands | How many strands. Up to 8000 on one entity. |
segments | Points along a strand, up to 16. More bends more smoothly. |
length | How long a strand is, as a fraction of the mesh's own size — so a groom reads the same whatever units the mesh was modelled in. |
thickness | Ribbon width at the root, in the same fraction-of-the-mesh terms. |
taper | How much narrower the tip is than the root, in [0, 1]. |
curl | How far a strand winds around its own axis on the way out. |
curlTurns | How many turns that winding makes over the strand's length. |
droop | How hard a strand bends toward the ground as it grows. |
regionMin / regionMax | The band of the mesh's height strands grow from: 0 at the bottom of its bounds, 1 at the top. Raise regionMin to keep hair on a scalp. |
seed | The same seed regrows the same strands. |
rootColor / tipColor | Shade at each end; every strand carries the gradient. |
highlight | Colour of the second, broader band — what makes dark hair read warm. |
shift | How far apart the two bands sit along the strand. |
gloss | How narrow the first band is. |
What it costs
A strand is two triangles per segment, so a groom's triangles are
strands * segments * 2 — 600 strands at 6 segments is 7 200. One groom is
capped at 8000 strands and 16 segments; past that, grow refuses rather than
building a mesh no one asked for. Split a bigger groom across entities.
The strands are ordinary geometry, so they are frustum-culled, shadowed and lit like any other mesh, and cost nothing per frame beyond their triangles. Growing is the expensive part and happens once, when a field changes.
How it renders
Roots are sampled area-weighted across the triangles inside the region, so a groom is as dense over a large triangle as a small one, and each root carries the surface normal it grew from. A strand marches out along that normal, bending toward the ground as it goes, and is written as a ribbon two vertices wide.
Each vertex carries the strand's own direction as its tangent — the axis the fibre highlights are taken around — and its root-to-tip shade as its colour, so one material covers a whole groom.
Both faces of a ribbon stand for the same round strand, so neither is culled and the back one is lit like the front.
Files
hair.package/
Hair.component/ -- the groom, authored and persisted
hair.module/ -- grow / clear / groom / stats / list / budget
The fibre shading model is @builtin::shaders.hair.