BlueRoseNote/02-Note/DAWA/AI/AIVirtualIdel动画方案.md

406 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 动画方案
预制开始/等待动画 -> 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<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 字节以内)进行捆绑,并且应由接收方进行适当的处​​理。
- 传输周期以发送方的任意间隔执行。并非所有消息都会在每个周期发送。
另外,发送方应该能够调整发送周期的间隔,或者以足够低的频率发送。
- 接收方应丢弃不必要的消息。您不必处理所有消息。
- 发送或接收哪些消息取决于两者的实现。
- 未知地址,应忽略太多参数。
- 如果您发现参数太少或类型与扩展规范中定义的参数不同,请将它们视为错误或忽略它们。
![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<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, "");
}
```