找回密码
 立即注册
查看: 255|回复: 0

Web前端体验Unity3D 虚拟人开发(二)

[复制链接]
发表于 2023-3-31 19:06 | 显示全部楼层 |阅读模式
我项目中使用的数字虚拟人骨骼绑定和BlendShape是非标准的。
实现细节和避坑指南在第三篇会出(只有3篇,我不拆了,真不拆了),这篇只讲细节,不涉及具体代码。
架构图还是再放下方便理解:


Render

模型:虚拟人的衣服,头发和其他配件进行了拆分。
光线:请教了专业Unity同事帮我布光,个别材质球也让专业的Unity同事帮我调整了。
上面两步对于Unity同学来说都是基操(基础操作!)。
虚拟人,头发,衣服和配件都绑在一个根节点上,再在这个节点上添加了一个Animator组件用于播放预设动画(别忘了勾上Apply Root Motion,否则头发和配件在动画播放过程就和人分离了)。
预设动画有别于TTA,前者是逐帧设定好的动作,后者是实时去驱动骨骼。



Animator组件

TTS

Text to Speech
TTS的特点:WAV格式,虚拟人回复(其实是来自NLP模型)的一句话可能被切成很多片返回,而这些切片的id值相同,表示都来自同个回复。
例如回复是:”欢迎来到中国“,被切片后可能是["欢迎",”来到“, ”中国“]。
切片的目的主要是为了流式返回,减少前端界面虚拟人响应时间。
处理思路:


  • 通过Websocket接收来在云端的音频(WAV格式)
  • 增加一个缓冲队列,云端下发的音频我经过初步处理后会不断塞入队列中
  • 每帧都会通过AudioClip.PCMReaderCallback这个Callback不断从队列中读取音频数据喂给AudioClip
  • 通过Audio Source播放AudioClip来发声



TTS播放流程

TTF/TTA

Text to Face / Text to Action
TTF/TTA的特点:JSON格式,对应TTS切片的面部和动作,TTF驱动的是Unity里的BlendShape,TTA驱动的是骨骼,云端是把这两个打包一起下发的。
例如刚才TTS切片["欢迎",”来到“, ”中国“],“欢迎”两字会对应TTF就是张嘴说“欢迎”的口型并面带微笑,对应的TTA可能就是挥手动作。
处理思路:


  • 提前收集BlendShapes做出Key-Value字典,虚拟人的面部其实有多个GameObject(为了眼神和脸部表情更细致),当TTF下发后,要找到对应GameObject里的BlendShape进行修改值,个人认为这个查找有点麻烦和耗时,在做性能优化时单独拎出来,提前做了字典映射方便后续直接驱动脸部表情,而骨骼只绑定在一个人体模型上而且查找比较方便就没有提前做字典映射。
  • 通过Websocket接收来在云端的TTF/TTA JSON数据
  • 通过一个缓冲队列RawData Queue来接收原始数据
  • 不断从RawData Queue拿出原始数据处理成每帧BlendShape数据和骨骼数据(Transform的Position和Rotation), 把这些帧数据作为一帧放到FrameData中
  • 在每帧Update时,驱动这些帧数据
// 相关伪代码
// blendshape驱动
SetBlendShapeWeight(expressionFrame.blendshapeIdx, expressionFrame.weight);
// 骨骼驱动
Vector3 newP = new Vector3(actionFrame.values[0], actionFrame.values[1], actionFrame.values[2]);
boneTransform.localPosition = newP;
Quaternion newR = Quaternion.Euler(actionFrame.values[0], actionFrame.values[1], actionFrame.values[2]);
boneTransform.localRotation = newR;



TTF/TTA流程

Dance/Interrupt

用户语音指令让虚拟人跳舞或者打断当前对话。
舞蹈的话虚拟人会说:"看看我的表演吧",然后开始尬舞。
打断会让虚拟人结束当前动作表情的驱动,回归Idle动画状态。
其实这两个本质是一样的,都是去播放一段预设动画,只不过Idle是循环播放的,Dance只播一次。
如果你是标准骨骼的,可以从以下网站找预设动画。
处理思路:


  • 通过WebSocket接收Dance/Interupt发指令
  • 判断当前状态,根据优先级 Interrupt > TTS/TTF/TTA > Dance 判断是否中断当前状态,状态变化有:
a).  TTA被 打断变Idle
b).  Dance指令下发,TTA不中断,完成这轮对话后开始Dance(用了协程挂起Dance)
c).  Dance状态下(包含等待Dance),有TTA进来,中断Dance(或取消等待Dance)
d).  Idle状态下有TTA 进来
e).  Idle状态下下发跳舞指令立即跳舞
Idle

这个其实和Dance都属于Animation,单独拿出来讲是因为Idle涉及关键帧
处理思路:


  • 中断当前状态
  • 判断是否需要开启animator
  • 播放Idle动画,播放时机:
a). 舞蹈停止到Idle(这部分可以通过Animator的Transition实现)
b).  TTA中断或者播放完变Idle,这部分就需要用到关键帧了
LookAt/Blink

眼神锁定幸好之前接触过纸片人(Web中加载VRM),类似bilibili的虚拟主播。在写BabylonJS的VRM插件也有研究过,其实就是3D人物Look at Camera(面对你,头转动眼神还是看着你),这次的虚拟人更简单,不涉及身体转动。
处理思路:


  • 获取主Camera当前的Transform
  • 找到眼球的GameObject,通过LookAt设置Camera的Transform
  • 修改眼神相关的BlendShape的weight值,不要直接设置weight的最终值(类似CSS animation), 中间动画的weight值需要自己去设置
transform.LookAt(cameraTransform);
眨眼类似眼神锁定的第3步,只要每隔一段时间修改下眼皮,眼睫毛相关BlendShape的weight即可,记住也不要一次直接设定weight最终值。可以理解为通过requestFrameAniamtion去逐帧处理眨眼动画
相关链接参考:

<hr/>第一篇:概览
第三篇:细节避坑

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-14 12:42 , Processed in 0.098435 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表