Unreal中的捏臉

《楚留香》《逆水寒》《天涯明月刀》等一批武俠遊戲都將捏臉系統作爲了標配,並且開放了大量的參數給玩家,從而能夠自由的發揮自己的想象力,捏出一堆鬼臉~在知乎(《Honey Select》)以及其他文章裏對捏臉的原理進行了詳細的分析,本文呢,主要記錄基於骨骼的捏臉在Unreal4中的實現。

原理

基於調整骨骼進行捏臉的核心就是修改臉部骨骼的Scale、Rotation,Position,從而改變骨骼對應的蒙皮的頂點的位置,以達到捏臉的效果:

上圖是在動畫藍圖裏添加一個內置的改變骨骼的節點(下圖)來修改鼻子的x座標的scale 的效果:
看起來捏臉也就這麼回事了!但是呢,要想達到遊戲中千人千面的效果,基於骨骼的捏臉有以下幾點要求:
  1. 需要設計一套有足夠表達能力的骨骼以及細緻的臉部蒙皮
  2. 大量的骨骼對應的大量參數帶來的自由度過高,不易調節,應便於用戶調節
  3. 性能消耗相對較少
  4. 跟現有的動畫系統以及基於blendshape的表情兼容
  5. 如果有AI能力,根據用戶提供的照片自動生成對應的模型是最好不過的了
  6. 有一套對應的妝容方案

其中 1 主要由3D建模師操作,另外對於臉部的對稱部分,設計其對應的骨骼爲對稱骨骼,從而方便調節;對於第二條,大部分的遊戲會設計一套叫做controller的第二層骨骼,每個controller同時操縱多根骨骼的多個參數的不同組合來調節局部區域,controller1控制眼部的整體的大小,需要添加眼部骨骼到controller控制的骨骼的列表中,controller的示意圖如下:
在這裏插入圖片描述
這樣用戶通過操縱controller的滑竿便可以一次性調節一個局部區域,實際上,通過二層骨骼我們降低了局部骨骼參數的自由度,從而方便用戶精細的調整角色臉部的細節表情。舉例:controller1通過控制三根骨骼的縮放參數來達到整體調節眼部大小的目的:
在這裏插入圖片描述
3暫且按下不表;接下來4的話會涉及到如何在unreal裏實現捏臉,因此會展開詳細記錄。

Unreal實現

分爲捏臉部分和與動畫系統融合部分

捏臉部分

首先,開篇所述的直接用ModifyBone藍圖節點來修改每根骨骼的話,對於程序非常的不友好,爲了捏臉的效果和充分的表達能力,SkeletalMesh中通常設置較多的骨骼,因此直接使用ModifyBone節點是不太方便的。
我們整體的邏輯應該是這樣:

  1. 根據json文件解析出的controller生成所有的調節滑桿,並加載其默認值;
  2. 如果滑桿值發生變化,則對應線性插值或者樣條插值該controller對應的所有的骨骼的對應的參數;
  3. 然後將變化的相對Transform更新到骨架的transform上;
  4. Rendering。

第一步和第二步實現比較簡單,略去。對於第三步在Unreal中針對骨架有多套數據結構,從捏臉的方便性上來說,這裏我們選擇PoseableMesh來操作,查看PoseableMeshComponent.h的源碼,可以看到以下函數:

class ENGINE_API UPoseableMeshComponent : public USkinnedMeshComponent
	{
	GENERATED_UCLASS_BODY()

	/** Temporary array of local-space (ie relative to parent bone) rotation/translation/scale for each bone. */
	TArray<FTransform> BoneSpaceTransforms;

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void SetBoneTransformByName(FName BoneName, const FTransform& InTransform, EBoneSpaces::Type BoneSpace);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh") 
	FTransform GetBoneTransformByName(FName BoneName, EBoneSpaces::Type BoneSpace);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void ResetBoneTransformByName(FName BoneName);

	UFUNCTION(BlueprintCallable, Category="Components|PoseableMesh")
	void CopyPoseFromSkeletalComponent(const USkeletalMeshComponent* InComponentToCopy);
	};

可以看到利用PoseableMesh我們可以方便的操縱Transform,從而達到捏臉的目的。下面放兩張Demo的截圖,左側爲直接調節單根骨骼,右側爲調節controller:

在這裏插入圖片描述在這裏插入圖片描述

與動畫系統的融合

PoseableMesh雖好,可不要貪杯哦(劃掉),但是不支持動畫,不支持Blendshape,換句話說,PoseableMesh就像專門的一套方便處理骨架transform的數據結構,其他的功能還是交由SkeletalMesh來做,那麼問題就來了,如何將那捏臉的數據傳到SkeletalMesh中,從而與動畫以及BlendShape融合呢?這裏我選擇在AnimationBlueprint裏實現一個自定義的AnimNode ModifyTransform來將PoseableMesh處理好的捏臉數據喂到SkeletalMesh的Render_Thread中,整個流程如下圖所示:
在這裏插入圖片描述

  1. 因爲我們有兩套mesh來處理不同的數據,因此在藍圖中我們選擇掛載倆mesh,將其中的PoseableMesh設爲不可見:
    在這裏插入圖片描述
  2. 新建一個UAvatarAnimInstance繼承自UAnimInstance,並添加以下數據:
UCLASS()
	class AVATAR_UE4_API UAvatarAnimInstance : public UAnimInstance
	{
		GENERATED_BODY()
		public:
		UAvatarAnimInstance(const FObjectInitializer& ObjectInitializer);

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FVector> BonesTranslation;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FRotator> BonesRotation;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FVector> BonesScale;

		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneTransform)
		TArray<FName> BonesName;
	};
  1. 在計算完捏臉滑桿邏輯即將捏臉的transform更新到PoseableMesh後,添加並調用以下函數將數據傳入到AnimInstance中:
void UAutoPinch::TransformBoneData2AnimInstance()
	{
		if(Animation)
		{ 
			for (int i = 0; i < Animation->BonesName.Num(); i++)
			{
				Animation->BonesTranslation[i] = PoseableMesh->GetBoneLocationByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
				Animation->BonesRotation[i] = PoseableMesh->GetBoneRotationByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
				Animation->BonesScale[i] = PoseableMesh->GetBoneScaleByName(Animation->BonesName[i], EBoneSpaces::ComponentSpace);
			}
		}
	}
  1. 創建一個藍圖類繼承自UAvatarAnimInstance,並將其指定爲第一步中的skeletalMesh的AnimClass中的動畫藍圖的父類。
  2. 接下來創建自定義動畫藍圖節點,主要分爲編輯器部分和runtime部分,編輯器部分的創建可參考其他文檔,這裏我們只記錄如何創建自定義動畫藍圖節點的runtime部分。
  3. 創建FAnimNode_ModifyTransform類繼承自FAnimNode_SkeletalControlBase
USTRUCT()
	struct AVATAR_UE4_API FAnimNode_ModifyTransform :public FAnimNode_SkeletalControlBase
	{
	GENERATED_USTRUCT_BODY()
	public:
	
	/*New Transform to use*/
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Translation, meta = (PinShownByDefault))
		FBonesTransfroms BonesTransfroms;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Translation)
		TEnumAsByte<EBoneModificationMode> TranslationMode;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Rotation)
		TEnumAsByte<EBoneModificationMode> RotationMode;

	/** Whether and how to modify the translation of this bone. */
	UPROPERTY(EditAnywhere, Category = Scale)
		TEnumAsByte<EBoneModificationMode> ScaleMode;

	/** Reference frame to apply Translation in. */
	UPROPERTY(EditAnywhere, Category = Translation)
		TEnumAsByte<enum EBoneControlSpace> TranslationSpace;

	/** Reference frame to apply Rotation in. */
	UPROPERTY(EditAnywhere, Category = Rotation)
		TEnumAsByte<enum EBoneControlSpace> RotationSpace;

	/** Reference frame to apply Scale in. */
	UPROPERTY(EditAnywhere, Category = Scale)
		TEnumAsByte<enum EBoneControlSpace> ScaleSpace;
	FAnimNode_ModifyTransform();
	//  // FAnimNode_Base interface  
	virtual void GatherDebugData(FNodeDebugData& DebugData) override;
	//  // End of FAnimNode_Base interface  

		// FAnimNode_SkeletalControlBase interface  
	virtual void EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext& Output, TArray<FBoneTransform>& OutBoneTransforms) override;
	
	bool IsValidToEvaluate(const USkeleton* Skeleton, const FBoneContainer& RequiredBones) override;
	// End of FAnimNode_SkeletalControlBase interface  

	private:
	// FAnimNode_SkeletalControlBase interface  

	virtual void InitializeBoneReferences(const FBoneContainer& RequiredBones) override;
	// End of FAnimNode_SkeletalControlBase interface  

	};
  1. 因爲自定義藍圖節點不支持TArray做爲輸入,這裏我們創建一個struct用來接收AnimInstance中傳過來的數據:
USTRUCT(BlueprintType)
	struct FBonesTransfroms
	{
	GENERATED_BODY()
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FName> BonesName;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FVector> BonesTranslation;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FVector> BonesScale;
		UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BonesTransfroms")
		TArray<FRotator> BonesRotation;
	};
  1. 然後實現FAnimNode_ModifyTransform中的虛函數,其中最重要的就是EvaluateSkeletalControl_AnyThread:
void FAnimNode_ModifyTransform::EvaluateSkeletalControl_AnyThread(FComponentSpacePoseContext & Output, TArray<FBoneTransform>& OutBoneTransforms)
	{
	check(OutBoneTransforms.Num() == 0);
	// the way we apply transform is same as FMatrix or FTransform
	// we apply scale first, and rotation, and translation
	// if you'd like to translate first, you'll need two nodes that first node does translate and second nodes to rotate.
	const FBoneContainer& RequiredBones = Output.AnimInstanceProxy->GetRequiredBones();
	const FBoneContainer& BoneContainer = Output.Pose.GetPose().GetBoneContainer();
	for (int i=0;i<BonesTransfroms.BonesName.Num();i++)
	{
		auto name = BonesTransfroms.BonesName[i];
		FBoneReference MyBoneToModify(name);
		auto ret = MyBoneToModify.Initialize(RequiredBones);
	
		FCompactPoseBoneIndex CompactPoseBoneToModify = MyBoneToModify.GetCompactPoseIndex(BoneContainer);
		FTransform NewBoneTM = Output.Pose.GetComponentSpaceTransform(CompactPoseBoneToModify);
		FTransform ComponentTransform = Output.AnimInstanceProxy->GetComponentTransform();
	
		FVector Scale = BonesTransfroms.BonesScale[i];
		FVector Translation = BonesTransfroms.BonesTranslation[i];
		FQuat Rotation(BonesTransfroms.BonesRotation[i]);
		
		if (ScaleMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, ScaleSpace);

			if (ScaleMode == BMM_Additive)
			{
				NewBoneTM.SetScale3D(NewBoneTM.GetScale3D() * Scale);
			}
			else
			{
				NewBoneTM.SetScale3D(Scale);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, ScaleSpace);
		}
		if (RotationMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, RotationSpace);

			const FQuat BoneQuat(Rotation);
			if (RotationMode == BMM_Additive)
			{
				NewBoneTM.SetRotation(BoneQuat * NewBoneTM.GetRotation());
			}
			else
			{
				NewBoneTM.SetRotation(BoneQuat);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, RotationSpace);
		}
		if (TranslationMode != BMM_Ignore)
		{
			// Convert to Bone Space.
			FAnimationRuntime::ConvertCSTransformToBoneSpace(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, TranslationSpace);

			if (TranslationMode == BMM_Additive)
			{
				NewBoneTM.AddToTranslation(Translation);
			}
			else
			{
				NewBoneTM.SetTranslation(Translation);
			}

			// Convert back to Component Space.
			FAnimationRuntime::ConvertBoneSpaceTransformToCS(ComponentTransform, Output.Pose, NewBoneTM, CompactPoseBoneToModify, TranslationSpace);
		}

		OutBoneTransforms.Add(FBoneTransform(MyBoneToModify.GetCompactPoseIndex(BoneContainer), NewBoneTM));
	}
	}
  1. 最後在skeletalMesh的動畫藍圖中添加以下節點:在這裏插入圖片描述
  2. 這樣整個數據流就跑通了,從捏臉的數據到最後的rendering。
    鏈接: https://buaaccj.github.io/.
    謝謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章