For the complete documentation index, see llms.txt. This page is also available as Markdown.

MetaHuman 限制驱动程序 — `mha` 必需

Unreal 的 MetaHuman 骨骼对流式数据所依据的内置通道耦合施加限制。在原始 glTF 角色上,这些耦合不存在——最明显的是, jawOpen 还必须驱动四个 mouthLipsTogether* 个通道,使嘴唇在下颌运动时保持接触。 如果没有这个,每次张嘴都会使嘴唇完全分开并露出牙齿 ——嘴巴只会乱动而不是说话,再怎么调也无济于事。

在写入 morph 之前,将它们应用到每一帧:

import { applyMhaLimits } from '@convai/web-sdk/lipsync-helpers';

applyMhaLimits(frame); // 原地:每个耦合的 target = max(target, source)

MHA_LIMIT_DRIVERS 如果你需要原始表,它会导出解析后的耦合表(例如 jawOpen → jawOpenExtreme, jawOpen → mouthLipsTogetherUL/UR/DL/DR, eyeBlink → eyeLidPress,以及同类项)。


让口型同步感觉自然且逼真

原始流播放听起来很机械。以下内容按系统分类,是区分演示和可信角色的关键。本指南是反复打磨一个 MetaHuman 风格(mha)角色,对照录制的近景 QA 反复迭代,直到开元音、爆破音、牙齿、眉毛、头部和眼睛都像真人视频一样自然——这里的每一个数值都是在这个过程中保留下来的,以及它所修复的失效问题。流形塑模式(§1)以一键处理器形式提供;程序化系统(§2–3)是实现指南,参考代码在 examples/react-three-fiber/src/hooks.

1. 流形塑——嘴部(mha)

一次调用即可按正确顺序封装逐帧管线:

import { createLipsyncProcessor } from '@convai/web-sdk/lipsync-helpers';

const lipsync = createLipsyncProcessor('mha', {
  skipEyeChannels: true,           // 你会在 §2 中运行眨眼/注视
  gainOverrides: { jawOpen: 0.72 } // 按角色微调,见下表
});
const bindings = lipsync.bindAll(gltf.scene); // 修复名称,映射 morph 槽位

// 每次渲染 tick:
lipsync.tick(frameOrNull, delta, bindings);

内部流程: 对称化 → 限制耦合 → 增益 → 包络 → morphs。

1.1 骨骼限制耦合(必需)

见上文——如果没有 jawOpen → mouthLipsTogether* 每次张嘴都会露出牙齿。处理器默认会应用它们。

1.2 各通道增益——制作表

该流是针对参考脸调好的;在真实角色上,原始值会把下颌张得过开、把嘴巴拖向一侧,并让嘴唇从牙齿上后缩。这张表是在“含糊不清/木偶嘴”(开口太小)和“整口露牙”(开口太大)之间找到的平衡点,并在镜头前沿着两个方向反复调出来:

通道
增益
原因

jawOpen

0.72

对话式开口。0.6 在开元音上会显得含糊;原始值(1.0)会露出牙齿。

jawOpenExtreme

0.42

同上,适用于限制驱动器提供的极限形状。

jawLeft/Right, mouthLeft/Right

0

一旦你做了对称化(§1.3),侧向位移只会被看成歪嘴。

mouthUpperLipRaiseL/R

0.6

抬起上唇会露出上牙;只保留一丝可见即可。

mouthLowerLipDepressL/R

0.45

现实中,下唇几乎从不低于下切牙。

mouthLipsTogetherUL/UR/DL/DR

0.8

在正常说话中,嘴唇会贴着牙齿;开元音仍会露出一点上牙。

mouthFunnel*, mouthLipsPurse*

1.15

流对 /u/ /w/ 的圆唇驱动不足。

mouthCornerPull*, mouthStretch*

1.0

不要增强:在 /i/ /e/(EE)上放大的嘴角外展会显得像夸张的鬼脸。

1.3 嘴部对称

流对 L/R 成对通道的驱动并不均匀(测得最高可达 1.5×),这会被读成歪嘴——每一帧都要对所有带侧向的嘴部/下颌对进行平均。

1.4 迸发与收敛包络

第一帧不能把嘴突然弹开,最后一帧也不能卡在半个 viseme 上:在说话开始时,增益包络会在约 0.25 秒内渐入(起音 τ ≈ 0.09 秒),并在最后一帧之后约 0.5 秒内渐出(释放 τ ≈ 0.18 秒)。

1.5 下颌硬性上限

不受增益影响,钳制 应用后的 jawOpen0.55。增益会缩放一切;上限只会抑制那些在镜头前显得夸张的罕见峰值偏移,同时不影响正常发音。

1.6 双唇闭合下限——为什么 /p/ /b/ /m/ 永远不会自行闭合

这是结构性问题,任何增益都无法修复: mouthLipsTogether* 这些值来自 jawOpen 通过限制耦合——而该耦合在 在爆破音闭口时恰好接近零。所以在“pumpkin”这个词里,无论你把 lips-together 增益拉多高,嘴唇都会明显分开。修复方法:在说话时,当下颌闭合时直接驱动接触:

这个 0.5 的上限很重要:在 0.9 时,爆破音会被读成嘴唇碰撞(硬钳制);在 0.65 时仍然压得过紧;0.5 才会被读成嘴唇 接触。闭合时测得的封口值约 0.5–0.6 才是目标。

1.7 瞬态保留

真实语音在硬辅音上有锐利的起音;过强的时间平滑会把它们糊成一团。使用轻度平滑系数(lerp ≈ 0.92 在 60 fps 下每帧一次,即几乎不平滑)——包络(§1.4)已经负责保护起止,因此逐帧平滑不必太强。

1.8 音频同步——让嘴部视觉上领先

人类读唇会略微 提前 于声音。如果录制内容显示嘴部落后于声音,则将帧播放提前约100 毫秒 于音频位置之前(60 fps 下为 6 帧)。使用 dt 累加器(每帧 1/60 秒)来消费队列——切勿将 getFrameAtTime()consumeFrames()混用,否则会双重推进并让嘴部跟不上。

1.9 牙齿与口腔着色——“牙齿过多”问题的另一半

只有当口腔内部像脸部一样被照亮时,开口大小和牙齿暴露才会互相冲突。真实的嘴巴处在阴影中。在这个栈里,最终让真实开口成为可能的修复是 材质,而不是动画:

  • 把牙釉质色调调暗(约 0.4 的暖灰,从 0.55 起点开始);

  • 降低牙齿材质的环境/IBL 响应(envMapIntensity ≈ 0.1)以及高光(≈ 0.28);

  • 把上下牙列往嘴唇后方缩进几毫米,如果可以,把牙龈侧和嘴唇侧向阴影烘焙进牙齿着色中。

结果:开元音会呈现 带阴影的一瞥 上排牙齿——像真实影像一样——而不是一条亮白色横带;同时下牙看起来是隐藏的,又不会影响发音。

2. 程序化面部生命——眼睛

流中的眼部通道应由 程序化系统接管 (设置 skipEyeChannels: true);不同计时器上的两个写入者会相互竞争,导致闪烁。

2.1 眨眼

人类每 2–6 秒眨一次眼。最简方案(约 10 行):维护一个倒计时;触发时驱动 eyeBlinkL/R 通过 0.2 秒的正弦(闭合 → 张开),然后重新设定为 2 + random()*4 秒。再加上静息时眼睑轻微下垂(约 0.2)以及偶尔的半眨眼。上镜时应偏向更频繁一些(中位间隔约 2.8 秒)——眨眼太少在视频里会显得在盯视。参考: useBlink.ts.

2.2 眼球跟踪(注视)

与镜头的眼神接触是最强的单一真实感线索:

  1. 每一帧,在头部局部空间中计算从角色眼睛位置指向相机的方向。

  2. 将其转换为归一化的水平/垂直角度,并进行钳制(水平 ±30°,垂直 ±22°)——在头部必须转动之前,眼球本身有范围限制。

  3. 驱动 眼骨 ,如果骨骼绑定有它们;否则驱动 eyeLook* morphs(MetaHuman 风格的骨骼通过 morph 来旋转眼球——按约 0.65 缩放,因为这些形状编码的旋转量比 1.0 应该施加的更多)。

  4. 使用较快的 lerp 过渡(眼球很快,约 0.2 因子),并让眼睑轻微跟随注视方向(eyeLook morph 也兼作眼睑跟随器)。

  5. 说话时,锁定注视 ——说话者会看着听者的眼睛。在活跃说话期间,将扫视幅度渐变到零……

  6. ……但不要变成死盯:在 持续能量低谷 (短语之间的自然停顿——语音能量在几百毫秒内低于约 0.06),让微扫视以半幅度恢复。词句流动时完全锁定,在间隙中保留细微生命感。参考: useEyeTracking.ts.

2.3 微扫视

真实的眼睛从不会静止:每隔 0.6–2.6 秒会迅速跳到一个小的随机偏移(大多不超过范围的 30%,偶尔会重新定焦到正中心),并以利落的过渡(约 30/秒)叠加在镜头注视之上。由 2.2 中的语音门控控制。

3. 头部与身体

3.1 头部跟踪

头部应朝向镜头,颈部共同分担转动:

  1. 在角色空间中计算从头部到相机的偏航/俯仰。对于 范围判定,从 骨骼根节点 (静态节点)来测量朝向——绝不要从动画脊柱上取值:脊柱会呼吸并摆动几度,任何由它驱动的阈值都会随着摆动翻转状态。

  2. 在极限处保持,只有在盲区才释放。 不要在 ±90° 边界处解除跟踪——观众在边界附近横移会让头部在完全跟踪/解除跟踪之间来回摆动(看起来像撞头)。只要观众还在可见范围内,就把目标钳制在最大转角;只有在角色后方很远的位置(约 max+20° … max+43°),即脸本来就看不见时,才淡回到动作片段姿态。

  3. 按解剖结构分配:头部本身只承担最初的约25° 偏航; 其余由颈部承担 (剩余部分约 0.75)。如果只有 60° 的头部区间,那么对于侧面观众,头部会转到约 50°——接近关节极限的姿势,任何修正都会显得抽搐。

  4. 基于时间的 平滑(1 − exp(−rate·dt),rate ≈ 5/秒)并限制重定向速度(约 80°/秒)。逐帧 lerp 因子会把掉帧变成可见的阶梯。

  5. 先对动画片段的头部姿态进行低通处理(τ ≈ 0.2 秒),再与动画混合,并保持相机锁定强度较高(静止约 0.9,说话约 0.95)——进入混合的原始片段运动正是头部晃动的来源。

  6. 再在上面叠加生命感:缓慢的多正弦漂移(约 2°)、偶尔的注视切换(保持 1.5–4 秒,按时间平滑),以及在偏航转动中轻微的滚转“倾侧”。参考: useHeadTracking.ts.

3.2 语音能量耦合——上半脸也会说话

计算一个 0–1 的 语音能量包络 ,来源于嘴部活动(下颌 + 嘴唇通道幅度,快起音 / 慢释放)。有两个系统会使用它:

  • 眉毛/脸颊: 随着能量抬升眉毛上提 / 脸颊上提通道(耦合约 0.9 到情绪叠层的上脸权重)。在提问时眉毛从不动的脸会显得像贴上去的。

  • 强调点头: 检测能量 起始 ——一个快包络(τ ≈ 0.1 秒)上升到其自身的慢平均值(τ ≈ 0.55 秒)之上。在起始时让头部下压几度,然后释放。点头本身要做非对称后平滑(下沉 τ ≈ 0.09 秒,释放 τ ≈ 0.22 秒)——如果直接用起始信号驱动头部,就会追踪能量抖动,显得生硬。持续的响度必须产生 点头(不是持续鞠躬);只有重读音节才会标记。

3.3 情绪叠层

保留一个低强度的静息情绪(细微微笑配方),它 以 max 方式合并 与语音帧合并,而不是覆盖它们,并在说话时抑制其嘴部区域通道,使其不会与 viseme 冲突。在社交节奏(挥手、问候)时短暂抬高微笑底值。参考: useEmotion.ts.

3.4 在动画混合器之后应用

如果身体动画片段也会影响脸部/头部,那么在混合器每帧更新之后,重新应用当前的口型同步帧和跟踪姿态,这样脸部始终以语音为准。

3.5 呼吸与静止微动

以约 13 次/分钟的节奏在胸部/脊柱上叠加 ±0.7° 的俯仰(不对称:吸气更快,呼气更长)可避免身体显得僵住。参考: useBreathing.ts。对于行走角色,二级物理(头发弹簧链、由加速度驱动的躯干倾斜、支撑脚 IK)会进一步叠加效果——见 useHairPhysics.ts, useLocomotionDynamics.ts 以及 useFootLock.ts.

4. 时序与帧节奏鲁棒性

只有在稳定帧率下,真实感才能保住;人们报告的大多数“故障”其实都是帧节奏伪影:

  • 说话结束检测: 如果约 300 毫秒没有新帧,就视为说话结束并开始收敛——正式的结束事件即使晚到,也不会造成明显冻结。

  • 中断: queue.consumeNormalizationSignal() 在机器人被中断时会返回 true(仅一次)——将 morph 用 lerp 拉回到零。

  • 将动画混合器步进钳制 (≤ 33 毫秒)。否则,主线程卡顿(UI 重新渲染、TTS 音频初始化、GC)会在一帧内让整个骨骼按卡顿的时间差前进——你的平滑随后会明显追着一个姿势瞬移。

  • 速度除以真实经过时间,绝不要除以被钳制的 dt——否则卡顿帧会把表观速度放大数倍,并惊吓状态机(脚锁、注视门控)。

  • 每个缓动都必须基于时间 (1 − exp(−dt/τ)),绝不要使用逐帧常数,否则长帧会变成可见的台阶。

  • 不要逐帧分配 在热点路径中(包括调试日志)——GC 暂停会正好落在最显眼的地方。

  • 叠加 UI 不能每帧重绘。 聊天面板的动画视觉效果若以 60 fps 重绘,会迫使合成器不断在 WebGL 画布上重新混合面板,从而可能将帧率减半;将可视化降到 30 fps,空闲时停止绘制,并且绝不要在动画循环中驱动逐帧 SVG 滤镜(投影模糊)或 React setState。

5. 角色准备

  • 命名就是契约: morph 必须与格式的通道名完全一致。在写代码之前,请先在 glTF 查看器中检查你的 GLB。

  • 名称混淆: 有些导出会丢失分隔符(CTRL_expressionsjawOpen)—— bindAll/bindMesh 会自动修复字典;如果你手动绑定,也请照做。

  • Lite 与 Full 骨骼: MetaHuman-Lite 模型(103 个 morph)只使用 251 流中的命名子集——它能工作,但缺少眉毛/脸颊/眼部细节通道。主角角色请使用完整骨骼。

  • morph 骨骼上没有下颌/舌骨: MetaHuman 风格的 glTF 导出通常完全由 morph 驱动——下颌运动完全存在于 jawOpen/jawOpenExtreme。不要为一个没有对象可驱动的下颌骨驱动器预留预算。

  • 嘴部材质出厂时过亮: 导出通常会让牙齿处于完整的 IBL 下。请把材质调整(§1.9)与动画调优一起纳入预算——“牙齿看起来不对”的一半问题都出在着色上。


缓冲调优

这个 frames_buffer_duration 选项控制服务器在随音频释放之前会累积多少秒的 blendshape。更高的值会提高口型同步精度,但会增加延迟。

提前交付(提前发送契约)

使用 deliver_chunks_ahead: true 服务器会发送 带索引的 NeuroSync 分块 在……之前 音频播放,因此客户端会持有一个真正的视觉缓冲区(实践中大约领先 2 秒),而不是追着语音跑。SDK 使用分块元数据(fps, start_frame_index, response_id, neurosync_turn_id, epoch)来对帧排序,并在对话被中断时丢弃已缓冲的视觉内容——使用 client.blendshapeQueue 以及 SDK 风格的播放器路径,这样按所有者范围的取消才能生效。

它是 需主动启用(默认关闭) 在契约的服务器端仍在验证期间。当前服务器状态(已于 2026-07-27 验证,session id 已存档):提前交付适用于 arkit, cc4_extended 以及 visemes; 通过以下方式请求它 format: 'mha' 目前会让 TTS/blendshape 流完全停滞 (文本回复仍会到达)——在 mha 之前保持该标志关闭,直到服务器发布修复。 cc5_hd 在任何模式下都不会传递帧。


示例:React Three Fiber

请在以下位置查看完整可运行示例: examples/react-three-fiber 其中演示了:

  • MHA-251 blendshape 直接按名称映射到 MetaHuman 风格角色(blendshapeConfig.format: 'mha',无需自定义映射器)

  • 上文完整的自然感栈:流淡出包络、眼部通道跳过掩码、各通道增益、嘴部 L/R 对称化、在动画混合器之后重新应用

  • 程序化眨眼、说话时锁定镜头的注视、头部跟踪、呼吸——与身体动画片段以及脚本化开场场景叠加

  • 角色动作(actionConfigactionResponse → 手势动画片段)以及驱动问候的叙事设计触发器

最后更新于

这有帮助吗?