这篇讲 UE 5.6 里的毛发系统(Groom)。它分成两半:前半 回答"头发这东西到底怎么变成一个能算的东西",尽量不假设你懂图形学或物理;后半 回答"UE 具体把它写成了哪些代码、调哪些参数",这部分会大量出现类名和函数名,看不懂可以先跳过,不影响读懂前半。
前言
游戏里的头发,常见的做法是贴图造假:在头皮上贴几片带头发图案的透明卡片,看起来像有头发,但它不是一根一根的。UE 的 Groom 是另一条路——它把每一根头发当成一根真的细线来算,所以它能做到真的甩动、互相碰撞、透光。
代价就是:贵。贵到需要一整套专门的流程,而这篇就是把这套流程从"它是什么"讲到"它怎么算出来的"。
太长不看(TL;DR)
给没时间通读的你,三句话先记住:
- 是什么:Groom 把每一根头发当成一根会动的细线逐根算(真发丝),而不是在头皮上贴带头发图案的卡片(发片);换来的是真的甩动、碰撞、透光,代价是贵、需要专门管线。
- 上手只盯三个层级:资产定基线 → 组件覆盖(总开关不开就不生效)→ 求解器组件 / CVar 最外层钳制。想调某个效果,先查"改哪里才算数"的参数地图。
- 最容易"没反应":三条驱动路(物理求解 / Deformer / 蒙皮绑定)或插值数据不满足时会静默失效——不报错、停在默认姿态。看到"毛发不动",先查这三条路的条件,而不是到处找 bug(见5.2 节)。
阅读说明(版本 / 读者 / 结构):本文以 UE 5.6 的 HairStrands 插件与渲染器源码为基准。默认读者是完全没接触过毛发系统的开发者或者美术创作者:前半(第 0
4 章)只讲物理原理,不依赖任何引擎知识;下篇(第 511 章)讲 UE 具体实现与上手,其中标注"源码细节,可跳过“的部分,看不懂类名不影响理解。想直接上手,跳到第 9 章;想先看全文骨架,看下面的目录。
名词解释
文中会出现一批固定叫法。先在这里各给一句定义,读到生词可以回来查:
| 术语 | 英文 | 解释 |
|---|---|---|
| 节点(珠子) | Node / point | 头发上被采样的一个点,记位置与朝向,是求解时最小的更新单位。 |
| 边(绳子) | Edge / segment | 相邻两个节点之间的一段,记长度、方向与相对旋转;三类形变约束都作用在它上面。 |
| 控制线 | Guides | 承载形变与模拟的稀疏曲线,是模拟和绑定实际作用的对象,数量少。 |
| 可见发丝 | Strands | 由控制线插值展开、最终画出来的发丝,数量远多于控制线。 |
| 发组 | Hair Group | 资产里配置的最小单元;同组的各项设置按组索引一一对齐。 |
| 模拟空间 | Simulation space | 求解采用的参考空间(通常是某根骨骼的局部空间),头发在其中被计算。 |
| 朝向 | Orientation / frame | 附在节点上的一组参考方向(或用四元数表示),用来区分"弯曲"与"整体旋转”,并让扭转可定义。 |
| 约束 | Constraint | 一条"满足时为零"的几何规则(如距离不变、夹角不变);求解器按梯度把违反量逐次拉回。 |
| 柔度 | Compliance | 约束的软硬程度;值越大越软,每次修正推得越少。 |
| 拉伸 / 弯曲 / 扭转 | Stretch / Bend / Twist | 一根发丝仅有的三种形变,刚度数量级相差很大,必须分别建模。 |
| 子步 | Substep | 把一帧时间切成几段分别求解;影响大位移与高速段是否跟手。 |
| 迭代 | Iteration | 每段内把约束反复求解的次数;影响头发"软还是硬"。 |
| 体素化 | Voxelization | 把毛发密度写进三维网格,用于近似自碰撞,或供间接光与阴影使用。 |
| 深阴影 | Deep Shadow(DOM) | 沿光线方向分层记录透射量的阴影表示,用来表现"一大把头发会透光"。 |
| 曲线页 | Curve page | 流送与显存分配的最小粒度,按固定曲线数对齐。 |
| 发卡 | Card | 用一片带头发图案的纹理近似一簇发丝的画法。 |
| 平行移动框架 | Parallel transport frame | 用相邻切线之间的最小旋转逐节点传播朝向,避免沿头发累积出虚假扭转。 |
| Darboux 向量 | Darboux vector | 描述相邻两朝向之间相对旋转的向量,同时携带弯曲与扭转。 |
| 双覆盖 | Double cover | 四元数 q 与 -q 表示同一旋转,取解时需取一致的一个,否则会在符号翻转处产生假弯曲。 |
| 位置动力学 | PBD / XPBD | 用"约束投影"而非弹簧力求解的范式,UE 的求解器基于它。 |
| Cosserat 杆 | Cosserat rod | 用朝向(而非只有位置)描述细杆的力学模型,能同时表达弯曲与扭转。 |
目录(TOC)
- 第 0 章 入门:头发难在哪
- 第 1 章 把一根头发变成能算的东西
- 第 2 章 让头发动起来:不"用力",而是"拉回去"
- 第 3 章 头发怎么和世界打交道
- 第 4 章 每一帧到底发生什么
- 下篇 · UE 是怎么把它实现的(第 5~9 章)
- 第 10 章 和别的方案比:Groom 与 gFur
- 第 11 章 附录
第 0 章 入门:头发难在哪
一句话:头发难做,是因为它"又多、又细、又软";UE 的选择是老老实实一根一根算,再用一堆工程手段把成本压下来。
0.1 头发为什么难做
把头发和普通物体(比如一个箱子)比一下,难点有三个:
- 多。一头长发动辄十几万根。箱子只有一个,头发有十几万个"独立的东西"都是软的。
- 细。一根头发比屏幕上的一个像素还细。也就是说,用普通的"画三角形"的方式,很多头发根本挤不进一个像素里,会互相盖住、闪烁。
- 软。它不是刚体,每一根会甩、会相互纠缠、会被风带着动、会透光(不然一大把头发会像一堵黑墙)。
这三点决定了:头发既不能用"画普通模型"的办法算,也不能用"算一个刚体"的办法算,需要一套专门的东西。
0.2 常见的两种做法
| 做法 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| 发片 / 发卡 | 在头皮上贴几片带头发图案的透明卡片 | 便宜、能大量用 | 不是真头发,动起来假,近看穿帮 |
| 真发丝(UE Groom 走的这条) | 每根头发当成一根会动的细线,逐根算 | 真实、能甩能碰撞 | 贵,需要专门管线 |
UE 的 Groom 属于后者。它并不排斥前者:同一条头发也可以按远近切换成"发卡",甚至用一片纹理来画(后面第 8 章讲)。
0.3 一根头发在引擎里长什么样
要"算"头发,第一步是把它变成能存、能改的东西。做法是:把一根连续的长头发,切成有限的一小段一小段,每一段的接头处记两件事——位置和朝向。
可以把一根头发想象成一根串珠手链:
- 珠子 = 头发上的采样点,它记住自己"在哪"(位置)。
- 珠子之间的绳子 = 一段头发,它记住相邻两颗珠子的"距离、方向、相对角度"。
- 每颗珠子上还别着一个小箭头(朝向),用来记录"这段头发是扭着的还是没扭"。
发根那颗珠子被钉在头皮上(跟着头动),发梢那颗珠子自由(可以乱飞)。头发怎么动,就变成了"这串珠子怎么动"。
0.4 整条流水线:三步
从"美术做好的头发"到"屏幕上会动的头发",一共就三步:
- 第一步产出的是"静止姿态"的头发(还没受力时的样子),它决定了后面所有计算"往哪儿弹回去"。
- 第二步是这篇前半(第 1~4 章)的主题:一根头发怎么算。
- 第三步在第 8 章详述:这么细的东西怎么画。
本章小结:头发难在"多、细、软",所以不能当普通模型画,也不能当刚体算。UE 的做法是把每根头发切成有限段,每段接头处记"位置 + 朝向",于是"头发怎么动"被转化为"这串珠子怎么动"。整条流水线只有三步:准备数据、每帧计算、画出来。
第 1 章 把一根头发变成能算的东西
一句话:一根头发只会三种变形(拉伸、弯曲、扭转);要算它,就得把它切成有限段、给每颗珠子补上一个"朝向",再配上"重量"。
1.1 头发只会三种变形
不管你多用力扯头发,它只会发生三件事:
| 变形 | 白话 | 有多难发生 | 对观感的影响 |
|---|---|---|---|
| 拉伸(沿长度方向拉长 / 压短) | 把头发当橡皮筋拉 | 几乎拉不动(头发几乎不可压缩) | 拉长了就露馅,所以规则要定得"很硬" |
| 弯曲(横着折) | 把头发弯成一个弧形 | 很容易,随便一甩就弯 | 决定头发是蓬松还是贴头 |
| 扭转(绕着自身的轴拧) | 拧毛巾 | 很难看出来,但影响很大 | 决定头发弯完之后"往哪边弯",从而改变高光走向 |
关键点:这三件事的"软硬程度"差了非常多个数量级。头发几乎拉不动(拉伸很硬),却很容易弯(弯曲很软)。所以"用一根弹簧来代表头发"是不行的——那根弹簧要么被拉长(违反"拉不动"),要么硬得像钢丝(弯不起来)。
正确做法是分开建模:拉伸、弯曲、扭转各算各的。[7]
1.2 把头发切成有限的段
真实的头发是一条连续曲线,上面有无数个点,计算机没法一个个处理。必须把它切成有限段。[1]
切完之后,一根头发由三样东西描述:
- 珠子(节点):记位置和朝向,是"可以被改"的最小单位。
- 绳子(边):记相邻两颗珠子之间的距离、方向、相对角度,三种变形规则都作用在它上面。
- 静止姿态(rest pose):还没受力时的样子,是所有规则"弹回去"的目标——就像橡皮筋松手后要回到原长。
代价:段数决定精度。段切得少,头发就表达不出细腻的层次(看起来一根根是直的);段切得多,效果好但更贵。这个"每根头发切几段"是后面最常调的旋钮之一。
1.3 为什么每颗珠子还必须记"朝向"
只记位置不够。举个反例:这两种情况仅靠位置无法区分:
只看两端位置,这两件事长得一模一样:
弯了: o o o (中间那颗偏离了直线)
转了: o o o (整根被扳到了另一个方向)
两者都表现为"端点位置变了"。
- 如果只有位置,“这根头发弯了"和"整根头发被转了个角度"分不出来。
- 一旦每颗珠子带上"朝向”(那个小箭头),两件事就能区分了:弯是"箭头和绳子不一致",转是"箭头自己也一起转了、和绳子仍然一致"。
正因为有了朝向,弯曲和扭转才有可定义的对象。[7]
1.4 朝向怎么沿着头发一路传下去
这里有个陷阱。假设你从头到尾给每颗珠子都"凭切线随便定一个朝向"——朝向里包含一个"绕头发自身转了多少"的分量,这个分量是自由的、没有唯一答案。结果就是:朝向会沿着头发一路越拧越麻花,最后算出来的头发是"扭成麻花"的。
解决办法叫平行移动框架:每次只按"相邻两段之间最小的那个旋转"去更新朝向,绝不额外引入绕自身的转动,于是不会累积出虚假的扭转。[5]
它有两种用法:
- 一开始初始化时:必须从头(发根)到尾(发梢)一根一根顺着传,保证整根头发上的朝向是连贯的。
- 每帧计算时:只按相邻珠子局部修正,这样才可以并行(每颗珠子各算各的,不用等别人)。
1.5 每颗珠子有多重
最后一件事:给每颗珠子配上"重量"和"转动惯量"(可以理解成"转起来有多费劲")。
为什么需要?因为规则被违反时,要决定"谁动多少"。直觉是:轻的动得多,重的动得少。发根那颗珠子通常"很重"(几乎不动),发梢那颗"很轻"(到处乱飞)。引擎用的正是这个直觉——按"重量/转动惯量的倒数"来分配修正量。
注意有两套,别混:
- 管位置的规则,看重量(质量)。
- 管朝向的规则,看转动惯量。
在 UE 里,这两套量由数据接口的 ComputeNodeMass 与 ComputeNodeInertia 提供(第 7 章细讲)。
本章小结:一根头发只会拉伸、弯曲、扭转三种变形,而且三者软硬差距极大,必须分开算。要能算,就把它切成有限段:珠子记位置和朝向,绳子记两颗珠子之间的关系,静止姿态是弹回的目标。朝向是必须的,否则"弯了"和"整根转了"无法区分;朝向的传播要用平行移动框架,避免沿头发累积出虚假的扭转(初始化时串行传、每帧并行局部修正)。最后给珠子配上重量和转动惯量,让规则被违反时按"谁轻谁多动"来分配修正。
第 2 章 让头发动起来:不"用力",而是"拉回去"
一句话:与其给珠子加力(容易算炸),不如先规定"头发本来应该是什么样",每帧把偏离的地方按比例拉回去。这就是 UE 求解器全部规则的共同做法。
2.1 为什么不直接"加力"
最直觉的想法是:把每段头发当成一根弹簧,受力就伸长/弯曲,算出力再让珠子动。
但弹簧有个麻烦:“有多硬"和"每帧算多长"是绑死的。弹簧越硬,就越需要把每帧切得非常非常小来算,否则会算飞(数值发散)。而头发恰恰需要"拉伸极硬、弯曲极软”——硬的那部分会把整根头发逼到用极小步长,成本爆炸。[3]
2.2 换个思路:写成"规则",再把违反的地方拉回去
换个问法:不问"力有多大",而问"本来应该是什么样,现在差了多少,把差的那部分补回去"。
这就是约束的思路。一条约束就是一句"应该满足的话",比如:
"这两颗珠子之间的距离,应该等于静止时的距离。"
如果现在被拉长了 3 厘米,那就把这两颗珠子往回推一点,让距离缩短一些——不是一次推到位,而是每次推掉一部分。一帧里把这条规则重复推很多遍(叫"迭代"),结果就越来越接近满足。[3]
这种"先把位置预测出来、再按规则反复拉回去"的做法,英文叫 PBD(位置动力学);UE 用的是它的改进版 XPBD。
2.3 一条规则上要写清楚的三件事
| 要写清楚的 | 白话 | 影响 |
|---|---|---|
| 规则是什么 | “距离应该等于静止长度” | 决定头发长什么样 |
| 有多软 | 叫柔度(compliance):软的就少推点,硬的就多推点 | 决定头发是硬挺还是绵软 |
| 影响谁 | 由参与珠子的"重量/转动惯量的倒数"决定 | 轻的动得多,重的动得少 |
其中"有多软"这一项,UE 会先把它除以"每步时长的平方"再用。为什么要这么绕?为了让同一套参数在不同帧率下含义一致——不想出现"帧率越高头发越硬"这种事。[4]
但要澄清一个常见误解:这样做并不代表"结果和帧率无关"。因为每帧迭代次数是固定的,步长变大时,同样的迭代次数收敛得更不充分,头发还是会变软、变滞后(第 4 章展开)。
2.4 拉伸、弯曲、扭转其实是同一件事
回到第 1 章那三种变形。实现上它们是同一个公式,区别只在"这条规则管几颗珠子":
| 规则 | 管几颗珠子 | 规则在说 |
|---|---|---|
| 拉伸 | 相邻两颗 | 你俩的距离别变 |
| 弯曲 | 隔一颗,管三颗 | 你这个弯的幅度别超 |
| 扭转 | 隔两颗,管四颗 | 你俩的"箭头"别相对拧太多 |
珠子越"轻"、规则越"软",这条规则被违反时就越容易拉回来。UE 里这是同一个函数用不同参数(NodeOffset)实现的,第 7 章细讲。[7]
2.5 “箭头"的两种记法
因为有了朝向(箭头),“弯曲"就有两种写法:
- 写法 A(看方向):把绳子方向拉回"父珠子的箭头转过静止角度后的方向”。简单、稳,弹簧一族用这种。
- 写法 B(看相对旋转):直接规定"相邻两颗珠子的箭头之间应该转多少”。它一次就把弯曲和扭转都管住了,是"杆"一族的用法;麻烦在于:一个旋转用两个相反的四个数字(四元数)都能表示,取解时要挑对,否则会在某个角度突然翻车(俗称"双覆盖"问题)。[6][7]
UE 两套求解路线正好对应这两种(弹簧族 vs 杆族),第 7 章细讲。
2.6 阻尼:让它别抖
阻尼(damping)像"泡在水里"的阻力。它不改变头发最终该停在哪,只影响"多快停下来"。
- 阻尼太小:头发来回弹、抖。
- 阻尼太大:头发变得迟钝、像泡在糖浆里。
对毛发来说,合适的阻尼是"看着不抖、也不像泡酱里"——这取决于迭代次数和子步数(第 4 章)。
2.7 发根硬、发梢软
真实头发是渐变的:发根粗硬、发梢细软。实现里不是用一个常数,而是沿头发逐点采样成一张表——从头到尾,刚度/摩擦/粗细都按位置给不同值。
这张表的密度 = 每根头发切了几段。段数不够时,渐变会变成一格一格的阶梯——这就是"怎么调都不够自然"时最该看的旋钮。
本章小结:直接给头发加弹簧力不可行,因为"刚度"和"步长"绑在一起,硬的部分会把步长逼到极小。UE 改用"约束":先规定头发应该满足的规则,每帧把违反的部分按比例拉回去,迭代多次逼近。一条规则要写清楚"规则是什么、有多软(柔度)、影响谁(重量/惯量)"。拉伸、弯曲、扭转其实是同一个公式,区别只在管几颗珠子;朝向有两种记法(看方向的、看相对旋转的),后者同时携带弯曲与扭转但要处理"四元数二义"。阻尼不改变终点,只影响多快稳住;刚度参数沿头发渐变,由"切几段"决定其分辨率。
第 3 章 头发怎么和世界打交道
一句话:头发会被角色"甩"起来,是因为它是在跟着角色一起动的坐标系里被算的;而碰撞和风都用了更省事、但有代价的近似。
3.1 撞到身体
头发撞到头皮/身体时,要处理两件事:
- 别穿进去:一旦珠子跑到身体里面,就把它推出来。
- 擦过去的摩擦:贴着身体滑动时,按摩擦决定"黏住不动"还是"滑过去"。
实现上有两种:硬约束(直接推出去,简单但可能抖)和软约束(允许暂时穿一点点,换来更顺滑,代价是需要多迭代几下)。UE 两种都有(第 7 章)。[6]
3.2 头发和头发之间
十万根头发两两判断"撞没撞"是不可能的(组合数太大)。省事的做法是体素化:把所有头发的密度灌进一个三维网格(就像把头发塞进一堆小格子里),格子之间互相"排斥",近似出头发互相推开的效果。
代价要说清楚:这种做法不产生具体的接触点和接触法线,所以它没有"逐点的摩擦"这回事;能分辨的最小间距由网格粗细决定——太粗会粘成一坨,太细又贵。UE 用一个速度网格 + 一个压力网格来做(第 7 章)。
3.3 风
把每一小段头发当成一根很细的圆柱,风对它的阻力是两项相加:
阻力 = 与速度成正比的部分(慢速时主导)
+ 与速度平方成正比的部分(快速时主导)
迎风面的大小,取决于"风的方向和头发方向的夹角"——正对着风时最粗,顺着风时最细。这也是为什么"甩得越快,阻力增长得越不成比例":快速时是平方项在起作用。
3.4 角色动,头发为什么会被甩
头发是在一个跟着角色一起动的坐标系里算的。于是角色本身的速度、转动、加速,就变成了作用在头发上的一股"假力"(就像公交车急刹车时你会往前倾)。
- 好处:头发默认跟着角色走,不会因为角色一停就整体"留在原地"。
- 前提:角色每帧的加速必须被正确地喂进去,否则头发会莫名其妙往某个方向飘。
要注意:喂进去的是每帧算出来的速度(用相邻两帧位置相减得到),不是动画师设计的速度曲线——所以它和帧率有关(第 4 章)。UE 里由组件的"线/角速度比例"和"瞬移阈值"控制(第 7 章)。
3.5 为什么一模拟头发就往下掉
美术摆好的头发姿态,通常不是"刚好被重力托住"的姿态。你一开模拟,重力立刻把它往下拽,头发就塌下来了,和美术想要的不一样。
常见解法:开模拟前,先让重力和头发自己达成一次平衡,得到一个"自然下垂"的姿态;然后把它和美术原始姿态按一个系数混合,当作新的"静止姿态"。说白了,就是把"往下掉"这件事本身定义成新的静止样子。UE 里对应"重力预载"(第 7 章)。
本章小结:头发是在跟随角色的坐标系里被算的,所以角色的运动变成一股"惯性假力",头发才会被甩起来;喂进去的是逐帧差分的速度,因此与帧率相关。碰撞分两条:和身体碰撞有"推出 + 摩擦",头发之间只用体素化近似排斥——不产生接触点和法线,也就没有逐点摩擦,能分辨的间距由网格粗细决定。风把每小段头发当细圆柱,阻力是"线性项 + 平方项",迎风面积随风与头发的夹角变化。静止姿态与重力天然矛盾,解法是先解一次重力平衡,再和原始姿态按系数混合,把"下垂"定义成新的静止姿态。
第 4 章 每一帧到底发生什么
一句话:一帧 = 先按力把珠子推一下 + 反复把规则拉回去 + 收尾;“换个帧率,头发手感就变了"的根子在"每步多长”,不在迭代本身。
4.1 一帧的三段
三段各司其职:第一段负责"顺着力动",第二段负责"别违反规则",第三段负责"收干净"。第 7 章会看到 UE 里这三段分别对应哪些函数。[3]
4.2 两个不同的旋钮:子步 与 迭代
它们常常被混为一谈,其实管的是两件不同的事:
| 旋钮 | 白话 | 主要影响 | 成本 |
|---|---|---|---|
| 子步数 | 把这一帧切成几段分别算 | 甩得厉害、动得快时准不准(跟不跟手) | 与段数成正比 |
| 迭代数 | 每段里把规则反复解几遍 | 头发"软还是硬"(规则拉得牢不牢) | 与遍数成正比 |
一句话:子步管时间切多细,迭代管规则解多透。少子步会在快速甩动时"跟不上、穿透";少迭代会让头发整体发软、拉不住。
4.3 为什么换个帧率,手感就变了
这里要分清三个时间量:
- 一帧多长(delta):机器跑得快就短,卡一下就长。
- 每帧算一次:求解器每帧被调用一次,拿到的就是这一帧的 delta。
- 每一步多长:delta 会被一个上限拦一下(太长就截断),再按子步数平均分。
由此得到三个结论:
- 超长帧会被截断:比如切关卡卡了半秒,求解不会让头发"瞬移半秒",而是让这一帧的模拟变慢。表现是"头发跟不上角色"——这是为了稳定付出的代价。
- 改帧率 = 改刚度:因为每步长度直接来自 delta,帧率高时步长小、收敛充分(靠近设定的硬度);帧率低时步长大、收敛不足(更软更滞后)。这就是"同一份头发在不同机器上手感不一样"的来源。
- 按"帧数"计数的东西会随帧率变:比如重置后有一段"启动缓冲",它是按帧数算的,60 帧下这段真实时长就是 30 帧下的一半。
成熟求解器的通常做法是两层补偿:位移大的帧自动多切几段(动态子步)+ 迭代数跟着步长放大,让等效手感不随帧率漂。UE 提供了 r.HairStrands.EnableAdaptiveSubsteps 等开关,但插件里没有做固定步长(第 7 章)。[4]
4.4 现象对参数
这张表是全篇"调参直觉"的来源:
| 你看到的 | 大概是哪里的事 | 先动哪个 |
|---|---|---|
| 整体发软、拉不住 | 迭代不够,或规则太软 | 迭代数、刚度/缩放曲线 |
| 抖、高频颤 | 子步不够、阻尼太小、步子太大 | 子步数、阻尼 |
| 穿模(穿过身体/自己) | 碰撞没开,或自碰撞网格太粗 | 碰撞开关、网格粗细 |
| 像塑料、不蓬松 | 弯曲太硬,或沿头发曲线太平 | 弯曲刚度、弯曲缩放曲线 |
| 高速甩动跟不上 | 子步不够,或 delta 被截断 | 子步数、帧率 |
4.5 为什么这类计算适合显卡并行
规则之间会互相牵扯(同一颗珠子被好几条规则用到),所以不能无脑并行。做法叫分组着色:把"不抢同一颗珠子"的规则放进同一组,组里可以同时算,组与组之间排队。
一条规则管几颗珠子,就能分成几组——比如管相邻两颗的(拉伸)按 x % 2 交替,管三颗的(弯曲)按 (x / 2) % 2 交替,管四颗的(扭转)按 (x / 3) % 2 交替。这三个式子的取值都只有 0/1,所以三种约束实际都是两色(红/黑)交替,差别只在取模步长;经典红黑着色就是前者的特例。UE 的实现与此一致(第 7 章)。
还要记住一点:分组的顺序会影响结果——这就是"同样的参数,不同求解器结果略有差异"的原因之一。
本章小结:一帧分三段——按力预测、反复拉回规则、收尾修正。子步与迭代是两个不同的旋钮:子步把一帧切成几段(管大位移准不准),迭代把规则反复解几遍(管软硬)。手感随帧率变化的根子在"每步多长":delta 先被上限截断、再按子步均分,所以低帧率会让模拟变慢变软,“改帧率等于改刚度”;按帧计数的机制(如重置后的启动窗口)也会随帧率改变真实时长。规则之间互相牵扯,需要按"管几颗珠子"做分组着色,组内并行、组间排队,而且分组顺序本身影响结果。
下篇 · UE 是怎么把它实现的(第 5~11 章)
下篇把前 4 章的每一条道理,对应到 UE 的具体类名 / 函数名 / 资产字段。看不懂类名没关系:每章开头都有一句"白话导读"和一个"一句话"结论;标注"源码细节,可跳过“的部分跳过去也不影响理解。真正要动手的人重点看第 9 章(上手地图)和第 11 章(控制台速查)。
第 5 章 系统组成:UE 把它拆成了几块
一句话:UE 把这件事分成"准备数据、计算、画出来"三块,谁管哪份数据分得很清楚;其中"计算"这块的流程是写在 Niagara 资产里的,不在 C++ 里。
白话导读:前 4 章讲的是"物理上应该怎么算”,不依赖任何引擎。从这章起进入 UE 的具体实现,会出现大量类名(如 UGroomAsset)。看不懂类名就先看每节的白话部分,它们讲的是同样的道理。
5.1 三块与一条数据流
毛发插件叫 Groom,位于 Engine/Plugins/Runtime/HairStrands,内部又拆成 7 个模块,各管一摊:
| 模块 | 管什么 |
|---|---|
HairStrandsCore | 资产、组件、绑定、每组实例、渲染代理、Niagara 数据接口 |
HairStrandsSolver | 求解组件(负责划分曲线、出设置) |
HairStrandsDeformer | 用 DeformerGraph / Optimus 驱动形变 |
HairStrandsDataflow | 编辑期的 Dataflow 节点 |
HairStrandsEditor | 资产菜单、导入器、细节面板 |
HairStrandsRuntime | 高速缓存流送管理器的注册 |
HairCardGeneratorFramework | 从网格生成发卡的框架 |
而"画出来"这部分不在插件里,在引擎渲染器的 Engine/Source/Runtime/Renderer/Private/HairStrands。
各层各自拥有什么(源码细节,可跳过):
| 层 | 类(文件) | 拥有什么 |
|---|---|---|
| 资产 | UGroomAsset(Source/HairStrandsCore/Public/GroomAsset.h) | 控制线/可见发丝/发卡/网格、插值、物理、渲染、LOD、材质 |
| 绑定 | UGroomBindingAsset(.../GroomBindingAsset.h) | 发根到网格三角形的预烘焙投影与权重 |
| 组件 | UGroomComponent(.../GroomComponent.h) | 资产/绑定/物理资产引用、Deformer、模拟设置、每组实例 |
| 每组实例 | FHairGroupInstance(.../GroomInstance.h) | 每组的 Rest / Deformed / Interpolation 等 GPU 资源 |
| 求解器 | UGroomSolverComponent(Source/HairStrandsSolver/Public/GroomSolverComponent.h) | Deformer 实例、动态曲线划分、关联的 Groom 组件 |
| 渲染 | FHairStrandsSceneProxy(GroomComponent.cpp 内)与 Engine/Source/Runtime/Renderer/Public/HairStrandsInterface.h | 跨模块交接的 FHairStrandsInstance / FHairGroupPublicData |
5.2 头发"怎么动"的三条路
一个发组怎么动,UE 提供了三条路,优先级从高到低、不是叠加:
- 物理求解:真正会自己算头发怎么动的那条。由资产开关和组件覆盖共同决定(
IsSimulationEnable)。 - Deformer:不是物理解算,而是用一张"骨骼驱动图"来摆头发;资产开关是
FHairSolverSettings::bEnableDeformation。 - 蒙皮绑定:默认路径。头发被"焊"在身体网格上,靠网格变形跟着动。绑定类型由
FHairLODSettings::BindingType决定(NoneBinding/Rigid/Skinning,默认Skinning)。
最容易踩的坑是:这三条路判定不满足时不会报错,而是"毛发不动 / 停在默认姿态"。看到"毛发没反应",先查这三条路的条件,而不是到处找 bug。
5.3 为什么把求解放在 Niagara 上
好处很直接:用 Niagara 就能在 GPU 上算、能通过"数据接口"把资产里的参数和网格喂进去、还能白拿它现成的调度和子步框架。
代价也很直接:求解的"流程编排"(先算什么、循环几次、怎么分支)写在了 Niagara 系统资产里,不在 C++ 代码里。于是:
- 调试时在 C++ 里看不到"它到底迭代了几次",只能看资产配置;
- 流程随二进制资产走,跨版本升级和版本管理比纯代码更麻烦。
这也是"同一个求解器组件,在不同项目里表现不一样"的根源。
5.4 三层时序
顺序由 tick group 推导,保证"在骨骼更新之后、渲染之前"发生。所谓"同一帧内",指的是渲染读到的是本帧算完的结果。
5.5 前半的量,在后半的哪里(对照表)
| 前 4 章的量 | 后面对应的东西 | 详见 |
|---|---|---|
| 珠子 / 绳子 / 朝向 | 数据接口里的节点位置、边方向、节点朝向 | 1.2 节 / 7.2 节 |
| 重量 / 转动惯量 | 求解 shader 里的等效质量项 | 1.5 节 / 7.2 节 |
| 柔度(有多软) | 资产里的弯曲/拉伸刚度与沿头发缩放曲线 | 2.3 节 / 7.4 节 |
| 拉伸 / 弯曲 / 扭转 | 同一族函数,按"管几颗珠子"区分 | 2.4 节 / 7.2 节 |
| “箭头"的两种记法 | 弹簧族 / 杆族两条求解路线 | 2.5 节 / 7.2 节 |
| 发根硬发梢软 | 逐点参数表(32 个采样点) | 2.7 节 / 7.4 节 |
| 自碰撞体素近似 | 速度网格 + 压力网格(同帧 ping-pong) | 3.2 节 / 7.3 节 |
| 风(线性 + 平方 + 迎风) | 外力计算入口 | 3.3 节 / 7.2 节 |
| 惯性假力 | 模拟空间 + 线/角速度比例 + 瞬移阈值 | 3.4 节 / 7.4 节 |
| 重力预载 | 按系数在当前/平衡姿态间插值 | 3.5 节 / 7.4 节 |
| 子步 / 迭代 | 资产与组件上的子步数、迭代数 | 4.2 节 / 7.7 节 |
| 每步多长 | delta 截断后按子步均分 | 4.3 节 / 7.7 节 |
本章小结:UE 把毛发分成"准备数据、计算、画出来"三块,按"谁拥有哪份数据"划边界。毛发怎么动有物理求解、Deformer、蒙皮绑定三条路,它们不是叠加关系,条件不满足时是静默失效而非报错。求解之所以放在 Niagara 上,是为了 GPU 计算、数据接口和现成的调度框架,代价是"流程编排"落在资产里而不在代码里。执行跨游戏线程、Niagara、GPU 三层,顺序由 tick group 决定。
第 6 章 数据从哪来:Groom 资产
一句话:屏幕上十万根头发,真正存下来、真正参与计算的只有少量"骨架”;其余是照着骨架"长"出来的,配置则以"发组"为单位。
白话导读:这章回答"这些头发是从哪来的、怎么存的"。如果你只想搞懂原理,记住一句就够:存的是少量骨架,画的是大量填充。
6.1 两级数据:控制线与可见发丝
- 控制线(Guides):真正参与计算的少量"骨架头发"。
- 可见发丝(Strands):屏幕上实际画出来的那一大把,每一根都记着"我最像哪根骨架、像多少",由骨架插值"长"出来。
两者的数量比就是"一根骨架撑起几根真发"。要注意的坑:插值数据一旦缺失,不会报错,而是让形变相关功能整体失效(头发停在默认姿态)——这是最容易被误判的故障。
6.2 发组:配置的最小单位
头发在资产里按"发组"组织(比如"刘海"、“后脑"各是一组)。每种配置都是一张数组,按组的编号一一对齐(第 0 张对应第 0 组,以此类推)(源码细节,可跳过):
| 数组 | 装什么 |
|---|---|
HairGroupsInfo | 每组有多少曲线、多少控制线、最长多长 |
HairGroupsRendering | 材质槽名 + 几何/阴影/高级渲染设置 |
HairGroupsPhysics | 求解、外力、约束、发丝参数 |
HairGroupsInterpolation | 抽稀与插值设置 |
HairGroupsLOD | 每组的 LOD 设置 |
HairGroupsCards / HairGroupsMeshes | 发卡 / 网格源描述 |
HairGroupsMaterials | 材质槽(材质 + 名称) |
HairGroupsPlatformData | 真正存几何的那份 Bulk Data |
改组数会让这些对齐关系和绑定一起失效,这是修改资产时最容易出事的地方。
6.3 绑定:把发根钉到网格上
头发要跟着身体动,就得知道"每根发根落在身体网格的哪个位置”。UGroomBindingAsset 就是把这件事提前算好存下来:每根发根落在哪个三角形上、用哪几个顶点按什么权重插值(源码细节,可跳过:RootToUniqueTriangleIndexBuffer / RootBarycentricBuffer / MeshInterpolationWeightsBuffer 等,封装在 FHairStrandsRootBulkData)。
两件事必须知道:
- 不提供绑定资产时,投影会在运行时现算,启动时有一笔较大的 GPU 开销——表现就是"第一次看到角色卡一下"。
- 绑定资产里存的是源网格的三角形编号,所以跨骨架复用会出问题:绑定与网格的 LOD 数量不一致时会直接判定不兼容(回退成未绑定、毛发不动),而 LOD 数量一致时不会报错、却会给出错误的形变。
6.4 LOD 与"第一次进游戏卡一下"
LOD(远近不同精度)有三种模式:
| 模式 | 行为 |
|---|---|
Default | 跟随项目设置 |
Manual | 用你手工设的每组 LOD |
Auto | 按屏幕占比自动增减,忽略你手设的 LOD |
另外,资产里的设置会被**编译成一份"派生数据"缓存在磁盘上。改动设置会改变它的"钥匙",从而触发重新生成——这就是“改完设置第一次进游戏卡一下”**的原因:它在现生成 PostLoad / PostEditChangeProperty 时触发重建。
本章小结:控制线是参与计算的少量骨架,可见发丝是由它插值长出来的一大把;插值数据缺失不会报错,而是让形变功能整体失效。配置以"发组"为单位、按编号对齐,改组数会连带失效。绑定资产把"发根落在网格哪个三角形、怎么插值"提前烘焙好,省掉运行时投影,代价是跨骨架复用会出错。LOD 有 Default / Manual / Auto 三种,Auto 会忽略手工设置;派生数据的重建就是"改完设置第一次进游戏卡顿"的来源。
第 7 章 怎么算:求解实现
一句话:求解器组件自己不算,它只负责"划分要算哪些头发、把设置准备好",真正的计算发生在 Niagara 系统图和 GPU 上。
白话导读:这章把第 2~4 章的每一条道理,对应到 UE 的具体类与函数。看不懂函数名就只看每节的结论句。
7.1 谁出设置、谁真正执行
这是最容易搞错的一点:UGroomSolverComponent 不是执行者。
| 做的事 | 谁来做 |
|---|---|
| 准备设置(参数、覆盖) | UGroomComponent::SimulationSettings + 资产里的 HairGroupsPhysics |
| 划分"这帧要算哪些头发" | UGroomSolverComponent::SelectDynamicCurves |
| 把任务入队 | UGroomSolverComponent::SendRenderDynamicData_Concurrent |
| 真正计算 | Niagara 系统(StableSpringsSystem / StableRodsSystem / 自定义) |
它的 TickComponent 里只有"标脏 + 划分曲线",没有任何解算代码。走哪套求解系统由枚举 EGroomNiagaraSolvers 决定。
7.2 前 4 章的公式落在哪(源码细节,可跳过)
| 前 4 章的量 | 对应函数(Niagara 数据接口 UNiagaraDataInterfaceHairStrands) |
|---|---|
| 珠子位置 / 朝向 | ComputeNodePosition / ComputeNodeOrientation |
| 重量 / 转动惯量 | ComputeNodeMass / ComputeNodeInertia |
| 绳子(长度/方向/旋转) | ComputeEdgeLength / ComputeEdgeDirection / ComputeEdgeRotation |
| 静止姿态 | ComputeRestPosition / ComputeRestOrientation |
| 预测(按力动一下) | AdvectNodePosition / AdvectNodeOrientation |
| 拉伸 / 弯曲 / 扭转 | Setup/Solve/Project{Stretch,Bend,Twist}SpringMaterial |
| 杆族(相对旋转) | Setup/Solve/Project{Stretch,Bend}RodMaterial |
| 硬 / 软碰撞 | Solve/ProjectHardCollisionConstraint、Setup/Solve/ProjectSoftCollisionConstraint |
| 风 | ComputeAirDragForce |
| 重力与预载 | GetGravityVector / GetGravityPreloading |
规则本身写在 Engine/Plugins/Runtime/HairStrands/Shaders/Private/ 下,统一用 Setup -> Solve -> Project(准备 -> 解 -> 收尾)三段命名:
| 文件 | 提供什么 |
|---|---|
NiagaraHookeSpringMaterial.ush | 弹簧族(方向路线) |
NiagaraCosseratRodMaterial.ush | 杆族(相对旋转路线) |
NiagaraAngularSpringMaterial.ush | 角度弹簧族 |
NiagaraElasticRodMaterial.ush | 弹性杆(Darboux)族 |
NiagaraStaticCollisionConstraint.ush | 碰撞 |
NiagaraStrandsExternalForce.ush | 风 |
NiagaraDataInterfaceHairStrandsTemplate.ush | 把上面这些包装成数据接口入口 |
第 2 章说的"同一条规则管几颗珠子",在这里就是参数 NodeOffset:
NodeOffset = 0 -> 拉伸(相邻两颗)
NodeOffset = 1 -> 弯曲(管三颗)
NodeOffset = 2 -> 扭转(管四颗)
与 4.5 节的"分组着色"一致:拉伸按 x % 2、弯曲按 (x / 2) % 2、扭转按 (x / 3) % 2——三个式子的取值都只有 0/1,因此三者都是两色(红/黑)交替,区别只在取模步长。
“有多软”(柔度)不是资产上的一个字段,而是在准备阶段算出来的:
柔度 = 4.0 / (杨氏模量 * pi * 杆半径 * 静止长度 * 杆半径 * dt^2)
式中"杆半径"在源码里对应的参数名是 RodThickness(它被填进截面积公式中 R 这一项,即 面积 = pi * R^2 里的 R)。
那个 dt^2 就是 2.3 节说的"按步长平方归一化"。
7.3 自碰撞网格长什么样
自碰撞用两个数据接口:速度网格(UNiagaraDataInterfaceVelocityGrid)和压力网格(UNiagaraDataInterfacePressureGrid)。
它们都用同帧 ping-pong:一边读一边写会读到"写坏一半"的数据,所以准备了两块缓冲(CurrentGridBuffer / DestinationGridBuffer),写完之后要 Swap() 一下才能读。分辨率由 GridDimension 控制,通道数是固定的(速度网格 6 个、压力网格 18 个)。分辨率越高,能分开的头发越细,显存也按分辨率的三次方增长。
7.4 参数的三个来源与覆盖边界
参数可以来自三个地方,“改哪里才算数"要先分清楚:
- 资产(
HairGroupsPhysics):基线值。 - 组件覆盖(
UGroomComponent::SimulationSettings):只有当"覆盖总开关"打开时才参与。 - 求解器组件(
FGroomSolverSettings)与 CVar:更外层的控制。
资产里能调的参数(源码细节,可跳过):
| 结构 | 字段 |
|---|---|
FHairStretchConstraint | StretchStiffness / StretchScale / SolveStretch / ProjectStretch / StretchDamping |
FHairBendConstraint | BendStiffness / BendScale / SolveBend / ProjectBend / BendDamping |
FHairCollisionConstraint | StaticFriction / KineticFriction / GridDimension / CollisionRadius / RadiusScale 等 |
FHairStrandsParameters | StrandsSize / StrandsDensity / StrandsThickness / ThicknessScale 等 |
FHairExternalForces | GravityVector / AirDrag / AirVelocity |
“发根硬发梢软"这张沿头发分布的表,对应数据接口里的 ParamsScale:
ParamsScale:32 * 4 个数
四条曲线:拉伸缩放 / 弯曲缩放 / 半径 / 粗细
以 VertexCoord = i / (StrandsSize - 1) 逐点采样
每条曲线最多 32 个采样点
关键上限:每条曲线最多 32 个采样点;而"每根头发切几段”(StrandsSize)的可选值本身只有 2/4/8/16/32,上限正好也是 32。也就是说采样点数与段数一一对应、上限同为 32,精度因此被封死在 32 点——这是 2.7 节说的"怎么调都不够自然"的原因之一。
另外,同名参数可能分属不同路径:资产上的 SubSteps / IterationCount、数据接口里的 SubSteps(默认 5)/ IterationCount(默认 20)、组件上的 NumSubSteps(默认 2)/ NumSolverIterations(默认 5)是三套不同的东西,不能互相推导。
7.5 按距离决定"这帧算多少”
SelectDynamicCurves 按"离镜头多远"决定这帧要算多少根控制线:
距离比例 = clamp((距离 - 最近距离) / (最远距离 - 最近距离))
要算的控制线数 = (1 - 距离比例) * 总控制线数
一个反直觉点:划分的结果和帧率无关,但划分本身的消耗和帧率成正比——每帧都要重算一遍,距离没变结果一样,CPU 时间照花。
还要区分:“算多少”(模拟 LOD)和"画多少"(渲染 LOD)不是一回事。
7.6 生命周期与重置
| 阶段 | 入口 |
|---|---|
| 创建 / 注册 | UGroomSolverComponent::OnRegister、CreateRenderState_Concurrent |
| 每帧推进 | TickComponent -> SendRenderDynamicData_Concurrent |
| 关联 Groom | AddGroomComponent / RemoveGroomComponent |
| 释放 | OnUnregister / DestroyRenderState_Concurrent |
重置会在三种情况发生:初始化、瞬移(位移超过 TeleportDistance)、不可见/隐藏;数据接口还会在"网格资产被换掉"时触发重置。
重置后有一段按帧计数的"启动缓冲"(上限由 r.HairStrands.SimulationMaxDelay 控制,默认 4 帧)。因为按帧计数,这段的真实时长会随帧率变化(4.3 节)。
7.7 一轮求解的完整时序
哪些机制会随帧率变化:
| 机制 | 随帧率变吗 | 说明 |
|---|---|---|
| delta 截断 | 是 | 超长帧被截断,模拟变慢 |
每步时长 dt | 是 | 等于截断后的 delta 除以子步数 |
| 迭代顺序 / 分组 | 否 | 由曲线结构决定 |
| 划分结果 | 否 | 但它的开销随帧率增加 |
| 重置启动缓冲 | 是 | 按帧计数,真实时长随帧率变 |
| 固定步长 | 没有实现 | 插件里没找到固定步长;设置项提示的 “24 fps” 在源码里没有对应实现,以源码为准 |
本章小结:求解器组件只负责"出设置 + 划分曲线",真正的计算在 Niagara 与 GPU 上。前 4 章的每一条道理都能落到具体函数:三类规则在同一族 shader 里靠
NodeOffset区分;柔度由杨氏模量、杆半径、静止长度和步长派生(dt平方在分母);“发根硬发梢软"由最多 32 个采样点的逐点表承载,所以精度上限就是"每根切几段”。自碰撞用读写型数据接口做同帧 ping-pong,写完必须交换缓冲才能读。参数有资产、组件覆盖、求解器组件三层,“总开关没开"和"这个字段本来就不给覆盖"是两种不同的没生效。按距离裁剪的是"算多少”,且开销随帧率增长;重置后的启动缓冲按帧计数,时长随帧率变。
第 8 章 怎么画:这么细的东西怎么显示
一句话:头发比像素还细,直接按普通方式画会互相盖住、闪烁;所以 UE 换了个顺序——先搞清楚哪些像素上有头发,再只给这些像素上色。
白话导读:前面都在讲"算"。这章讲"画"。只要记住"先找哪些像素有头发、再只算这些像素"这一句,剩下都是细节。
8.1 三种画法(可切换)
| 画法 | 怎么画 | 真实度 | 成本 | 开关 |
|---|---|---|---|---|
| 曲线 | 真的一根根画 | 最高 | 最高 | r.HairStrands.Strands(默认 1) |
| 发卡 | 用贴了头发图案的片 | 中 | 中 | r.HairStrands.Cards(默认 1,需启动时开) |
| 网格 | 直接当普通模型画 | 低 | 低 | r.HairStrands.Meshes(默认 1,需启动时开) |
坑:强制某一种画法时,如果资产里没有那类数据,会什么都不显示(不是报错)。r.HairStrands.UseCardsInsteadOfStrands 的提示就明确说了这点。
8.1.1 Groom(发丝)与 Cards(发卡)之间怎么用 LOD 切换
三种画法不只"能手动强制切",更常规的用法是把它们做成一套 LOD 降级链:近处/主角用真发丝(Strands,最真最贵),拉远后自动换成发卡(Cards,中等),更远再换网格(Meshes,最省)。“怎么切"由 Groom 的 LOD 系统驱动:
- 每个 LOD 等级可指定一种画法:LOD 0 用 Strands、LOD 1 用 Cards、LOD 2 用 Meshes——画法本身就是 LOD 的一部分(9.5.2 节 的
Visible Geometry Type就是在选当前 LOD 用哪种几何)。 - 靠"屏幕占比"触发:相机拉远,头发在屏幕上占的面积变小,一旦跌破该等级的
Screen Size阈值,就自动降到下一级(Strands→Cards→Meshes)。LOD Mode(Default / Manual / Auto)+Auto LOD Bias决定这套判定是自动还是手写;Minimum LOD钳制"最低允许降到哪一级”(9.5.2 节)。 - 资产必须有那级的数据:想切到 Cards,资产里必须先
Add Card asset;想切到 Meshes,必须先Add Mesh asset。缺数据的等级一旦被选中,会什么都不显示——这是"远处头发突然没了"最常见的根因(9.5.4 节 / 9.5.5 节)。 - 渲染 LOD 与模拟 LOD 分开:换画法(渲染 LOD)降的是"怎么画";而"算多少根"由独立的模拟 LOD 控制(
SelectDynamicCurves,7.5 节)。Cards/Meshes 通常不参与逐根求解,所以远处头发连物理也一起省了。
一句话:Groom↔Card 的切换 = “LOD 等级里把画法降级 + 屏幕占比触发 + 资产得有那级数据”。要启用,先确认资产里建了 Cards/Meshes 数据,再在 LOD 面板把对应等级的 Visible Geometry Type 设好、调 Screen Size 阈值;别直接强制 r.HairStrands.UseCardsInsteadOfStrands——那是不看距离的硬切换,没数据照样黑屏。
8.2 一条专用的流水线
毛发走的是自己的一套流程,入口在 Engine/Source/Runtime/Renderer/Private/HairStrands:
两个关键设计:
- 先可见性、再着色:先把"哪个像素有头发、有几层"整理成一份很紧凑的清单,然后只给清单里的东西算光照。这样光的计算量正比于"真正看得见的头发",而不是"画了多少段"。
- 按小块聚合(tile):把屏幕切成 8x8 的小块,只挑"里面有头发的块"去处理,没有头发的块直接跳过。这是毛发在大屏幕上还能跑得动的关键。
8.3 光照怎么打
头发和普通物体不一样,它会透光——一大把头发不是一堵黑墙。所以光照分三路:
| 光 | 怎么处理 |
|---|---|
| 环境光 / 间接光 | 用上面那份 3D 体素(密度网格)去"穿一遍",估算被头发挡住了多少 |
| 直射光的阴影 | 用"深阴影":不是简单记"挡住/没挡住",而是沿光线分层记每层透过去多少 |
| 头发内部多次反弹 | 提前算好一张表(LUT),运行时查表,省掉实时计算 |
头发的散射模型(光打到一根头发上往哪散)在源码里以 R / TT / TRT 等分量 + “双重散射"命名,本质是经典头发散射模型加近似,对应参考 8。
8.4 远近与"头发突然冒出来”
- 远处的头发会按"页"流送(一页固定装多少根),加载/卸载的粒度正是"页"。所以不看细节时,一旦发生流送,表现是头发突然出现或突然消失,不是渐显。
- CPU 侧还有两个开关决定"数据用完就扔"和"异步加载"(
ReleaseAfterUse/AsyncLoading),它们是在内存和"第一次进场景卡顿"之间做取舍。
8.5 光追与抗锯齿
- 光追:毛发可以参与光追(默认开,且需要资产主动勾选)。两个调优项是"每段拆成几个小包围盒"(拆得越多光追越快、构建越慢)和"光追时把头发半径缩放多少"。
- 抗锯齿:毛发太细,会闪。可见性那一步会额外产出一张"遮罩"和"速度",交给 TAA/TSR 这套时序抗锯齿去处理,避免边缘闪烁和拖影。
8.6 几个"看起来像 bug"的开关
| 开关 | 默认 | 看起来像什么 bug |
|---|---|---|
r.HairStrands.BoundMode | 0 | 设为 1 时用网格包围盒判断可见性;头发大幅甩动时会被整块裁掉(像"毛发突然没了") |
r.HairStrands.ComposeAfterTranslucency | 1 | 控制毛发和半透明/景深的叠加顺序;设错会出现覆盖顺序错乱 |
r.HairStrands.UseCardsInsteadOfStrands | 0 | 强制发卡;资产没发卡数据时什么都不显示 |
r.HairStrands.WriteGBufferData | 1 | 毛发写不写 GBuffer;影响依赖 GBuffer 的后续效果 |
另有一个"说明和实现不一致"的例子:r.HairStrands.SwapType 的提示写 Default:1,实际默认是 2。遇到这种一律以源码为准。
本章小结:毛发比像素细,所以不走普通画法:先算可见性、按 8x8 小块聚合、只给"有头发的块"着色、最后合成。这个顺序就是它的性能设计——tile 聚合是关键。画法有曲线、发卡、网格三种,缺数据时会什么都不显示。光照分三路(体素间接光、深阴影直射光、LUT 多次散射),因为头发会透光。流送以"页"为粒度,表现是突然出现/消失。光追有两个调优项,细几何的抗锯齿靠可见性产出的遮罩交给 TAA/TSR。
第 9 章 上手:改哪里、会出什么问题、多贵
一句话:同一个效果往往在三个地方都能改,先弄清"改哪里才生效";再记住成本大致由哪些量相乘决定。
白话导读:这章是给真正动手的人看的"地图",遇到问题可以直接查表。
9.0 第一次上手:最小路径与最易踩的坑
如果你是第一次接 Groom,按下面 5 步走完,就能看到一个能动的、不出错的毛发(每步对应正文出处):
- 导入资产并烘焙绑定:把头发导成
UGroomAsset,并为角色网格烘焙一份UGroomBindingAsset。不烘焙,首次显示会现算投影、卡一下(6.3 节)。 - 挂组件:给角色加
UGroomComponent,把资产和绑定填进去(5.1 节)。 - 确认"三条路"之一成立:开物理求解(
IsSimulationEnable),或开 Deformer,或走蒙皮绑定。三条都不满足时毛发会静默停在默认姿态,不报错——这是新手"毛发没反应"的头号原因(5.2 节)。 - 开模拟后先做重力预载:否则头发会被重力直接拉塌,和美术摆的姿态不一致(3.5 节)。
- 看效果再按表调:软 / 抖 / 穿模 / 像塑料,分别对应哪个旋钮,见4.4 节的现象对参数表;“改哪里才算数"见下文的参数地图。
最易踩的 5 个坑(都不会报错,靠查条件定位):
| 现象 | 根因 | 定位 |
|---|---|---|
| 毛发完全不动 / 停在默认姿态 | 三条驱动路都不满足,或插值数据缺失 | 5.2 节 / 6.1 节 |
| 毛发不动或形变错乱 | 绑定资产跨骨架复用 / 与网格不匹配 | 6.3 节 |
| 强制发卡/网格后什么都不显示 | 资产里没有那类数据 | 8.1 节 |
| 改了组件参数却没变化 | 覆盖总开关没开 | 7.4 节 / 9.1 节 |
| 改完设置第一次进游戏卡一下 | 派生数据在重新生成,是正常现象 | 6.4 节 |
9.1 改哪里才算数(参数地图)
| 你想调的 | 资产 | 组件覆盖 | 求解器组件 | 全局开关 |
|---|---|---|---|---|
| 拉伸/弯曲刚度、沿头发曲线 | HairGroupsPhysics | 需"覆盖总开关"打开 | — | — |
| 摩擦、自碰撞网格粗细 | HairGroupsPhysics | 同上 | — | — |
| 子步数 / 迭代数 | 资产里的求解设置 | — | NumSubSteps(2) / NumSolverIterations(5) | r.HairStrands.EnableAdaptiveSubsteps |
| 算多少根(按距离) | — | — | MinLODDistance / MaxLODDistance | r.HairStrands.MaxSimulatedLOD |
| 总开关 | 组的求解开关 | bOverrideSettings + bEnableSimulation | — | r.HairStrands.Simulation(1) |
判据:资产是基线;只有"覆盖总开关"打开,组件上的值才会参与;CVar 在最外层统一钳制。
9.2 常见故障:约束 + 触发条件 + 引擎行为
写成"在什么前提下,出现什么条件,引擎会怎样”,方便直接对照:
| 类别 | 前提 | 触发条件 | 结果 |
|---|---|---|---|
| 设置无效 | 要覆盖生效 | 覆盖总开关没开 | 组件值被忽略,用资产的 |
| 设置无效 | 字段可覆盖 | 这个字段本来就不给覆盖 | 开了总开关也没用 |
| 设置无效 | 要能形变 | 插值数据缺失 | 形变功能整体失效,不报错 |
| 没反应 | 绑定要匹配 | 绑定资产与网格不匹配/跨骨架复用 | 毛发不动或形变错乱 |
| 没反应 | 三条路要满足 | 求解/Deformer 判定都不满足 | 停在默认姿态 |
| 观感不对 | 迭代要够 | 迭代不足 | 整体发软、拉不住 |
| 观感不对 | 网格要够细 | 自碰撞网格太粗 | 头发互相穿插/粘连 |
| 观感不对 | 包围盒要够大 | BoundMode=1 且甩动大 | 被错误裁掉 |
| 随时间变 | 步长来自帧 | 帧率低 | 变软、变滞后 |
| 随时间变 | 按帧计数 | 帧率变化 | 重置缓冲的真实时长改变 |
9.3 成本大概怎么算
求解侧 约等于 头发根数 x 每根切几段 x 迭代次数 x 子步数 x 每秒帧数
渲染侧 约等于 头发段数 x 屏幕占用(涉及的 tile 数)
两侧的瓶颈通常不同:求解吃"头发规模 + 迭代",渲染吃"屏幕覆盖率"。想加效果时,先判断瓶颈在哪一侧,再决定加曲线/段数(更细)还是加迭代/子步(更稳)。
9.4 出问题先看哪
| 入口 | 看什么 |
|---|---|
r.HairStrands.Dump / .GroomAsset / .GroomComponent | 打印资产/组件里到底有什么 |
r.HairStrands.ViewMode | 切换各种调试视图(LOD 着色、控制线、朝向、AO、深阴影 等) |
r.HairStrands.BindingValidation | 检查绑定是否匹配 |
r.HairStrands.DeepShadow.DebugMode | 看深阴影 |
r.HairStrands.PlotBsdf | 看头发散射函数 |
注意"字段在、但提示与行为不一致 / 存疑"的情况(如 SimulationFrameRate 存在但未被使用、SubSteps 的 “24 fps” 提示无固定步长实现、r.HairStrands.SwapType 提示与默认疑似不一致;以下均基于 UE 5.6 源码观察,需在高版本复核):这类清单统一汇总在11.2 节,遇到一律以你所用版本的源码和资产为准,别被提示误导。
9.5 界面上的选项逐一说明(附全部面板截图)
GroomAsset 编辑器各面板都藏着性能选项。决定"这套毛发整体贵不贵"的规模类选项(发量、每根段数、控制线数、碰撞网格、求解密度)在 Strands / Interpolation / LOD / Physics(StrandsParameters) ;决定"每帧怎么算得更准更省"的在 Physics(迭代 / 子步 / 约束开关)。 下文按面板逐一说明每个字段,⚠=影响运行性能(影响不分大小都写入,并给出相对量级:高 / 中 / 低——量级是按 9.3 节 成本模型推导的相对排序,非官方数字)。截图取自 UE 5.6 的 MetaHuman Groom 示例资产。
9.5.1 Strands(发丝渲染)——发量规模类

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Pt/Curve(每根节点数) | 每根头发的曲线节点/细分数量 | 高:决定发丝几何细分,直接进"每根段数"成本项 |
⚠ Hair Shadow Density(发丝阴影密度) | 阴影采样密度 | 中:越高阴影越贵 |
⚠ Use Hair Raytracing Geometry(使用光追几何) | 是否用真实发丝做光追 | 中:光追开关 |
⚠ Voxelize(体素化) | 自遮挡/体积化预计算 | 中:构建期成本(8.5 节) |
⚠ Use Stable Rasterization(稳定光栅化) | 光栅化稳定性 | 中:渲染路径成本 |
⚠ Scatter Scene Lighting(散射场景光照) | 发丝间散射光照 | 中:光照成本 |
⚠ Raytracing Radius Scale(光追半径缩放) | 光追射线半径 | 低-中:光追相关 |
Hair Root Scale / Hair Tip Scale(发根/发梢宽度比例) | 根部/梢部粗细 | ⚠(间接)低:越粗光栅面积越大 |
Max. Width(最大宽度) | 发丝宽度上限 | ⚠(间接)低:同上 |
9.5.2 LOD(细节等级)——算多少 / 画多少

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Minimum LOD / Hair LOD(最小 LOD) | 最低允许的 LOD 等级,设定"再怎么降也保底多少" | 高:决定最低渲染/模拟规模 |
⚠ Curve Decimation / Vertex Decimation(曲线/顶点抽稀) | LOD 时的抽稀比例 | 中-高:降采样后规模 |
⚠ LOD Mode(Global / Auto)+ Auto LOD Bias(自动偏移) | 是否启用自动 LOD 系统 | 中:开关 |
⚠ Visible Geometry Type(可见几何类型:Strands) | 用哪种画法显示 | 中:Strands 最贵,Cards/Meshes 更省(8.1 节) |
⚠ Simulation(RBF Interpolation) | 模拟是否随 LOD 降精度 | 中 |
⚠ Angular Threshold(角度阈值)/ Screen Size(屏幕尺寸) | LOD 切换判定条件 | 低 |
Binding Type(绑定类型:Skinning) | 绑定方式 | 低-中 |
Thickness Scale(厚度缩放) | 观感 | 低 |
9.5.3 Interpolation(控制线 / 插值)——求解骨架规模

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Imported Guides(导入控制线数,Min / Max) | 参与求解的"骨架"曲线数量 | 高:直接决定物理求解规模(6.1 节 / 9.3 节) |
⚠ Curve Decimation / Vertex Decimation(抽稀) | 控制线降采样 | 中-高 |
⚠ Interpolation Quality(High / Parametric / Smooth / Transform) | 从控制线插值出全头发的质量 | 中:High 最贵 |
⚠ Generated guide density(生成控制线密度) | 自动生成控制线的密度 | 中 |
⚠ Rigged guide num curves/points(绑定控制线数/点数) | 绑定驱动用的控制线规模 | 中 |
⚠ Enable Guide-Cache Support(启用控制线缓存) | 控制线结果缓存 | 中:内存/带宽 |
⚠ Hair Interpolation / RBF Interpolation | 插值算法选择 | 低-中 |
Interpolation Distance(插值距离) | 插值影响半径 | 低 |
Randomize Guide / Use Unique Guide | 控制线随机化/独立化 | 低(观感/一致性) |
Guide Type / Hair Interpolation Type / Rigged Skeletal Mesh | 控制线来源与类型 | 低 |
9.5.4 Cards(发卡)

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Add Card asset(添加发卡资产) | 是否提供发卡数据;有则运行时可用发卡画法 | 中:发卡通常比 Strands 曲线便宜(8.1 节) |
9.5.5 Meshes(网格)

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Add Mesh asset(添加网格资产) | 是否提供网格数据;有则可用网格画法 | 中:网格通常是最便宜的画法(8.1 节) |
9.5.6 Dataflow(数据流)

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
Dataflow Asset / Dataflow Terminal(数据流资产 / 终端) | 资产生成管线(GroomAssetTerminal) | 构建期成本,运行时无直接开销 |
9.5.7 Material(材质)

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
Material Slots(材质槽) | 绑定渲染材质 | ⚠(间接)低:着色复杂度由材质决定 |
9.5.8 Binding(绑定)

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
⚠ Bindings(绑定资产,如 GB_Hair_MyGroom) | 烘焙的绑定/投影资产 | 中(启动/首次显示):不烘焙则每次现算投影会卡(6.3 节) |
9.5.9 Physics(物理)· 上:求解器与外力

| 字段(英文 · 译文) | 分组 | 作用 | 性能影响 |
|---|---|---|---|
⚠ Sub Steps(子步数) | SolverSettings | 每帧切成几段分别算,成本 ≈ 段数 × 每段计算 | 高 |
⚠ Iteration Count(迭代数) | SolverSettings | 每段内把规则反复解几遍,成本 ≈ 遍数 × 每遍计算 | 高 |
⚠ Enable Niagara Simulation(启用 Niagara 模拟) | SolverSettings | 是否物理求解 | 高(开关) |
⚠ Enable Deformer Simulation(启用 Deformer 模拟) | SolverSettings | 是否走 Deformer 驱动路 | 中(开关) |
⚠ Niagara Solver(Niagara 求解器:StableSpringsSystem) | SolverSettings | 选求解系统 | 中(Springs 轻 / Rods 重) |
Gravity Preloading(重力预载) | SolverSettings | 一次性预计算平衡姿态 | 低 |
Gravity Vector(重力向量)/ Air Drag(空气阻力)/ Air Velocity(风速) | ExternalForces | 重力与风,外力项 | 低(观感为主) |
Force Visible(强制可见) | SolverSettings | 强制显示 | 无性能意义 |
9.5.10 Physics(物理)· 下:约束与求解参数

| 字段(英文 · 译文) | 分组 | 作用 | 性能影响 |
|---|---|---|---|
⚠ Solve Collision / Project Collision(求解/投影碰撞) | CollisionConstraint | 碰撞约束阶段开关;碰撞是最贵的一类约束 | 中-高 |
⚠ Solve Bend / Project Bend(求解/投影弯曲) | BendConstraint | 弯曲约束阶段开关 | 中 |
⚠ Solve Stretch / Project Stretch(求解/投影拉伸) | StretchConstraint | 拉伸约束阶段开关 | 中 |
⚠ Grid Dimension(碰撞网格分辨率) | CollisionConstraint | 自碰撞网格;显存与计算 ≈ 按分辨率三次方增长 | 高(7.3 节 / 3.2 节) |
⚠ Strands Size(求解每根点数) | StrandsParameters | 物理求解用每根头发节点数 | 高:求解规模核心 |
⚠ Strands Density(求解密度) | StrandsParameters | 物理求解用头发密度/根数 | 高:求解规模核心 |
⚠ Collision Radius(碰撞半径)/ Radius Scale(半径缩放) | CollisionConstraint | 碰撞半径;越大参与碰撞的对越多 | 中 |
⚠ Strands Viscosity(粘度) | CollisionConstraint | 求解阻尼项 | 低-中 |
Bend/Stretch Stiffness + Stiffness Scale(刚度 + 缩放) | Bend/StretchConstraint | 软硬/蓬松;刚度太高会收敛不足、需加迭代 | ⚠(间接)低 |
Bend/Stretch Damping(阻尼) | Bend/StretchConstraint | 防抖 | 低 |
Static Friction(静摩擦)/ Kinetic Friction(动摩擦) | CollisionConstraint | 摩擦行为 | 低 |
Strands Smoothing / Strands Thickness / Thickness Scale | StrandsParameters | 平滑/粗细观感 | 低 |
9.5.11 Preview settings(编辑器预览)——非运行时

| 字段(英文 · 译文) | 作用 | 性能影响 |
|---|---|---|
Lighting / Post Processing / Rendering Features(Path Tracing 等) | 仅编辑器预览窗口的照明与后期 | 只影响预览帧率,不进入运行时资产 |
调参直觉(更新):想大幅降本,先在规模类动手——降 Strands Pt/Curve、Interpolation Imported Guides、Physics Strands Size/Density、Collision Grid Dimension,或切 LOD / Cards / Meshes 画法;想每帧更省,动 Physics 的 Sub Steps / Iteration Count / Solve·Project 开关。观感旋钮(刚度、阻尼、摩擦、重力、风、粗细)几乎不省钱。标注"(推断)“的是按计算模型推导、非官方明示的结论;标注”(间接)“的表示通过影响别的东西(如收敛、光栅面积)间接产生成本。截图均取自示例资产,字段默认值随资产不同。
本章小结:同一效果可在资产、组件覆盖、求解器组件、CVar 多处调,“改哪里生效"靠参数地图:资产定基线,覆盖总开关打开后组件值才生效。故障可统一写成"前提 + 触发条件 + 结果”:设置无效来自总开关/字段范围/插值缺失,没反应来自绑定不匹配或三条路都不满足,观感与时间异常来自迭代不足、网格过粗、包围盒过小或帧率变化。成本大致是"求解侧相乘"与"渲染侧相乘”,加效果前先定位瓶颈在哪一侧。
第 10 章 和别的方案比:Groom 与 gFur
一句话:UE Groom 是"真发丝 + 逐根物理求解"的重路线,gFur 是"发壳 + 骨骼级近似"的轻路线;一个追求"动得对",一个追求"看起来像、还很省"。
白话导读:这章把两条路线并排比。结论先给:它们不是替代关系,而是互补——远景/群体用 gFur,近景/主角用 Groom。下面分设计、性能、工程三块看。
对照对象是市场插件 gFur(GiM,自述 “Shell based fur plugin”,官方支持 UE 5.0~5.4、另有 UE 4.27 版;本地源码在 Source/GFur、Source/GFurEditor、Shaders/Private)10。
10.1 设计对比
| 维度 | UE Groom | gFur |
|---|---|---|
| 方案类型 | 真曲线毛发(strand-based) | 发壳毛发(shell-based) |
| 几何数据 | 控制线 + 可见发丝,逐根独立曲线 | GrowMesh 顶点沿法线复制成 N 层壳(层数 1~128),顶点多带一个 FurOffset 与几组 UV |
| 引导 | 控制线(Guides)承载形变与模拟 | 可选 FurSplines:可导入 ABC、程序生成,或用引导网格 |
| 梳理 | 导入 groom,再在编辑器里梳理/调物理 | 编辑器笔刷直接改 splines(长度/弯曲/簇/扭转/噪声/放松/增删) |
| 驱动路径 | 三条路:物理求解 / Deformer / 蒙皮 | 骨骼运动 -> 顶点偏移(自定义 vertex factory),另支持 Morph Target |
| 动态(“物理”) | 逐根约束求解(XPBD / Cosserat 杆),带朝向,能表达弯曲与扭转 | 骨骼级近似:每帧算骨骼的线/角速度与偏移,沿发丝按分布曲线施加 |
| 自碰撞 | 有(体素速度/压力网格近似) | 无 |
| 扭转 | 显式表达 | 无法表达 |
| LOD | 三种几何表示(曲线/发卡/网格)+ 曲线 LOD + 按页流送 | 每级壳层数 + 切 GrowMesh 的 LOD + 每级可单独关物理 |
| 光照 / 阴影 / 光追 | 专用 pass:体素间接光 + 深阴影 + 多次散射表 + 原生光追 | 走常规材质渲染,靠材质近似;无专用透光、深阴影、光追 |
| 参数面 | 大:资产 / 组件 / 求解器组件 / CVar 四级 | 小:组件 + 材质为主 |
10.2 性能对比
| 维度 | UE Groom | gFur |
|---|---|---|
| 动态成本量级 | 曲线数 x 每根段数 x 迭代数 x 子步数 x 帧率 | 骨骼数 x 顶点数(约等于壳层数 x GrowMesh 顶点数) |
| 渲染成本量级 | 曲线段数 x 屏幕占用(tile 数) | 壳层数 x 顶点数 x 覆盖像素 |
| 主要瓶颈 | 求解吃曲线规模与迭代;渲染吃屏幕覆盖率 | 几何量主导(层数 x 顶点数),层数是最直接的旋钮 |
| 时间性 | 步长来自帧 delta(截断后按子步均分),对帧率敏感 | 每帧一次、由骨骼运动驱动;稳定,但没有"求解收敛"这回事 |
| 缩放手段 | 降曲线/段数、降迭代/子步、按距离裁剪、切发卡 | 降层数、切 LOD、关物理、用最小屏幕尺寸让它消失 |
| 平台 | 跨平台(Windows / macOS / Linux / 主机等) | 开发平台仅 Windows(Win32/Win64);目标平台 Win64(DX11)、PS4、Xbox One(可经 whitelist 扩展 Switch) |
读法:Groom 的成本是"多项相乘"(曲线 x 段数 x 迭代 x 子步 x 帧率),gFur 的成本主要压在"几何量"(层数 x 顶点数)。让 gFur 变便宜,第一件事就是减层数;让 Groom 变便宜,要先判断瓶颈在求解侧还是渲染侧(见第 9 章的成本模型)。
10.3 工程与集成对比
| 维度 | UE Groom | gFur |
|---|---|---|
| 集成步骤 | 导入 groom、烘焙绑定、配置 Niagara 系统 | 加组件 + 指定 GrowMesh + 套材质 |
| 依赖 | Niagara / ComputeFramework / Chaos / Dataflow / Deformer | 仅 Core / Engine / RHI 等基础模块 |
| 与对方的关系 | 可接收 gFur 导出的 groom | 可把 splines 导出给 UE Groom(示例内容自带 UE_Hair 资产) |
10.4 怎么选
| 你的场景 | 建议 |
|---|---|
| 主角近景,要真实甩动、透光、光追 | 用 Groom |
| 大量个体 / 远景 / 毛发团块,要省 | 用 gFur |
| 既要近景真实又要群体便宜 | 混用:远景发壳、近景 Groom |
设计侧重一句话:gFur 侧重"看起来像"——把钱花在分层观感(壳偏置、噪声、材质光照)上,动态用骨骼级近似换,参数故意做得很少、谁都能接;Groom 侧重"动得对"——把钱花在逐根约束、时间基准、自碰撞和光照/阴影/光追的正确性上,代价是集成复杂、参数面庞大。
本章小结:两条路线的分野在"几何表示"与"动态粒度":gFur 用发壳 + 骨骼级近似,换来极低的集成与性能成本,代价是无法表达扭转与自碰撞、不参与透光/深阴影/光追、且仅限 Windows 开发(目标平台覆盖 Win64 与主机);Groom 用真曲线 + 逐根求解,换来物理正确与光照可扩展,代价是成本高、参数面大、集成重。二者互补:gFur 能把 groom 导出给 UE Groom,而 UE 自身也有发卡/网格这类廉价表示(8.1 节),所以"远景发壳、近景曲线"可以并存。
第 11 章 附录
11.1 源码文件索引
| 层次 | 位置(相对引擎根) | 说明 |
|---|---|---|
| 资产与形变 | Engine/Plugins/Runtime/HairStrands/Source/HairStrandsCore/Public/(GroomAsset.h、GroomComponent.h、GroomInstance.h、GroomAssetPhysics.h、GroomAssetInterpolation.h) | 资产、组件、每组实例、物理与插值设置 |
| 绑定 | .../HairStrandsCore/Public/GroomBindingAsset.h、HairStrandsDatas.h、GroomBindingBuilder.h | 发根投影、权重、构建器 |
| 求解 | .../HairStrandsSolver/Public/GroomSolverComponent.h | 求解器组件、动态曲线划分 |
| 求解数据接口 | .../HairStrandsCore/Public/Niagara/(NiagaraDataInterfaceHairStrands.h、...VelocityGrid.h、...PressureGrid.h) | 参数与网格资源交给 Niagara |
| 求解 shader | Engine/Plugins/Runtime/HairStrands/Shaders/Private/ | 约束族、碰撞、外力 |
| 形变(Deformer) | .../HairStrandsDeformer/Public/(DeformerGroom*) | Optimus / DeformerGraph 读写通道 |
| 渲染 | Engine/Source/Runtime/Renderer/Private/HairStrands/、.../Public/HairStrandsInterface.h | 可见性、tile、体素化、深阴影、合成 |
| 渲染 shader | Engine/Shaders/Private/HairStrands/ | 光栅、散射、深透射率 |
11.2 控制台速查
| 分组 | 代表变量 | 默认 | 作用 |
|---|---|---|---|
| 几何表示 | r.HairStrands.Strands / .Cards / .Meshes | 1/1/1 | 三种画法开关(后两个需启动时开) |
| 模拟 | r.HairStrands.Simulation / .EnableAdaptiveSubsteps / .SimulationMaxDelay / .MaxSimulatedLOD | 1/0/4/-1 | 总开关、自适应子步、重置缓冲、模拟 LOD 上限 |
| 阴影与照明 | r.HairStrands.DeepShadow.Resolution / .KernelType / .DensityScale、r.HairStrands.SkyLighting | 2048/2/2/1 | 深阴影分辨率与核、密度补偿、天光 |
| 可见性与光栅 | r.HairStrands.Visibility.ComputeRaster / .MSAA.SamplePerPixel / .PPLL、r.HairStrands.RasterizationScale | 0/8/0/0.5 | 光栅路径与采样、吸附光栅缩放 |
| LOD 与流送 | r.HairStrands.MinLOD / .AutoLOD.Bias / .Streaming.CurvePage / .StreamOutThreshold | 0/0.1/2048/2 | LOD 钳制与偏置、按页流送 |
| 内存 | r.HairStrands.Strands.BulkData.ReleaseAfterUse / .AsyncLoading / .Validation | 0/-1/1 | 用完释放、异步加载、加载校验 |
| 光追 | r.HairStrands.Raytracing / .RaytracingProceduralSplits / .RaytracingRadiusScale | 1/4/0 | 光追开关与两个调优项 |
| 体素化 | r.HairStrands.Voxelization / .DensityScale / .Virtual.VoxelPageResolution | 1/2.0/32 | 体素化总开关、密度、页分辨率 |
| 包围盒 | r.HairStrands.BoundMode | 0 | 运行时包围盒模式(0/1/2) |
| 构建期 | r.HairStrands.ClusterBuilder.MaxVoxelResolution / .HairGroupBuilder.MaxVoxelResolution | 256/64 | 聚类与组索引构建分辨率 |
| 调试 | r.HairStrands.ViewMode / .Dump / .BindingValidation | —/—/0 | 视图模式、数据打印、绑定校验 |
基于 UE 5.6 源码观察、待高版本复核的"字段/提示与行为不一致"情况(以下结论只对当前所见 5.6 成立,不保证高版本仍然如此,作者会在更高版本复核):FGroomSolverSettings::SimulationFrameRate——字段存在,但全项目搜索仅命中声明与 UHT 反射生成、无业务引用,蓝图亦搜不到,判为"未被使用";FHairSolverSettings::SubSteps——提示写 “solver calls done at 24 fps”,但源码里没有固定 24fps 步长的实现(求解仍按帧 delta),字段本身有效;r.HairStrands.SwapType——提示写 Default:1,实际默认值疑似为 2,待源码复核。
11.3 参考
- [1]《计算机图形学基础》(Fundamentals of Computer Graphics, 5th ed.):曲线表示, 1.2 节。
- [2]《计算机图形学基础》:计算机动画, 4.1 节。
- [3] Position Based Dynamics(2007): 2.1 节 / 4.1 节。
- [4] XPBD: Position-Based Simulation of Compliant Constrained Dynamics(2016): 2.2 节 / 2.3 节 / 4.3 节。
- [5] There Is More Than One Way to Frame a Curve(1975): 1.4 节。
- [6] Strands: Interactive Simulation of Thin Solids using Cosserat Models(2002): 2.5 节 / 3.1 节。
- [7] Discrete Elastic Rods(2008): 1.2 节 / 2.4 节。
- [8] Light Scattering from Human Hair Fibers(2003): 8.3 节。
- [9] UE 5.6 引擎源码(HairStrands 插件、渲染器毛发 pass 集、着色器目录):以文件名 / 类名 / 函数名定位。
- [10]
gFur(GiM,Shell based fur plugin,官方支持 UE 5.0~5.4、另有 UE 4.27 版)。