# 相关类 - TsArkitDataReceiver(ArkitDataReceiver) - TsChingmuMocapReceiverActor(ChingmuMocapReceiverActor) - TsMotionReceiverActor(MotionReceiverActor) => BP_MotionReceiver:定义了MotionNetConfig.ini。 - TsMotionSenderActor(MotionSenderActor) # TsChingmuMocapReceiverActor ***地图里只会有一个生成的TsChingmuMocapReceiverActor来管理动捕数据接收*** 1. Init():在Server才会Spawn TsChingmuMocapReceiverActor。 2. ConnectChingMu():**ChingmuComp.StartConnectServer()** 3. Multicast_AligmMotionTime():寻找场景中的BP_MotionReceiver,并且调用Receiver.AlignTimeStamp()。 ## ChingmuMocapReceiverActor 核心逻辑: - ***FChingmuThread::Run()*** - ***AChingmuMocapReceiverActor::Tick()*** - AChingmuMocapReceiverActor::DoSample() ```c++ void AChingmuMocapReceiverActor::BeginPlay() { Super::BeginPlay(); MaxHumanCount = 10; MaxRigidBodyCount = 10; CacheLimit = 240; SampledHumanData = NewObject(); ThreadInterval = 0.002; BackIndexCount = int64(UMotionUtils::BackSampleTime / (1000.0 / CHINGMU_SERVER_FPS));//BackSampleTime = 100ms CHINGMU_SERVER_FPS =120ms ChingmuComp = Cast(GetComponentByClass(UChingMUComponent::StaticClass())); if (ChingmuComp == nullptr) { UE_LOG(LogTemp, Error, TEXT("Chingmu Component is missing!!")); } Thread = new FChingmuThread("Chingmu Data Thread", this); Sender = GetMotionSender(); } ``` FChingmuThread::Run()中处理完[[#ST_MocapFrameData]]之后,将几个演员动补数据存入FrameQueue之后。在Tick()出队,之后数据存入AllHumanFrames/AllRigidBodyFrames。 - AllHumanFrames - ID - std::vector Frames - ID - TimeStamp - FrameIndex - BonesWorldPos - BonesLocalRot ```c++ void AChingmuMocapReceiverActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if(!Sender) { Sender = GetMotionSender(); } const auto CurTime = ULiveDirectorStatics::GetUnixTime();//获取当前系统时间 if(UseThread) { // 线程方式 // 在数据队列中获取青瞳数据 while (!FrameQueue.IsEmpty())//处理完所有 { ST_MocapFrameData* Frame; if (FrameQueue.Dequeue(Frame))//出队 { PutMocapDataIntoFrameList(Frame);//将帧数数据塞入对应HuamnID/RigidBodyID的AllHumanFrames/AllRigidBodyFrames中。 } } } DoSample(AllHumanFrames); DoSample(AllRigidBodyFrames); // 每隔1s计算一次平均包间隔 if (CurTime - LastCheckIntervalTime > 1000) { if (AllHumanFrames.Num() > 0) { AllHumanFrames[0]->CalculatePackageAverageInterval(this->PackageAverageInterval); LastCheckIntervalTime = CurTime; } } } ``` ### 采样相关逻辑 - ***SampleByTimeStamp***() ```c++ void AChingmuMocapReceiverActor::DoSample(TArray& Frames) { for (auto i = 0; i < Frames.Num(); i++) { Frames[i]->CheckSize(CacheLimit);//判断当前帧数据是否超过指定长度(240帧,2~4秒数据),移除超出长度的数据。 if (SampleByTimeStamp(Frames[i]->Frames))//对数据进行插值,当前插值数据存在SampledHumanData。 { SendFrameToCharacter();//执行对应的TsChingmuMocapReceiverActor.ts中的逻辑,主要是触发一个事件,讲数据传递给TsMotionRetargetComponent.ts 或者 TsSceneLiveLinkPropActor.ts(动捕道具) } } } class MocapFrames { public: int ID; std::vector Frames = {}; public: MocapFrames(): ID(0) { } bool CheckSize(const int Limit) { if (Frames.size() > Limit) { const int DeletedCount = Frames.size() / 2; for (auto i = 0; i < DeletedCount; i++) { auto Data = Frames[i]; if (Data) { delete Data; } Data = nullptr; } Frames.erase(Frames.cbegin(), Frames.cbegin() + DeletedCount); return true; } return false; } }; ``` 对数据进行插值,当前插值数据存在**SampledHumanData**。 ```c++ bool AChingmuMocapReceiverActor::SampleByTimeStamp(std::vector& DataList) { const int64 SampleTime = ULiveDirectorStatics::GetUnixTime() - UMotionUtils::BackSampleTime;//UMotionUtils::BackSampleTime = 100ms,采样100ms的数据。 int Previous = -1; int Next = -1; for (int Index = DataList.size() - 1; Index > 0; Index--)//从Last => First遍历所有数据,确定插值的2个数据Index。 { const ST_MocapFrameData* Data = DataList[Index]; if (Data == nullptr) { continue; } if (Data->TimeStamp - SampleTime > 0) { Next = Index; } else { Previous = Index; break; } } if (bShowSampleLog) { UE_LOG(LogTemp, Warning, TEXT("prev: %d, next: %d, total: %llu"), Previous, Next, DataList.size()); } if (Previous != -1 && Next != -1) { const auto p = DataList[Previous]; const auto n = DataList[Next]; const float Factor = (n->TimeStamp - p->TimeStamp) > 0 ? (1.0 * (SampleTime - p->TimeStamp) / (n->TimeStamp - p->TimeStamp)) : 1.0; // Bone world pos cannot lerp like this // It will cause bone length changes all the time SampledHumanData->ID = p->ID; SampledHumanData->TimeStamp = SampleTime; SampledHumanData->FrameIndex = p->FrameIndex; for (auto Index = 0; Index < 23; Index++)//对23个骨骼进行差值。 { SampledHumanData->BonesWorldPos[Index] = UKismetMathLibrary::VLerp( p->BonesWorldPos[Index], n->BonesWorldPos[Index], Factor); SampledHumanData->BonesLocalRot[Index] = UKismetMathLibrary::RLerp(p->BonesLocalRot[Index].Rotator(), n->BonesLocalRot[Index].Rotator(), Factor, true).Quaternion(); } return true; } if (Previous != -1)//容错处理,全都是Previous,数据太旧直接清空。 { SampledHumanData->CopyFrom(DataList[Previous]); if(SampleTime - DataList[Previous]->TimeStamp > UMotionUtils::MotionTimeout) { // data is too old, clear the data list. DataList.clear(); } return true; } if (Next != -1)//没有Previous,直接复制Next的数据。 { SampledHumanData->CopyFrom(DataList[Next]); return true; } return false; } ``` ### FChingmuThread 用途为: - 获取当前系统时间。 - 使用异步Task的方式,通过调用**UChingMUComponent::FullBodyMotionCapBaseBonesLocalSpaceRotation()** 来更新每个演员的动捕数据。动捕数据存储在**ChingMUComponent**中的***LocalRotationList***、***GlobalLocationList***中。 - 管理HumanToLastReceiveTime,以此管理每个动捕演员的动画数据时长。 - OwnerActor->OnGetHumanData_NotInGameThread(), - 根据当前时间与当前Frames,从UChingMUComponent中将数据复制到[[#ST_MocapFrameData]]中。 - 将[[#ST_MocapFrameData]]转换成JSON后,使用AMotionSenderActor::OnGetRawMocapData_NotInGameThread()发送。 - 将当前帧数据加入FrameQueue队列。 - 线程睡眠0.001s。以此保证AChingmuMocapReceiverActor::Tick()中可以把数据都处理完。 ```c++ uint32 FChingmuThread::Run() { FTransform Tmp; while (bRun) { if (OwnerActor && OwnerActor->UseThread && OwnerActor->ChingmuComp && OwnerActor->ChingmuComp->IsConnected()) { CurTime = ULiveDirectorStatics::GetUnixTime(); // Human for (auto HumanIndex = 0; HumanIndex < OwnerActor->MaxHumanCount; HumanIndex++) { const auto bRes = OwnerActor->ChingmuComp->FullBodyMotionCapBaseBonesLocalSpaceRotation( OwnerActor->ChingmuFullAddress, HumanIndex, TmpTimeCode); if (bRes) { if (!HumanToLastReceiveTime.Contains(HumanIndex))//空数据处理。 { HumanToLastReceiveTime.Add(HumanIndex, 0); } if (HumanToLastReceiveTime[HumanIndex] != TmpTimeCode.Frames)//判断是否收到新的Frame数据 { HumanToLastReceiveTime[HumanIndex] = TmpTimeCode.Frames; OwnerActor->OnGetHumanData_NotInGameThread(HumanIndex, CurTime, TmpTimeCode.Frames); } else { // get same frame, skip break; } } } // Rigidbody for (auto RigidBodyIndex = OwnerActor->RigidBodyStartIndex; RigidBodyIndex < OwnerActor->RigidBodyStartIndex + OwnerActor->MaxRigidBodyCount; RigidBodyIndex++) { OwnerActor->ChingmuComp->GetTrackerPoseTC(OwnerActor->ChingmuFullAddress, RigidBodyIndex, Tmp, TmpTimeCode); if (!RigidBodyToLastReceiveTransform.Contains(RigidBodyIndex)) { RigidBodyToLastReceiveTransform.Add(RigidBodyIndex, FTransform::Identity); } // 道具的TmpTimeCode.Frames永远为0,所以无法用帧数判断 // 改为transform判断 if (!RigidBodyToLastReceiveTransform[RigidBodyIndex].Equals(Tmp)) { RigidBodyToLastReceiveTransform[RigidBodyIndex] = Tmp; OwnerActor->OnGetRigidBodyData_NotInGameThread(RigidBodyIndex, Tmp, CurTime, TmpTimeCode.Frames); } } } if (bRun) { FPlatformProcess::Sleep(OwnerActor ? OwnerActor->ThreadInterval : 0.004); } else { break; } } UE_LOG(LogTemp, Warning, TEXT("%s finish work."), *ThreadName) return 0; } ``` ## ST_MocapFrameData - ST_MocapFrameData为动捕数据的原始帧数据。 ```c++ #define MOCAP_BONE_COUNT 23 enum E_MotionType { Human, RigidBody }; enum E_SourceType { Mocap, CMR, Replay }; struct ST_MocapFrameData { int ID; int64 TimeStamp; int FrameIndex; E_MotionType MotionType; E_SourceType SourceType; FVector BonesWorldPos[MOCAP_BONE_COUNT]; FQuat BonesLocalRot[MOCAP_BONE_COUNT]; }; class LIVEDIRECTOR_API UMocapFrameData : public UObject { GENERATED_BODY() public: UPROPERTY(BlueprintReadWrite, EditAnywhere) int ID; UPROPERTY(BlueprintReadWrite, EditAnywhere) TArray BonesWorldPos; UPROPERTY(BlueprintReadWrite, EditAnywhere) TArray BonesLocalRot; UPROPERTY(BlueprintReadWrite, EditAnywhere) int64 TimeStamp; UPROPERTY(BlueprintReadWrite, EditAnywhere) int FrameIndex; UPROPERTY(BlueprintReadWrite, EditAnywhere) int MotionType; // 0 human; 1 rigidbody UPROPERTY(BlueprintReadWrite, EditAnywhere) int SourceType; // 0 mocap, 1 cmr public: void CopyFrom(const ST_MocapFrameData* Other) { ID = Other->ID; TimeStamp = Other->TimeStamp; FrameIndex = Other->FrameIndex; MotionType = Other->MotionType; SourceType = Other->SourceType; for (auto Index = 0; Index < 23; Index++) { BonesWorldPos[Index] = Other->BonesWorldPos[Index]; BonesLocalRot[Index] = Other->BonesLocalRot[Index]; } } }; class MocapFrames { public: int ID; std::vector Frames = {}; void CalculatePackageAverageInterval(float& Res) { if(Frames.size() > 0) { auto First = Frames[0]; auto Last = Frames[Frames.size() - 1]; if(Last->FrameIndex > First->FrameIndex) { Res = 1.0 * (Last->TimeStamp - First->TimeStamp) / (Last->FrameIndex - First->FrameIndex); } } } }; ``` # MotionCapture(青瞳插件) 实现1个组件与3个动画节点: - [[#ChingMUComponent]]: - [[#AnimNode_ChingMUPose]]:接受骨骼动捕数据。 - [[#AnimNode_ChingMURetargetPose]]:接受重定向后的骨骼动捕数据。 - AnimNode_ChingMURetargetPoseForBuild: ## ***ChingMUComponent*** 1. Init 1. BeginPlay():取得ini文件中的配置信息;取得当前角色的SkeletonMesh => CharacterSkinMesh;取得BoneName=>BoneIndex Map、TPose状态下骨骼的旋转值、TposeParentBonesRotation。 2. Connect 1. StartConnectServer():motionCapturePlugin->ConnectCommand = "ConnectServer"。具体逻辑会在FMotionCapture::Tick()处理。 2. DisConnectServer():motionCapturePlugin->ConnectCommand = "DisConnect"。 3. [[#CalculateBoneCSRotation()]] 4. [[#FullBodyMotionCapBaseBonesLocalSpaceRotation]] ### CalculateBoneCSRotation > Get Human Fullbody Tracker data ,including of 23joints localRotation and root joint world Position 1. m_motioncap->CMHuman():调用DLL的CMHumanExtern(),获取一个Double数组,前3个是RootLocation,后面全是Rotation。 2. 计算最终的四元数旋转值。 3. 返回的形参 FQuat* BonesComponentSpaceRotation,数组指针。 ### FullBodyMotionCapBaseBonesLocalSpaceRotation 相比CalculateBoneCSRotation,增加了时间码以及GlobalLocation的动捕数据获取。 1. m_motioncap->CMHuman():调用DLL的CMHumanExtern(),获取一个Double数组,前3个是RootLocation,后面全是Rotation。 2. motionCapturePlugin->CMHumanGlobalRTTC():调用DLL的CMHumanGlobalRTTC(),1-24 New Features。计算**VrpnTimeCode**以及**GlobalLocationList**。 数据存在**ChingMUComponent**中的***LocalRotationList***、***GlobalLocationList***。 ## FAnimNode_ChingMUPose 1. Initialize_AnyThread():取得**ChingMUComponent**。 2. Update_AnyThread():调用**ChingMUComponent->CalculateBoneCSRotation()** 3. Evaluate_AnyThread():对23根骨骼进行遍历;取得RefPose后,将从Update_AnyThread()获得动捕数据(**Rotation**)覆盖到上面(ComponentSpace),**根骨骼需要额外添加Location数据**。最后将数据从ComponentSpace => LocalSpace。 ## AnimNode_ChingMURetargetPose 1. Initialize_AnyThread():创建曲线逻辑(TCHour、TCMinute、TCSecond、TCFrame)。 2. Update_AnyThread(): 3. Evaluate_AnyThread():相关逻辑都实现在这里。 ### AnimNode_ChingMURetargetPose::Evaluate_AnyThread() # TsMotionReceiverActor 只在BeginPlay()中调用了this.MarkAsClientSeamlessTravel(); 具体逻辑在`AMotionReceiverActor` ## MotionReceiverActor ![[动捕逻辑思维导图.canvas]] # Config与BoneName相关逻辑 1. Config/FullBodyConfig.json储存了对应的骨骼名称、Morph以及RootMotion骨骼名称。 1. 通过 UMotionUtils::GetModelBones()、UMotionUtils::GetMoveableBones()、UMotionUtils::GetMorphTargets()获取名称数组。 2. GetModelBones() 1. 主要在FAnimNode_FullBody::Initialize_AnyThread()被调用。 2. 填充`TArray BoneRefList;`,顺带初始化SampledFullBodyData。 3. InitBoneRefIndex(),初始化BoneRefList中每个FBoneReference的BoneIndex(通过骨骼名称找到),如果没有找到会提示对应的Log。 4. FAnimNode_FullBody::Evaluate_AnyThread(),作用在[[#ApplyDataToPose()]]。 3. GetMorphTargets() 1. 主要在FAnimNode_FullBody::Initialize_AnyThread()被调用。 ## ApplyDataToPose() ### BoneTransform 1. 遍历BoneRefList(从UMotionUtils::GetModelBones()获得) 2. 对BoneIndex有效的骨骼进行一些操作。 1. 取得当前动画蓝图输出Pose的**骨骼Index**以及**采样后动捕数据的旋转值**。 2. 如果骨骼名是Hips,就将当前Index设置给HipsIndex。 3. 将旋转值应用到OutputPose中。 4. 判断当前骨骼名是否为MoveableBones中的名称,将这些骨骼的Location设置到OutputPose中。 ### MorphValues 将对应MorphTarget数据应用到对应的CurveChannel上。 ### RootMotion 根据bUseHipsTranslation变量执行不同的逻辑: #### MapTranslationToHips 调用函数形参如下: ```c++ MapTranslationToHips(Output, EvaluatedFullBodyData, 0, HipsIndex); ``` 1. 获取Joints骨骼的Locaiton作为RootMotion数据 2. 判断Joints骨骼是不是根骨骼,如果**是**则调整RootMotion数据轴向。 3. 将Joints骨骼的Location归零。 4. 如果Hips骨骼有效,则将RootMotion数据加到其Location上。 #### ExtractRootMotionInfo 1. 获取Joints骨骼的Locaiton作为RootMotion数据。 2. 判断Joints骨骼是不是根骨骼,如果**不是**则调整RootMotion数据轴向。(**轴向与MapTranslationToHips()不同**) 3. 将Joints骨骼的Location归零。 4. 将RootMotion设置给AnimInstance的RootMotionLocation。 5. 如果Hips骨骼有效,进行一堆计算,最终将Rotation设置AnimInstance的RootMotionRotation。 # 采样接收数据 - [x] 确定ChingMu重定向后Transform数组长度。 - 数据长度150个,但中间有很多事空值。 - [ ] TArray 使用cmr:二郎拖。 # UE4移植Mocap问题确定 ASoul的Control有一步摆初始Pose的操作,让下列骨骼的初始化Pose发生了改变。 Spine UE4角度<33.144 0.356 0.649> => UE5角度<33.418,-1.266,0.351> Spine~Spine3 Chest ChestMid Neck1 Head LeftArm LeftForeArm LeftHand RightArm RightForeArm RightHand # UE4 ChingMu重定向 - 移植FullBody的以下语句。 ```c++ bGetMotionData = Recv->SampleFullBodyData_AnimationThread(ValidIdentity, ULiveDirectorStatics::GetUnixTime() - UMotionUtils::BackSampleTime * 2, SampledFullBodyData); ``` ## - AChingmuMocapReceiverActor(创建线程)=> - FChingmuThread(负责接收数据并且塞入AChingmuMocapReceiverActor的FrameQueue)=> - [x] 添加时间轴判断,避免加入重复的帧。 - AChingmuMocapReceiverActor(Tick)=> - 从FrameQueue提取动捕帧数据并且塞入`TArray AllHumanFrames`(PutMocapDataIntoFrameList())。 - DoSample() 1. SampleByTimeStamp():对所有帧进行采样。 2. SendFrameToCharacter():逻辑在Puerts中,发送给MotionProcess动捕数据。 - CalculatePackageAverageInterval():相关逻辑感觉没用。 - FAnimNode_FullBody => - Update_AnyThread():bGetMotionData = Recv->SampleFullBodyData_AnimationThread():取得对应HumanID的动捕数据。 - Evaluate_AnyThread():取得SampledFullBodyData => ApplyDataToPose()。