UE的 AimOffset

参考:https://docs.unrealengine.com/5.0/en-US/aim-offset-in-unreal-engine/
参考:https://www.youtube.com/watch?v=LzDw9VhlWJs&t=441s&ab_channel=NiceShadow
参考:https://forums.unrealengine.com/t/whats-the-difference-between-aimoffset-and-blendspace/348209
参考:https://blog.csdn.net/opk8848/article/details/116710545


什么是Aim Offset

An Aim Offset is a type of Blend Space that uses additive poses, typically for creating aim-spaces.

具体到UE里,AimOffset表现为一种动画文件,后缀为.uasset,比如UE4_Mannequin_Skeleton_AimOffset2D

A blend space source only animation source, this means that it is only outputs an animation similar to a regular sequence player node dose. On the other hand AimOffset is applied as an additive animation on to the animation you feed into it’s input.

在UE里,AimOffset其实是BlendSpace的子类,无非BlendSpace输出的是完整的Animation Pose,而AimOffset输出的是基于Base Pose,再加上Additive Pose的结合。类比在Unity里,AimOffset等同于Unity里的Base Animation Layer和Additive Animation Layer的叠加。

AimOffset类代码如下:

// UAimOffsetBlendSpace.h

// 甚至没有添加任何的数据
UCLASS(config=Engine, hidecategories=Object, MinimalAPI, BlueprintType)
class UAimOffsetBlendSpace : public UBlendSpace
{
	GENERATED_UCLASS_BODY()
		
	virtual bool IsValidAdditiveType(EAdditiveAnimationType AdditiveType) const override;
	virtual bool IsValidAdditive() const override;
};


//	AimOffsetBlendSpace.cpp

#include "Animation/AimOffsetBlendSpace.h"

UAimOffsetBlendSpace::UAimOffsetBlendSpace(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bRotationBlendInMeshSpace = true;
}

bool UAimOffsetBlendSpace::IsValidAdditiveType(EAdditiveAnimationType AdditiveType) const
{
	return (AdditiveType == AAT_RotationOffsetMeshSpace);
}

bool UAimOffsetBlendSpace::IsValidAdditive() const
{
	// ContainsMatchingSamples用于判断输入的BlendSpace的动画类型, 与实际的Sample的动画数据是否匹配
	return ContainsMatchingSamples(AAT_RotationOffsetMeshSpace);
}

BlendSpace资源是否能转成AimOffset资源的逻辑应该是这样是:只要BlendSpace里所有Sample点对应的动画资源类型设置为Additive Type(必须为MeshSpaceAdditive类型的动画),而且是ValidAdditive动画,那么BlendSpace就可以转换成AimOffset资源


搭建AimOffset效果

创建AimOffset资源

首先选择对应的Skeleton,或者Skeletal Mesh,右键创建Aim Offset:
在这里插入图片描述
打开界面,发现它跟UE的BlendSpace界面非常像:
在这里插入图片描述
由于AimOffset的动画都是作为Additive Pose的,为了方便预览效果,这里需要加上默认的底板动画,需要在左边的Preview Base Pose里选择一个底板动画,或者底板Pose。


更改动画类型为Additive

然后把Animation Sequence拖到里面就可以了,跟BlendSpace操作一样,这里需要保证your animations are additive,对应在UE里的设置是,把动画资源的Additive Settings改为Mesh Space:
在这里插入图片描述

然后需要选择Base Pose的类型,这里选择Animation frame,也就是选择动画的某一帧作为Reference Pose:
在这里插入图片描述
然后选择动画、再选择帧数即可:
在这里插入图片描述


Mesh Space与Local Space的区别

这里有两种Additive Animation类型:

  • Local Space(AAT_LocalSpaceBase ): Create Additive from LocalSpace Base
  • Mesh Space(AAT_RotationOffsetMeshSpace): Create Additive from MeshSpace Rotation Only, Translation still will be LocalSpace

Mesh Space applies its additive effect in the space of the Skeletal Mesh Component

两种类型主要是在旋转上有区别,这里的Mesh Space applies its additive effect in the space of the Skeletal Mesh Component,也就是说它的Mesh Space指的是Skeletal Mesh Component对应的Space,类似于渲染里的Model Space。在Mesh Space类型下,每个Joint是基于Model Space的Transform计算的DeltaRotation,而不是原本的基于Parent Bone计算Delta Local Rotation

在我理解,二者的区别在于:

  • 设置为Local Space时,应该是计算每个关节的Delta Local Transform得到Delta Pose,然后作为LocalDeltaRotation应用到每个关节上,这应该也就是我理解的Unity里的Additive Layer的方法
  • 设置为Mesh Space时,它对关节添加的Rotation是不受关节的Parent的Rotation影响的,每个关节的Addtivve Rotation数据记录的是它相对于SkeletalMeshComponent对应Component Transform的delta rotation数据,而Additive Position跟Local Space模式的计算方法相同

在使用AimOffsets时,所有的动画资源的Additive Type都应该被设置为Mesh Space。举个例子,比如说我有个人物,他此时倾斜着身子,如下图所示:
在这里插入图片描述
那么此时如果让他向上瞄准,那么他枪应该指向Model坐标系的上方,而不是他当前位置的头部朝向代表的上方。但如果我用Local Space类型的Additive Animation,则会在其对应Pose的基础上,朝上瞄准:
在这里插入图片描述
如果采用的是Mesh Space,那么它相对于Model(其实是SkeletalMeshComponent)的旋转是不变的,则无论其他关节怎么样,这里的胸和两手的Rotation对应的朝向都是固定不变的:
在这里插入图片描述

看了下这个帖子,貌似Unity没有Mesh Additive类型的Additive Layer


相关动画节点和源码分析

有一个可供蓝图使用的节点叫FAnimNode_MakeDynamicAdditive

you can use a Make Dynamic Additive node, to negatively(反向的) blend additive animation poses. With this node, you can subtract the additive pose from the base pose to create an output pose.

正常的Apply Addtive的Pose是Base Pose + Additive Pose,这里的FAnimNode_MakeDynamicAdditive是Base Pose - Additive Pose,如下图所示:
在这里插入图片描述

Runtime节点的代码如下:

USTRUCT(BlueprintInternalUseOnly)
struct ANIMGRAPHRUNTIME_API FAnimNode_MakeDynamicAdditive : public FAnimNode_Base
{
	GENERATED_USTRUCT_BODY()

	// Reference pose for additive delta calculation
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Links)
	FPoseLink Base;

	// Pose to make additive
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Links)
	FPoseLink Additive;

	// Do additive delta calculation in mesh space
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Settings)
	bool bMeshSpaceAdditive;

public:	
	FAnimNode_MakeDynamicAdditive();

	// FAnimNode_Base interface
	virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
	virtual void CacheBones_AnyThread(const FAnimationCacheBonesContext& Context) override;
	virtual void Update_AnyThread(const FAnimationUpdateContext& Context) override;
	virtual void Evaluate_AnyThread(FPoseContext& Output) override;
	virtual void GatherDebugData(FNodeDebugData& DebugData) override;
	// End of FAnimNode_Base interface
};

可以看看里面的函数实现:

void FAnimNode_MakeDynamicAdditive::Evaluate_AnyThread(FPoseContext& Output)
{
	DECLARE_SCOPE_HIERARCHICAL_COUNTER_ANIMNODE(Evaluate_AnyThread)
	FPoseContext BaseEvalContext(Output);

	Base.Evaluate(BaseEvalContext);
	Additive.Evaluate(Output);

	// 如果是MeshSpace Additive的类型
	if (bMeshSpaceAdditive)
	{
		// 把Pose里的Bone的Rotation数据从Local Space改成Mesh Space
		FAnimationRuntime::ConvertPoseToMeshRotation(Output.Pose);
		FAnimationRuntime::ConvertPoseToMeshRotation(BaseEvalContext.Pose);
	}

	FAnimationRuntime::ConvertPoseToAdditive(Output.Pose, BaseEvalContext.Pose);
	Output.Curve.ConvertToAdditive(BaseEvalContext.Curve);

	UE::Anim::Attributes::ConvertToAdditive(BaseEvalContext.CustomAttributes, Output.CustomAttributes);
}

这里的关键函数就是FAnimationRuntime::ConvertPoseToMeshRotation了:

void FAnimationRuntime::ConvertPoseToMeshRotation(FCompactPose& LocalPose)
{
	SCOPE_CYCLE_COUNTER(STAT_ConvertPoseToMeshRot);

	// Convert all rotations to mesh space
	// only the root bone doesn't have a parent. So skip it to save a branch in the iteration.
	if (bAnim_Runtime_ISPC_Enabled)
	{
#if INTEL_ISPC
		ispc::ConvertPoseToMeshRotation(
			(ispc::FTransform*)&LocalPose.GetBones()[0],
			(int32*)&LocalPose.GetBoneContainer().GetCompactPoseParentBoneArray().GetData()[0],
			LocalPose.GetNumBones());
#endif
	}
	else
	{
		// 转换的过程还挺简单的, 无非就是把Joint的Rotation从LocalRotaion, 不断乘以Parent的Rotaion, 最后算出Component Space下的
		// Rotation而已, 注意这里的遍历是从index = 1开始的, 所以是相对root的Rotation
		// 这里是先计算parent, 才能计算出children, 所以是顺序遍历的
		for (FCompactPoseBoneIndex BoneIndex(1); BoneIndex < LocalPose.GetNumBones(); ++BoneIndex)
		{
			const FCompactPoseBoneIndex ParentIndex = LocalPose.GetParentBoneIndex(BoneIndex);
	
			const FQuat MeshSpaceRotation = LocalPose[ParentIndex].GetRotation() * LocalPose[BoneIndex].GetRotation();
			LocalPose[BoneIndex].SetRotation(MeshSpaceRotation);
		}
	}
}

相应的,UE还提供了反向的转换函数,可以在进行处理之后,还原Pose数据(毕竟感觉Pose里的Joints存LocalTransform更合理一点):

void FAnimationRuntime::ConvertMeshRotationPoseToLocalSpace(FCompactPose& Pose)
{
	SCOPE_CYCLE_COUNTER(STAT_ConvertMeshRotPoseToLocalSpace);

	// Convert all rotations to mesh space
	// only the root bone doesn't have a parent. So skip it to save a branch in the iteration.
	if (bAnim_Runtime_ISPC_Enabled)
	{
#if INTEL_ISPC
		ispc::ConvertMeshRotationPoseToLocalSpace(
			(ispc::FTransform*)&Pose.GetBones()[0],
			(int32*)&Pose.GetBoneContainer().GetCompactPoseParentBoneArray().GetData()[0],
			Pose.GetNumBones());
#endif
	}
	else
	{	
		// 转换的过程正好相反, 这里每个Joint的LocalRotation是由它和它的parent计算得到的
		// 所以需要先计算和改变children的Rotation, 所以是逆序遍历的
		for (FCompactPoseBoneIndex BoneIndex(Pose.GetNumBones() - 1); BoneIndex > 0; --BoneIndex)
		{
			const FCompactPoseBoneIndex ParentIndex = Pose.GetParentBoneIndex(BoneIndex);

			FQuat LocalSpaceRotation = Pose[ParentIndex].GetRotation().Inverse() * Pose[BoneIndex].GetRotation();
			Pose[BoneIndex].SetRotation(LocalSpaceRotation);
		}
	}
}
posted @ 2022-12-03 13:07  弹吉他的小刘鸭  阅读(148)  评论(0编辑  收藏  举报