Skip to content

Animation

云风 edited this page May 11, 2024 · 5 revisions

动画

骨骼关键帧动画

Ant 引擎使用 ozz-animation 这个第三方库处理骨骼关键帧动画。它把动画视为一系列的关键帧 (key frame) ,每个关键帧是由若干骨骼点组成的姿态。关键帧的时间间隔通常超过实际渲染帧,动画库通过关键帧之间的插值得到当然渲染帧上的姿态。

即:我们让动画库管理预先定制好的骨骼以及关键帧,在运行时通过动画库的计算,得到当前需要的姿态。每个姿态是由若干骨骼点构成,每个骨骼点由动画库计算出最终的空间状态。

动画库由 ECSant.animation 这个特性中定义的 system 驱动。所以,如果游戏需要使用这些动画功能,需要一开始导入这个特性。(参见 HelloWorld

Ant 有两种方法使用姿态信息。姿态可以用来驱动带蒙皮的模型,或是驱动场景对象。

蒙皮动画

我们可以给一个 3d 模型附加蒙皮信息。蒙皮数据记录的是模型上的每个顶点会受哪些骨骼点的影响,以及受影响的权重。蒙皮数据是一种 Asset ,通常用通用工具制作生成,引擎可以从 gltf 文件中导入。有了蒙皮之后,我们给一个模型调整姿态时,就不需要调整所有模型顶点,而只需要调整骨骼点。引擎会通过蒙皮数据计算出模型每个顶点在该姿态下的位置。

在 Ant 引擎中,拥有模型 (mesh) 组件和材质 (material) 组件的 entity 会参于渲染。我们还可以给这样的 entity 添加一个蒙皮 (skinning) 的组件。带蒙皮的模型是一个 Scene 对象。引擎在组织场景对象时,会先构造一个包含 animation 组件的场景对象,这个组件内有骨骼姿态信息,它在运行时由动画系统更新。这个对象作为父节点,不带有 mesh ,所以不参与渲染。而带蒙皮的模型节点是它的孩子。在渲染过程中,渲染器会根据这个场景对象的蒙皮和模型,加上它父节点中的骨骼姿态,(通过 GPU) 计算出模型的最终姿态。

animation 组件中还包含动画控制器及关键帧数据。动画系统专注于操控的这个组件。

动画驱动场景对象

动画还可以直接驱动场景对象,只需要将动画中的骨骼点和场景对象关联在一起。我们可以通过 ant.modifier 这个特性完成这个工作。

修饰器 (modifier) 可以用来在运行时修改 entity 的各种属性,修改 scene 组件中的空间信息只是功能之一。当 entity 中包含修饰器 (modifier) 组件时,ant.modifier 特性中的修饰器系统 (modifier_system) 会去修改其绑定的场景对象。对于骨骼修饰器来说,当动画驱动的姿态变化,修饰器会取出姿态中对应的骨骼的矩阵,作用到目标 entity 的 scene 组件。这样,动画的运动就能影响场景中的 entity 了。

创建修饰器对象可以先引入:

local imodifier = ecs.require "ant.modifier|modifier"

然后调用 imodifier.create_bone_modifier() 方法。

制作关键帧动画

关键帧动画几乎不可能手工制作,必须使用专门的工具软件。我们可以从 blender 这样的动画制作软件导出包含动画的 gltf 文件。Asset 资产管理模块会将 gltf/glb 文件转换为 Prefab 。在预制件中,所有的动画控制信息,都放在一个被打上 anim_ctrl 标签的 entity 模板上。

除了用专业动画制作工具外,引擎自带的编辑器也能制作一些简单的动画关键帧。编辑器制作的动画关键帧以 DataList 格式保存为后缀为 .anim 的文件。这个动画描述文件最终会被资产编译器转换为和其它动画制作软件导出动画相同类型的数据。在我们制作游戏的过程中,美术创作人员有些场合喜欢使用专业动画软件,另一些场合更偏爱引擎的编辑器。如果需要编辑一些动画关键帧驱动场景对象的运动,引擎编辑器更为方便。

播放关键帧动画

通过 world:create_instance() 实例化预制件,在 on_ready 回调中,用 inst.tag.animation[1] 就能找到包含动画控制器的 entity 。一个预制件只会有一个动画控制器,所以这里写的是 [1]

动画控制器组件内包含有这个预制件里所有的预制关键帧动画数据,存放在以字符串名字索引的表内。我们不需要关心它的内部数据结构,而只需要使用这个模块:

local iani = ecs.require "ant.anim_ctrl|state_machine"

ant.anim_ctrl|state_machine 模块中的 iani.play() 可以驱动动画组件播放。

材质动画

所谓材质动画,就是在运行时不断的修改可渲染对象的材质,得到的动态效果。例如,循环修改材质中的颜色,可以产生闪烁效果。材质动画修改的是着色器 (shader) 中的参数 (uniform) ,参数能控制什么,取决于具体着色器的实现。

时间线

控制一组模型的运动,往往不只一个简单的动画就够了。通过引擎的编辑器制作预制件时,我们可以细致的控制在什么时刻,对什么对象,做什么操作。这些和时间相关的操作,不仅仅是驱动动画,还可以驱动声音和特效。

引擎实现了时间线 (timeline) 特性,引入 feature ant.timeline 后,就可以使用它。

时间线 (timeline) 组件可以被附加在 entity 上,按时间管理一组动画控制操作。一般不会在运行时构造时间线,而是通过编辑器制作在预制件中。

播放 timeline 的 api (在 ant.timeline|timeline 中)和播放关键帧动画的 api 接口非常类似。但 timeline 可以将关键帧动画、材质动画、特效发射器 Effect 、声音等等放在一个时间轴上统一管理。我们可以在编辑器中把这些控制逻辑变成数据持久化在预制件中,从而简化了动画控制的过程。

运动插值器

在游戏中,如果一个场景对象需要在场景中运动,每帧都需要修改它的空间状态。对于固定轨迹,我们可以用上面提到的动画骨骼去驱动这个对象。对于不固定轨迹,可以动态生成骨骼动画(动画模块中提供了相应的 api),但有一定的成本。

在许多场合,我们希望每个渲染帧在 Lua 中计算运动轨迹,设置给 scene 组件。场景对象不多时,这是一个简单有效的方案。如果需要运动的对象非常多,可能需要对这部分做一些优化:

可以不在每个渲染帧都在 Lua 计算并设置场景对象的位置,而是以更低的频率(例如每秒 10 次或更少)计算。然后使用 ant.motion_sampler 这个特性对这些关键帧做线性插值,再逐帧设置 scene 。ant.motion_sampler 中定义的 motion_sampler 系统是用 C++ 调用 ozz-animation 实现的,比直接在 Lua 中计算并修改 scene 组件要高效的多。

如果想让一个场景对象沿直线运动,更可以只设置起点、终点和运行时间就够了。

Clone this wiki locally