UnityUnity

Unity 的动画图(PlayableGraph)和人形(Hum

2019-01-30  本文已影响0人  加菲教主

概述

最近在试做一个射击游戏的人物动画 Demo,尝试使用了部分 Unity 的人形动画(Humanoid),以及 Playable Graph + Animation Job 的功能。目前和美术同事配合,在 Unity 2018.3.0f2 中初步实现了空手移动和持枪瞄准的功能,在此做一小结。为简单起见,不使用 Root motion,使用原地动画,并将动画部分视为表现层,可以读取逻辑层提供的数据,但是不写入这些数据。

基本结构

核心类

下面类图简单表示了这些类的关系:


类图
逻辑数据的获取

如下 IAnimDataProvider 接口用来将数据传递给控制动画的代码。

    public interface IAnimDataProvider
    {
        float GetFloat(string key);
        float GetFloat(int keyId);
        int GetInt(string key);
        int GetInt(int keyId);
        bool GetBool(string key);
        bool GetBool(int keyId);
        int GetStateId(int stateGroupId);
        int GetStateId(string stateGroupName);
    }

使用这个接口就可以通过给定的关键字(key)去获取相应的数据,以及获取给定的一个状态机的当前状态。具体的数据类可以实现这个接口,每一帧由业务逻辑填充好数据。

为什么每个函数有两个重载版本呢?这是仿照 Animator 和 Material 中查找属性的思路,如果具体数据提供者类是以散列表(如 Dictionary)实现,其关键字可用 int 而非 string,使用的时候可以将 key 用 Animator.StringToHash 转换为 int 缓存起来,以提高性能。毕竟,求 string 的散列值比较费时。

未来还可以仿照 Animator 加入触发器类型的功能。

动画图资产和动画图实例

这部分内容可以参考 [1] 中的代码。动画图资产(AnimGraphAsset)基类继承自 ScriptableObject,用于对动画进行配置,如下:

    public abstract class AnimGraphAsset : ScriptableObject
    {
        public abstract IAnimGraphInstance CreateInstance(IAnimDataProvider animDataProvider, 
            TransformBindingCollection transformBindings,
            Animator animator, PlayableGraph playableGraph);
    }

从上面代码可以看出,它可以根据若干参数构造出 IAnimGraphInstance 的具体对象。IAnimGraphInstance 类似下面的代码:

    public interface IAnimGraphInstance
    {
        // 动画图销毁时做必要的清理。
        void Shutdown();
        
        // 设置 this 表示的动画子图的输入。
        void SetPlayableInput(int portId, Playable playable, int playablePort);

        // 获取 this 表示的动画子图的输出。
        void GetPlayableOutput(int portId, ref Playable playable, ref int playablePort);

        // 轮询。
        void Update(float deltaTime);
    }

AnimGraphAsset 的每个具体子类中,可以留配置数据字段,并且要有一个实现接口 IAnimGraphInstance 的子类用于 AnimGraphAsset.CreateInstance 返回。AnimGraphAsset 资产文件之间可以具有无环的依赖,以便 AnimController 可以在运行时,递归的创建必须的 IAnimGraphInstance 子类的实例,并将它们连成树状。

举例来说,角色四方向的移动需要一个混合节点,站立和四方向移动的混合又是根据 IAnimDataProvider 中读到的某个状态确定的。因此可以考虑一下几种 AnimGraphAsset:

四方向跑的动画图资产 状态选择器动画图资产
动画控制器(AnimController)类——整个系统的中枢

AnimController 继承自 MonoBehaviour,持有数据的引用、Animator、节点绑定集合等(以便提供给 AnimGraphAsset 以及 IAnimGraphInstance),并持有一个作为根的 AnimGraphAsset。

动画图资产、实例和 Playable 的关系

设有 A, B, C 三种动画图资产类,其 .asset 文件有如下依赖关系(这种依赖关系体现在编辑器拖拽的序列化字段上,箭头方向表示持有/依赖)。

动画图资产 .asset 文件的依赖关系

运行时代码中,D 的 CreateInstance 方法将多态地调用 B 和 C 的 CreateInstance,后两者各自要调用 A 的 CreateInstance。因此作为 PlayableGraph 的子图,各个 IAnimGraphInstance 的关系如下所示。

IAnimGraphInstance 之间的逻辑关系

这里,箭头表示的就是获取输入的来源。即 D 的输入是 B, C 的输出,B, C 的输入分别是两个 A 实例的输出。由于每个 IAnimGraphInstance 表示的是 PlayableGraph 的一部分,一般都会有一个 Playable 作为根节点(用于输出到下一级),除此可能有若干其他 Playable 以代码指定的方式连接起来。最终的 PlayableGraph 大致是下面这个样子。

PlayableGraph

其他动画图资产

线性连接

除了状态选择器(AnimGraph_StateSelector),目前我还照搬了 [1] 中的 AnimGraph_Stack,这是将其依赖的若干 AnimGraphAsset 线性连接,将前一个作为后一个的输入。运行的时候,就是第 i 个 AnimGraphAsset 生成的 IAnimGraphInstance 的输出(即 实现 GetOutputPlayable 方法得到的 Playable 的输出)作为第 i + 1 个 AnimGraphAsset 生成的 IAnimGraphInstance 的输入(实现 SetInputPlayable 方法)。

持枪的上下半身融合

这里尝试了运行时动态改变 Playable 之间的连接。

在角色空手的站立和四向跑融合得到结果(记为 x)之后,希望根据它所持武器,将相应的上半身动画和 x 融合。设该模块的 IAnimationGraphInstance 子类中,最终输出的 Playable 为 out(这里使用一个 AnimationLayerMixerPlayable 以便使用 AvatarMask)。将 x 的输出 Playable 连接 out 的输入端口 0,将第 k 种武器(k >= 1)的持枪动画(或者持枪动画和射击动画的选择结果)的 Playable 输出连接 out 的输入端口 k。对于 k > 0 的情况,设置层 k (也就是输入端口 k 的 AvatarMask)即可。

上下半身融合
目视方向和瞄准的 IK

这里分了三个阶段实现,每个阶段对应一个 AnimationScriptPlayable。

var humanStream = stream.AsHuman();
humanStream.SetLookAtPosition(targetPos);
humanStream.SetLookAtEyesWeight(EyesWeight);
humanStream.SetLookAtHeadWeight(HeadWeight);
humanStream.SetLookAtBodyWeight(BodyWeight);
humanStream.SetLookAtClampWeight(ClampWeight);
humanStream.SolveIK();

这个实现有几个问题:

var effectorRot = OtherHandEffector.GetRotation(input);
var goalPos = OtherHandEffector.GetPosition(input) + effectorRot * OtherHandEffectorLocalOffset;
var goalRot = effectorRot * OtherHandEffectorLocalRotation;

其他问题

模型导入

导入模型 FBX 的时候,需要采取如下设置。


模型 FBX 导入设置

此后展开模型 FBX 资产,可以看到下面有一个 Avatar 子节点。

这里有两个一个额外的问题

强制 T-pose
动画导入

导入动画 FBX 时,上面这个 Rig 标签页就需要将 Avatar Definition 改为 Copy From Other Avatar,意为使用其他的 Avatar。选次项后将上面生成的 Avatar 子节点拖上去即可。

动画 FBX 的 Rig 选项卡

为了使得根节点没有动画曲线,除了需要在 Animator 上去掉 Apply Root Motion 选项,对于使用了 Humanoid 导入的动画,还需要在 FBX 文件 Inspector 中,选中动画选项卡,做如下设置:


动画 FBX 的 Animation 选项卡

如果只是在 Animator 上去掉了 Apply Root Motion,而没有做上述设置,Unity 仍然在计算时将一部分曲线算在根节点上,只是没有应用到渲染结果上,于是动画看起来会是很怪异的。

Animation Job 的可用性

实际上这是产品化问题。我们不知道 Unity 什么时候会将 Animation Job 正式推出,目前它毕竟是试验性代码,在名字空间 UnityEngine.Experimental.Animation 中。另外就是,在这个部分作为正式 API 之前,有没有一种替代方式,能结合 PlayableGraph 实现上面提到的这些功能?

参考资料

[1] Unity 官方 FPS Demo

[2] TransformSceneHandle 和 TransformStreamHandle 的区别

[3] Unity 关于 RootMotion 的官方文档

上一篇下一篇

猜你喜欢

热点阅读