弹簧骨骼
头发、尾巴、围巾和绳子会在身体动过之后才动:它们滞后、摆动、再安定下来。弹簧骨骼不用动画师手工做这些,就能给骨架加上它。把 SpringBone 放在一条链的根上,根以下的每个实体就成了弹簧:动画仍然说每根骨头本来该在哪里,链则带着惯性拖在那个姿势后面,受重力下坠,并被拉回到那个姿势。根本身不参与模拟,它由动画或身体驱动。
它是核心功能,不是可选模块,在引擎里用 C++ 运行,所以每个目标上开销一样,包括没有 JIT 的目标。
把 SpringBone 放在一束头发最上面的骨头上(它的父骨头是身体,它的子孙是发束)。它下面的一切,沿每个分支,都会被模拟。
| 字段 | 作用 |
|---|---|
enabled |
关闭后,链完全保持动画摆出的姿势。 |
stiffness |
每根骨头被拉回动画姿势的力度,按 60 Hz 每帧计,0 到 1。0 是自由悬垂。默认 0.1。 |
drag |
每帧骨头损失的速度比例,0 到 1。越高越快安定;默认值(0.5)阻尼足够,不会来回晃。 |
gravity |
世界单位每二次方秒(世界 y 向上)。默认是 {0, -980, 0}:一米一百个单位。 |
wind |
作用在链上的稳定风,世界单位每二次方秒。0 是无风。 |
windGust |
阵风让风力强弱摆动的幅度,0 到 1。0 是稳定的风;1 让风力在无风到两倍强度之间变化。每根骨头比上一根晚一点遇到阵风,所以发丝会起伏。 |
windFrequency |
每秒几阵风(默认 0.5)。 |
endLength |
末端骨头没有子骨头可以瞄准;这个值给它一段这么长的尖端。0 表示末端骨头不模拟。 |
radius |
发丝的粗细,与碰撞体保持这个距离。 |
weight |
用多少模拟,0 到 1(默认 1)。0 就是只有动画。 |
colliders |
带 SpringBoneCollider 的实体,链被挡在它们之外。 |
SpringBoneCollider 是一个球(默认,radius 为 10),或者沿它自己实体 Y 轴的胶囊(height 为 20,即两端球心的距离),位置由实体的变换(和它的 offset)决定。把它们放在头和肩膀的骨头上,头发就不会穿过身体。
把模型的 Spring Bones 导入设置改成 From File Data and Names,导入器就按文件里已有的信息建链,不用手动设置几十根发丝。它按下面的顺序查找:
VRMC_springBone(VRM 1.0 扩展):弹簧、碰撞体、碰撞体组和它们的数值。一条弹簧变成它第一个关节上的SpringBone(这根骨头固定,它下面的骨头会动),VRM 里按秒的刚度和重力强度会换算成引擎按帧的刚度和加速度。关节少于两个、或会带动它没有列出的骨头的弹簧,会在导入警告里报告。- 节点自己的属性:链根上的
extras.estella.springBone(stiffness、drag、gravity、radius、endLength)和碰撞体节点上的extras.estella.springCollider(shape、radius、height、offset)。Blender 的自定义属性就是这样带着精确尺寸进到引擎里的。在同一个节点上,它优先于 VRM。 - 名字,补全前两者没说的部分:
| 名字 | 变成 |
|---|---|
Hair_<分区>_<链号>_00(下面有骨头) |
这根骨头上的 SpringBone;链的其余部分就是它下面挂着的骨头 |
SBC_Sphere_<名>_r<厘米> |
球形 SpringBoneCollider,半径以厘米计 |
SBC_Capsule_<名>_r<厘米>_h<厘米> |
胶囊,半径与两端球心的距离,沿节点本地 +Y |
分区(Ponytail、Temple_L、Fringe、Crest、Crown)决定起始的刚度和阻尼:刘海贴得紧,马尾摆得开。每条链都被挡在模型里所有碰撞体之外。尺寸是模型的米,随导入缩放,重力是这些米里的地球重力。没有长度的胶囊,或下面没有骨头的链根,会被跳过。名字只是约定,导入后在 Details 里看它做了什么,再在那里调整。
Details 把链的字段分成 Motion(stiffness、drag、gravity)、Chain(end length、radius、weight)和 Collision(colliders)三组,第一行说明这条链在它的实体下有几根骨头、几个末端尖端;没有子骨头时用警告色说明它什么也不会模拟。
选中链的根,视口就会画出它,和它要避开的碰撞体,开关在 Gizmo 菜单的 Spring bones 一项:动画摆出的骨头是琥珀色的线,每个关节有一个与发丝 radius 同宽的圈(为 0 时是一个点),endLength 给出尖端的地方有一段尖端,每个碰撞体是碰撞体绿色的线框球或胶囊。选中 SpringBoneCollider 就画它自己的形状,并带六个方形手柄来调整大小:球上六个都改半径,胶囊中间一圈的四个改半径,两端的两个改高度(两端球心之间的距离)。在形状背面的手柄会变淡。一次拖拽是一步撤销,数值显示在指针旁边,开着预览特效时发丝会实时跟着变。没选中的什么也不画,所以角色的头发不会把视口填满。
视图 → 预览特效(新编辑器里默认开,和粒子、拖尾用同一个开关)会让链在编辑视口里运行:拖动头,或者改一个字段,头发就会摆动。开着时,模拟出的链画成实线,盖在虚线画的动画姿势上,所以你在调的那个差别就是你看到的差别。关掉它会结束模拟,并把骨头还给场景里原来的样子。这个模拟是编辑器自己的:它从不写进场景文件。
动画先运行,写入骨骼的局部旋转。然后对每条链:
- 沿着链从根往下,用动画自己的旋转算出动画给的姿势。(上一帧被本系统转过、这一帧没有任何东西动画它的骨头,会按动画原本的样子读取,而不是按被留下的样子,所以链不会朝自己上一次的结果弹。)
- 链朝这个姿势推进:每根骨头保留速度并减去阻力,受重力下坠,被拉向它的姿势,被放回离父骨头的长度处(父在前,所以骨头不会被拉长),并被推出碰撞体。时间被切成不超过一个 60 Hz 帧的步长。
- 每根骨头被转向它的子骨头跑到的位置。只写旋转;骨头的位置从不改变,所以它不会离开父骨头,蒙皮也不需要别的。
根一次跳过链长的好几倍,或者停顿半秒,会把链放回姿势,而不是甩出去。关闭组件或移除它,骨头会按动画原本的样子还回去。SpringBoneRes(app.getResource(SpringBoneRes))有 reset(root),用于你想自己标记的瞬移。
求解器每根骨头只有几次算术运算。在编辑器的运行时里、wasm 引擎中实测:**三百条六根骨头的链(2100 个节点)每次更新 0.28 毫秒。**测试是编辑器检查里的 springbone;test_springbone 是求解器和系统的 C++ 单元测试。
- 有多个子骨头的骨头瞄准它的第一个子骨头;其他的因为是子骨头而跟着动,并各自瞄准自己的。
- 缩放取骨头合成后的缩放;链上的非均匀缩放没有建模。
- 碰撞是骨头对球或胶囊:没有发丝之间的碰撞,没有自碰撞,也没有和世界物理形状的碰撞。
- 风是每条链自己的字段,不是场景里的力:还没有一个场景级的风源可以一次设置给所有链。碰撞体的偏移在 Details 里输入,或者移动它的实体来设置;手柄只调大小,不改位置。链是按名字建的,导入器不看网格的形状,所以没有
Hair_骨头的发束不会有动作。