Hair & Groom Rendering — Marschner BRDF & Strand Pipeline毛发渲染 — Marschner BRDF 与发丝管线
Why Hair Needs Its Own Rendering Pipeline为何毛发需要专用渲染管线
Hair strands are fundamentally different from every other surface in real-time rendering:
实时渲染中,毛发发丝与其他所有表面有根本性的不同:
- Sub-pixel geometry: A single strand is 40–80 µm wide — far thinner than one pixel on screen. Standard rasterization produces aliased, flickery results. Hair uses a screen-door coverage mask and stochastic techniques.
- 次像素几何体:单根发丝宽度为 40–80 µm——远比屏幕上的一个像素细。标准光栅化产生锯齿状、闪烁的结果。毛发使用屏幕门覆盖遮罩和随机技术。
- Cylindrical BRDF: Flat-surface BRDFs are wrong for cylinders. The Marschner model is specifically derived for circular cross-section fibers.
- 圆柱形 BRDF:平面 BRDF 对圆柱体是错误的。Marschner 模型专门为圆形截面纤维推导。
- Massive overdraw: A single Groom asset can have 100,000+ strands, each rendered as a camera-facing quad or tube. Deep overdraw (10–100× per pixel) makes standard shadow maps too expensive — a dedicated deep shadow map system is required.
- 巨大过绘制:单个 Groom 资产可以有 10 万+根发丝,每根作为面向相机的四边形或管道渲染。深度过绘制(每像素 10–100×)使标准阴影贴图过于昂贵——需要专用的深度阴影贴图系统。
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 使用可见性缓冲区方法: 不是对每个片段进行着色,而是光栅化器首先记录每个像素处最近的哪根发丝("可见性缓冲区")。 每像素多根发丝通过瓦片分类处理——发丝密度高的瓦片获得更深的逐样本解析。
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 使用带有额外艺术控制参数的修改版本。
// 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 Color | Melanin pigment color (absorption σ_a)黑色素色素颜色(吸收 σ_a) | Dark = dark hair. White = albino. Blonde = light yellow. Red = orange-red.深色 = 深发色。白色 = 白化病。金发 = 浅黄色。红色 = 橙红色。 |
| Roughness | Longitudinal 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 资产
- Export hair curves from DCC (Maya, Houdini, XGen) as .abc (Alembic) format
- 从 DCC 工具(Maya、Houdini、XGen)将毛发曲线导出为 .abc(Alembic)格式
- In UE5: drag .abc into Content Browser → Import → select Groom as asset type
- 在 UE5 中:将 .abc 拖入内容浏览器 → 导入 → 选择 Groom 作为资产类型
- In Groom Import Options: set LOD count, interpolation quality, simulation settings
- 在 Groom 导入选项中:设置 LOD 数量、插值质量、模拟设置
- Add a Groom Component to your Skeletal Mesh actor, assign the Groom asset
- 向骨骼网格 Actor 添加 Groom Component,分配 Groom 资产
- Set Binding Asset — connects Groom root UV to the scalp mesh for skinning
- 设置 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 / Setting | Effect效果 |
|---|---|
| r.HairStrands.Enable | 0 = disable all strand rendering (fallback to card-based if available)0 = 禁用所有发丝渲染(如可用则回退到卡片式) |
| r.HairStrands.DeepShadow.Enable | 0 = no deep shadows (cheap but no self-shadowing)0 = 无深度阴影(便宜但无自阴影) |
| r.HairStrands.Voxelization | 0 = no voxel AO (skips voxelization pass)0 = 无体素 AO(跳过体素化通道) |
| r.HairStrands.LOD.Bias | Positive = use lower LOD (fewer strands) globally. Each LOD halves strand count.正值 = 全局使用更低 LOD(更少发丝)。每个 LOD 将发丝数减半。 |
| Groom LODs | In 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.ShadowMask | 1 = use shadow mask for hair (reduces shadow pass overdraw)1 = 为毛发使用阴影遮罩(减少阴影通道过绘制) |
Source Navigation源码导航
- HairStrands/HairStrandsVisibility.h/.cppVisibility buffer, tile classification, coverage rasterization可见性缓冲区、瓦片分类、覆盖率光栅化
- HairStrands/HairStrandsRendering.h/.cppMain hair rendering orchestrator: schedule all passes主毛发渲染协调器:调度所有通道
- HairStrands/HairStrandsDeepShadow.h/.cppDeep shadow map generation and lookup深度阴影贴图生成和查找
- HairStrands/HairStrandsVoxelization.h/.cppVoxel grid generation for AO and occlusion用于 AO 和遮蔽的体素网格生成
- HairStrands/HairStrandsTransmittance.h/.cppPer-strand transmittance: shadow + environment逐发丝透射率:阴影 + 环境
- HairStrands/HairStrandsEnvironment.h/.cppSky light and environment map hair integration天空光和环境贴图毛发集成
- Shaders/Private/HairBsdf.ushMarschner R/TT/TRT lobe implementation, LUT tablesMarschner R/TT/TRT 叶瓣实现,LUT 表
- HairStrands/HairStrandsLUT.h/.cppPrecomputed LUT for longitudinal/azimuthal integrals纵向/方位积分的预计算 LUT
- HairStrands/HairStrandsTile.h/.cppTile classification: sparse vs dense hair dispatch瓦片分类:稀疏与密集毛发调度