《楚留香》《逆水寒》《天涯明月刀》等一批武侠游戏都将捏脸系统作为了标配,并且开放了大量的参数给玩家,从而能够自由的发挥自己的想象力,捏出一堆鬼脸~在知乎(《Honey Select》)以及其他文章里对捏脸的原理进行了详细的分析,本文呢,主要记录基于骨骼的捏脸在Unreal4中的实现。
基于调整骨骼进行捏脸的核心就是修改脸部骨骼的Scale、Rotation,Position,从而改变骨骼对应的蒙皮的顶点的位置,以达到捏脸的效果:
上图是在动画蓝图里添加一个内置的改变骨骼的节点(下图)来修改鼻子的x坐标的scale 的效果: 看起来捏脸也就这么回事了!但是呢,要想达到游戏中千人千面的效果,基于骨骼的捏脸有以下几点要求:其中 1 主要由3D建模师操作,另外对于脸部的对称部分,设计其对应的骨骼为对称骨骼,从而方便调节;对于第二条,大部分的游戏会设计一套叫做controller的第二层骨骼,每个controller同时操纵多根骨骼的多个参数的不同组合来调节局部区域,controller1控制眼部的整体的大小,需要添加眼部骨骼到controller控制的骨骼的列表中,controller的示意图如下:
这样用户通过操纵controller的滑竿便可以一次性调节一个局部区域,实际上,通过二层骨骼我们降低了局部骨骼参数的自由度,从而方便用户精细的调整角色脸部的细节表情。举例:controller1通过控制三根骨骼的缩放参数来达到整体调节眼部大小的目的:
3暂且按下不表;接下来4的话会涉及到如何在unreal里实现捏脸,因此会展开详细记录。
分为捏脸部分和与动画系统融合部分
首先,开篇所述的直接用ModifyBone蓝图节点来修改每根骨骼的话,对于程序非常的不友好,为了捏脸的效果和充分的表达能力,SkeletalMesh中通常设置较多的骨骼,因此直接使用ModifyBone节点是不太方便的。
我们整体的逻辑应该是这样:
第一步和第二步实现比较简单,略去。对于第三步在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中,整个流程如下图所示:
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;
};
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);
}
}
}
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
};
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;
};
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));
}
}