前言

笔者接触unity的时间也不算短了,但常感自己对它很多方面的了解还不够深入。正好最近在独自做一个RPG游戏,便决定就开发过程中使用到的引擎模块进行更深入的学习和理解,以此博客作为学习纪录。如果有幸帮助到正在阅读此文的人,那就再好不过了。

在这一篇中,我们一起来看看unity的角色动画系统。本文主要参考内容来自Unity官方文档和《游戏引擎架构》(Jason Gregory著,叶劲峰译,电子工业出版社)这本书的动画系统部分,探究在unity这个特定的引擎中是如何实现动画系统的。使用的引擎版本为2020.3.10f1,如有不严谨之处还望指出。

建议使用右侧目录。

一、游戏角色动画发展简史

根据《游戏引擎架构》,角色动画在游戏引擎中主要经过了以下形式的演变。

1. 精灵图动画

精灵图即sprite,其来源于赛璐璐动画(cel animation)。后者是在塑料片上绘画,将各种赛璐璐片重叠放在静态背景上,就能实现角色在场景中的运动,ps等软件中的图层概念与之类似。

精灵图是赛璐璐的数字版本,一般是背景透明的二维图片。把一组精灵图有序地播放,就能呈现角色的动画效果。有些动画首尾相接以保证能流畅地重复播放,称为一个cycle,基本的有idle、walk、run等循环动画。精灵图在二维游戏中运用十分广泛,例如马里奥、合金弹头等经典作品。

在unity中将图片的texture type设为sprite,就变成了一张精灵图。如果包含了不同帧,可选择multiple模式后使用Sprite Edtior(需要2D sprite包)进行切分,切分后将需要的切片全选定,拖入场景就会自动生成有序播放的2d动画。

2. 刚性层阶式动画

精灵图每一帧都要艺术家亲自绘制,而且在完成后不易调整动作,复用性也不高。而三维图形技术的发展,给游戏界带来了全新的动画方式,刚性层阶式动画(rigid hierarchical animation)就是早期时候的产物。

这个词乍一看有点摸不着头脑,我们从其英文名来拆分下。rigid的意思是“刚性的,不弯曲的”,在这种方案下,角色整体由各个部分的独立模型组成,它们各自的形状是不变的,只能改变相对的位置。hirerarchical指“分等级的”,即角色的各个部件按等级结构组织起来。以下图为例,可将躯干作为最高层,其下是头、左右大臂、左右大腿,而大腿之下又有小腿、足部……整体形成树状的层级结构。当树中的父结点运动时,子节点受到约束也跟着运动,如大腿能带动小腿和足部运动。

使用刚性层阶式动画能高效直观地调整角色的动作,但其调节的灵活程度取决于关节的数量,诸位小时候是否玩过奥特曼玩具?有的奥特曼只有俩胳膊俩腿能旋转,能摆的pose很有限。同时对于人体等模型来说,由于身体部件只是堆放在一起,失去了模型的完整性,且在运动时关节处可能会产生缝隙。


3. 逐顶点动画和变形动画

上面我们提到刚性动画的缺点,部件的分离破坏了角色的整体性。那么在保证角色模型是一个整体的情况下,要如何实现躯干的运动呢?

一种方法是直接移动模型的顶点,即逐顶点动画(per-vertex animation),这种方法能实现角色的任何变形,但由于需要存储每个顶点在时间轴上的位置信息,且数据量随模型精度而显著增长,在实时渲染的游戏引擎中并不常用到。

另一种方法是使用变形动画(morphing animation),对比逐顶点动画,只需制作部分关键帧的顶点动画,在相邻关键帧之间对顶点位置使用线性插值计算其位置,节省了制作时间。这一技术常用于角色面部动画,能模拟面部肌肉的运动,实现非常细微的表情变化。

4. 蒙皮动画

蒙皮动画(skinned animation)是我们最常接触到的技术,其在游戏和影视界的广泛应用证明了这一方案的合理性。

它集成了上述几种方案的优点,我总结有三点:

1.使用类似于刚性层阶式动画中的刚性部件——骨骼(skeleton)来控制角色的运动,带来了高效的动画调整方式和更低的数据量要求,只需要操作和存储少量的骨骼运动。

2.骨骼并不渲染,玩家真正看到的是绑定在其上的皮肤(skin),保证了角色模型的整体性。

3.模型的各顶点受绑定的骨骼约束而移动,能产生逐顶点动画中的形变效果。

二、运动的基础——角色的骨与肉

在上一节我们简单回顾了角色动画的发展历史,并了解到蒙皮动画这一现在流行的技术。那么在unity中,为了让角色能够动起来,引擎为我们提供了哪些功能?我们不妨走一遍这个过程,从将角色模型导入unity,到能够播放一个角色动画为止。

1. 模型导入设置

我们在blender中简易制作一个Low poly风格的人物模型,并绑定一套骨骼,使用软件自动绘制的权重,之后导出为FBX格式的文件,拖入unity的Asset中。



选中FBX文件,在Inspector面板中显示了导入设置(import settings),分为Model、Rig、Animation、Materials四栏,我们先只看Rig,即骨骼部分。unity提供了三个可调节项,分别是Animation Type、Avatar Definition和Skin Weights。前两项关乎动画重定向的问题,最后一项关于蒙皮动画的数据存储,我们将在后面对它们分别再详细阐述,可以从目录跳转查看。总之我们先按下图勾选,apply后将模型拖入场景。

2. 导入场景

将人物模型拖入场景,在Hierarchy中,我们看到其包含了层级结构,父物体名字与FBX文件一致,其包含了Transform和Animator组件,Animator的部分我们将在后面再说,其主要用来控制角色的运动。

在父物体之下,有Armature和Body两个子物体,它们的名字分别与blender中制作的模型的骨骼和网格模型相同,我们可以理解为角色的骨与肉。展开Armature物体,看到其下还包含了复杂的层级结构,以Hips即臀部(髋)为根节点,其下又有左右大腿和脊椎,脊椎向上为整个上半躯体。Armature中的所有物体都只包含一个Transfrom组件,即只有它们各自的位置信息,而不会渲染在场景中。

那场景中我们看到的是人物的哪一部分呢?答案位于Body物体下,我们选中它,在Inspector面板中除了Transform外,还包含了一个叫做Skinned Mesh Renderer的组件,有许多可供调节的选项,这一组件就是我们将要研究的重点。

3. Skinned Mesh Renderer

Skinned Mesh Renderer(简称SMR代替),直译为蒙皮网格渲染器。这是unity用来实现蒙皮动画的方案,我们从以下不同方面来一探究竟。

3.1. 骨骼

骨骼,英文名一般为skeleton或者armature,控制着蒙皮动画中的角色运动。骨骼并非单个的物体,而是由不同层级的子物体以树状结构组合而成,这些子物体就称为骨头(bones)。在我们的例子中,人物的一条胳膊就被简化为了大臂、小臂、手掌的三段骨头。

在skinned mesh renderer的Inspector面板中,我们只能看到Root Bone项,即骨骼的根骨头,上图中可见是Hips。当然,在C#脚本中我们还是能获取到所有的bones信息,我们新建一个Test脚本,写一段代码测试下:

public class Test : MonoBehaviour
{[SerializeField] private SkinnedMeshRenderer skinnedMeshRenderer;private void Start(){if(skinnedMeshRenderer != null){PrintSmrBones(skinnedMeshRenderer);}}public static void PrintSmrBones(SkinnedMeshRenderer smr){Debug.Log("Root bone: " + smr.rootBone.name);Debug.Log(smr.bones.Length + "bones:");foreach (var trans in smr.bones){Debug.Log(trans.name); }}
}

SkinnedMeshRenderer类继承自Render,后者为组件(Component)类。它包含了rootBone和bones属性,分别为Transform和Transform[]数组类型,表示整个骨骼的根骨头和所有骨头。我们将人物的Body物体拖给Test脚本并运行游戏,部分输出如下:

在实现角色换装时,我们需要用到skinned mesh renderer中的骨骼。以笔者在做的demo为例,笔者在blender中制作好了人物的模型,以及几套盔甲、靴子和头盔,这些服装模型与人物身体绑定了同一套骨骼,但各自的权重不同。将以上所有模型连同骨骼导入unity并设置后,在场景中进行测试,服装模型能正确地跟随人物骨骼运动。选中一个服装模型,在Inspector面板中,它也带有skinned mesh renderer组件,且经过测试其root bone和bones属性和人物body的属性完全相同,即共享了一套骨骼。

当然,在RPG游戏中,我们希望角色能动态地更换盔甲等防具,一种朴素的想法是把所有服装都做好后一起导入unity,这样角色身上就挂载了所有的服装模型,在运行时控制穿着的服装被激活,其他的部分全用SetActive(false)。这种方法在服装数很少时也许很方便直接,但当服装数目增多时,挂载这么多模型会产生不必要的性能浪费,而且当我们在三维软件中制作了新的服装后,恐怕又得重新导入一遍。因此这一方案并不适用于我们的需求。

既然如此,不如将服装模型全都作为预制体(prefab)拖到项目Asset中,在运行时再动态地挂载到角色身上,同时这些服装也就能分离出来,作为游戏中可掉落、拾取、交易的物品,听起来非常合理。但导出预制体后,我们发现其skinned mesh renderer的root bone为空,同时对bones进行输出测试,发现其保留了骨头数组的长度Length,但每一项都为null。

因此为了给角色穿上衣服,我们需要重新给服装的root bone和bones赋值,由于在三维模型中,它们与人物的身体绑定的是同一套骨骼,因此可以用下面的简短代码实现:

 [SerializeField] private Transform m_TargetTrans;[SerializeField] private SkinnedMeshRenderer m_TargetBodySmr;public void DressUp(GameObject clothePrefab){GameObject clotheObj = Instantiate<GameObject>(clothePrefab);SkinnedMeshRenderer smr = clotheObj.GetComponent<SkinnedMeshRenderer>();clotheObj.transform.parent = m_TargetTrans;smr.rootBone = m_TargetBodySmr.rootBone;smr.bones = m_TargetBodySmr.bones;PrintSmrBones(smr);}

注意这种方式仅适用于服装和身体绑定了完整的同一套骨骼,笔者曾遇到的坑就是把人物连同几套服装一起上传Mixamo,并使用了自动绑定,照上面的方法将服装动态挂到人物身上,运行时发现服装并不能正确地跟随骨骼运动,后来测试发现原因是Mixamo为服装绑定的骨骼不完整,例如头盔只绑定了上半身的部分骨骼,因此不能直接赋值。

3.2. 网格与蒙皮

我们提到骨骼在游戏场景中是不可见的,玩家真正看到的是各种各样的网格模型。在skinned mesh renderer的inspector面板中,我们看到有一项叫做Mesh(网格),也就是角色的“肉体”部分。

我们在场景中新建一个Cube,它带有Mesh Filter组件,其中也有Mesh项,我们当然也可以把其替换成人物的body模型,那么它和SMR中的mesh有何区别呢?为什么前者能跟随骨骼运动而后者大多数情况下是静态的?

通过查询UnityAPI,我们了解到skinned mesh renderer中提供了sharedMesh属性,也就是inspector中的Mesh项,mesh filter中则可以通过mesh或者sharedMesh访问(根据笔者查阅资料后的理解,mesh是每个mesh filter独有的,用sharedMesh产生的一个实例),它们都属于Mesh类。Mesh类中包含了模型顶点、法线、uv等基础属性,但我们关心的是boneWeights属性,这是一个BoneWeight数组,记录每顶点的骨骼权重。

        // Summary://     The BoneWeight for each vertex in the Mesh, which represents 4 bones per vertex.public BoneWeight[] boneWeights { get; set; }

BoneWeight则是unity定义的一个结构体,主要存储顶点绑定的4段骨头的索引和权重值。

我们在Test脚本中新加部分代码如下,分别打印出SMR和mesh filter中的网格信息。

 public static void PrintSmrMesh(SkinnedMeshRenderer smr){Mesh smrMesh = smr.sharedMesh;if (smrMesh != null){PrintMesh(smrMesh);}}public static void PrintMesh(Mesh mesh){HashSet<int> hs = new HashSet<int>();foreach (var weight in mesh.boneWeights){hs.Add(weight.boneIndex0);hs.Add(weight.boneIndex1);hs.Add(weight.boneIndex2);hs.Add(weight.boneIndex3);//Debug.Log(weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3);}Debug.Log("Bone nums:");Debug.Log(hs.Count);}


输出如上图,可见两者区别在于skinned mesh renderer中的蒙皮网格带有骨骼权重信息,而普通静态网格不具有。而且SMR中网格的所有BoneWeight的骨头索引数量与bones数组的数量一致,在本例中都是22段。

由于蒙皮网格模型的每个顶点都带有BoneWeight信息,又能通过其中的boneIndex访问到SMR中bones的特定成员——即该顶点绑定的所有骨头,那当我们驱动骨骼中的骨头运动时,相应的网格顶点也就能随之运动。

在游戏引擎中,一个顶点绑定4个骨头最为常见。一方面在于4个8位的骨头索引正好能填满一个32位数,或者4元的vector例如RGBA颜色;另一方面当高于4个关节时,增加每顶点绑定关节数量带来的差异不够显著,且会影响性能。从BoneWeight结构体可见unity默认采用的也是这种方案。

还记得在我们导入fbx模型时,Rig栏中提供了Skin Weights选项吗?默认为4 Bones,也可以选择Custom后自定义,由下图可见,最大支持每个顶点绑定255个骨头。同时我们还需要将Project Settings>Quality>Skin Weights项改为Unlimited,以及SMR中的Quality设为Auto。


但BoneWeight结构体不是最多只支持4根骨头吗?超过4根骨头时如何访问它们的权重和索引呢?原来在Mesh类中,还提供了一个GetAllBoneWeights方法,返回一个BoneWeight1类型的数组。

        //     Gets the bone weights for the Mesh.//// Returns://     Returns all non-zero bone weights for the Mesh, in vertex index order.public NativeArray<BoneWeight1> GetAllBoneWeights();

而BoneWeight1则是unity提供的另一个结构体,与BoneWeight不同,BoneWeight1表示一个顶点绑定的一根骨头的权重和索引信息,也就是说如果选择每顶点绑定255骨头,BoneWeight1的数量将会是255x网格顶点数。当我们设定每顶点绑定骨头数高于4时,unity会使用BoneWeight1来计算运动。

我们不妨顺带了解一下三维软件中的蒙皮,比起游戏引擎中单纯数据形式的存储,三维软件提供了更直观、可视化的操作。以blender为例,除了在绑定骨骼时由软件自动绘制权重外,也可以手动修改模型的蒙皮,这一操作一般称为“权重绘制”(Weight Paint)或者“刷权重”。选中一根骨头后,网格模型表面的颜色显示了不同顶点受到该骨头的影响程度,蓝色表示不受该骨头约束,红色则是骨头对此顶点的权重为1,即完全跟随其运动。我们可以使用多种笔刷,来方便快捷地增加或减少骨头在某些顶点上的权重。

3.3. 其他

除了最重要的骨骼与网格外,skinnned mesh renderer还有一些值得注意的特性。

3.3.1. BlendShapes

上面的gif图展示了BlendShapes的效果:人物身体提供了4个可调节的值,调节其值使人物网格模型的顶点移动,从而改变了人物的体型。

BlendShapes其实是逐顶点动画的一种,我们在1-3中提到过,它通过直接操作顶点来改变网格外形。可见unity并非只使用蒙皮动画,而是还融合了逐顶点动画能够细微调节的优点。像上图中调整BlendShapes的值以缩小人物身体,能减少人物穿上装备后的穿模现象。另一个重要的应用就是捏脸系统,使用BlendShapes让玩家能够在一定范围内调整角色的外观,如面部形状、五官大小、身体胖瘦等等。

当然,我们需要在三维软件中预先制作好BlendShapes,但名字可能有区别,在blender中这一功能位于Object Data Properties栏的Shape Keys,新建key后在编辑模式下改变相应的顶点即可。注意在制作完成后导入fbx到unity时,需要在Model栏中勾选Import BlendShapes。

3.3.2. Update When Offscreen

这是skinned mesh renderer的inspector面板中的一个选项,决定当该物体不在摄像机视野中时是否执行其动画,默认不会勾选。

如果不勾选这一项,unity会对场景中带有SMR的物体进行culling操作,只更新视野范围内的物体的骨骼动画,以减少一定的性能消耗。判断依据是网格模型的朝向包围盒(OBB, Oriented Bounding Box)与摄像机视锥体是否有交集,即模型周围的白色线框。在默认情况下,这个包围盒的能完全覆盖绑定姿势时的网格模型,在运动过程中,大小不发生变化,但始终跟随人物骨骼的root bone旋转。

但当人物做一些幅度较大的动作时,包围盒不一定能完全包裹住人物模型。例如在下图中,人物右手向前挥出一记直拳,手臂的一部分可能会突破包围盒,在不勾选update when offscreen的情况下,有可能会发生这样的问题:人物部分身体在视野内,而由于包围盒被摄像机剔除,不会更新其动画。为此,建议为重要角色勾选update when offscreen,以保证其骨骼动画随时都更新,或者在运行时通过脚本来动态调整Bounds,以保证包围盒的有效性。

4. Avatar

我们在2-1节导入模型时,inspector中有两项分别是Animation Type和Avatar Defination,由于这是一个人类模型,我们分别选择了Humanoid类型和Create From This Model。Animation Type比较好理解,那么Avatar又是个什么东西?

卡梅隆的电影《阿凡达(Avatar)》里,杰克在联结舱内通过神经连接,将意识转移到阿凡达身体内,得以操纵这具全新的躯体在潘多拉星球上奔驰。Avatar一词一般理解为“化身”,而这部电影也有助于我们理解unity中的avatar,我们一步步来说:

  1. 首先,资源的复用对游戏制作十分重要,能够节省开发成本。骨骼动画就是一项重要的资源,而由于人形生物的骨骼形态基本一致,在制作好某个角色的一套动画之后,是否能应用在其他人物模型上呢?动画重定向(Animation Retargeting)就能解决这一问题,在虚幻和unity中都提供了这一技术。
  2. 通过动画重定向,为主角设计的走路动画,同样也可以用在小兵身上,就像《阿凡达》中,杰克既能控制自己原本的躯体,又能操控阿凡达的身体。当然,电影中能实现这种转换,靠的是杰克的DNA和阿凡达能匹配,而在unity中,要使骨骼动画为其他人形角色使用,需要借助Avatar来匹配不同的骨骼。
  3. 因为对于不同的人物模型,一方面其骨骼结构可能不同,例如我们导入的low poly模型很简单,人物并没有手指和面部骨头,而对于高质量、商业级别的模型,其骨骼结构会十分精细和复杂。另一方面,骨骼的命名方式也很难完全一致,例如从髋部到胸部如果分为4段骨头,常见的命名方式就包括Hips-Spine-Chest-Upper Chest和Hips-Spine-Spine2-Spine3等。这两点使得骨骼间无法直接通用,而需要一个中间处理。
  4. unity中的Avatar实际上提供了一种从具体骨骼到通用人形的映射关系,即对于一副人形骨骼,对其主要位置例如躯体、四肢的骨头进行标记,这样在使用动画重定向和反向动力学(IK,Inverse Kinematics)等功能时,就能使用通用的处理方式。

4.1. Avatar的使用范围

  1. 角色模型需要Avatar,既可以像上面一样,对新导入的模型生成新的专用Avatar,也可以使用其他模型已生成的Avatar,勾选Copy From Other Avatar并指定Source即可,当然需要保证两者的骨骼相同。
  2. 有的fbx文件中,只包含了骨骼和动画信息,而没有网格模型,这些文件也需要指定Avatar,否则它们包含的动画片段将无法使用。
  3. unity的Animator组件用来控制角色动画,对于人形角色,需要为其指定Avatar。

4.2. 骨骼映射

我们按2-1节的导入设置后,点击Avatar Definition后面的Configure按钮,进入上图所示的界面,场景中为该角色模型,摆成了T-pose形,右边inspector面板中显示Avatar的相关内容。

右侧这些绿点表示人体通用的结构,其中实线边框的表示人形骨骼必须具有的骨头(关节),虚线边框的是可选的骨头,即使没有,骨骼也能正常运动,例如下图中头部的mapping,除了头部骨头外,脖子、眼睛、下巴都可以没有。双手手掌的全部骨头也都是可选的。

在我们初次导入fbx模型,并选择Create From This Model建立新的Avatar后,unity就自动为我们建立了骨骼映射关系。我们也可以在configure界面中,通过将hierarchy下的物体拖到inspector面板中对应栏来修改,或是点击Mapping按钮来进行清除、自动映射,还可以将当前的映射保存为Human Template(.ht)文件到本地,或是导入ht文件。

我们可以在场景中选中人物身上的骨头,对其进行移动缩放等操作以调整姿势,方便查看骨骼绑定效果,也可点击Pose按钮来重置人物姿势、变为T-pose。也可以在Muscles&Settings页面中进行姿势预览和微调,具体可以看Unity API。

4.3. Avatar Mask

我们在制作动作游戏时,可能会面临这样的需求:人物能够奔跑,能在原地挥舞大刀,也能边跑边耍大刀,前两种可以分别制作骨骼动画,那第三种情景能否不制作新的动画呢?使用Avatar Mask就可以实现。

遮罩(Mask)的思想在许多美术软件中很常见,本质上就是将操作区域进行划分,使得一些部分避免被影响。Avatar Mask则是指定了骨骼中受动画影响的区域,而其他区域不受影响。

我们在asset中新建一个Avatar Mask,在inpector面板中,展开Humanoid,点击人物身上的位置以改变遮罩。在下图中,人物上肢的骨头不会受到骨骼动画的影响。

大多数情况下,人形骨骼直接在Humanoid中可视化地操作即可,因为通过Avatar,unity能识别人形骨骼中的这些区域。如果Mask应用在非人形对象上,或者需要更细节的控制,则可以在Transform中直接导入Avatar,然后勾选受影响的骨头即可。

要实现我们前面提到的人物边跑边攻击的功能,一般还需要结合Animator Controller中的动画层功能,我们将在第四部分具体来说说。

三、动起来——片段与混合

我们在前面花了不少的篇幅来讲skinned mesh renderer和avatar,因为它们是保证角色能够正常运动的基础,接下来这一部分,我们将让角色真正动起来。

1. 姿势

在骨骼动画中,姿势(pose)可理解为在一个静止的时刻,各个骨头相对于某个参考系的状态集合,包括位置、旋转、缩放等。既然我们打算让角色动起来了,为什么又要提姿势呢?这是因为从本质上来说,动画是由一些静止的画面连续播放而产生的,对于骨骼动画来说,连续播放的是角色的姿势。

现实生活中我们做任何动作,例如抬起一只胳膊,骨骼的运动一定是连续的。但在计算机中模拟角色的骨骼运动时,采用的是一些离散的姿势,一般称为关键帧(key frame),以每秒24、30、60或者其他速率来播放一系列姿势,或者在相邻的关键帧之间进行插值(interpolation),就使角色动了起来。

T-pose是我们经常接触到的姿势,人物双臂抬起,整个身体呈现T字形。T-pose一般常用于为网格模型绑定骨骼,因为此时角色四肢与身体更为分离,容易进行顶点的绑定操作。

在游戏引擎中,整个骨骼的姿势由所有骨头的姿势的集合构成。每个骨头的姿势可用SQT(缩放Scale, 旋转Quaternion, 位移Translation)格式表示,例如下面的形式:

struct BonePose
{float m_Scale;Quaternion m_Rot;Vector3 m_Trans;...
}

而整个骨骼的姿势可能表示为:

struct SkeletonPose
{BonePose[] m_BonePoseList;...
}

2. 动画片段

动画片段(Animation Clip)是游戏引擎动画系统的重要部分。在影视作品或者游戏的过场CG中,场景中物体的运动经过了事先编排,如主角的走位、动作都已按时间定好,整个场景以一长串连续的帧播放产生动画。而在游戏中,往往是玩家的操作决定角色如何运动、播放怎样的动画,因此无法像CG一样把这一长串帧做“死”。为此,需要将角色的动画拆分为单个的动画片段,这些片段能明显区分出不同的动作,在游戏中实时改变角色当前播放的片段,例如行走或攻击。

在unity中,既可以使用外部文件如fbx中导入的动画片段,也可以使用unity内置的动画编辑器直接制作动画,在此只探讨前一种形式。我们可以从Asset Store或者Package Manager中导入官方的Standard Asset包,在Characters\ThirdPersonCharacter\Animation中包含了一些不错的骨骼动画片段。

2.1. 时间线

由于动画片段是不同关键帧连续播放而成,需要决定这些关键帧/姿势播放的先后顺序,因此每个动画片段都需要有自己的时间线(timeline),不同关键帧的播放时机是这条线上的坐标。

我们选中Standard Asset中的一个动画fbx文件,在Animation栏下的Clips窗内选择一段片段,下面的数轴就是它的时间线。点击动画预览的播放按钮,两个白色游标同步前进,显示当前播放的姿势在时间线上的位置。

可以调整动画片段在时间线上的开始和结束帧,以改变其覆盖的长度。或者截取不同范围内的片段,将整段动画分割为一些独立的片段,在这个例子中,除了人物跳跃的完整片段,还分割出了下落、跳起和空中的短片段。

在游戏中,动画片段从start帧播放到end帧后就结束了,除非勾选下面的Loop Time选项,在达到结束帧后又会从头开始播放,例如行走、奔跑动画一般会设置为循环。也可以设置Cycle Offset使片段不从0位置开始播放,但只会在第一次循环时起作用,要使每一次循环都改变起始帧应调整Start项。


在游戏引擎中,每个角色可能会播放多个动画片段,为此需要有一个全局时间线。当角色播放动画片段时,实际上是把它加入了全局的时间线里。当我们调整动画片段的播放速度时,改变的是片段的时间线到全局时间线的映射关系。

2.1.1. 动画曲线

我们有时需要根据动画片段的播放进度来改变一些值,举个例子:从起跳到落地过程中,角色对脚下平面的压力会发生变化,如果是在一片漂浮于水面的平板上,浮板受到的压力变化而产生非常真实的上下浮动效果。

unity中的动画曲线(Animation Curve)就能实现这样的需求,它是一条二次曲线,其x轴的范围是动画片段的范围,因变量y的值随当前帧而改变,用来表示某个数值在播放动画片段过程中的变化方式。*我们在此只探讨导入的动画片段中的curve,unity自带的动画编辑器中的curve功能有所不同。*在fbx文件的Animation页面内,展开Curves项并新建一条曲线。这条曲线也包含了关键帧,可以双击曲线图后手动添加关键帧,并靠关键帧调节曲线的形状;或者调整动画预览窗的游标,点击按钮新建关键帧。unity提供了一些常用的曲线模板。

在Curves下的输入框中,填入的是这个随动画播放而改变的数值的名字,下面的进退按钮能在关键帧之间移动。


在设置了动画曲线后,我们可以通过代码获取曲线中的y值,有两种方法。

  1. 通过动画控制器Animator Controller调用,但这个值的名字需要与控制器中的对应参数名一致,关于控制器在第四部分会详细说说。代码非常简单:
 public Animator m_Animator;public float m_Param;...param = m_Animator.GetFloat("ParamName");
  1. 得到这个曲线的引用,直接获取在某一时刻曲线上y的值。代码如下:
 public AnimationCurve m_Curve;public float m_Time;...param = m_Curve.Evaluate(m_Time);

2.1.2. 动画事件

使用动画曲线,我们能根据动画片段的播放进度调整一些值,值的变化是连续的。动画事件(Animation Event)则提供了离散的处理方式:当播放到标记的某些帧时,触发对应的事件,即执行某个脚本的函数。

例如在动作游戏中,角色的武器是否击中某物体,主要靠两者的碰撞检测。但当角色没有攻击时,我们可能希望武器的碰撞体不生效,以防止误判。简单的解决方法可以是在角色攻击的动画片段上添加动画事件:挥出武器的那一帧,将碰撞体激活,不希望再判定时再使碰撞体不生效。

我们从mixamo网站导入一段挥拳动画。选中fbx文件,在Animation栏中选择该片段后,展开下面的Events项。注意到其中也有一段时间线,只不过时间是0:00-1:00,对应该片段的开始和结束帧。在预览窗口中播放,在需要使碰撞体生效的位置暂停,然后在Events中点击按钮生成一个标签,即该时间点对应的动画事件。

其中Function栏填写需要执行的函数名称,Float、Int、String、Object中填写传递的参数。

如果游戏中的某角色有Animator组件,且Animator里有该动画片段,当角色播放这一动画时,就会在相应帧执行动画事件Function栏中的函数。函数的执行者是该角色绑定的所有C#脚本,只要该脚本中有名称一致的函数。如下图所示,Animator和脚本需要绑定在同一物体下

我们在角色身上绑定了两个脚本,EmptyScript为默认空脚本,在HumanController中添加如下代码:

    private void ColliderOn(Object pObject){Debug.Log(pObject.name);}

将动画片段设置为循环播放,运行游戏,控制台中不断输出"mat_Test"字段,即我们在动画片段中传递的Object参数的名字,执行的很顺利。那如果我们再添加一个重载的函数呢?或者在EmptyScript中也添加一个ColliderOn函数呢?我们分别在HumanController和EmptyScript中添加两段代码:

private void ColliderOn(float pFLoat){Debug.Log(pFLoat);}
private void ColliderOn(int pInt){Debug.Log(pInt);}

运行游戏,输出如gif图所示。可见unity只执行了HumanController中的第一个ColliderOn函数,以及EmptyScript中的该函数。

我们大致归纳一下

  1. 动画片段播放到相应位置时,会调用角色所有脚本中的与Function栏里函数同名的函数,这些脚本需要和Animator在同一角色身上。
  2. 如果脚本有重载的该函数,unity只会执行第一个重载函数。
  3. 该函数只能有0或1个形参,且形参只能是float、int、string、enum、Object或者AnimationEvent类型,否则会报错。

  1. 如果使用一个AnimationEvent类型的形参,可以获取到动画事件中的全部4种参数。代码如下:
    private void ColliderOn(AnimationEvent pEvent){Debug.Log(pEvent.intParameter);Debug.Log(pEvent.floatParameter);Debug.Log(pEvent.stringParameter);Debug.Log(pEvent.objectReferenceParameter.name);}

当然,也可以通过代码动态地为动画片段添加事件,具体的可以查阅官方API或者网上的例子,在此就不赘述了。

2.2. Root Motion

艺术家在制作骨骼动画时,常见的有两种形式:

  1. 角色在原地运动,例如某些人物奔跑、走路的动画。在这些动画中,角色好像在跑步机上运动一般,在三维空间中的整体位置和旋转基本不会发生变化。
  2. 角色产生整体位移和旋转。这种动画比较符合现实生活中的运动,例如下图是mixamo中人物挥舞双手巨剑的动画,当完成一套攻击动作后,人物的位置也改变了不少,人物的朝向则在运动中时刻变化。

主流的游戏引擎,如unity和虚幻等,一般会为某件事情提供多种解决方案,开发者可以根据具体的需求而选择合适的方案。在播放角色动画时,角色可能同时会位移或旋转。我们有时希望将动画控制和角色Transform的改变分离开来,例如在角色奔跑时,只让Animator播放原地的跑步动画,并通过代码控制其Transform来产生移动的效果;但有时又希望把控制权交给动画,例如上面大剑攻击的动画中,人物本身的位移和旋转较为复杂,通过代码控制难以保证效果。

在unity中,这样的抉择主要围绕Root Motion展开。Root Motion可翻译为“根运动”,指在播放骨骼动画时,这个角色的身体,或者说Body Transfrom,跟随骨骼动画中的Root Transform而改变。

Root Transform是在xz平面上的一个箭头,当动画片段中骨骼运动时,箭头的根跟随人物而移动(因为向量没有位置,所以用箭头形容),方向则与人物朝向在xz平面的分量始终一致,如下图中的红色箭头。在我们为模型指定Avatar时,这个箭头就由unity生成了,在T-pose下,默认在人物双脚中间,指向人物正前方。

我们也可以手动调整这个箭头的位置,在Animation页面的Motion栏下,可以从下拉菜单中选择Root Motion Node,指定箭头的位置为骨骼结构中的某个节点。下图中我们指定了Hips为Motion的节点。

在unity中,我们可以选择是否开启Root Motion功能。一种方法是在角色的Animator组件中,勾选Apply Root Motion选项;另一种则是通过代码动态控制,例如在角色悬空时关闭Root Motion,使其能正常受到重力影响而下落。示例代码如下:

 if(m_IsGrounded){m_Animator.applyRootMotion = true;}else{m_Animator.applyRootMotion = false;}

2.3. Bake

我们在上一小节提到了Transform在动画中的两种控制方式:完全由代码掌控和交给动画操纵。但unity中还有折中的方式,在fbx文件的Animation页面,分别有3个Bake into Pose的选项。Bake,即烘焙,是渲染中常用到的概念,例如把光照渲染为贴图加到物体上。

在unity的骨骼动画中,烘焙的则是骨骼动画中的Transform信息,右边的绿灯或红灯显示该动画片段首尾的旋转或位置是否一致。以Root Transform Rotation下的Bake in Pose为例,如果勾选了Bake选项,即使关闭Root Motion,人物身体也能随动画而正确地旋转,但Transform组件不会变化。关于这部分的理解,建议看下面这篇博文,十分有用。

Root Motion深度解析[Unity]

笔者在此就只放一些图片的对比,大家可以结合参考文章来看,体会此功能的作用。

启用Root Motion,不进行任何Bake,挥拳动画

关闭Root Motion,不进行任何Bake

关闭Root Motion,Bake三项,注意Transform组件

另外是一组跳跃动画,注意人物身体的移动和Transfrom组件:

关闭Root Motion,不进行任何Bake,跳跃动画

启用Root Motion,不进行任何Bake

关闭Root Motion,Bake三项

3. 动画混合

在游戏中,角色的动画常会受玩家输入而实时改变,例如在奔跑时突然攻击,需要播放不同的动画片段。如果这些动画片段之间只是简单地切换,由于切换前后骨骼的姿势不能保证一致,可能会显得很生硬和突兀,例如跳帧(pop)现象,因此需要一种更顺畅的过渡。

又或者我们已经有了角色正常行走的动画,以及角色身受重伤的行走动画,能否免去制作其他负伤程度的动画,而由这两个极端情况程序化地生成中间的动画呢?

这两种需求的核心其实是一样的,都是要在不同的骨骼姿势之间插值(interpolate)。动画混合(Animation Blending)就能实现这样的需求,它的本质就是结合多个骨骼姿势产生混合的姿势。

在三、1小节中,我们提到单个骨头的姿势可以分解为缩放、旋转和位移三部分,而骨骼整体的姿势由所有骨头组合而成。使用动画混合时,需要对SQT分别进行插值,例如缩放和位移使用线性插值,而旋转的四元数使用球型线性插值。

在unity中,动画混合主要体现在两部分,动画过渡和混合树。

3.1. 动画过渡

动画过渡就是一个动画片段切换到另一个片段的过程,就像电影中的转场效果。除非切换前后两个片段的骨骼姿势完全一致,否则使用过渡能使这种切换更顺畅自然。

在unity的动画状态机中,不同状态间的转移使用到了动画过渡,状态转移的具体内容会在后面提到。我们选中一个转移,在inspector中显示了一个过渡的图表。图表告诉了我们一些信息:转移中由Swiping片段过渡到Walk片段,以及过渡的起始位置和时长。

还有一处关键信息就是图表中的白色曲线,很明显每个片段都有一条独立的曲线。unity官方文档中并没有提到曲线的作用,但不难推测它应该反映了两个片段之间的相似或者说同步程度。

假设我们只关注某根骨头的Transform.y值,那在进行动画片段的过渡时,两者的同步程度很容易度量——用两条曲线分别表示两个片段中该骨头的Transform.y随时间线的变化即可。如果不进行其他处理,我们希望从片段A切换到片段B时,两条曲线中此时y的值能够相同,即构成一条c0连续的曲线,甚至更高阶的连续。

由于引擎并不开源,我们只能猜测unity中的这种曲线可能反映的是动画片段中,整体骨骼姿势的某个度量值随时间的变化情况,在上面的转移图表中,可见两个片段此时同步程度并不高,两条曲线连c0连续都没有达到。如果希望效果更好,我们可以调整转移设置来使两者更同步,如下图所示,近乎达到了c1级别的连续。

当然,有时我们希望最大程度保留动画片段,上面为了提高连续性舍弃了Walk动画中较长的一部分。但即使不进行这样的手动同步,只要保证合适的过渡时间,片段间的过渡也能较为自然,这主要靠动画混合的作用。

在过渡过程中,引擎播放的既不是Swiping片段,也不是Walk片段,而是根据两个片段混合而成的过渡片段,过渡片段的骨骼姿势是两者之间插值的结果。假设使用的是线性插值即Lerp,并设骨骼姿势为G,则在过渡过程其值为:

其中α表示过渡的进度,G是随时间变化的函数。α为1时完成过渡并切换到下一个片段。

3.2. 混合树

我们提到过动画混合的两大作用:平滑过渡和产生新动画。混合树(Blend Tree)用于实现后者,依靠它我们能混合行走和奔跑动画,从而产生中间速度的其他动画,或是由正向的奔跑产生身体左倾和右倾的动画。

在动画过渡中,过渡前后的两个动画差异可能非常大,但靠动画混合两者能自然地转换。而在混合树中,树中包含的动作应该确保相似,如攻击动画不应该加入含有行走和奔跑的混合树中。

我们以Standard Asset中的Third Person Animator Controller为例,如下图所示。

可见这颗混合树上有4种状态,每个状态有一段做好的动画片段,分别是人物蹲下的闲置动画、蹲下并前进、蹲下并向右行以及蹲下并向左走。依靠动画混合技术,能通过这些已有的动画产生新的动画,比如向左前方蹲伏潜行。如图所示,红点代表的位置,其动画由圈起来的三个动画片段混合产生。

与前面动画过渡部分使用的一维线性插值不同,在这颗混合树中动画受两个参数控制而进行二维插值,分别是身体偏转程度Turn和向前的速度Forward。红点受到三个片段的影响,但影响程度受与片段的相近程度而不同,因此需要为每个片段分配一个权值,混合出的动画是它们的加权平均:

其中α可为该点在三个片段构成的三角形上的重心坐标,满足

当然混合树也可以使用一维插值,只要选择1D类型即可,此时只有一位参数控制。

四、运动控制——状态机与控制器

我们在前面已经实现了从导入模型到播放角色动画的过程,为接下来能真正掌控操纵角色提供了基础。在unity中,对于角色运动的控制,主要靠状态机和控制器来实现。

1.动画状态机

在Asset下,新建一个Animator Controller,即动画控制器。再选中场景中的角色,为其添加Animator组件,并将刚才的控制器拖到Controller一栏。

双击该控制器,在Animator窗口中显示了三个节点,分别是Entry、Any State和Exit,这些节点称之为状态(state)。

状态机(state machine)则是包含这些状态和它们之间相互关系的一种结构。所谓相互关系,即状态之间可以相互转移,我们在Animator窗口中右键新建一个空状态,Entry状态自动连接到了该状态,表示了两者的单向转移。

1.1. 状态

动画状态(Animation State)是动画状态机中的基本单元。除了默认的Entry、Any State、Exit状态外,每个状态应包含一段Motion(运动),即动画片段,或者状态本身是一棵Blend Tree。因为状态中包含了动画片段和一些播放设置,在状态机切换到该状态后,角色就会进行相应的运动。

点击新建的状态,在inspector中显示了该状态的信息。其中Motion栏是该状态的动画片段,Speed设置了片段的播放速度。注意在Speed下方还有一个Multiplier(乘数),如果勾选右方的Parameter,就能在默认速度的基础上,再乘上控制器中某参数的值,以动态改变片段的播放速度。

Motion Time代表动画片段播放的位置,在默认不勾选的情况下,进入该状态后,动画片段会按照Speed x Multiplier的速度从头到尾进行播放,如果片段设置了Loop Time的话还会循环播放。同样的,也可以勾选后面的Parameter,使片段的播放受参数控制,这个参数应该是从0到1之间归一化的浮点值,表示现在处在该片段的哪一位置。

我们可以这样理解,在进入一个状态后,开始播放动画片段,有一个游标在片段的时间线上移动。在不勾选Parameter的情况下,游标从头到尾行进,还可能循环。但有时我们希望自己选择怎么播放,比如在小电影中直接定位到关键情节开始,或者在某个精彩画面暂停,此时游标就完全由参数控制,参数则可以通过代码等动态地改变。举一个论坛中应用的例子,楼主制作了一段人物举枪瞄准的动画,在该片段的时间线上,瞄准的位置从正下方均匀改变到正上方,因此可以使用Motion Time的参数来控制动画播放的位置,使得人物瞄准的朝向精准地匹配。

Mirror并不代表动画倒放,而是角色身体左右进行镜像翻转,如本来是左手挥拳镜像后变成右手,因此这也只适用于人形的动画,在动画片段自身的inpector中也能设置Mirror。

Cycle Offset则是片段播放的起始位置,但对于循环动画来说,它只会在第一次循环时起作用。Mirror可通过bool参数控制,Cycle Offset可通过float改变,关于控制器中的Parameter将在后面展开。

Foot IK控制人形动画足部是否启用脚部IK,反向运动学(IK)比较复杂,本文无法对此展开。Write Defaults代表退出状态后重置一些参数,具体请看下面的文章。

[Unity] AnimatorStates中的write defaults详解

Transition列表中自动列出了从该状态出发的所有转移,具体请看1.2节。

另外再提一下几个特殊的状态。黄色的为默认状态,可以在状态上右键设置为默认。Entry和Exit状态都是空节点,只表示状态机的开始和结束,不包含动画片段。Any State代表了所有的状态,如果从Any State连一根箭头到某个状态,即表示不管状态机现在的状态如何,都可以转换到这个状态,例如角色在任何时候都可能被攻击而播放BeHit动画。注意Any State只出不进,因为如果允许一个状态转移到Any State,引擎并不能知道转移到了具体的哪个状态。

1.2. 状态转移

状态是动画状态机中的基本节点,而状态转移(transition)则表示了状态之间的相互关系,就像有向图中连接节点的边。点击状态机中的一条连线,在inspector中显示了该转移的信息。


首先在inspector上部,有两个勾选框分别是Solo和Mute,Solo表示只播放该转移,Mute表示禁用该转移。

例如下图中,从Idle状态出发的三个转移都没有限制条件,默认播放完Idle状态后,会执行有最高优先级的转移,即此状态的Transitions列表的第一项。但其中一个勾选了Solo,即带绿色三角的,带红色三角的勾选了Mute。由图可见,引擎选择了带Solo的状态转移。

在调整状态机时,可以把希望生效的转移全部设置为Solo,而把需要禁用的转移设置为Mute,以方便预览效果。


Conditions栏中包含了该转移的条件列表,可以通过加减号增删条件。每个条件有控制器中的一个Parameter,以及一个随后的判定条件,只有当列表中的所有condition都满足——即每一个表达式都为true、且所有trigger触发时,这个状态转移才会执行。如果Conditions置空,状态机会按既定的顺序转移状态。

剩下的部分则是转移设置和动画的预览窗口。在转移设置中,第一个重要的属性是Has Exit Time。在勾选了这一项后,需要进行状态转移时,转移前的状态会先播放到Exit Time项对应的位置,再开始转移过程。

Exit Time也是一个归一化的值,表示结束位置在动画片段中的百分比。如果转移前状态的动画片段为Loop,则可以把Exit Time设置为大于1的值,以实现重复播放多次的效果,如果不是循环动画且Exit Time大于1,则角色会静止一段时间后进行转移。可以通过在时间线上移动游标,或者直接输入Exit Time的值。

如果不勾选Has Exit Time,此时又有两种情况:

  1. Conditions列表为空,即该转移不需要条件。我们也许会误以为此时会从上个状态直接转移,但实际是转移并不会发生,而是会一直停留在前一个状态。这是因为引擎并不知道该在什么时候退出前状态,因此转移是无效的,unity也会发出相应提示。
  2. 转移有至少一个条件。当转移的条件全部满足时,状态转移会立刻执行,而不管前一个状态播放进度如何,效果就像是打断了施法后摇而直接放了下一个技能。

接下来是转移的一些其他设置。Fixed Duration和Transition Duration(s)决定了转移中动画过渡的时间长度,在勾选了Fixed Duration的情况下,转移时长就是Transition Duration中的实际长度,单位是秒;如果不勾选,则Transition Duration是归一化的值,表示是转移前状态的动画长度的多少倍,如0.5表示过渡了2.4s x 0.5 = 1.2s。

Transition Offset改变的是转移后状态中,动画片段开始播放的位置,为0时默认从起始帧开始播放。这些值都可以在时间线中通过拖动的方式直接调整。关于过渡的方式,在动画混合部分我们已经探究过。

Interruption Source和Ordered Interruption控制了转移能否被打断以及如何会被打断。具体请看下面的原文或知乎老哥翻译后的版本,笔者并不觉得自己能写的更好了。

原文:Wait, I’ve changed my mind! State Machine Transition interruptions
译文:Animator- Interruption Source用法

1.3. 混合树

我们在动画混合部分已经提到了混合树的原理,这里主要说说相关操作。在Animator窗口中,右键点击新建一棵混合树,由inspector可见它也是状态的一种。双击混合树节点,进入这棵树的状态机页面。


在inspector中,可以选择混合树的类型,上图我们选择了1D,即一维类型。Parameter则是控制器中的某个浮点参数,通过改变该参数生成不同的混合动画。

在Motion列表中通过加减号增删状态,在每一栏状态中,需要选择其动画片段,或是一棵新混合树。Threshold译为“阈值”,在此表示该状态的权值为100%的参数值,1D的混合树明显使用的是线性插值。如果勾选了Automate Thresholds,则unity会自动用Threshold把参数值从0到1均分,如4个状态时,阈值分别为0、1/3、2/3和1(实际为浮点数形式)。

时钟图标那一栏表示状态的动画播放速度。如果在Adjust Time Scale中选择了Homogeneous Speed,会调整各状态的值,以使它们之间的相对速度一致。后面的镜像图标则决定人物动画是否要左右镜像。

其他几种混合树类型都大同小异,关键在于选择恰当的参数和动画片段。混合树在3D游戏中应用很常见,例如使用二维树实现多朝向不同速度的人物移动,使用横向偏转Turn和纵向速度Forward两个正交的参数控制。

2. 控制器

最后一部分我们讲讲对于Animator的外部控制,主要分为Layers和Parameters。

2.1. 动画层


在Animator窗口的左边栏,有一个叫做Layers的tab,在这里可以为动画状态机分层。默认情况下只有Base Layer一层,可以自行增删层级。在每个层级中,角色都有一个独立的状态机。

点击层级右侧的齿轮按钮,出现设置窗口。

自上而下来看,Weight表示该层级的影响权重,Base Layer的权重为1且只读。当存在多个动画层时,角色所处的状态是在每个层级中当前状态的混合,根据权重大小而影响程度不同。建议通过代码动态控制:

 int newLayerIndex = m_Animator.GetLayerIndex("Upper Layer");m_Animator.SetLayerWeight(newLayerIndex, 0.5f);

Mask中可以选择一个Avatar Mask,从而定义此层级中动画会影响角色的哪些身体部分,详见前面的部分。

Blending中选择该层级在混合中的类型,默认为Override,即覆盖其他层,而Additive会将该层添加到上一层之上。区别如下两图所示。

override

additive

点击Sync按钮,当前层级的状态机变为和Source Layer一样的结构,其中状态的名字和状态转移都完全一致,且在当前层级和Source Layer中修改状态机时,另一方都会同步修改。

Sync提供了状态机结构的复用,但不同层级的区别在于状态中的动画片段不同,而且Blend Tree也是自定义的,在Sync时这两部分都是置空的。例如在角色受伤时,其状态机结构可以和正常状态下一样,于是添加一个动画层并选择Sync,之后选择受伤状态的各个动画片段。

在每个层级的inspector面板中, 还可以点击Add Behaviour来添加脚本,该脚本继承自StateMachineBehaviour,提供了一些接口,可在 状态机运行时打印一些信息。例如想知道何时进入了跳跃状态:

public class NewLayerBehaviour : StateMachineBehaviour
{// OnStateEnter is called before OnStateEnter is called on any state inside this state machineoverride public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){if (stateInfo.IsName("Jump")){Debug.Log("Jumping!");}}}

2.2. 参数

我们在前面多次提到了Animator中的参数(Parameter),可见其作用十分重要。

在Animator窗口的左边栏的Parameters中显示了状态机中的所有参数。参数一共有4种类型:float、int、bool和trigger。

参数的作用主要体现在两部分:

  1. 在状态转移中,参数的值可决定转移条件是否成立。
  2. 在混合树中,参数影响了动画混合的权重。

参数的动态控制方式也主要分两种:

  1. 使用动画曲线改变,在状态中的某段动画片段中添加动画曲线,将因变量设置为Animator中对应浮点值的名称,当运行游戏后,我们发现该参数置为了灰色,表示无法手动更改其值。转移到该片段所在状态后,参数值才发生了变化。

  1. 使用代码直接控制参数,这是最直接的方法。示例如下:
 m_Animator.SetInteger("Int", 2);m_Animator.SetFloat("Float", 0.6f);m_Animator.SetBool(2, true);m_Animator.SetTrigger("Trigger");

函数中的第一个实参既可以是参数的名字,也可以是参数的序号。

总结

以上就是笔者对于unity中角色动画这一部分的一些记录,没想到写这篇水文断断续续花了几周时间,在这之中也对unity有了更多的了解和认识。不禁感叹游戏引擎包含的内容实在太深太广,目前对于很多方面还只是略微了解,因此要多实践、对于不懂的地方深入探究、多翻阅官方文档和逛技术论坛,以提升自己的见解和技术。感谢您的观看!

Unity角色动画详细学习记录相关推荐

  1. 《强化学习周刊》第39期:近似最优深度、多智能体广义、角色动画强化学习...

    No.39 智源社区 强化学习组 强 化 学  习 研究 观点 资源 活动 周刊订阅 告诉大家一个好消息,<强化学习周刊>已经开启"订阅功能",以后我们会向您自动推送最 ...

  2. 《强化学习周刊》第39期:近似最优深度、多智能体广义、角色动画强化学习

    文章转载|智源社区 本期贡献者|李明.刘青.小胖 关于周刊 强化学习作为人工智能领域研究热点之一,其研究进展与成果也引发了众多关注.为帮助研究与工程人员了解该领域的相关进展和资讯,智源社区结合领域内容 ...

  3. unity 角色 动画脚本_Unity Animation --动画剪辑(外部来源的动画)

    外部来源的动画 来自外部源的动画以与常规3D文件相同的方式导入到Unity中.这些文件,无论是通用FBX文件还是3D软件(例如Autodesk®Maya®,Cinema 4D,Autodesk®3ds ...

  4. Unity人物角色动画系统学习总结

    使用动画系统控制人物行走.转向.翻墙.滑行.拾取木头 混合树用来混合多个动画 MatchTarget用来匹配翻墙贴合墙上的某一点,人物以此为支点翻墙跳跃 IK动画类似于MatchTarget,控制两只 ...

  5. ESP32超详细学习记录:wifi配网,AP配网,浏览器配网,无线配网

    不想了解代码实现,可直接到代码部分copy!ESP32亲测可用. 使用设备ESP32开发板(ESP32-WROOM-32) 搜了好多别人写的资料,很多都是copy+copy,也没有什么解释.啪,代码放 ...

  6. Unity --- 角色动画的使用以及按键控制角色运动

    1.对前面那篇文章的补充:动画器控制器不会自动获取所有的动画片段,如果我们要添加动画片段到动画器控制器中作为动画状态的话,我们要被对应的动画片段文件拖到动画器控制器中 让我们实现一个功能 --- 角色 ...

  7. Unity角色动画之面部动画——SALSA插件

    目录 概述 组件 LipSync EmoteR Eyes 2D面部动画 搭建2D模型 添加SALSA 2D组件 添加RandomEyes2D组件 运行效果 3D面部动画 添加SALSA 3D组件 添加 ...

  8. Keras中文文档:图像预处理ImageDataGenerator 类详细学习记录

    写在前面的话 受教于学长,Keras的探究还需进行 要把Keras用得熟练并非那么容易 1.ImageDataGenerator 类 参考文献:https://keras.io/zh/preproce ...

  9. ESP32超详细学习记录:NTP同步时间

    本来想从开源项目找找灵感的,但是!那些代码真烂!!!!! 开源项目免不了的就是抄抄抄代码,想知道 NTP 是怎么实现的还要读那一堆烂代码,烦! 就算开源项目能怎么样!不还是一样的  烂!代!码! 自己 ...

  10. 【Unity】UI ToolKit 学习记录

    Unity推出的这个 UI ToolKit,据说是要用来替代UGUI.既然这么有野心,那肯定要搞来看一看.这次使用目标就是用这个 UI ToolKit 生成一堆类似HUD的头标,然后看看使用难易程度和 ...

最新文章

  1. nodejs 日志规范
  2. showModalDialog数据缓存问题
  3. CSharpGL(23)用ComputeShader实现一个简单的ParticleSimulator
  4. 配置FCKeditor_2.6.3+fckeditor-java-2.4
  5. 构建maven项目插件_如何构建一个Maven插件
  6. 神经网络最常用的10个激活函数,一文详解数学原理及优缺点
  7. Notification的学习,4.0前后的差别,和在设置声音的时候获取资源的uri方法
  8. 在latex或者mathtype中如何输入花体,如拉式量L
  9. 谁说程序员的老婆和代码不可兼得?!
  10. 《电子商务安全》考试重点/学习重点
  11. Macbook换SSD硬盘 备份OS、Win10双系统 完全攻略
  12. matlab画出n的阶乘,matlab计算n的阶乘函数程序
  13. iis运行html提示500错误,IIS发布网站出现Http—500错误
  14. 用计算机怎么计算税率表,个税计算器2016年税率表公式
  15. C# MemoryStream
  16. 芭比娃娃缘何泪洒上海滩?
  17. Three TextureLoader纹理贴图不显示图片(显示黑色)
  18. mysql之聚簇索引与非聚簇索引
  19. 论文写作——如何作图(visio/ppt+Adobe Acrobat Pro)
  20. 美国打车应用Lyft公布IPO招股书 预计3月底挂牌交易

热门文章

  1. 计算机蓝屏代码0xc0000020,电脑运行程序时出现“损坏的映像错误0xc0000020”提示怎么办?...
  2. 服务器主机密码忘记了怎么破解?
  3. TM16xx LED驱动和键盘扫描芯片使用
  4. ArcGIS在水文水资源、水环境中的实践技术应用及案例分析
  5. Laravel学习笔记(33)后台切换前台模板(修改默认的加载模版路径)
  6. 协方差矩阵的计算方法
  7. 关于交流半波与全波整流输出的电压(或电流)有效值和平均值的讨论
  8. Android-Studio中AndroidManifest-xml文件中application标签
  9. 用python 创建英语自定义词典
  10. 小程序input绑定输入保存数据