Skip to content

活着

背景:用户 VMD 加载后呼吸消失;程序化动作脚穿透地面;程序化动作只有 idle 和 autodance,缺少中间状态。 过程:感知层六子模块拆分(呼吸/眨眼/视线/表情/重心/lipsync)→ 独立于 VMD 生命周期;脚部地面跟随(射线检测 + 差值传播);Catwalk 程序化动作(节拍驱动步频 + 重心摇摆 + crossfade)。


一、不在场的生命

王逸加载一段用户 VMD。角色开始跳舞。

然后他注意到了什么。

呼吸停了。眨眼停了。角色变成了一具精致的木偶——动作有,但生命没了。

updateProcMotion 里有一句 if (hasUserVmd) stopProcMotion()。程序化动作停止,呼吸眨眼随之消失。

呼吸不是动画效果。呼吸是生命体征。


二、六根独立的线

王逸在 src/scene/motion/ 下新建了六个文件。

perception-breathing.ts——呼吸。躯干骨骼的正弦微动,频率 0.25Hz,每 4 秒一次,幅度 2 毫米。人对呼吸的感知是潜意识的——你不会注意到角色在呼吸,但你会注意到角色不在呼吸。

perception-blinking.ts——眨眼。每 3 到 5 秒(随机间隔)眨一次眼,持续 100ms。morph 权重从 0 到 1 再到 0。固定间隔会假,随机间隔才像人。

perception-gaze.ts——视线追踪。最复杂的一个。从相机位置计算视线目标,转换到模型本地空间,用 Slerp 插值平滑头部和眼部旋转。头部最多转 30 度,眼部最多转 15 度。头部转动更慢(smoothing 0.1),眼部转动更快(smoothing 0.3)——因为人不会快速甩头,但会快速扫视。

perception-expression.ts——微表情。情绪 morph 的叠加层。

perception-balance.ts——重心摇摆。站立时重心微微摇晃,X 轴 0.3Hz,Z 轴 0.25Hz,轨迹是椭圆形的。人站立时不是完全静止的。

perception-lipsync.ts——口型同步。音频振幅驱动 morph 权重。

六个子模块,六个独立的、可开关的、不依赖 VMD 生命周期的存在。

每帧,在 Babylon.js 的 onBeforeRenderObservable 回调里,按开关依次执行各感知模块。这个 observer 不随 VMD 生命周期变化。用户加载 VMD,observer 还在。卸载 VMD,observer 还在。角色永远活着。


三、穿过去的脚

程序化动作没有地面碰撞检测。

王逸看着屏幕上的初音未来。她的右脚在某个动作帧里穿过了地面网格。脚掌在地板下面,小腿在地板上面。

MMD 动作不会这样——MMD 的 IK 系统会自动修正。但程序化动作只有关键帧——骨骼位置、旋转、时间戳。没有碰撞检测,没有 IK 修正。

所以脚的位置只取决于"生成的参数",不取决于"地面在哪"。

从脚骨位置向下发射射线,检测与 ground 网格的交点。如果脚低于地面,计算差值,往上传播到腿部骨骼链——小腿→大腿→盆骨。

直接修正骨骼位置会导致抖动。加一个 Slerp 插值,smoothing 0.3。

跳跃时禁用地面跟随——检测脚骨的 Y 速度,大于阈值说明在向上跳。


四、中间状态

王逸看着初音未来。她要么站着,要么跳舞。没有中间状态。

idle 是纯正弦波——呼吸、眨眼、重心微动。autodance 是节拍驱动的全身律动——动作幅度大,节奏快。但角色在"放松"和"全力跳舞"之间,是空的。

Catwalk:节拍驱动步频,重心左右摇摆,手臂与腿部反向。

120 BPM 的音乐,步频 2Hz——每秒两步。重心在 X 轴上正弦波,amplitude 0.02m——2 厘米。人走路时重心左右移动大约 2-3 厘米。

从 catwalk 切换到 autodance 时,加一个 crossfade 机制——transition 从 0 到 1,1 秒过渡。


尾声

王逸加载初音未来,加载一段用户 VMD。

角色开始跳舞。同时,胸腔在起伏,眼皮在眨动,视线在跟随相机,重心在微微摇晃。脚不再穿模。步频与音乐同步。

她活着。她永远活着。

不是走路,不是跳舞。是两者之间的韵律。


教训:生命体征不是动画效果。呼吸、眨眼、重心微动——这些是角色"活着"的证明。它们不应该寄生在动作系统里。地面是角色活着的第二个证明。角色活着的第三个证明是:在放松和全力之间,还有一种状态——猫步。