〖Array王锐大神力作〗osg与PhysX结合系列内容——角色动画效果(上)
物理引擎先放一边
完成了上一节有关角色动画的内容之后,我们还是有必要把物理引擎的工作先放在一边,解决一下过于粗放的渲染效果问题。这原本并不是这个系列教程的主旨,因为实时渲染是另一个非常复杂的技术领域,并不是短短几百几千字就可以描述清楚的。不过,能不能看到一个人形的动画角色在玩家的操纵下自由运动,这一点对于游戏和可视化应用来说还是有核心价值的;毕竟没有人愿意把一个胶囊体或者立方体当作是游戏中的自己,这在“元宇宙”概念盛行的如今,实在是太过抽象和难以理解了。
OSG本身提供了osgAnimation库来实现多种动画效果,包括角色的骨骼和蒙皮动画效果。但是,osgAnimation库自身存在非常明显的问题,主要包括:
- 完全依赖于osgAnimation的EaseMotion系统,适合于预设关键帧动画的播放;不适合实时数据驱动的动画。
- 完全采用节点和回调来管理角色骨骼系统,虽然符合OSG的一贯设计,但是访问起来较为繁琐。
- 角色动画主要来自于FBX插件,读取时容易发生问题;且只能播放和切换各个动画,缺少其它常用的角色动画调整手段(例如权重控制,动画过渡,IK运动学等)。
在这个小节中,我们将花费一些额外的时间,去研究一下其它合适的开源角色动画引擎,并将它合并到OSG中使用,进而与PhysX驱动的物理角色相结合,让整个交互过程更加丰富和逼真。
动画库ozz-animation
角色系统的基本功能组成,大致包括:
- 骨骼系统(Skeleton):角色的各个关节以及它们之间的连接关系;
- 蒙皮系统(Skinning):角色模型网格的每个顶点与骨骼关节的映射关系以及权重,也就是说,模型顶点是跟随哪些关节运动的,跟随的程度有多少;
- 动画系统(Animation):也就是各个关节的移动和旋转动画数据,以及多个动画之间的过渡、叠加关系的实现;
- 其它功能组件,例如FK/IK系统(前向/反向运动学),武器绑定系统(绑定后的武器将跟随关节运动,例如手中持枪),以及随着VR的发展而逐步流行的步态系统(Gait)等,都可以认为是一个完整的角色系统的组成部分。它们在各类游戏和仿真需求中都有着十分广泛的应用。
不过,随着Unity和Unreal这样的商业引擎逐步成为主流开发者群体的首选,它们内置的角色系统已经足以满足一般的用户需求。独立且开源的角色系统,例如多年前的Cal3D之类,就变得越来越少,而且基本上都停止了更新。
好在,我们还是找到了一丝光明,那就是这次将要介绍的ozz-animation这个开源的“纯粹”角色系统。
Ozz这个库的Github地址是: https://github.com/guillaumeblanc/ozz-animation
它包含两个主要的模块,即runtime模块(实时运行中解算)和offline模块(离线状态下预处理)。其中runtime代码只依赖于C++11,并且可以在Windows,Linux,MacOSX这些常见操作系统,以及ARM平台下运行。
Ozz这个库的核心目标明确,只关注于角色动画的功能实现和效率提升,而不涉及到具体的渲染效果,格式转换,图形接口支持。当然,它也提供了一些与其它开源库结合的案例,从而实现例如FBX和GLTF格式的转换,或者OpenGL渲染等。
当然,对我们来说,渲染自然是通过OSG来实现。Ozz只需要做好自己的本行即可。此外,Ozz提供了自己的预处理格式.ozz,可以通过预处理模块来输出,我们在这里直接使用这个格式的数据来完成实时角色骨骼渲染的需求。
动画资源管理
本文作者并非美术人员出身,大多数阅读者恐怕也不是。因此在讨论如何实现舒服的角色动画之前,我们先要有合适的角色动画资源可用。
自己从头制作耗时耗力,且艺术造诣有限,出来的结果可能惨不忍睹;从网上购买资源耗费金钱,并且因为每个人的建模习惯不同,数据格式和内部结构可能各异,徒增难度。因此,我们有必要推荐一款在线的“角色建模神器”,也就是在美术界大名鼎鼎的Mixamo。
Mixamo的地址如下所示(页面如下图),在正常使用之前,请读者自行注册一个新的用户账户: https://www.mixamo.com/
注册用户可以在Mixamo主页面的左上角点击“Characters”,这里已经提供了一些优质的角色素材,在我们做试验的阶段,完全可以作为我们的资源所用。
这里我们选择一个看起来比较酷的角色(例如Exo Gray),稍后即可在右侧的预览窗口中看到这个角色的默认状态(T-pose,也就是双手侧平举的站姿,这个姿态通常也是美术人员进行角色蒙皮的初始姿态)。然后我们点击Download按钮,并在弹出的对话框中选择“FBX格式”“T-pose”并确认下载。下载得到的单个fbx文件(其中事实上也包含了纹理图数据),我们将其保存下来稍后使用。
注意:Mixamo真正强大的地方在于,它可以帮助美术人员对一个模型做自动骨骼和蒙皮的操作,这简直就是美工的福音产品!不过这个功能并不在本章的讨论范畴之内,有兴趣的朋友不妨赶紧拉身边的美术同事过来拜见一下“上帝”……
选择了合适的角色之后,在左上角选择“Animations”,此时左侧的选项切换成了各种实用的角色动作动画,并且可以轻松地搜索自己所需的动作,例如“idle”(角色站立不动的姿势),“walk”“run”以及其它你需要的动作名称(比如射击、中弹、死亡等等)。你甚至可以找到一些非常酷的免费动作拿来自用(例如巴西战舞“卡巴耶拉”,或者像巫师一样释放火球术)。
在右侧窗口预览之后,如果觉得合适,就可以继续点击Download按钮,然后在弹出的对话框中选择“FBX格式”“Without skin”(这次只需要保存动画数据,不需要再记录蒙皮信息了)并下载。
下载后的动画文件和之前的模型fbx文件放在一起存储,并且保持一个良好的命名习惯,因为你很可能需要更多的动画文件来满足一个复杂角色控制系统/游戏的需求。
载入和预处理动画
现在我们回到角色动画库ozz。如果读者想自行编译ozz-animation的话,直接将它的CMakeLists.txt文件传递给cmake中生成解决方案文件即可。不过我们在osgPhysX中已经将最重要的runtime模块包含进来,不需要再依赖于外部库文件;同时我们还附带了一些模型数据预处理必需的exe程序,在osgPhysX根目录的utils\ozz\tools子目录中:
ozz主要包括三种类型的数据文件,即骨骼数据文件(skeleton),模型数据文件(mesh)和动画数据文件(animation)。其中模型数据文件暂时并不能包含纹理信息,它主要是作为示例数据使用的,我们在本节中暂时先使用这个文件来实现基本的动画数据载入和显示。
第一步,我们从第一次下载的Exo Gray角色fbx中生成骨骼数据,命令行格式为: fbx2ozz.exe --file=exo_gray.fbx
此时会生成两个文件,一个名为skeleton.ozz,另一个名为mixamo.com.ozz。前者是骨骼数据文件,将它另存为exo_gray.skeleton.ozz(这是为了后续便于数据管理)或者读者觉得合适的名字。后者是默认的动画数据,也就是T-Pose姿态对应的动画。
注意:Mixamo免费版导出的所有动画名称都是“mixamo.com”,我们必须将它改成自己容易管理的名称,例如exo_gray@tpose.ozz。
第二步是生成模型数据文件,这里我们暂时先继续使用ozz提供的示例模型格式,它并不包含纹理信息,因此本节我们所呈现的渲染效果也是无纹理的。在之后的章节中我们会“魔改”ozz中的内容并改进这个部分。幸好这并不是一个角色动画引擎所关注的核心内容,所以我们可以随心所欲地自行处理,不用担心和ozz自己后续的更新冲突。
命令行格式为: fbx2mesh.exe --file=exo_gray.fbx –skeleton=exo_gray.skeleton.ozz --mesh=exo_gray.mesh.ozz
根据–mesh参数的设置,这里会生成一个新的文件exo_gray.mesh.ozz。这就是所有的模型几何数据了。下一步,我们要处理更多的动画数据文件了,找到刚才下载的“Idle状态动画”fbx文件,然后命令行执行: fbx2ozz.exe --file=exo_gray@idle.fbx
生成的还是两个文件,其中的skeleton.ozz可以忽略,因为它应当和之前的exo_gray.skeleton.fbx完全一致。另一个动画ozz文件,我们将它改名为exo_gray@idle.ozz,并且统一进行保存。更多的动画数据,可以依此继续进行预处理,这里不再赘述。
注意:Mixamo可以保证其输出数据标准的统一,而所有的动画都必须来自于同一套骨骼系统!
合并到OSG显示
osgPhysX中已经将ozz的runtime部分合并到osgPhysicsUtils中,名为PlayerAnimation。使用案例可以参看tests/character_animation_test.cpp,目前这个例子演示了最基本的动画加载和显示。
具体的加载过程代码如下:
- osg::ref_ptr< osgPhysicsUtils::PlayerAnimation > animManager = new osgPhysicsUtils::PlayerAnimation;
- if (!animManager->initialize(“exo_gray.skeleton.ozz”, “exo_gray.mesh.ozz”)) return 1;
- if (!animManager->loadAnimation(“idle”, “exo_gray@idle.ozz”)) return 1;
- animManager->select(“idle”, 1.0f, true); //设置idle动画的权重为1,循环播放为true
- animManager->seek(“idle”, 0.0f); // 设置idle动画从时刻0开始播放
如果需要加载更多的动画,可以多次调用loadAnimation(),给每个动画设置一个唯一的名字(例如idle,walk,run等等),并注意设置每个动画的权重值。权重大于0的动画会同时播放并自动混合,权重为0的动画不会主动开始播放。
除了加载过程之外,PlayerAnimation还需要在场景更新的过程中调用对应的代码来执行动画流程。它本身并不是一个NodeCallback,不过我们可以在Node或者Viewer的更新回调中执行它。
可渲染的模型数据要保存到一个预先建立的Geode节点中,例如: osg::ref_ptr< osg::Geode> playerNode = new osg::Geode;
动画数据的更新过程代码如下:
- animManager->update( *viewer.getFrameStamp(), false); // 更新动画,暂停为false
- animManager->applyMeshes(*playerNode, true); // 更新模型数据,蒙皮效果为true
第一次更新动画之后,Geode节点中会自动增加多个Geometry对象,即角色的顶点和图元数据。之后随着角色的动画更新,角色的顶点数据和属性数据也会随之更新,不需要用户进一步干涉。
这个例子的播放效果如下方,显然这比一个胶囊体要好多了!
|