408 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 动画方案
 | 
						||
预制开始/等待动画 -> VMC推流动画 -> 预制结束/等待动画
 | 
						||
## VMC推流
 | 
						||
[[AnimNode & 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<FUEOSCElement> Elements;  
 | 
						||
};
 | 
						||
```
 | 
						||
其中`FUEOSCElement`存储的具体数据,里面是一个结构体存储着基础数据类型数据以及数据类型枚举。
 | 
						||
 | 
						||
#### 基础数据类型
 | 
						||
数据以结构体形式进行序列化/反系列化。可携带的数据类型为:
 | 
						||
- int32、int64、uint64
 | 
						||
- float32
 | 
						||
- String(`FName`)
 | 
						||
- blob(`TArray<uint8>`)
 | 
						||
- 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 字节以内)进行捆绑,并且应由接收方进行适当的处理。
 | 
						||
- 传输周期以发送方的任意间隔执行。并非所有消息都会在每个周期发送。  
 | 
						||
    另外,发送方应该能够调整发送周期的间隔,或者以足够低的频率发送。
 | 
						||
- 接收方应丢弃不必要的消息。您不必处理所有消息。
 | 
						||
- 发送或接收哪些消息取决于两者的实现。
 | 
						||
- 未知地址,应忽略太多参数。
 | 
						||
- 如果您发现参数太少或类型与扩展规范中定义的参数不同,请将它们视为错误或忽略它们。
 | 
						||
 | 
						||

 | 
						||

 | 
						||
 | 
						||
### 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笔记]]
 | 
						||
 | 
						||
# 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<Animator>();
 | 
						||
		blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>();
 | 
						||
		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<Animator>();
 | 
						||
            blendShapeProxy = Model.GetComponent<VRMBlendShapeProxy>();
 | 
						||
            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, "");
 | 
						||
 | 
						||
    }
 | 
						||
 | 
						||
``` |