Three.js From Zero · Article s4-07
Hair & Fur Rendering
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.
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
| Pro | Con |
|---|---|
| Easy to implement | Visible "layer" artifacts at shell boundaries when zoomed |
| One mesh, N passes | Doesn't bend — hairs always point along the surface normal |
| Works with any geometry | Animation looks rigid |
| Fast | No 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
- Implement fins: extrude along silhouette edges (
n·vnear 0), textured with a hair card. Combine with shells. - Simulated hair strands: 200 Verlet chains rooted to the head mesh, wind + gravity. Stiffness tuning.
- 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.