Three.js From Zero · Article s4-07

Hair & Fur Rendering

← threejs-from-zeroS4 · Article 07 Season 4
Article S4-07 · Three.js From Zero

Hair & Fur Rendering

Hair is adversarial to standard rendering. Strands are 0.05mm wide, you have ~100,000 per head, they're translucent, anisotropic, move every frame. Real-time hair is the final boss of character graphics.

The demo uses shell rendering — the cheapest convincing method. Render the underlying mesh N times at increasing offsets along the normal, textured with a hair pattern. Fast, stylized, ships in countless indie games.

loading…

Method 1 — Shell rendering

Draw the mesh many times, pushing each copy outward along the normal by a fraction. Each "shell" is textured with tiny circles (one per hair). Alpha-test kills non-hair pixels. The result is a 3D illusion of hairs poking out.

// Pseudo-code: draw N shells
for (let i = 0; i < SHELLS; i++) {
  const t = i / (SHELLS - 1);
  shellMesh.material.uniforms.uT.value = t;
  renderer.render(shellMesh, camera);
}

Vertex shader pushes along normal:

vPos = position + normal * uT * uHairLength;

Fragment shader samples the hair-density texture, kills fragments outside hair strands, darkens based on depth (bottom = base of hair, darker):

vec2 uvScaled = vUv * uDensity;
float h = hash21(floor(uvScaled));   // hair position per cell
float dist = distance(fract(uvScaled), vec2(0.5));
float hairAlpha = step(dist, 0.3) * step(uT, h);  // kills non-hair + longer-than-strand
if (hairAlpha < 0.5) discard;

vec3 color = baseColor * mix(0.4, 1.0, uT);   // darker at root, lighter at tip

Pros + cons of shells

ProCon
Easy to implementVisible "layer" artifacts at shell boundaries when zoomed
One mesh, N passesDoesn't bend — hairs always point along the surface normal
Works with any geometryAnimation looks rigid
FastNo proper self-shadowing

Method 2 — Fins

Shells handle short fur facing the camera. They look bad at silhouettes (edge on) — no hairs visible. Fins fix it: extrude flat cards outward from silhouette edges, textured with a fur pattern. Combine shells + fins ("Shells & Fins" technique, pioneered by Jak and Daxter).

Method 3 — Kajiya-Kay lighting

Hair is anisotropic — a cylinder reflects in a ring around its tangent axis, not around its normal. Standard lighting fails (highlights in the wrong place). Kajiya-Kay (1989) is the go-to lighting model:

vec3 kajiyaKay(vec3 T, vec3 V, vec3 L, vec3 hairColor, float shininess) {
  // Diffuse: sin between tangent and light (not normal!)
  float sinTL = sqrt(1.0 - pow(dot(T, L), 2.0));
  vec3 diffuse = hairColor * sinTL;

  // Specular: Phong-like around the reflected direction
  vec3 H = normalize(V + L);
  float sinTH = sqrt(1.0 - pow(dot(T, H), 2.0));
  float spec = pow(sinTH, shininess);

  return diffuse + vec3(spec);
}

The key is "T" — hair tangent direction, encoded as either a texture or derived from the hair geometry. For shells, use the surface tangent.

Method 4 — Strand-based hair (AAA)

Each hair is an actual line primitive (or quad billboard). 100,000 strands at 10 segments each = 1M vertices. The Groom workflow: Blender's hair system exports hair as polyline curves, renderer draws them as camera-facing quads with alpha.

Three.js LineSegments + a billboard geometry shader works. For performance: use instancing (each strand = one instance of a base geometry).

Hair simulation

Static hair looks like a wig. Animated hair needs physics:

  • Per-strand spring physics — each strand is a chain, root pinned to skull, gravity pulls, spring resists deformation
  • Verlet integration — stable at large timesteps (S3-05 pattern)
  • Collision with head mesh — hair shouldn't intersect the scalp
  • Wind + movement forces — external forces driving the spring

In TSL compute shaders, you can simulate 10,000 strands in <1ms per frame. We'll cover that in S5 (advanced rendering) when we deal with compute pipelines.

Eyebrows + eyelashes

Different technique: alpha-tested cards placed individually. 5-10 cards cover an eyebrow. For eyelashes, use normal maps — a thin plane with transparency gives the silhouette shape cheaply.

In Three.js today

  • No first-party hair system. Roll your own.
  • Community: three-hair, spite's experiments
  • For production: bake Blender groom to a set of billboards + use strand simulation
  • VR characters (Ready Player Me, Meta avatars) typically use hand-authored hair cards

Common first-time pitfalls

  • Shell boundaries visible at grazing angles. Use more shells near silhouettes (adaptive) or pair with fins.
  • Fur looks painted on. Kajiya-Kay missing — anisotropic highlight is what sells it.
  • Transparent hair sorts wrong. Enable alpha-to-coverage or use premultiplied alpha with proper sort.
  • Root color wrong. Shell near root should be SKIN color (peeking through), not hair. Gradient the base.

Exercises

  1. Implement fins: extrude along silhouette edges (n·v near 0), textured with a hair card. Combine with shells.
  2. Simulated hair strands: 200 Verlet chains rooted to the head mesh, wind + gravity. Stiffness tuning.
  3. Eyelash cards: 2-3 alpha-tested planes per eye, normal-mapped, auto-oriented to face the camera ~horizontally.

What's next

S4-08 — Ocean & water. Gerstner waves, FFT ocean, refraction, foam, caustics. From puddle to open sea.