学过几年Unity动画,接手过不少数字人、对话NPC的项目,我敢说在“让角色开口说话”这件事上,最让我省心的方案就是SALSA With RandomEyes。这个插件从名字就能看出来,它干两件事:SALSA负责说话时的口型同步,RandomEyes负责让角色的眼睛像活人一样随机转动和眨眼。有了它,你再也不用手动K几十帧的口型动画,也不用为了让角色看着不死板而硬调瞳孔高光。
今天这篇不是官方文档翻译,是我在实际项目里从接入到调优、从踩坑到绕坑的完整记录。如果你正准备做会说话的角色、语音对话NPC、或者是虚拟主播类演示,这篇应该能帮你省下几天甚至几周的摸索时间。
1. SALSA With RandomEyes到底解决了什么问题
先说清楚一个容易被忽略的事实:Unity场景里让角色动起来不难,难的是让它“像个人”。
1.1 手动K帧做口型的痛苦
早期项目里,我接到需求要让NPC说一句“欢迎光临”并配合点头动作。常规思路是去动画编辑器里手动调BlendShape权重:A口型的权重在第10帧拉到1,O口型在第14帧拉到0.6,E口型在第20帧再叠加上去。一次短对话下来,光口型关键帧就两百多个,而且不同威化、不同语调还要重调。
这还只是单句台词。一旦台词改成一个字,整个口型曲线就得推翻重来。做过一次这种活之后你就会明白,语音生成嘴型这件事情,靠手K是绝对不可持续的。SALSA在这个环节替代了大约80%的人工工作量,它通过实时分析AudioSource里的音频频谱,自动把音量、基频映射成口型张合和表情强度。
1.2 RandomEyes解决“僵尸眼”问题
另一个常被忽略但特别影响观感的点,是角色眼睛的处理。很多项目把面补动画K得很细,但眼睛死盯一个方向或者连续一两分钟不眨眼,最后的效果比口型不对还诡异。
RandomEyes做的事情包括:随机看不同方向、随机眨眼、眼球微动补偿,以及在看某个目标时加入头部细微偏移。它本质上是给眼睛加了一层“生命感噪声”,让观感从“模型”变成“人”。
1.3 不只是语音:对白系统的天然搭档
在实际使用中,我不仅拿它做语音驱动,还接了不少文字驱动对话。SALSA内置了一个简单的文字触发口型机制——你只需在角色说话时传入单个字符或者拼音序列,它会按照预先配置的“Emote”映射关系,匹配到对应的口型姿态。这意味着你的对话系统不用等完整语音,就能先驱动角色张嘴说话。
另外,由于它底层核心是围绕BlendShape和骨骼旋转两组系统设计的,所以哪怕是MMD模型、VRM模型、甚至是纯骨骼驱动的低模,只要骨架命名规范,都能接入。
2. 核心模块拆解:从音频频谱到嘴型系数
想用好SALSA,就必须理解它的数据链路。SALSA不是魔法,它就是一条典型的“信号处理管线”,每一步都是可以配置的。
2.1 音频分析引擎的主循环
SALSA在主线程上每帧获取AudioSource当前播放的音频采样,然后做一次快速傅里叶变换,把时域信号转为频域信号。再根据预划分的频段(低频区、中频区、高频区),分别计算能量强度。
这里有几个关键参数会在Inspector里暴露:
- Sensitivity:控制口型对音量的敏感度。数值越高,轻微说话就会张很大嘴;我一般把它设置在5~10之间,看电影级别配音则常设在3左右。
- Loudness:实际参与口型计算的最终音量幅度,它会随时间做平滑,避免音量抖动导致口型颤抖。
- Pitch:基频值,用于判断音节的高低变化,通常会影响闭口/开口的比例。
我建议在参数调整时打开SALSA自带的Debug视图(Gizmos窗口),直接查看口型系数变化曲线,比盲调快很多。
2.2 BlendShape映射的逻辑
SALSA渲染口型不是直接改Mesh的BlendShape数值,而是通过“口型类别”间接映射。它内置了常见口型类别,包括A、I、U、E、O,以及几个扩展类别如Rest、Oh、Ch、Consonant等。
对应关系是:
| 口型类别 | 常见触发场景 | 对应BlendShape建议 |
|---|---|---|
| A | 大声、惊讶 | 下颌张开、上唇提升 |
| I | 微笑、露齿 | 唇角外展 |
| U | 嘟嘴、元音 | 唇前突 |
| E | 咧嘴 | 唇角拉伸 |
| O | 圆唇 | 双唇内收 |
| Rest | 静默、句中停 | 所有口型归零 |
| Consonant | 辅音瞬间 | 舌面位置变化 |
每个类别都会在两个或者多个BlendShape振幅之间插值,最终输出给SkinnedMeshRenderer。如果你的模型没有标准的A/I/U/E/O口型BlendShape,也可以在映射面板里手动指定“接近”的BlendShape。比如没有A,就用下颌角+下唇下降来拼出一个近似效果。
2.3 RandomEyes的视线模型
RandomEyes会在场景里建立几个虚拟的“关注点”:一个主注视点(比如对话者头部)、一个随机扫视点、一个瞳孔微调点。它通过插值算法把这几个点混合成最终的视线方向,再映射到眼睛骨骼的旋转。
它的核心参数有三组:
- Head Movement(头部转动权重):控制视线偏移时头部是否跟着转,我一般设置在0.3~0.5,让头转幅度比眼睛小,更自然。
- Blink Interval(眨眼间隔):随机眨眼的时间范围,通常设2~10秒一次。
- Pupil Dilation(瞳孔缩放):缩放幅度一般设置0.1~0.2即可。
还有一个很实用的“[Override Look Target]”参数,用来强制眼睛看向某个Transform。做对话双方眼神接触时,这个功能是必需品。
3. 快速接入的完整流程:从下载到跑通第一个Demo
不废话,直接按我实测过的踩坑路径给你一套可靠步骤。
3.1 第一步:准备带BlendShapes的角色模型
SALSA需要BlendShape作为口型渲染载体,但也不是非要原生自带。我常用的方式:
- 如果模型自带标准BlendShape(比如VRM模型),直接导入Unity,勾选Rig的Humanoid。
- 如果是无表情模型,可以用Blender或Mixamo的Auto Landmark功能先生成基础口型表情,然后导出FBX,再在Unity里给SkinnedMeshRenderer添加BlendShape Clip。
这里有个关键点:SALSA操作的口型BlendShape最好全部存在于同一个SkinnedMeshRenderer上。如果口型拆在多个MeshRenderer,你需要为每个Renderer单独挂一个SALSA组件并把它们分别绑定到同一个AudioSource上,管理麻烦不说,同步也容易出问题。
3.2 第二步:挂载核心组件
在角色根节点上挂载三个核心组件:
Salsa3D:口型同步主控制器RandomEyes3D:眼睛与视线同步控制器Audio Source(可以是场景里其他对象上的,不一定在角色身上)
然后在Salsa3D的Inspector中找到“Audio Link”或者“Audio Source”字段,把对应的AudioSource拖进去。
SALSA默认会创建一个“AudioLink”子物体,你可以在这里做一些延迟补偿。如果语音有硬件播放延迟(比如蓝牙耳机),通过调整“Coyote Time”参数可以稍微对齐视觉和听觉。
3.3 第三步:配置BlendShape名映射
打开SALSA3D组件面板,点击“Auto Configure”按钮(需要先选中带有SkinnedMeshRenderer的子对象)。插件会自动扫描所有BlendShape名称并按照名称里的关键字(如“A”“I”“U”“E”“O”“Blink”)自动填充到口型槽位里。
如果你的模型命名不那么标准,需要手动调整映射表。这里有一个很实用的建议:在Auto Configure之后记得导出配置保存为asset,后面如果再调整模型,直接拖进来就可以复用映射,不用重新设置。
3.4 第四步:配置RandomEyes
在RandomEyes3D组件上做两件事:
- 设置左右眼球的骨骼Transform(或BlendShape的BlinkLeft/BlinkRight)。
- 设置一个注视点(Look At Target),没有的话就让它处于“自由模式”。
之后点击运行,角色应该已经能出现随机的眨眼和视线游移。如果眨眼只眨一只眼或者眼皮被口型覆盖,记得把“Eye Gaze”映射里的“Blink”权重调低一点,避免和口型的Rest姿态冲突。
3.5 第五步:把对话系统接进来
对话接入的核心接口是Salsa3D.Speak()和Salsa3D.Stop()。你可以直接在对话播放的同时调用它们:
using CrazyMinnow.SALSA; public class DialoguePlayer : MonoBehaviour { public Salsa3D salsa; public AudioSource voiceSource; public void PlayLine(AudioClip clip) { voiceSource.clip = clip; voiceSource.Play(); salsa.Speak(voiceSource); // 让SALSA实时分析该AudioSource } public void StopLine() { voiceSource.Stop(); salsa.Stop(); // 停止当前SSalsa驱动 } }如果要做纯文字驱动模式,可以调用Salsa3D.SetExpression("哦豁")这样的方法,它会根据字数自动生成一组口型序列并播放,适合演算对话文字同步效果。
4. 实测中的常见坑与排查链路
这块是我个人最想写的部分,因为没有哪篇教程会告诉你这些小细节。
4.1 中文字幕和音节的匹配问题
SALSA的本地语音分析本质是对声学特征做反应,并不会直接理解中文音节。因此,如果你用中文语音驱动它,偶尔会出现“嘴型在动,但和文字对不上”的感觉,尤其是一句话里连续出现“鱼”“圈”“云”这类圆唇音时。
解决办法:
- 在口型映射里把“U”“I”类别的权重分别提得高一点,让圆唇和舒展口型变化更明显。
- 给音频加轻微负延迟(如设为-0.05秒),让口型比声音先出一点点,大脑会对“先看到张嘴再听到声音”有更强的同步感。
- 极端情况下,可以直接用“文字触发模式”替代实时分析,给每个中文字符配一个预设口型。
4.2 口型被头发、帽子模型遮挡
这个坑在配好模型后很容易出现:BlendShape把下巴拉伸、嘴唇前突后,如果口型BlendShape权重影响到了脸部下方区域的网格,头发和帽子就会穿透。
排查思路:
- 在预览窗口依次把A/I/U/E/O权重拉到1,观察哪个口型穿透。
- 对应调整SALSA的“Max Blend”值(比如从1降到0.7),让口型幅度缩小。
- 如果仍不够,需要回建模软件,把头发网格和面部网格分开,或者给头发加一层vertex paint遮罩,不让它跟随面部变形。
4.3 眨眼时眼皮和眼球穿插
RandomEyes的眨眼是通过骨骼Rotation或BlendShape实现的,但很多模型的“上眼皮”和“眼球”之间只有很小间隙,一旦眨得幅度偏大就会穿插。
我的做法是在RandomEyes3D面板里把“Blink Down”(下眼皮权重)设低一点,把“Blink Up”(上眼皮权重)设高一点,同时把眨眼速度调快(Duration设为0.1秒左右),快速眨动比慢吞吞的闭合穿插感更弱。
4.4 和Animator动画控制器的冲突
如果角色身上同时挂了普通动画(待机、走路),Animator会在每一帧覆盖骨骼旋转和BlendShape的变化,SALSA的口型和眨眼很容易被顶掉,表现就是角色嘴里说话但脸不动。
解决方案:
- 在Animator状态机里给SALSA相关的Layer设置“Avatar Mask”,让Mask只包含身体骨骼而不包含面部。
- 或者干脆把面部的BlendShape、眼部骨骼驱动放到LateUpdate里强制写回:
void LateUpdate() { // 确保在Animator更新后覆盖面部结果 salsa3D.OnLateUpdate(); }还有一个低配方案:把Animator的Update Mode设为“Animate Physics”,并在SALSA的Update Mode里选“LateUpdate”,这样更新顺序错开,也能缓解很大一部分冲突。
4.5 低端手机上说话了但看不到变化
这个问题我排查了很久才定位到:移动端GPU对SkinnedMeshRenderer的合批处理可能导致BlendShape更新及时,但SALSA的调试曲线在编辑器里一切正常。
最终定位是需要修改SkinnedMeshRenderer的“Skinned Motion Vectors”选项,把它关闭;同时在SALSA3D的Quality设置里把“FFT计算频率”从60FPS降为30FPS。这样在保证效果前提下明显降低CPU开销。真正做移动端项目时,建议在真机上观察,不要在编辑器的Game窗口里下结论。
5. 参数调优与扩展实战经验
跑通基础流程后,剩下的就是在“自然度”和“性能”之间做取舍。
5.1 自然度调优清单
- Sensitivity不宜全局一致,可以在“Expression Wave”变化时叠加一点随机偏移,避免每句话口型强度完全一致。比如当角色情绪激动时,把全局Sensitivity临时乘上1.3。
- RandomEyes视线滞留时间:人在专注对话时不会频繁乱瞟,一般3~5秒才换一次注视点。而“紧张”、或“敌意”情绪下,视线切换频率会提高。你可以根据对话状态动态调整RandomEyes的“Look Interval”。
- 头部微动:说话时头部会有轻微大小不一的晃动,表现在听觉上的“前倾、后仰”和情绪紧密相关。我给常用NPC预设了“点头”“摇头”“歪头”三个头部动画,通过代码在SALSA的
Salsa3D.TriggerGesture()接口里切换。
5.2 性能预算建议
SALSA不是重插件,但在高密场景(比如同时五六个角色对话)里仍需注意:
| 角色数量 | 建议配置 |
|---|---|
| 1~2个 | 默认配置,60FPS分析 |
| 3~5个 | FFT分析频率降至30FPS,RandomEyes只保留固定眨眼,不开自由视线 |
| 超过5个 | 建议只对“当前说话者”实时驱动,其余角色用预烘焙的假口型动画 |
实测在移动端,一个角色的SALSA约占0.2ms CPU(iPhone 8),RandomEyes约0.05ms。如果你同时开启多个,配合Profile发现CPU占用过高,优先检查FFT buffer size是否设置太大(一般推荐1024)。
5.3 和语音合成、远程输入的结合
如果你做的是远端语音输入驱动角色说话(比如在线会议系统里的人物化身),SALSA的音频流来自网络,延迟会有明显波动。
我的方案是把远端的音频先缓存在内存里,等Buffer达到200ms再交给AudioSource播放,为此给SALSA做了大概30ms的补偿。这样虽然会让语音反应慢半拍,但口型和实际声音的同步度明显变高,不会出现声音已经说完、嘴还在动的尴尬。
另一个进阶玩法:利用SALSA的“Ducking”功能(即在有人工语音时自动把背景音乐或群杂音量降下来),可以省去大量混音工作量。这个功能在SALSA组件的Audio Link配置里开启,设置一个衰减量就行。
5.4 别忽视Closed-Beta版本差异
Unity官方商店里SALSA的版本时不时会调整,如果你之前用的是老版本(比如1.x),升到2.x后要注意命名空间从CrazyMinnow.SALSA变成新命名空间,并且增加了一个独立的Runtime程序集定义。这意味着你的旧配置文件(.asset)可能需要重新拖一次BlendShape映射。
我踩过最大的坑是升级后在打包时遇到“SALSA程序集找不到”的错误。解决方法是:删除Assets/Crazy Minnow Studio下旧的SALSA文件夹,再重新导入新版本,让程序集定义和代码引用彻底同步。不要只替换dll,那会留下残留引用。
6. 进阶:把SALSA玩出花样的三个方向
最后聊几个我试过觉得很有价值的扩展方向,给想超越基础口型同步的读者当参考。
6.1 情绪化口型叠加
SALSA支持在口型播放的同时叠加“Emote”表情(比如惊讶、愤怒、开心)。你可以通过代码在不同的对话节点间切换表情强度,让同一个词在不同情绪下带出不同嘴型变化。
6.2 非人类角色的适配
做动物或怪物时,同样可以用SALSA,只是口型类别不再用A/I/U/E/O,而是映射“张嘴”“闭嘴”“露齿”“吐舌”等自定义BlendShape。这种情况下,会给SALSA挂上“Tongue Controller”(插件内置的控制器,可控制舌头伸缩),实现蜥蜴人吐信子、狗喘气等效果。
6.3 与LipSync的对比和共存
如果你对SALSA的效果不满意,或者遇到了中文支持不理想的问题,可以对比试试Unity官方的Oculus Lipsync(其实也是UMA项目的一部分)或者第三方OVRLipSync,它们侧重于基频和共振峰分析,更接近“音素识别”,对英文和多语种支持更好。SALSA则胜在“快速、开箱即用、生态完善”。
但SALSA文档里其实提到过:它和OVRLipSync不冲突,可以把OVRLipSync的音素输出作为驱动源来连接SALSA的BlendShape输出。这个玩法我试通了,原理就是把OVRLipSync的GetViseme结果数值直接赋给SALSA对应的BlendShape权重。
如果项目一开始就决定用SALSA作为主方案,那就没必要折腾额外的音频分析插件,它的核心逻辑已经足够可靠。
最后分享一个我个人常用的实用技巧:给SALSA加一个“Debug化”阶段的配置备份。每调好一版映射和参数,就把所有Salsa相关组件的Inspector参数全导出为ScriptableObject配置文件保存起来。后面一旦测试效果不理想,一键还原,不用再从头慢慢摸索。项目迭代过程中,这套流程帮我省了大量重复配置的时间。