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

14 KiB
Raw Blame History

动画方案

预制开始/等待动画 -> VMC推流动画 -> 预制结束/等待动画

VMC推流

迭代动画状态机方案

  1. 由ChatGPT模型AI使用之前录制动画素材拼凑出N组排列组合。
  2. 动画资产以及排列数据进行定期热更新。(自动 | 人工)
  3. 实时直播时由ChatGPT发送指定排列组合的名称或者ID给客户端之后客户端播放对应的排列组合动画。

推流方案

推流视频:

协议

OSC

一种基于UDP的远程控制协议传输的数据主要分为Bundle 与 Message。

反序列化步骤

  1. 调用ReadOSC()
  2. ReadOSCString读取Address。主要分为#bundle#message
  3. #bundle
    1. 读取uint64 Time。
    2. 调用ReadOSC(),递归序列化之后的数据。
  4. #message:基础数据反序列化逻辑
    1. 读取FString Semantics里面每个字符代表之后基础数据的类型。
    2. 根据基础数据类型进行反序列化一个数据生成一个FUEOSCElement。
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实现分析

一个数据包的格式为:

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协议基本上实现了开放声音控制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 500

performer-spec

这是Marionette→PerformerAssistent→Performer流程中的发送数据的规范。

主要有:

  • 虚拟设备转换
  • 帧周期
  • 虚拟 MIDI CC 值输入
  • 虚拟摄像机变换和 FOV
  • VRM BlendShapeProxyValue
  • 眼动追踪目标位置
  • [事件发送]信息发送请求(Request Information)
  • [事件传输] 响应字符串
  • [事件发送] 校准(准备)请求(校准/校准就绪请求)
  • [事件发送]请求加载设置文件
  • 通过信息
  • DirectionalLight 位置/颜色DirectionalLight 变换和颜色)
  • [事件传输] 快捷调用Call Shortcut

虚拟设备转换

主要为虚拟头显、控制器Controller和追踪器Track。(HMD被视为跟踪器) 结构为:虚拟序列号 -> Position ->Quaternion

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 帧间隔发送。

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基本信息
  • [低频] 选项字符串
  • [低频]背景色
  • [低频]窗口属性信息
  • [低频]加载设置路径
  • 通过信息

根骨骼变换

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 合成的比例。
通过使用它,可以将虚拟人物的位置和大小调整为实际的身体尺寸。

骨骼变换

/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

Message方式

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时会填入一个时间戳。之后

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方式

 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, "");

    }