Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

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

fieldeffect
strandsHow many strands. Up to 8000 on one entity.
segmentsPoints along a strand, up to 16. More bends more smoothly.
lengthHow 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.
thicknessRibbon width at the root, in the same fraction-of-the-mesh terms.
taperHow much narrower the tip is than the root, in [0, 1].
curlHow far a strand winds around its own axis on the way out.
curlTurnsHow many turns that winding makes over the strand's length.
droopHow hard a strand bends toward the ground as it grows.
regionMin / regionMaxThe 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.
seedThe same seed regrows the same strands.
rootColor / tipColorShade at each end; every strand carries the gradient.
highlightColour of the second, broader band — what makes dark hair read warm.
shiftHow far apart the two bands sit along the strand.
glossHow 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.