关于使用 Sequence Record 无法录制面部 Morph Target 问题的解决方法
这里 Sequence Recorder 包含动画蓝图中默认的录制窗口:
总体上来说有两种方法,一种是基于蓝图和代理骨骼,一种是通过C++代码实现。
1. 蓝图加代理骨骼的实现
主要是通过这个节点实现:
选中节点,然后选中充当代理的骨骼(注意,最好是动画用不到的骨骼,不然大概率会出现奇怪的效果,如果没有这种可以自己在导入引擎后的 Skeleton 文件中创建虚拟骨骼):
上图中选中了 TONGLE
的 Translation X
作为数据来源,即骨骼的坐标 X
值作为数据来源。
再设置下要设置数据的目标:
这里设置的目标类型是名为 jawOpen
的 Morph Target。此外,目标也可以设置为另一个骨骼,以及 Material Parameter,这里不作展开。
最后类似这样连接,就可以通过 TONGLE 骨骼的坐标 X 来控制 jawOpen 了:
最左侧的 Transform (Modyfy) Bone 只是演示用,实际上工程中都是动画源。
1.1. 优缺点
优点:
- 蓝图实现方便,能够快速验证;
- 适用于只需要更新少量 Morph Target 的场合;
缺点:
- Morph Target 一多起来(例如 ARKit 那种52个 Morph Target 的情况)就会很难管理,代码会很杂乱(这算是蓝图通病);
- 不适合协作修改(蓝图 Merge 起来还是不太方便);
- 可能需要创建大量的代理骨骼(一个骨骼最多只能传数据到9个 Morph Target);
2. 代码实现
我比较推荐通过代码实现,总体步骤是创建插件,创建新的继承AnimNode_Base
动画节点(或者其他继承AnimNode_Base
的子类例如AnimNode_SkeletalControlBase
),然后在动画节点的Evaluate_AnyThread
(如果是其他AnimNode_Base
的子类那么可能是EvaluateXXX_AnyThread
,例如AnimNode_SkeletalControlBase
的EvaluateComponentSpace_AnyThread
)。下面是详细步骤。
2.1. 创建插件
这一步没什么好说的,直接通过引擎 Plugin 界面创建就行:
插件名字等内容可以随意填,插件类型建议选Blank:
2.2. 创建动画节点
这里以AnimNode_Base
作为基类为例子,实际上可以用别的动画节点类,可以根据业务需求选择。
我们重点看AnimNode
部分,除了AnimNode
还需要创建AnimGraphNode
,这里不详细说,因为创建默认的就可以了。
假设我们创建的类为:
USTRUCT(BlueprintInternalUseOnly)
struct DATALINK_API FAnimNode_DLMoCap : public FAnimNode_Base {
// 这个一定要有
GENERATED_USTRUCT_BODY()
// ...这里先省略
}
重点是重载FAnimNode_Base
的这个函数:
virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
virtual void Update_AnyThread(const FAnimationUpdateContext & Context) override;
virtual void Evaluate_AnyThread(FPoseContext& Output) override;
其他函数基本上不怎么需要修改,而Initialize_AnyThread
和Update_AnyThread
主要用来初始化、加载数据,所以实际上重点只是Evaluate_AnyThread
。在Evaluate_AnyThread
函数中,最重要的部分则是:
void FAnimNode_DLMoCap::Evaluate_AnyThread(FPoseContext& Output)
{
// 忽略这一行,主要是用来接受上游动画节点的动画数据,和我们要做的事情不太相关
SourcePose.Evaluate(Output);
// 获取骨骼
const USkeleton* Skeleton = Output.AnimInstanceProxy->GetSkeleton();
// 从骨骼上获取UID,实际上相当于是根据名字获取动画曲线 Curve 的 UID
// 这里的 jawOpen 最好是一个 FName 类型的变量,这样方便传入 Morph Target 名字,这里只是演示用所以直接输入了名字
const SmartName::UID_Type NameUID = Skeleton->GetUIDByName(USkeleton::AnimCurveMappingName, "jawOpen");
if (NameUID != SmartName::MaxUID)
{
// 这里的 1.0f 最好是一个 float 变量,这一行相当于设置曲线当前值为 1.0f
Output.Curve.Set(NameUID, 1.0f);
}
}
我对这里操作的理解是,创建一个对应给定 Morph Target 的 Curve(获取 UID 的时候是通过名字获取的,所以如果输入的 Morph Target 名字是模型没有的 Morph Target,那么应该就会获取失败,最终返回结果会是 SmartName::MaxUID
),然后写入当前帧对应的值。
最后录制好之后我们可以直接打开动画文件,看到 Morph Target 的曲线:
此时表情数据也可以正常导出为 FBX 文件给美术进一步修改、使用。