关于使用 Sequence Record 无法录制面部 Morph Target 问题的解决方法

2022-06-12 13:28:21 浏览数 (2)

关于使用 Sequence Record 无法录制面部 Morph Target 问题的解决方法

这里 Sequence Recorder 包含动画蓝图中默认的录制窗口:

总体上来说有两种方法,一种是基于蓝图和代理骨骼,一种是通过C 代码实现。

1. 蓝图加代理骨骼的实现

主要是通过这个节点实现:

选中节点,然后选中充当代理的骨骼(注意,最好是动画用不到的骨骼,不然大概率会出现奇怪的效果,如果没有这种可以自己在导入引擎后的 Skeleton 文件中创建虚拟骨骼):

上图中选中了 TONGLETranslation X 作为数据来源,即骨骼的坐标 X 值作为数据来源。

再设置下要设置数据的目标:

这里设置的目标类型是名为 jawOpen 的 Morph Target。此外,目标也可以设置为另一个骨骼,以及 Material Parameter,这里不作展开。

最后类似这样连接,就可以通过 TONGLE 骨骼的坐标 X 来控制 jawOpen 了:

最左侧的 Transform (Modyfy) Bone 只是演示用,实际上工程中都是动画源。

1.1. 优缺点

优点:

  1. 蓝图实现方便,能够快速验证;
  2. 适用于只需要更新少量 Morph Target 的场合;

缺点:

  1. Morph Target 一多起来(例如 ARKit 那种52个 Morph Target 的情况)就会很难管理,代码会很杂乱(这算是蓝图通病);
  2. 不适合协作修改(蓝图 Merge 起来还是不太方便);
  3. 可能需要创建大量的代理骨骼(一个骨骼最多只能传数据到9个 Morph Target);

2. 代码实现

我比较推荐通过代码实现,总体步骤是创建插件,创建新的继承AnimNode_Base动画节点(或者其他继承AnimNode_Base的子类例如AnimNode_SkeletalControlBase),然后在动画节点的Evaluate_AnyThread(如果是其他AnimNode_Base的子类那么可能是EvaluateXXX_AnyThread,例如AnimNode_SkeletalControlBaseEvaluateComponentSpace_AnyThread)。下面是详细步骤。

2.1. 创建插件

这一步没什么好说的,直接通过引擎 Plugin 界面创建就行:

插件名字等内容可以随意填,插件类型建议选Blank:

2.2. 创建动画节点

这里以AnimNode_Base作为基类为例子,实际上可以用别的动画节点类,可以根据业务需求选择。

我们重点看AnimNode部分,除了AnimNode还需要创建AnimGraphNode,这里不详细说,因为创建默认的就可以了。

假设我们创建的类为:

代码语言:javascript复制
USTRUCT(BlueprintInternalUseOnly)
struct DATALINK_API FAnimNode_DLMoCap : public FAnimNode_Base {
    // 这个一定要有
    GENERATED_USTRUCT_BODY()
    // ...这里先省略
}

重点是重载FAnimNode_Base的这个函数:

代码语言:javascript复制
virtual void Initialize_AnyThread(const FAnimationInitializeContext& Context) override;
virtual void Update_AnyThread(const FAnimationUpdateContext & Context) override;
virtual void Evaluate_AnyThread(FPoseContext& Output) override;

其他函数基本上不怎么需要修改,而Initialize_AnyThreadUpdate_AnyThread主要用来初始化、加载数据,所以实际上重点只是Evaluate_AnyThread。在Evaluate_AnyThread函数中,最重要的部分则是:

代码语言:javascript复制
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 文件给美术进一步修改、使用。

0 人点赞