513 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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.

# 相关类
- 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<UMocapFrameData>();
ThreadInterval = 0.002;
BackIndexCount = int64(UMotionUtils::BackSampleTime / (1000.0 / CHINGMU_SERVER_FPS));//BackSampleTime = 100ms CHINGMU_SERVER_FPS =120ms
ChingmuComp = Cast<UChingMUComponent>(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<ST_MocapFrameData*> 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<MocapFrames*>& 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<ST_MocapFrameData*> 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<ST_MocapFrameData*>& 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<FVector> BonesWorldPos;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
TArray<FQuat> 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<ST_MocapFrameData*> 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<FBoneReference> 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<MocapFrames*>
使用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] 添加时间轴判断,避免加入重复的帧。
- AChingmuMocapReceiverActorTick=>
- 从FrameQueue提取动捕帧数据并且塞入`TArray<MocapRetargetFrames*> AllHumanFrames`PutMocapDataIntoFrameList())。
- DoSample()
1. SampleByTimeStamp():对所有帧进行采样。
2. SendFrameToCharacter()逻辑在Puerts中发送给MotionProcess动捕数据。
- CalculatePackageAverageInterval():相关逻辑感觉没用。
- FAnimNode_FullBody =>
- Update_AnyThread()bGetMotionData = Recv->SampleFullBodyData_AnimationThread()取得对应HumanID的动捕数据。
- Evaluate_AnyThread()取得SampledFullBodyData => ApplyDataToPose()。