Why Hair Needs Its Own Rendering Pipeline为何毛发需要专用渲染管线

Hair strands are fundamentally different from every other surface in real-time rendering:

实时渲染中,毛发发丝与其他所有表面有根本性的不同:

Strand Data Structures发丝数据结构

Hair strands in UE5 are stored in a highly compact GPU buffer format. Each strand is a polyline of control points; each control point stores position and a radius. The data is organized for fast GPU traversal during rasterization and voxelization.

UE5 中的毛发发丝以高度紧凑的 GPU 缓冲区格式存储。 每根发丝是控制点的折线;每个控制点存储位置和半径。 数据组织以便在光栅化和体素化期间快速 GPU 遍历。

// FHairStrandsData — GPU buffer layout (HairStrandsData.h)

// Per control point (FPackedHairVertex, 16 bytes)
struct FPackedHairVertex
{
    uint32 CoordX    : 21; // position X (quantized to hair root space)
    uint32 CoordY    : 21;
    uint32 CoordZ    : 21;
    uint32 Radius    :  8; // strand radius at this point (quantized)
    uint32 UCoord    :  8; // UV along strand length [0=root, 255=tip]
};

// Per-strand metadata
struct FHairStrandInfo
{
    uint16 PointOffset;  // index of first point in PointBuffer
    uint16 PointCount;   // number of points in this strand
    uint8  RootUV_U;     // scalp UV (for hair color lookup)
    uint8  RootUV_V;
};

// The full Groom buffers on GPU:
// - StrandPointBuffer: FPackedHairVertex × TotalPoints
// - StrandInfoBuffer:  FHairStrandInfo × TotalStrands
// - TangentBuffer:     float3 × TotalPoints (precomputed tangent = fiber direction)
// - AttributeBuffer:   per-strand: roughness, width scale, color from root texture

Visibility & Rasterization — The Coverage Problem可见性与光栅化 — 覆盖率问题

Because strands are sub-pixel, UE5 uses a visibility buffer approach: instead of shading each fragment, the rasterizer first records which strand is closest at each pixel (the "visibility buffer"). Multiple strands per pixel are handled by tile classification — tiles with high strand density get a deeper per-sample resolve.

由于发丝是次像素的,UE5 使用可见性缓冲区方法: 不是对每个片段进行着色,而是光栅化器首先记录每个像素处最近的哪根发丝("可见性缓冲区")。 每像素多根发丝通过瓦片分类处理——发丝密度高的瓦片获得更深的逐样本解析。

Hair Rasterization Pipeline (HairStrandsVisibility.cpp): 1. Bounding Box Cull (CPU): → Remove Groom components outside frustum 2. Curve Rasterization (GPU): → Each strand segment → screen-space AABB → rasterize as quads → Sub-pixel width: use a stochastic coverage mask (one bit per 8×8 tile, stochastic per-frame rotation) → Write: StrandID + depth into HairVisibilityBuffer 3. Tile Classification: → Classify tiles by strand density: Tile type 0: no hair → skip Tile type 1: sparse hair → 1 strand/pixel, fast path Tile type 2: dense hair → PPLL (Per-Pixel Linked List, slow path) 4. Shading Dispatch: → For each visible strand pixel: - Read strand tangent from TangentBuffer - Evaluate Marschner BRDF for all lights - Accumulate in SceneColor (blended over background)

Marschner BRDF Deep DiveMarschner BRDF 深度解析

The Marschner model (2003) decomposes fiber scattering into three physical interaction paths. UE5 uses a modified version with additional parameters for artistic control.

Marschner 模型(2003)将纤维散射分解为三种物理交互路径。 UE5 使用带有额外艺术控制参数的修改版本。

Cross-section of a hair fiber — the three light paths: Light → Light → Light → ↓ ↓ ↓ ╔══════╗ ╔══════╗ ╔══════╗ ║ ║ ║ IOR ║ ║ ║ R → ╚══════╝ TT: → → ╚══════╝ → → TRT: → ╚══╗ ╝ ← (reflect off (refract in, (refract in, cuticle) exit other side) internal reflect, exit same side) R lobe: Sharp specular, shifted toward root (~2°) Color: nearly white (reflects before pigment) Width: narrow (β_M ≈ 0.03) TT lobe: Soft, forward-scattered (halo when backlit) Color: tinted by melanin (pigment inside cortex) Width: medium (β_M ≈ 0.1) TRT lobe: Wide, warm highlight — the main colored sheen Color: tinted 2× by melanin (enters and exits through pigment) Width: broad (β_N ≈ 0.3) → DOMINANT lobe for brown/black hair in direct light
// HairBsdf.ush — simplified Marschner evaluation

// Longitudinal scattering function M(β, sinθ_i, sinθ_r)
// Gaussian over the difference angle θ_r - θ_i - α
float M_lobe(float Beta, float SinThetaI, float SinThetaR, float Alpha)
{
    float DeltaTheta = SinThetaR - SinThetaI - Alpha;
    return exp(-0.5f * DeltaTheta*DeltaTheta / (Beta*Beta))
         / (Beta * sqrt(2.0f * PI));
}

// Azimuthal scattering function N(β_N, φ)
// Different for each lobe (R: single, TT: two, TRT: broad forward)
float N_lobe(float BetaN, float Phi, float p /*lobe order: 1,2,3*/) { ... }

// Full hair BRDF (per-lobe weights depend on Fresnel)
float3 HairBRDF(float3 L, float3 V, float3 Tangent, FHairMaterial M)
{
    float SinThetaI = dot(Tangent, L);   // longitudinal (along fiber)
    float SinThetaR = dot(Tangent, V);
    float CosThetaD = cos(asin(SinThetaR) - asin(SinThetaI)) * 0.5f;

    // Per-lobe Fresnel (controls energy partition between R, TT, TRT)
    float F_R   = FresnelDielectric(CosThetaD, 1.55f);  // hair IOR=1.55
    float F_TT  = (1-F_R)*(1-F_R);
    float F_TRT = (1-F_R)*(1-F_R)*F_R;

    // Melanin absorption (determines hair color)
    // Sigma_a: per-channel absorption coefficient (higher = darker hair)
    float3 T = exp(-M.AbsorptionColor * (2.0f / CosThetaD));

    float3 Result =
        F_R   * M_lobe(M.LongRoughness, SinThetaI, SinThetaR, M.Alpha_R)   // R
              * N_lobe(M.AziRoughness_N, Phi_R, 1)
      + F_TT  * T * M_lobe(M.LongRoughness*0.5f, SinThetaI, SinThetaR, M.Alpha_TT)  // TT
              * N_lobe(M.AziRoughness_N*0.5f, Phi_TT, 2)
      + F_TRT * T*T * M_lobe(M.LongRoughness*2, SinThetaI, SinThetaR, M.Alpha_TRT) // TRT
              * N_lobe(M.AziRoughness_N*2, Phi_TRT, 3);

    return Result / (max(cos(asin(SinThetaI)), 0.001f) * max(cos(asin(SinThetaR)), 0.001f));
}

Key Material Parameters for Hair Shading ModelHair 着色模型的关键材质参数

Pin引脚Meaning含义Effect效果
Base ColorMelanin pigment color (absorption σ_a)黑色素色素颜色(吸收 σ_a)Dark = dark hair. White = albino. Blonde = light yellow. Red = orange-red.深色 = 深发色。白色 = 白化病。金发 = 浅黄色。红色 = 橙红色。
RoughnessLongitudinal roughness β_M (along fiber axis)纵向粗糙度 β_M(沿纤维轴)Low = sharp needle-like highlight. High = broad, fuzzy highlight.低 = 尖锐针状高光。高 = 宽而模糊的高光。
Scatter (CustomData.x)Azimuthal roughness β_N (around fiber)方位粗糙度 β_N(围绕纤维)Controls how wide the TRT lobe spreads around the fiber circumference.控制 TRT 叶瓣在纤维周长周围的宽度。
Backlit (CustomData.y)TT lobe weight — how much back-lit glowTT 叶瓣权重——背光光晕量High = strong rim/halo when backlit. 0.5 is default.高 = 背光时强烈边缘/光晕。0.5 为默认值。

Deep Shadow Maps — Transmittance Through Hair深度阴影贴图 — 穿越毛发的透射率

Standard shadow maps produce binary results: lit or not. Hair has thousands of overlapping semi-transparent strands — a binary shadow would make all hair underneath completely black. The Deep Shadow Map (DSM) system stores a transmittance function along each shadow ray, recording how much light survives at each depth. This allows the lighting shader to compute how much light reaches each strand through the hair above it.

标准阴影贴图产生二值结果:有光或无光。毛发有数千根重叠的半透明发丝—— 二值阴影会使下方所有毛发完全变黑。深度阴影贴图(DSM)系统 沿每条阴影光线存储透射率函数,记录每个深度处有多少光线存活。 这使光照 Shader 能够计算有多少光线穿过上方的毛发到达每根发丝。

// HairStrandsDeepShadow.cpp — Deep Shadow Map concept

// Instead of binary depth comparison, store transmittance as a function:
// T(d) = exp(-σ × ∫₀ᵈ opacity(s) ds)

// Per texel in the DSM, store a 4-layer quantized transmittance curve:
struct FHairDeepShadowTexel
{
    float4 Depth;         // depth of each of 4 opacity layers
    float4 Transmittance; // transmittance at each layer boundary
};

// During hair shadow pass (AddHairDeepShadowPass):
// For each strand in shadow frustum:
//   Scatter strand opacity into the DSM at its depth layer
//   Each strand contributes: opacity = 1 - exp(-ExtinctionCoeff × StrandRadius)

// During hair lighting:
// ShadowTransmittance = LookupDeepShadow(lightSpacePos, depth)
// Returns: [0,1] how much light survives at this depth
// → Used to attenuate the light contribution for this strand

Voxelization — Self-Shadowing & AO体素化 — 自阴影与 AO

For ambient occlusion and environment occlusion (sky light blocked by hair), UE5 voxelizes the hair volume. The hair strands are rasterized into a low-resolution 3D voxel grid (HairStrandsVoxelization.cpp), storing per-voxel opacity. Indirect and environment lighting uses this voxel representation to determine how occluded each strand is.

对于环境光遮蔽环境遮蔽(被毛发阻挡的天空光), UE5 将毛发体积进行体素化。发丝被光栅化到低分辨率 3D 体素网格(HairStrandsVoxelization.cpp)中, 存储逐体素不透明度。间接和环境光照使用此体素表示来确定每根发丝的遮挡程度。

Transmittance & Environment Lighting透射率与环境光照

Hair receives environment lighting differently from opaque surfaces. Because light can pass through hair, the sky light contribution is modulated by the voxel-based transmittance. For direct lighting, HairStrandsTransmittance.cpp computes per-strand directional transmittance using either the deep shadow map or voxel data, then applies it to the Marschner BRDF evaluation.

毛发接收环境光照的方式与不透明表面不同。由于光线可以穿过毛发, 天空光贡献由基于体素的透射率调制。 对于直接光照,HairStrandsTransmittance.cpp 使用深度阴影贴图或体素数据 计算逐发丝方向透射率,然后将其应用于 Marschner BRDF 计算。

Groom Asset Setup & Material ConfigurationGroom 资产配置与材质设置

Importing Groom Assets导入 Groom 资产

  1. Export hair curves from DCC (Maya, Houdini, XGen) as .abc (Alembic) format
  2. 从 DCC 工具(Maya、Houdini、XGen)将毛发曲线导出为 .abc(Alembic)格式
  3. In UE5: drag .abc into Content Browser → Import → select Groom as asset type
  4. 在 UE5 中:将 .abc 拖入内容浏览器 → 导入 → 选择 Groom 作为资产类型
  5. In Groom Import Options: set LOD count, interpolation quality, simulation settings
  6. 在 Groom 导入选项中:设置 LOD 数量、插值质量、模拟设置
  7. Add a Groom Component to your Skeletal Mesh actor, assign the Groom asset
  8. 向骨骼网格 Actor 添加 Groom Component,分配 Groom 资产
  9. Set Binding Asset — connects Groom root UV to the scalp mesh for skinning
  10. 设置 Binding Asset——将 Groom 根 UV 连接到头皮网格以进行蒙皮

Creating a Hair Material创建毛发材质

// Hair material setup in Material Editor:

// 1. Shading Model: Hair
//    (Normal pin disappears — replaced by tangent-based BRDF)

// 2. Base Color = hair color (melanin pigment)
//    Sample a root-to-tip color texture for natural variation:
float4 HairColor = Texture2DSample(HairColorAtlas, HairAtlasSampler,
    float2(V_StrandCoord, RootUV.y));  // U=tip position, V=scalp location

// 3. Roughness: longitudinal roughness (0.05–0.15 for most hair)
float Roughness = 0.08;

// 4. Scatter (CustomData.x): azimuthal roughness
float Scatter = 0.7;  // high = wide TRT lobe = more sheen

// 5. Backlit (CustomData.y): TT lobe weight
float Backlit = 0.5;  // default. Increase for backlit glamour look.

// Tangent is automatically read from strand geometry — do NOT connect Normal

Performance Tuning性能调优

CVar / SettingEffect效果
r.HairStrands.Enable0 = disable all strand rendering (fallback to card-based if available)0 = 禁用所有发丝渲染(如可用则回退到卡片式)
r.HairStrands.DeepShadow.Enable0 = no deep shadows (cheap but no self-shadowing)0 = 无深度阴影(便宜但无自阴影)
r.HairStrands.Voxelization0 = no voxel AO (skips voxelization pass)0 = 无体素 AO(跳过体素化通道)
r.HairStrands.LOD.BiasPositive = use lower LOD (fewer strands) globally. Each LOD halves strand count.正值 = 全局使用更低 LOD(更少发丝)。每个 LOD 将发丝数减半。
Groom LODsIn Groom asset settings: add LODs with strand count reduction. LOD 0=100%, LOD1=50%, LOD2=25%在 Groom 资产设置中:添加发丝数递减的 LOD。LOD0=100%,LOD1=50%,LOD2=25%
r.HairStrands.ShadowMask1 = use shadow mask for hair (reduces shadow pass overdraw)1 = 为毛发使用阴影遮罩(减少阴影通道过绘制)
Practical tip — Hair performance budget: A typical hero character hair budget on PC (60fps target): 100–200K strands at LOD0, 2ms GPU budget for hair rendering (visibility + deep shadow + lighting). On console: 50–100K strands, 1.5ms. Always add LOD levels that kick in at 2m+ from camera. Use r.HairStrands.DeepShadow.MaxSampleCount to limit shadow resolution. 实用技巧 — 毛发性能预算:PC 上典型英雄角色毛发预算(60fps 目标):LOD0 100–200K 根发丝,毛发渲染(可见性 + 深度阴影 + 光照)GPU 预算 2ms。主机:50–100K 根发丝,1.5ms。始终添加在相机 2m+ 处触发的 LOD 级别。使用 r.HairStrands.DeepShadow.MaxSampleCount 限制阴影分辨率。

Source Navigation源码导航

Unreal Engine 5.6 · Engine/Source/Runtime/Renderer/Private/HairStrands/ · Engine/Shaders/Private/
Source analysis · For learning purposes