Skip to content

感知层的重生

背景:呼吸/眨眼/视线追踪曾寄生在程序化 VMD 生命周期内,用户 VMD 加载后角色"不再活着"。ADR-071 决策重构为 Always-on 感知层。 过程:拆分为 6 个子模块(breathing/blinking/gaze/expression/balance/lipsync)→ 独立于 VMD 生命周期 → 每帧实时叠加。


序、不在场的生命

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

然后他注意到了什么。

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

updateProcMotion 发现用户 VMD 就 stopProcMotion()。程序化动作停止,呼吸眨眼随之消失。因为它们寄生在程序化 VMD 的生命周期里。

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


一、六根独立的线

王逸在 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 生命周期的存在。


二、每帧

perception.ts 是主入口。barrel 导出、状态管理、observer 调度。

每帧,在 Babylon.js 的 onBeforeRenderObservable 回调里,按开关依次执行各感知模块。

typescript
scene.onBeforeRenderObservable.add(() => {
    if (perceptionState.breathingEnabled) {
        _applyBreathing(breathingBone, time, config);
    }
    if (perceptionState.blinkingEnabled) {
        _applyBlinking(morphWeights, time, config);
    }
    if (perceptionState.gazeEnabled) {
        _applyGaze(gazeBones, camera, time, config);
    }
    // ...
});

这个 observer 不随 VMD 生命周期变化。用户加载 VMD,observer 还在。卸载 VMD,observer 还在。角色永远活着。


尾声

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

角色开始跳舞。同时,胸腔在起伏,眼皮在眨动,视线在跟随相机,重心在微微摇晃。

她活着。她永远活着。

感知层不是动画效果。它是角色的生命体征。