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 预算。
保持 control hairs 可调试,再用 packed lookup maps 和 opacity shadow map 控制实时成本。
本章主题 Hair Animation and Rendering in the Nalu Demo 的核心是分层:控制结构负责可解释的状态,生成阶段负责空间密度,shading 负责方向性散射,shadowing 负责沿光线的透射损失。
↡数量较少、从 scalp 生长并承担动力学、碰撞与艺术控制的代表性发丝;最终渲染发丝由它们生成或插值而来。 ↡用当前位置与上一帧位置推算粒子下一位置、避免显式速度状态的积分方法;它适合配合约束迭代实现稳定发丝运动。1. 从 scalp layout 生成控制发丝
控制结构从专用 scalp mesh 生长:每个根顶点沿 scalp normal 生成一根 control hair,初始长度和方向描述发型,而不是直接把最终数千根发丝交给 dynamics。这样既保留了发型布局,又让动画系统只处理可控规模的粒子链。
控制结构不是纯物理系统。程序动力学会让运动自然,却也可能让发型失去导演需要的形状;工程上应该提供 root pin、局部阻尼、外力、碰撞层和手工修正通道。所谓“更真实”的自动系统,如果没有可回放和可修正的状态,仍然难以用于镜头制作。
2. dynamics:Verlet、长度约束与碰撞
↡把相邻发丝粒子之间的目标距离反复投影回约束面的过程;它通过多次局部修正减少 stretching,并让一条发丝趋向保持长度。在控制发丝上,每个 vertex 作为 particle。Verlet 的基本预测可写成
预测之后,逐段修正相邻粒子的距离,使 接近目标 segment length。一次修正会影响邻居,因此通常需要多轮迭代;迭代次数越少越便宜,但越容易出现拉伸和碰撞穿透,迭代次数越多则会增加 CPU/GPU 工作与延迟。
碰撞可以从简单开始:用 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 topology 在 control hairs 之间填充发丝密度。对于 scalp triangle 顶点 A、B、C,内部发丝 Y 可以写成
随机采样 barycentric coefficients 时必须保证样本落在三角形内部;同一个 interpolated hair 应与 control hair 拥有相同的 vertex count,方便逐点混合和动态更新。随机性应使用可回放 seed,否则美术、测试和网络重现都无法稳定比较。
插值只负责填充空间密度,不负责独立物理。若每根密集发丝都跑完整 collision,会让成本随可见密度爆炸;更常见的分层策略是 control hair 解决运动,dense hair 继承控制结果并只增加轻量 jitter、phase 或材质差异。
4. shading:Marschner 路径与 lookup textures
↡把单根圆柱状发丝的方向性反射与透射拆成 R、TT、TRT 路径,并用角度函数描述其高光、折射和内部反射的头发反射模型。头发的外观不只是 diffuse。Marschner 模型把主要光路拆成反射 R、透射-透射 TT、透射-反射-透射 TRT,整体可以写成
每条路径又可以分解为关于入射/出射角的 与关于差分角的 。直接在每个像素计算四维函数代价高,因此可以把两个二维函数打包进纹理:用 light/eye 与 hair tangent 的 dot 得到 sin 参数,用 perpendicular vectors 的 dot 得到 cos 参数,避免大量 inverse trigonometric functions。
两张 2D lookup texture 通常比三张或更多独立纹理更容易压低带宽,但 packing 需要明确 channel layout、精度、scale factor、mipmap 和颜色空间。若改动 IOR、highlight width 或 hair color,表是否重建、哪些参数留在 shader 中,也应成为材质契约。
5. opacity shadow map:沿深度积累透射
↡沿 light-space 深度切片记录发丝吸收或 opacity thickness,并在 shading 时插值这些层得到透射率的阴影表示。普通 shadow map 只回答“这个 sample 是否被挡住”,对体积状、半透明且高度细碎的头发过于二值。opacity shadow map 在 light space 沿 z 方向存储累计密度 ,再通过
得到到达发丝的光比例 。运行时在相邻 depth slices 之间线性插值 density,再进行指数转换;这样头发自阴影会随着发量和深度逐渐变软。
实现上可以把 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。
第一步:稳定 control hair
固定 scalp roots 和外力,调整 constraint iterations,观察 Verlet 粒子链的长度误差与 sphere/pearl collision。确认少量控制发丝可回放后,再进入密度生成。
本章小结
- 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