# 动画方案 预制开始/等待动画 -> VMC推流动画 -> 预制结束/等待动画 ## VMC推流 ## 迭代动画状态机方案 1. 由ChatGPT模型AI使用之前录制动画素材拼凑出N组排列组合。 2. 动画资产以及排列数据进行定期热更新。(自动 | 人工) 3. 实时直播时由ChatGPT发送指定排列组合的名称或者ID给客户端,之后客户端播放对应的排列组合动画。 # 推流方案 推流视频: - https://www.bilibili.com/video/BV1ub4y1Y74K/?spm_id_from=333.337.search-card.all.click&vd_source=d47c0bb42f9c72fd7d74562185cee290 - https://www.youtube.com/watch?v=ufU9me5pDYE&t=2s # 协议 ## OSC 一种基于UDP的**远程控制协议**,传输的数据主要分为Bundle 与 Message。 - OSC:https://opensoundcontrol.stanford.edu/index.html - Nodejs的OSC实现:https://www.npmjs.com/package/osc - 案例代码库:https://github.com/colinbdclark/osc.js-examples ### 反序列化步骤 1. 调用`ReadOSC()` 2. ReadOSCString,读取Address。主要分为`#bundle`、`#message`。 3. `#bundle` 1. 读取uint64 Time。 2. 调用`ReadOSC()`,递归序列化之后的数据。 4. `#message`:基础数据反序列化逻辑 1. 读取FString Semantics,里面每个字符代表之后基础数据的类型。 2. 根据基础数据类型进行反序列化,一个数据生成一个FUEOSCElement。 ```c++ UENUM() enum class EUEOSCElementType : uint8 { OET_Int32 UMETA(DisplayName = "Int32"), OET_Float UMETA(DisplayName = "Float"), OET_String UMETA(DisplayName = "String"), OET_Blob UMETA(DisplayName = "Blob"), OET_Bool UMETA(DisplayName = "Bool"), OET_Nil UMETA(DisplayName = "Nil") }; ``` ### UEOSC实现分析 一个数据包的格式为: ```c++ USTRUCT(BlueprintType) struct UEOSC_API FUEOSCMessage { GENERATED_USTRUCT_BODY() public: FString Address; TArray Elements; }; ``` 其中`FUEOSCElement`存储的具体数据,里面是一个结构体存储着基础数据类型数据以及数据类型枚举。 #### 基础数据类型 数据以结构体形式进行序列化/反系列化。可携带的数据类型为: - int32、int64、uint64 - float32 - String(`FName`) - blob(`TArray`) - bool ## VMC 全名为Virtual Motion Capture Protocol,一种基于OSC的虚拟偶像动作输出传输协议。 **存在问题**: 1. 数据没有压缩,不适合互联网传输。 2. 基于OSC这种UDP协议数据没有可靠性。 ### 协议分析 - VMC协议可视化工具:https://github.com/gpsnmeajp/VMCProtocolMonitor - https://protocol.vmc.info/specification - performer-spec:https://protocol.vmc.info/performer-spec - marionette-spec:https://protocol.vmc.info/marionette-spec VMC协议基本上实现了开放声音控制(OSC)单向UDP通信来进行通信。 因此,VMC 协议定义了自己的术语: - **木偶**(**Marionette**) - 接收动作、绘制等。(必填) - 它的存在最终是为了在屏幕、视频、通讯上产生结果。 (示例:EVMC4U、VMC4UE、其他运动接收兼容应用程序) - **表演者**(**performer**) - 主要处理运动并向 Marionette 发送**全身骨骼信息 (IK)** 和**辅助信息**。(必填) - (例如虚拟动作捕捉、Waidayo、VSeeFace、MocapForAll、TDPT 等) - **助理**(**Assistant**) - 主要不处理动作并向表演者发送辅助信息。(可选) - 仅负责**发送辅助**信息。(**一些骨骼、跟踪器姿势、面部表情**等) (例如,face2vmc 模式下的 Waidayo、Sknuckle、Simple Motion Tracker、Uni-studio 等) 具体沟通规定如下: - 通信时使用适当类型的 OSC。 - 字符串采用 UTF-8 编码,可以用日语发送。 - 至于端口号,Marionette 将监听端口:39539,而 Performer 将监听端口:39540,但从 UX 角度来看,我们建议您更改发送地址 和接收端口。 - 数据包在适当的范围内(1500 字节以内)进行捆绑,并且应由接收方进行适当的处​​理。 - 传输周期以发送方的任意间隔执行。并非所有消息都会在每个周期发送。 另外,发送方应该能够调整发送周期的间隔,或者以足够低的频率发送。 - 接收方应丢弃不必要的消息。您不必处理所有消息。 - 发送或接收哪些消息取决于两者的实现。 - 未知地址,应忽略太多参数。 - 如果您发现参数太少或类型与扩展规范中定义的参数不同,请将它们视为错误或忽略它们。 ![800](https://protocol.vmc.info/flow.gif) ![500](https://protocol.vmc.info/layer.png) ### performer-spec 这是`Marionette→Performer`或`Assistent→Performer`流程中的发送数据的规范。 主要有: - 虚拟设备转换 - 帧周期 - 虚拟 MIDI CC 值输入 - 虚拟摄像机变换和 FOV - VRM BlendShapeProxyValue - 眼动追踪目标位置 - [事件发送]信息发送请求(Request Information) - [事件传输] 响应字符串 - [事件发送] 校准(准备)请求(校准/校准就绪请求) - [事件发送]请求加载设置文件 - 通过信息 - DirectionalLight 位置/颜色(DirectionalLight 变换和颜色) - [事件传输] 快捷调用(Call Shortcut) #### 虚拟设备转换 主要为:虚拟头显、控制器Controller和追踪器Track。(HMD被视为跟踪器) 结构为:虚拟序列号 -> Position ->Quaternion ```json V2.3 /VMC/Ext/Hmd/Pos (string){serial} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} /VMC/Ext/Con/Pos (string){serial} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} /VMC/Ext/Tra/Pos (string){serial} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} ``` #### 帧周期 设置虚拟动作捕捉的数据传输间隔。 以 1/x 帧间隔发送。 ```json V2.3 /VMC/Ext/Set/Period (int){Status} (int){Root} (int){Bone} (int){BlendShape} (int){Camera} (int){Devices} ``` ### marionette-spec 这是`Performer → Marionette`流程中的发送数据的规范。 - 内容:基础状态描述,校准状态、校准模式、追踪状态 - 发送者相对时间(Time) - 根变换 - 骨骼变换 - VRM BlendShapeProxyValue - 相机位置/FOV(相机变换和FOV) - 控制器输入 - [事件传输]键盘输入 - [事件传输] MIDI 音符输入 - [事件传输] MIDI CC 值输入 - [事件传输] MIDI CC 按钮输入 - 设备改造 - [低频] 接收使能 - [低频] DirectionalLight 位置/颜色(DirectionalLight 变换和颜色) - [低频]本地VRM信息 - [低频]远程VRM基本信息 - [低频] 选项字符串 - [低频]背景色 - [低频]窗口属性信息 - [低频]加载设置路径 - 通过信息 ### 根骨骼变换 ```json v2.0 /VMC/Ext/Root/Pos (string){name} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} v2.1 /VMC/Ext/Root/Pos (string){name} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} (float){s.x} (float){s.y} (float){s.z} (float){o.x} (float){o.y} (float){o.z} ``` p=位置 q=旋转(四元数) s=MR 合成的比例 o=MR 合成的偏移 作为模型根的对象的绝对姿势 名称固定为“root”。 建议将 前半部分视为Position,后半部分视为接收侧Loal姿势的四元数(以与Bone匹配)。 从 v2.1 开始,添加了 MR 合成的比例。 通过使用它,可以将虚拟人物的位置和大小调整为实际的身体尺寸。 ### 骨骼变换 ```json /VMC/Ext/Bone/Pos (string){name} (float){p.x} (float){p.y} (float){p.z} (float){q.x} (float){q.y} (float){q.z} (float){q.w} ``` 作为模型根的对象的局部姿势名称 是UnityEngine沿HumanBodyBones的类型名称 前半部分是Position,后半部分是Quaternion 所有 HumanBodyBone 都将被发送。还包括 LastBone。 这还将传输手指运动和眼骨。 ## UE Remote Control https://docs.unrealengine.com/5.1/en-US/remote-control-for-unreal-engine/ 基于WebSocket # VMC4UE的实现 ## AnimNode 实现: - FAnimNode_ModifyVMC4UEBones - FAnimNode_ModifyVMC4UEMorph ![[AnimNode]] # VMC APP代码参考 - [VirtualMotionCaptureProtocol](https://github.com/sh-akira/VirtualMotionCaptureProtocol)提供了最基础的实现。 - ~~[EasyVirtualMotionCaptureForUnity](https://github.com/gpsnmeajp/EasyVirtualMotionCaptureForUnity)~~ - ThirdParts - https://github.com/digital-standard/ThreeDPoseTracker - 逻辑在VMCPBonesSender.cs、uOscClientTDP.cs - BufferSize = 8192;int MaxQueueSize = 100; ## VirtualMotionCaptureProtocol Message方式: ```c# void Update() { //モデルが更新されたときのみ読み込み if (Model != null && OldModel != Model) { animator = Model.GetComponent(); blendShapeProxy = Model.GetComponent(); OldModel = Model; } if (Model != null && animator != null && uClient != null) { //Root var RootTransform = Model.transform; if (RootTransform != null) { uClient.Send("/VMC/Ext/Root/Pos", "root", RootTransform.position.x, RootTransform.position.y, RootTransform.position.z, RootTransform.rotation.x, RootTransform.rotation.y, RootTransform.rotation.z, RootTransform.rotation.w); } //Bones foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) { if (bone != HumanBodyBones.LastBone) { var Transform = animator.GetBoneTransform(bone); if (Transform != null) { uClient.Send("/VMC/Ext/Bone/Pos", bone.ToString(), Transform.localPosition.x, Transform.localPosition.y, Transform.localPosition.z, Transform.localRotation.x, Transform.localRotation.y, Transform.localRotation.z, Transform.localRotation.w); } } } //ボーン位置を仮想トラッカーとして送信 SendBoneTransformForTracker(HumanBodyBones.Head, "Head"); SendBoneTransformForTracker(HumanBodyBones.Spine, "Spine"); SendBoneTransformForTracker(HumanBodyBones.LeftHand, "LeftHand"); SendBoneTransformForTracker(HumanBodyBones.RightHand, "RightHand"); SendBoneTransformForTracker(HumanBodyBones.LeftFoot, "LeftFoot"); SendBoneTransformForTracker(HumanBodyBones.RightFoot, "RightFoot"); //BlendShape if (blendShapeProxy != null) { foreach (var b in blendShapeProxy.GetValues()) { uClient.Send("/VMC/Ext/Blend/Val", b.Key.ToString(), (float)b.Value ); } uClient.Send("/VMC/Ext/Blend/Apply"); } //Available uClient.Send("/VMC/Ext/OK", 1); } else { uClient.Send("/VMC/Ext/OK", 0); } uClient.Send("/VMC/Ext/T", Time.time); //Load request uClient.Send("/VMC/Ext/VRM", filepath, ""); } void SendBoneTransformForTracker(HumanBodyBones bone, string DeviceSerial) { var DeviceTransform = animator.GetBoneTransform(bone); if (DeviceTransform != null) { uClient.Send("/VMC/Ext/Tra/Pos", (string)DeviceSerial, (float)DeviceTransform.position.x, (float)DeviceTransform.position.y, (float)DeviceTransform.position.z, (float)DeviceTransform.rotation.x, (float)DeviceTransform.rotation.y, (float)DeviceTransform.rotation.z, (float)DeviceTransform.rotation.w); } } ``` Bundle将数据打包成一个Bundle,创建Bundle时会填入一个时间戳。之后 ```c# foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) { ...  boneBundle.Add(new Message("/VMC/Ext/Bone/Pos",                             bone.ToString(),                             Transform.localPosition.x, Transform.localPosition.y, Transform.localPosition.z,                             Transform.localRotation.x, Transform.localRotation.y, Transform.localRotation.z, Transform.localRotation.w)); ... } ``` Bundle方式: ```c# void Update() { //Only model updated if (Model != null && OldModel != Model) { animator = Model.GetComponent(); blendShapeProxy = Model.GetComponent(); OldModel = Model; } if (Model != null && animator != null && uClient != null) { //Root var RootTransform = Model.transform; if (RootTransform != null) { uClient.Send("/VMC/Ext/Root/Pos", "root", RootTransform.position.x, RootTransform.position.y, RootTransform.position.z, RootTransform.rotation.x, RootTransform.rotation.y, RootTransform.rotation.z, RootTransform.rotation.w); } //Bones var boneBundle = new Bundle(Timestamp.Now); foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones))) { if (bone != HumanBodyBones.LastBone) { var Transform = animator.GetBoneTransform(bone); if (Transform != null) { boneBundle.Add(new Message("/VMC/Ext/Bone/Pos", bone.ToString(), Transform.localPosition.x, Transform.localPosition.y, Transform.localPosition.z, Transform.localRotation.x, Transform.localRotation.y, Transform.localRotation.z, Transform.localRotation.w)); } } } uClient.Send(boneBundle); //Virtual Tracker send from bone position var trackerBundle = new Bundle(Timestamp.Now); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.Head, "Head"); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.Spine, "Spine"); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.LeftHand, "LeftHand"); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.RightHand, "RightHand"); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.LeftFoot, "LeftFoot"); SendBoneTransformForTracker(ref trackerBundle, HumanBodyBones.RightFoot, "RightFoot"); uClient.Send(trackerBundle); //BlendShape if (blendShapeProxy != null) { var blendShapeBundle = new Bundle(Timestamp.Now); foreach (var b in blendShapeProxy.GetValues()) { blendShapeBundle.Add(new Message("/VMC/Ext/Blend/Val", b.Key.ToString(), (float)b.Value )); } blendShapeBundle.Add(new Message("/VMC/Ext/Blend/Apply")); uClient.Send(blendShapeBundle); } //Available uClient.Send("/VMC/Ext/OK", 1); } else { uClient.Send("/VMC/Ext/OK", 0); } uClient.Send("/VMC/Ext/T", Time.time); //Load request uClient.Send("/VMC/Ext/VRM", vrmfilepath, ""); } ```