GPU Gems 2 · Chapter 23. Hair Animation and Rendering in the Nalu Demo

用 control hairs、约束动力学、Marschner 反射与 opacity shadow maps,把长发的动画、密度和实时 shading 拆成可预算的数据流。

学习目标

  • 能解释 Hair Animation and Rendering in the Nalu Demo 如何用少量 control hairs 承担动力学,再通过 Bezier、tessellation 和插值生成可渲染密度
  • 能实现并诊断 Verlet 粒子、长度约束、球体碰撞与 barycentric interpolation 的数据流,区分稳定性问题和密度生成问题
  • 能调节 HairRenderingLab 的 hair density、constraint iterations、shadow slices 与 reflectance maps,比较发丝质量、透射阴影和 GPU 预算

先把“真实头发”拆成四条管线

GPU Gems 2 Chapter 23 的 Hair Animation and Rendering in the Nalu Demo 不是单一的 hair shader 教程。Nalu 的长发需要同时解决四件事:少量控制结构如何产生可信运动,控制发丝如何变成足够密的几何,单根发丝如何产生方向相关的散射,以及大量发丝如何形成柔和的自阴影。任何一条管线失败,最终画面都会暴露出“塑料感”、穿模、稀疏或闪烁。

先打开实验,保持 control hairs,逐步提高 constraint iterations;再切到 dense interpolation,比较密度放大后稳定性是否仍然可信。最后改变 shadow slices 和 reflectance maps,观察阴影透射与查表带宽的取舍。实验强调一个边界:可见发丝数量可以很多,但昂贵的 dynamics、collision 和 authoring 不能盲目按最终数量复制。

Nalu hair rendering 实验

调整 control/dense hair、约束迭代、shadow slices 和反射查表打包,观察稳定性、密度与 GPU 预算。

Nalu hair lab:control hairs / 8 shadow slicesscalp roots → Bezier strands · iterations 4frame budgetrender strands6constraint passes4lookup textures2light transmittance0.673packed M/N channels密度、稳定性、阴影精度和 lookup 数量需要一起做目标 GPU 的预算

保持 control hairs 可调试,再用 packed lookup maps 和 opacity shadow map 控制实时成本。

本章主题 Hair Animation and Rendering in the Nalu Demo 的核心是分层:控制结构负责可解释的状态,生成阶段负责空间密度,shading 负责方向性散射,shadowing 负责沿光线的透射损失。

Hair Animation and Rendering:控制结构到最终发丝scalp layoutnormals → rootscontrol hairsauthoring handlesdynamicsVerlet particleslength constraintscollisions + fixesgeometry + shadingBezier / tessellatebarycentric densityreflectance + shadows只让少量 control hairs 承担昂贵动力学,再把密度与外观交给并行生成

1. 从 scalp layout 生成控制发丝

控制结构从专用 scalp mesh 生长:每个根顶点沿 scalp normal 生成一根 control hair,初始长度和方向描述发型,而不是直接把最终数千根发丝交给 dynamics。这样既保留了发型布局,又让动画系统只处理可控规模的粒子链。

控制结构不是纯物理系统。程序动力学会让运动自然,却也可能让发型失去导演需要的形状;工程上应该提供 root pin、局部阻尼、外力、碰撞层和手工修正通道。所谓“更真实”的自动系统,如果没有可回放和可修正的状态,仍然难以用于镜头制作。

2. dynamics:Verlet、长度约束与碰撞

在控制发丝上,每个 vertex 作为 particle。Verlet 的基本预测可写成

xt+1=xt+(xtxt1)+atΔt2. x_{t+1}=x_t+(x_t-x_{t-1})+a_t\Delta t^2.

预测之后,逐段修正相邻粒子的距离,使 xi+1xi\lVert x_{i+1}-x_i\rVert 接近目标 segment length。一次修正会影响邻居,因此通常需要多轮迭代;迭代次数越少越便宜,但越容易出现拉伸和碰撞穿透,迭代次数越多则会增加 CPU/GPU 工作与延迟。

dynamics:粒子、约束与碰撞的稳定循环p0p1p2p3p4p5collision sphereframe loopVerlet predictlength iterationssphere / pearl testrender only after converge约束要迭代:拉动一个粒子会让相邻 segment 失效,单次修正并不闭合整条链

碰撞可以从简单开始:用 sphere rig 代表头部和身体,对粒子做 point-versus-sphere 投影。如果发丝 segment 远大于 sphere,只有检查粒子端点会留下中段穿透;可以在粒子上增加 pearl-like collision samples,或者对 segment 做更完整的测试,但要以稳定性和预算为代价。碰撞 response 还应该设置 friction、restitution 和最大 correction,避免一帧中反复弹飞。

3. geometry generation:Bezier、tessellation 与密度

控制发丝每帧都会变化,因此最终 render hair 需要重建。一个可解释的数据流是:从控制粒子估计 tangent,转为 Bezier curve,沿曲线插入顶点进行 tessellation,再用 scalp triangle 的拓扑在三根平滑 control hairs 之间插值,生成 dense hair。动态 vertex buffer 可以承接这批每帧变化的顶点。

对于 scalp triangle 顶点 A、B、C,内部发丝 Y 可以写成

Y=bAA+bBB+bCC,bA+bB+bC=1,bi0. Y=b_AA+b_BB+b_CC,\qquad b_A+b_B+b_C=1,\qquad b_i\geq 0.

随机采样 barycentric coefficients 时必须保证样本落在三角形内部;同一个 interpolated hair 应与 control hair 拥有相同的 vertex count,方便逐点混合和动态更新。随机性应使用可回放 seed,否则美术、测试和网络重现都无法稳定比较。

barycentric interpolation:从三根 control hairs 填充发量A control hairB control hairC control hairY = A·bA + B·bB + C·bCdensity rulebA + bB + bC = 1sample inside trianglesame vertex count per hairinterpolation ≠ dynamics插值扩展空间密度,但不能替代控制发丝的碰撞与长度约束

插值只负责填充空间密度,不负责独立物理。若每根密集发丝都跑完整 collision,会让成本随可见密度爆炸;更常见的分层策略是 control hair 解决运动,dense hair 继承控制结果并只增加轻量 jitter、phase 或材质差异。

4. shading:Marschner 路径与 lookup textures

头发的外观不只是 diffuse。Marschner 模型把主要光路拆成反射 R、透射-透射 TT、透射-反射-透射 TRT,整体可以写成

S=SR+STT+STRT. S=S_R+S_{TT}+S_{TRT}.

每条路径又可以分解为关于入射/出射角的 MM 与关于差分角的 NN。直接在每个像素计算四维函数代价高,因此可以把两个二维函数打包进纹理:用 light/eye 与 hair tangent 的 dot 得到 sin 参数,用 perpendicular vectors 的 dot 得到 cos 参数,避免大量 inverse trigonometric functions。

Marschner reflectance:把发丝散射拆成可查表的路径vector setuptangentlighteyetwo 2D lookup mapssin θi, sin θo → Mcos φd, cos θd → Npacked channels + mipmappaths sumS = SR + STT + STRThighlight + transmissionhair color / IOR先用切线投影避免 inverse trig,再让 2D texture 承担昂贵角度函数

两张 2D lookup texture 通常比三张或更多独立纹理更容易压低带宽,但 packing 需要明确 channel layout、精度、scale factor、mipmap 和颜色空间。若改动 IOR、highlight width 或 hair color,表是否重建、哪些参数留在 shader 中,也应成为材质契约。

5. opacity shadow map:沿深度积累透射

普通 shadow map 只回答“这个 sample 是否被挡住”,对体积状、半透明且高度细碎的头发过于二值。opacity shadow map 在 light space 沿 z 方向存储累计密度 σ\sigma,再通过

T(x,y,z)=ekσ(x,y,z) T(x,y,z)=e^{-k\sigma(x,y,z)}

得到到达发丝的光比例 TT。运行时在相邻 depth slices 之间线性插值 density,再进行指数转换;这样头发自阴影会随着发量和深度逐渐变软。

opacity shadow map:从二值遮挡到可积累透射率light-space slicesz0z1z2z3z4additive density / opacitylookup + interpolatenearest z sliceslinear weightsdensity σpack 4 channels / MRTtransmittanceT = exp(-kσ)light reaches hairsoft volumetric shadow阴影保存的是沿光线的 opacity thickness,不是一个硬的 depth hit

实现上可以把 16 个深度层分成多通道纹理,也可以用 MRT 一次生成多个 texture。通道 packing 降低 pass 数,但不能消除 shadow resolution、slice spacing、filtering 和 bandwidth 的预算。若 slice 太少,密集发束会出现 banding;若分布不匹配 hair volume,某些深度区间会过暗或漏光。

6. 把效果拆成可发布的预算

最终验收不要只看一张 Nalu 风格截图。至少建立四组回归:control hair 的约束误差和碰撞穿透,dense interpolation 的密度/seed 稳定性,Marschner lookup 的 tangent 高光,opacity shadow map 的 slice 插值和透射单调性。每组都要在静止、快速摆动、极端相机角度和不同发量下记录结果。

性能上要分开统计:control dynamics 粒子数与 constraint iterations,Bezier/tessellation 和动态 vertex buffer 带宽,dense hair 的 overdraw,reflectance texture fetch,以及 shadow map 的 pass、MRT 和 slice storage。增加最终发丝数量不应自动增加 dynamics;增加 lookup 精度也不应掩盖 tangent 或 shadow range 的语义错误。

三步实验:从控制结构到实时阴影

每一步只改变一类变量,记录 control state、constraint error、生成 seed、tangent、lookup 版本和 light-space slices。若最终画面异常,先回放上游数据流,不要先增加发丝密度或提高 shadow resolution。

分步1 / 3

第一步:稳定 control hair

固定 scalp roots 和外力,调整 constraint iterations,观察 Verlet 粒子链的长度误差与 sphere/pearl collision。确认少量控制发丝可回放后,再进入密度生成。

dynamics:粒子、约束与碰撞的稳定循环p0p1p2p3p4p5collision sphereframe loopVerlet predictlength iterationssphere / pearl testrender only after converge约束要迭代:拉动一个粒子会让相邻 segment 失效,单次修正并不闭合整条链

本章小结

  • control hair 把昂贵 dynamics、collision 和艺术控制限制在可管理规模;dense hair 通过 Bezier、tessellation 和 barycentric interpolation 扩展可见密度。
  • Verlet integration 配合 length constraint 迭代能以较低状态成本保持发丝长度,但必须记录 constraint error、碰撞策略和迭代预算。
  • barycentric interpolation 负责空间填充,不负责独立物理;可回放 seed 和相同 vertex count 是稳定生成的工程前提。
  • Marschner reflectance 用 R、TT、TRT 路径解释方向性反射与透射,dot/tangent 参数和 2D lookup textures 能降低实时数学成本。
  • opacity shadow map 沿 light-space 深度积累 opacity thickness,再插值得到 transmittance;它比二值 depth test 更适合细碎、半透明的发束阴影。

练习

问题 1|选择 dynamics 层级。 最终需要数千根可见发丝,但目标 GPU 无法让每根发丝都执行完整碰撞和约束。你会如何拆分计算?

问题 2|定位高光错误。 头发有颜色和阴影,但沿发丝方向移动光源时高光几乎不变。应检查哪些数据?

问题 3|设计 opacity shadow 回归。 8 层 shadow map 在稀疏发束上稳定,但密集发束出现 banding 和远端漏光。如何修改实验?

名词解释

本章出现的专业名词,用大白话再讲一遍。

control hair
Verlet integration
length constraint
barycentric interpolation
Marschner reflectance
opacity shadow map

讨论

评论区加载中…