17 KiB
Raw Blame History

相关类

  • 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()
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
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()
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

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中的LocalRotationListGlobalLocationList中。
  • 管理HumanToLastReceiveTime以此管理每个动捕演员的动画数据时长。
  • OwnerActor->OnGetHumanData_NotInGameThread()
    • 根据当前时间与当前Frames从UChingMUComponent中将数据复制到#ST_MocapFrameData中。
    • #ST_MocapFrameData转换成JSON后使用AMotionSenderActor::OnGetRawMocapData_NotInGameThread()发送。
    • 将当前帧数据加入FrameQueue队列。
  • 线程睡眠0.001s。以此保证AChingmuMocapReceiverActor::Tick()中可以把数据都处理完。
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为动捕数据的原始帧数据。
#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

  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中的LocalRotationListGlobalLocationList

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

调用函数形参如下:

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。

采样接收数据

  • 确定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的以下语句。
bGetMotionData = Recv->SampleFullBodyData_AnimationThread(ValidIdentity,  
                                                          ULiveDirectorStatics::GetUnixTime() -  
                                                          UMotionUtils::BackSampleTime * 2,  
                                                          SampledFullBodyData);

  • AChingmuMocapReceiverActor创建线程=>
  • FChingmuThread负责接收数据并且塞入AChingmuMocapReceiverActor的FrameQueue=>
    • 添加时间轴判断,避免加入重复的帧。
  • AChingmuMocapReceiverActorTick=>
    • 从FrameQueue提取动捕帧数据并且塞入TArray<MocapRetargetFrames*> AllHumanFramesPutMocapDataIntoFrameList())。
    • DoSample()
      1. SampleByTimeStamp():对所有帧进行采样。
      2. SendFrameToCharacter()逻辑在Puerts中发送给MotionProcess动捕数据。
    • CalculatePackageAverageInterval():相关逻辑感觉没用。
  • FAnimNode_FullBody =>
    • Update_AnyThread()bGetMotionData = Recv->SampleFullBodyData_AnimationThread()取得对应HumanID的动捕数据。
    • Evaluate_AnyThread()取得SampledFullBodyData => ApplyDataToPose()。