弹簧骨:头发为什么会自己摇
全课骨架:动画 = 每一帧先把骨架的姿态算出来,再把顶点按权重「挂」到骨头上。 「读姿态」那一半是第 1 课的蒙皮;「写姿态」有两种做法 —— 第 2 课是反解(IK:给定目标,解出角度), 这一课是另一种,也是更省事的那一种:不求解,直接让骨头自己变成一根弹簧。 头发、尾巴、挂坠、耳朵、披风全都归它管,而它的全部代码只有四行。 保真度:真机可证(画面与读数都由本页自己的 SpringBone 解算器给出;对照物是"位移比 = 1−drag"与"下垂角 tanθ = g·h²/(stiff·L)"两条闭式,读者能拿笔复核)。 算法按 VRM SpringBone 规范的四步(惯性 → 外力 → 刚度 → 长度约束)逐行落地。
本页的原理与数值表照常可读(画面只是把同一个数画出来)。
(脚本启动中…)
定点观察:先点「复位(静止)」,把「节数」设为 1,然后把「刚度 stiffness」从 0.3 拉小到 0.01 → 看那根骨头垂下去多少,以及表② 的「实测下垂角」与「闭式」两列 → 下垂角恰好满足 tanθ = g·h² / (stiff·L):把刚度调到 1/4,下垂角就增大到约 4 倍。 这不是"调参手感",是一条能拿笔算出来的平衡式。
本帧的读数
(脚本启动中…)
3.1 段 · 让它跟不上:Verlet 的"速度"藏在两个位置里
本段只增加一样东西:惯性(用上一帧的位置当速度)(画面里是一根骨头;其余控件与本课各段一致)。 保真度:真机可证。上一课的 IK 是"立刻到位",可头发不能立刻到位 —— 它要滞后。 弹簧骨最省事的地方就在这里:它不存速度,速度就是"这一帧的位置减上一帧的位置"。 于是一行就能写出"带着惯性往前漂、每帧衰减一点"。
| drag | 1 − drag(闭式) | 实测位移比(第 2/1 帧) | 实测位移比(第 6/5 帧) | 差 |
|---|
算式:把"上一帧的速度"记作 v = cur − prev。惯性那一步做的是
next = cur + v·(1 − drag),于是下一帧的速度 = v·(1 − drag) ——
位移逐帧按固定比例缩,是一条几何级数,公比就是 1 − drag。
拿 drag = 0.2 验算:1 − 0.2 = 0.800;表① 的「实测位移比」两列都应当是这个数(差 0)。
这一条把"阻尼"从一个手感参数变成了一个可算的量:想让头发在一秒内基本停住,
就该让 (1 − drag)⁶⁰ 足够小 —— drag = 0.2 时 (0.8)⁶⁰ ≈ 1.5e−6,也就是一秒后速度只剩百万分之一。
3.2 段 · 把它拉回来:重力与刚度的平衡
本段只增加一样东西:刚度(把末端往"静止位"回拉)(其余控件一致)。 保真度:真机可证。3.1 里骨头会一直漂下去 —— 惯性会累积,它不会自己"垂下来"。 刚度那一步就是每帧把末端朝静止位(骨长方向上、跟着锚点走的那个点)拉回一点。 于是"重力往下拽"与"刚度往回拉"形成一个可以解出来的平衡,这就是头发的下垂角。
| stiffness | g·h²/(stiff·L) | 闭式 θ (°) | 实测 θ (°) | 差 (°) |
|---|
算式(本课的中心结论):稳态时末端停在"重力与刚度切向分量相等"的位置。
把骨向记作 u、它的垂线记作 t,则 刚度的切向分量 = stiff·L·sinθ(偏离静止位的角度 θ),
而重力的切向分量 = g·h²·cosθ。令两者相等得 tanθ = g·h²/(stiff·L) ——
但这条是近似式:实测发现它系统性地偏大,比值恰为 1−stiff(stiff = 0.2 时实测/近似 = 0.800)。
把"刚度那一步是施加在已经含重力的那个位置上"这件事算进去,精确解是 tanθ = g·h²·(1−stiff)/(stiff·L)。
拿 L = 0.30、stiff = 0.05 验算:g·h² = 9.81/60² = 0.002725,
tanθ = 0.002725×0.95/(0.05×0.30) = 0.17258 ⇒ θ = 9.7918° —— 表② 那一行逐位对上(差 1e−15)。
★ 这条修正就是"先量后写"换来的:近似式读起来完全合理(切向平衡),但它漏了"顺序"这一层;
表格一跑,四档的比值都恰好是 1−stiff,修正项当场就现形了。
注意这里为什么会有下垂角:静止位是水平朝右(建模时的姿态),重力竖直向下,两者不共线。
如果静止位本身就朝下(比如一根垂直的长发),重力与骨向共线、切向分量为 0 —— 那根头发会笔直垂着不动,
这正好解释了"为什么贴着头皮的长发几乎不摇,而翘起来的呆毛摇得很欢"。
3.3 段 · 骨长不能变:长度约束
本段只增加一样东西:长度约束(把"回拉"之后的位置强行拉到骨长上;可开关,其余控件一致)。 保真度:真机可证。3.2 的回拉是"每帧朝静止位挪一点",它会顺手把骨头拉长或压短 —— 而骨头在动画里是刚体,长度必须是个常数。所以四步里的最后一步是: 从锚点出发,把方向保留、长度改成 L。这一步同时把"重力与惯性的位移"转化成"角度"。
| 配置 | 第 1 节 (m) | 第 2 节 (m) | 第 3 节 (m) | 第 4 节 (m) | 最大 |长度 − L| (m) |
|---|
算式:长度约束那一步是 next = anchor + (next − anchor)/|next − anchor| · L ——
它是"只改长度、不改方向"的投影,所以执行之后长度必然恰好是 L(实测差 0,是浮点意义的 0)。
关掉它则每帧都留下一点误差,而且这个误差会逐节累积(表③ 的最后一列就是它累积到什么程度)。
这一步是弹簧骨和"软体"的分界线:允许长度变,得到的是绳子(第 5 课的 PBD);
不许长度变,得到的是骨头 —— 同样四行代码,去掉这一步就换了东西。
3.4 段 · 串成一条:每节都挂着它下面那一串
本段只增加一样东西:节数(1 → 6;其余控件一致)。 保真度:真机可证。前三段都只有一节。真实的头发、尾巴、披风是一串: 每一节的"锚点"是上一节的末端 —— 于是惯性与重力会顺着链往下传,越靠末端越晚、越弯。
| 节数 n | 第 1 节 θ (°) | 末节 θ (°) | 末节 / (n × 局部角) | 末端竖直下落 (m) |
|---|
算式(级联从哪来):单节的平衡是 tanθ = g·h²·(1−stiff)/(stiff·L) = 9.7918°(局部角,相对父节的朝向)。
串起来之后每一节的静止方向是上一节现在的朝向 —— 于是第 i 节的绝对角是前面所有局部角之和,
塌得越靠末端越明显。表④ 的「末节 θ」就是"垂到底"的程度:n = 1 / 2 / 3 / 4 / 6 时实测
9.79 / 19.17 / 27.94 / 35.97 / 49.62°。
但它不是简单相加:最后一列「末节 / (n × 局部角)」从 1.0000 一路降到 0.8446 ——
因为重力对某一段的切向分量是 g·h²·cosθ,这一节越接近竖直,切向驱动越小、局部增量也越小
(实测各段增量 9.79 → 9.38 → 8.77 → 8.03 → …,比值与该段中点的 cosθ 同量级)。
所以"头发越长发尾越贴着背"这件事,是余弦项自然饱和的结果,不需要额外加什么。
本页如实说明一处限制:四行代码只做"一遍自根向梢",链一长、速度一大时末端会甩过头
(表现出来的是抖动而不是滞后)。真引擎靠子步 + 每帧多迭代几遍压住它 —— 本页在附录 A3 里点名,不在这里展开。
课末:三种"写姿态"的对比
把这一课和上一课并起来看:「写姿态」的两种做法在这里彻底分道扬镳 ——
IK 是"解"(给定末端目标,解出角度;有唯一解族、有可达性、有迭代收敛);
弹簧骨是"演"(给四行规则,让姿态自己长出来;没有目标、没有解、只有稳定态)。
它们的适用面正好互补:脚要踩住地面、手要扶住栏杆 —— 那些位置是硬要求,用 IK;
头发、尾巴、挂坠 —— 那些只要"看起来对",用弹簧骨。真实角色一般两者都用:
腿用 IK 踩地面(动画师给目标),头发用弹簧骨(程序自己算)。
如果只记一句话:弹簧骨没有"目标"这个概念 —— 它只有"上一帧"与"四行规则",而运动是它们的副产品。
本课题到此收口(蒙皮 → IK → 弹簧骨);附录里留了三样下一层的:子步与迭代、碰撞与限位、以及"布料 vs 骨头"的分界。