Skeletal Model and Skinning Animation

骨骼模型与皮肤动画
本文介绍了骨骼模型(skeletal model)及其在动画中的应用。包括模型的构成、层级结构、顶点变换及皮肤动画(skinning animation)实现原理。探讨了不同场景下模型渲染的优化方法。

Skeletal Model and Skinning Animation

仅供个人学习使用,请勿转载,勿用于任何商业用途。 

 

      为了方便讨论,先定义几个术语:Model,一系列MeshPart(MP)(类似d3d里的subset)的集合;MeshPart,组成Model的单元,包含定义几何体的实际数据以及材质,也是最小的渲染单元。

 


      第一个问题,为什么Model需要由多个MeshPart组成,如何划定一个Model分成几个MP?理想情况下,Model所包含的MP越少越好,最好是一个Model只包含一个MP,这样一次DrawPrimitive/DrawIndexedPrimitive,就能完成整个模型的渲染。但通常有两种情况需要把Model分为不同MP:1,材质(纹理,材质参数)不相同的部分;2,能独立移动的部分。

       通常情况下,作为程序员,你不必关心如何划分Model,模型师会做好一切,并且保存为文件给你使用,你所要关心的是如何找出文件中所保存的Model和MP。假设导出文件里储存的就是图中这个模型,那么可能遇到3种情况:1, 文件把模型记录为14个不同的model,它们共同组成了另外一个model(人);2,文件中只包含一个model,但这个model有14个MP;3,这种情况则是前两种混合的结果。

 

     上面是两个不同的fbx文件,左边那个是以第三种方式组织的,右边那个则是以第1种方式组织。图里Geometry和我们所说的MP概念类似,但不完全一样,所以数值上看起来有些奇怪。

     如何导入模型超出了本文的讨论范围,现在假设你已经通过某种方式把模型数据加载到程序里,得到了14个MP,但仅仅有几何体数据是不够的。如果查看导出文件中的数据,会发现所有MP都以自己的局部坐标来保存顶点。也就是说如果你直接渲染这14个MP,他们会全部重叠在原点。因此,还需要知道每个MP在这个model中的位置。

 

      上图三个Lcl开头的数据就是fbx文件中记录的MP变换信息。Lcl表示局部变换,这里的局部相对于谁呢?相对于他的父节点。这里就需要引入一个新概念skeletal structure或者bone hierarchy,前者指模型的实际拓扑结构,后者指对这种拓扑结构的描述。具体来说,图中你看到的人物轮廓形象,就是这个模型的skeletal structure,而 “手链接到手臂,后手臂链接到肩膀”这样的描述,就是bone hierarchy。描述bone hierarchies最直观(但并非最优化)的方式就是用Tree。显然,对同一skeletal structure的描述可以有无数多种,依据你选择哪个部分为root,不同的描述方式之间并没有本质上的差别,通常,模型文件里都会包含特定的bone hierarchy数据。为了方便讨论,下文不再对structure和bone hierarchy做区分。最后解释一下另外一个术语bone/join(两者其实是一样的),这是一个很容易误导人的概念,对计算机模型来说,其实并没有bone这种东西,通常所说的bone应该包含两个概念:变换以及连接。假设你用Tree来保存模型的hierarchy信息,每个node里保存了相应的变换信息(比如matrix),那么可以认为每个node就是一个bone。但也有可能用一个数组来保存hierarchy信息,再用另外一个数据记录所有Matrix,这时就很难说哪一部分是bone。为了方便,下文的bone只表示变换,或者说代表一个matrix。

      再次假设你已经通过某种方式导入了hierarchy信息,以躯干为根节点,有以下数据结构:

Trunk
|
|-----neck----head
|
|-----left shoulder----left arm---left hand
|
|………………….

      那么现在已经有足够数据计算每个MP的world matrix,并且渲染它们了,假设在(0,0,0)点渲染这个模型:


       你不必总是通过这样的方式来计算每个MP的实际世界坐标。对于静态模型来说,最好的方式是在加载模型或者预处理的过程中,就把顶点变换到模型空间中,假设图中的模型是个木头人,可以这样预处理他头上的顶点:

 

     现在,如果在x,y,z点渲染这个模型,只需要:
 

      完全省略了一步步通过父节点,计算当前MP实际世界坐标的步骤,同时也不再需要保存模型的hierarchy信息。

      对于部分动态模型来说,可以预先把局部变换转变为模型空间的变换,比如:
 

    在x,y,z点渲染这个模型时
 

    这种方法同样不需要保存模型的hierarchy信息。

    如果这两种方法那么好,那为什么还要讲解最初那种复杂的方法呢?应为最初的方法是最通用的,无论什么样的模型都可以处理。预变换顶点的方法通常只适用于静态模型。至于预计算变换,假设你希望通过程序修改了手臂的位置,手掌也自动随之一起移动的话,显然就无能为力了,因为我们已经丢失了hierarchy信息。此时,必须独立计算手掌应该如何移动,这比简单的通过父节点计算出变换复杂的多。

     目前,我们已经知道了如何渲染skeletal model,进一步渲染skeletal animation也就非常简单了。

     先来看看如何描述动画。最基本的模型动画有三种形式:位移变化,缩放变化和旋转变化。只要记录下动画时间内每个时刻的变换信息,也就是关键帧,就能重现这个动画:


     目前我们知道了如何渲染skeletal model和skeletal animation,但它们通常只适用于刚体,也就是不会发生形变的模型,比如汽车,机器人。对于人或者动物这样更高级的对像来说,需要使用skinning animation。对skinning mesh来说,一个MP中的顶点不再只受到一个bone的影响,而有可能最多受到n个bone的影响,为了兼顾效率,实时计算中通常1<n<= 4。比如肩膀部位的顶点会受到脖子,躯干,手臂等部分骨骼的影响。此外,每个MP也不一定再对应着一个bone,有可能包含多个。假设用一个数组来保存模型中的所有骨骼:

matrix[] bones;

      每个顶点就必须记录下它将受到哪几个骨骼的影响,这称为boneIndex。此外,每个顶点还需要记录每个bone对它影响的权重,称为boneWeight,所有权重的和必须为1。可以用独立的数据结构来保存boneIndex和boneWeight,但最常见的还是把它们作为顶点数据的一部分,可能使用这样的顶点结构:

 

 计算顶点最终位置的公式为:

 
      现在问题来了,由于我们不知道一个MP中的某个顶点究竟受到哪几个bone的影响,所以不能再像之前那样,只为每个MP提供一个matrix就渲染。而是需要把整个bone数组都提供给MP。当然,要先计算出正确的matrix才行:

 

     注意,每个bone在数组中的位置必须和顶点中记录的相同。

     最后,把渲染skeletion animation和skinning mesh的技术结合起来,就得到了skinning animation。相关方法就不再详细讨论了。

    最后附上GPU渲染skinning mesh的HLSL代码:

 

 

ps:注意,文中所有伪代码以及所用数据结构,shader只是出于演示目的,并未经过任何优化,请根据实际情况参考使用。

 

 

以下是关于 Panda3D 骨骼动画的相关内容: ### 基本示例 之前给出的示例代码展示了如何加载一个 GLB 格式的模型并播放其第一个动画: ```python from direct.showbase.ShowBase import ShowBase class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # 加载 GLB 文件 self.actor = self.loader.loadModel("your_model.glb") self.actor.reparentTo(self.render) # 获取可用动画名称 anim_names = self.actor.getAnimNames() if anim_names: # 播放第一个动画 first_anim = anim_names[0] self.actor.loop(first_anim) app = MyApp() app.run() ``` ### 更多功能示例 #### 动画的暂停和继续播放 可以通过 `pause` 和 `resume` 方法来实现动画的暂停和继续播放功能: ```python from direct.showbase.ShowBase import ShowBase class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # 加载 GLB 文件 self.actor = self.loader.loadModel("your_model.glb") self.actor.reparentTo(self.render) # 获取可用动画名称 anim_names = self.actor.getAnimNames() if anim_names: # 播放第一个动画 first_anim = anim_names[0] self.actor.loop(first_anim) # 绑定按键来暂停和继续动画 self.accept("p", self.pause_animation) self.accept("r", self.resume_animation) def pause_animation(self): self.actor.pause() def resume_animation(self): self.actor.resume() app = MyApp() app.run() ``` #### 多个动画之间的无缝切换 使用 `mix` 方法可以实现多个动画之间的无缝切换: ```python from direct.showbase.ShowBase import ShowBase class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # 加载 GLB 文件 self.actor = self.loader.loadModel("your_model.glb") self.actor.reparentTo(self.render) # 获取可用动画名称 anim_names = self.actor.getAnimNames() if len(anim_names) >= 2: # 播放第一个动画 self.actor.loop(anim_names[0]) # 绑定按键来切换动画 self.accept("s", self.switch_animation, [anim_names[0], anim_names[1]]) def switch_animation(self, from_anim, to_anim): self.actor.mix(from_anim, to_anim, fadeIn=0.5, fadeOut=0.5) app = MyApp() app.run() ``` #### 添加相机控制 可以使用 Panda3D 提供的 `MouseWatcher` 来实现相机控制,方便观察骨骼动画: ```python from direct.showbase.ShowBase import ShowBase from direct.task import Task from panda3d.core import Vec3 class MyApp(ShowBase): def __init__(self): ShowBase.__init__(self) # 加载 GLB 文件 self.actor = self.loader.loadModel("your_model.glb") self.actor.reparentTo(self.render) # 获取可用动画名称 anim_names = self.actor.getAnimNames() if anim_names: # 播放第一个动画 first_anim = anim_names[0] self.actor.loop(first_anim) # 启用鼠标控制 self.disableMouse() self.camera.setPos(0, -10, 2) self.camera.lookAt(self.actor) # 绑定相机控制任务 self.taskMgr.add(self.camera_control, "camera_control") def camera_control(self, task): if base.mouseWatcherNode.hasMouse(): x = base.mouseWatcherNode.getMouseX() y = base.mouseWatcherNode.getMouseY() # 相机旋转 self.camera.setHpr(self.camera.getH() + x * 10, self.camera.getP() + y * 10, 0) return Task.cont app = MyApp() app.run() ``` ### 教程及相关资料 - **Panda3D 官方文档**:Panda3D 的官方文档提供了详细的 API 文档和教程,其中包含了关于骨骼动画的相关内容,可以在 [Panda3D 官方网站](https://www.panda3d.org/) 上找到。 - **Panda3D 论坛**:Panda3D 论坛上有很多开发者分享的经验和示例代码,可以在论坛上搜索关于骨骼动画的帖子,获取更多的学习资源。 - **在线教程网站**:像 YouTube 上有很多关于 Panda3D 的教程视频,其中也会涉及到骨骼动画的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值