骨骼蒙皮动画(SkinnedMesh Animation)的实现
http://blog.csdn.net/zjull/article/details/11529695
1、简介
骨骼蒙皮动画,简称骨骼动画,因其占用磁盘空间少并且动画效果好被广泛用于3D游戏中,它把网格顶点(皮)绑定到一个骨骼层次上面,当骨骼层次变化之后,可以根据绑定信息计算出新的网格顶点坐标,进而驱动该网格变形;一个完整的骨骼动画一般由骨架层次、绑定网格以及一系列关键帧组成,一个关键帧对应于骨架的一个新状态,两个关键帧之间的状态可以通过插值得到;下面介绍骨骼蒙皮动画在SPE中的实现细节,包括骨骼层次的表示、动画数据(关键帧)的组织、关键帧之间的插值方法、软件蒙皮以及硬件蒙皮的实现。
2、骨骼层次的表示
图1:骨骼层次
一个骨骼层次由一系列离散的关节(joint)构成,它们通过父子关系联系在一起,如图1所示;为了方便建模,每个关节的方位信息(位置和朝向)是在它的父空间中定义的,每个关节自身也定义了一个子空间;那怎么表示一个空间呢?一个joint定义的空间可以由该joint在父空间中的位置和朝向确定,位置可以由一个三维向量表示,朝向用一个3x3的矩阵表示,这样一个空间可以用一个3x4的矩阵表示,其中左边3x3部分表示朝向,第4列表示位置,为了后面表示和计算方便,在矩阵中添加一行(0,0,0,1),形成4x4矩阵。如上图所示,Mi表示了各个joint的空间矩阵,M2定义在它的父空间M1中,M1定义在M0中,M0所在的关节joint0叫根关节,因为它直接定义在建模坐标系空间W中,W是个单位矩阵,另外,一个骨骼层次一般只有一个根关节。
既然关节的方位是定义在父关节空间中的,那如何知道自己的全局方位呢?只要把它的所有父关节的空间矩阵乘起来再乘以它自己的空间矩阵就可以了,如joint2的全局方位矩阵为:G2 = M0*M1*M2,我们把G2称为joint2的全局矩阵(global matrix),这样若要把一个定义在joint2空间中的顶点变换到建模空间,只需要左乘G2即可;若要把定义在模型空间W中的顶点变换到某个关节的子空间,如joint2,我们只要左乘G2的逆:S2=inverse(G2),我们把S2称为joint2的偏移矩阵(offset
matrix)。
基于上面的信息,设计了下面的结构来表示骨骼层次:
- struct sJoint
- {
- std::string name_;
- int parent_;
- MATRIX4X4 localMatrix_;
- MATRIX4X4 globalMatrix_;
- };
上面的结构体表示一个关节,由关节名称、父关节的id(根节点无父关节,可设为-1)、局部空间矩阵以及全局空间矩阵组成,骨骼层次由根关节开始按照父子关系存储在一个sJoint数组中;sJoint中的localMatrix_和globalMatrix_在动画播放中会经常更新,globalMatrix_通过下面的函数可计算得到:
- void UpdateGlobalMatrix(sJoint *skeleton, int numJoints)
- {
- for (int i=0; i<numJoints; ++i)
- {
- sJoint &joint = skeleton[i];
- if (joint.parent_ == -1)
- joint.globalMatrix_ = joint.localMatrix_;
- else
- joint.globalMatrix_ = skeleton[joint.parent_].globalMatrix_*joint.localMatrix_;
- }
- }
3、动画数据的组织
制作动画时,一般会把整个骨架层次画出来,调整成一个初始姿势,又叫绑定姿势,并由这个姿势来制作动画关键帧;每帧中记录了各关节相对于绑定姿势的旋转,平移,缩放,一般只有根关节才有平移跟缩放,其他关节只有旋转。平移可以用三维向量表示,旋转可以用一个四元数表示,暂不考虑缩放,因为基本用不到。假设骨骼层次有30个关节(实际可能更多),一个三维向量占用3*4=12字节,一个四元数用四维向量表示,占用4*4=16字节,这样一个关键帧占用30*28=840字节,平均每个动作10个关键帧的话,保存一个动作就要8400字节,约8KB,看起来不大,但其实还可以压缩。因为关节不一定在每个关键帧中都有不同的状态,甚至有些关节的状态在数个关键帧中都具有相同的状态,如图2(上)所示,图中是某个关节在具有5个关键帧的动画中的状态变化,用A、B表示其状态,可见在前4帧中该关节的状态都是不变的,在第1和第2帧为该关节保存状态是不必要的,图2(下)显示了压缩后的该关节关键帧;采用压缩机制之后,每个关节对应的关键帧数或者同一关节中的平移与旋转关键帧数都可能不一样,所以我们要对每个关节独立保存关键帧,并且每个关节的不同状态也分开保存,这里只有平移和旋转状态。
图2:关键帧压缩
基于上面的分析,动画数据就可以组织成下面的形式:
- animation name: walk
- total channels: m
- total keys: n
- total time: xx seconds
- channel 0:
- translation keys: 0(0,0,0) 2(0,0.5,0.5) n-1(0,0,0)
- rotation keys: 0(0,0,0,0) 1(0.5,0.5,0.5,0) 5(0.5,0.5,0.1,0) 8(0.5,0.1,0.5,0) n-1(0.1,0.5,0.5,0)
- channel 1:
- .....
- .....
- channel m-1:
一个动画数据文件头保存有动画名字、关节数、关键帧数以及动画的持续时间;接着分别为每个关节保存关键帧数据,其中平移和旋转关键帧都分开存,平移key的格式为:frame index(translation vector),旋转key的格式为:frame index(quaternion)。一般从动画制作软件导出的动画格式可能跟上面的不一样,但可以自己写导出插件进行导出,或者用其他的模型导入工具如Assimp进行转化。在程序中通过下面的数据结构处理动画:
- // key frame
- struct sSklKeyframe
- {
- float time_; // in seconds
- // for position key, the first 3 values of value_ represent the translation
- // for rotation key, value_ is a quaternion
- VECTOR4D value_;
- };
- // channel (one bone one channel)
- struct sSklChannel
- {
- std::string boneName_;
- std::vector<sSklKeyframe> positionKeys_;
- std::vector<sSklKeyframe> rotationKeys_;
- };
- // animation
- struct sAnimation
- {
- std::string animName_;
- std::vector<sSklChannel> channels_;
- };
从上到下分别表示关键帧,通道(一个关节对应的关键帧的集合),以及动画。有了一个完整的animation关键帧集合,就可以播放该动画了,如果单纯播放关键帧可能会出现动作不平滑的问题,解决方法是帧间平滑插值,见下一节。
4、关键帧之间的平滑插值
假设一个动画有10s,每个整数秒处都设有一个关键帧,那么整数秒之间的骨架状态就通过帧间插值获得;插值的基本思想是:给定一个时间t,找出t处在哪两个关键帧之间,假设为p和q,然后根据p,q处的关节状态和时间t计算出关节在t时间的状态;因为我们每个关节的关键帧是分开存的,因此我们对每个关节也要分开插值,而且对同一个关节的位置和旋转也要分开插值;为了更好的描述之,看下面的伪码:
- void Play(sJoint *skeleton, int numJoints, sAnimation &anim, float time)
- {
- for (int i=0; i<anim.channels_.size(); ++i)
- {
- sSklChannel &channel = anim.channels_[i];
- // find two keys for interpolation
- find two position keys pi,pj,in channel.positionKeys_ contain time;
- find two rotation keys ri,rj,in channel.rotationKeys_ contain time;
- // calculate interp. parameters, pp,rp;
- ..
- // do interpolation
- VECTOR3D pos@time = position_interpolate(pi,pj,pp);
- VECTOR4D rot@time = rotation_interpolate(ri,rj,rp);
- // make a matrix from pos@time and rot@time
- MATRIX4X4 animMatrix = MakeMatrix(pos@time,rot@time);
- // update local matrix of the bone
- int boneId = FindBoneIdByNane(skeleton, numJoints, channel.boneName_);
- sJoint &bone = skeleton[boneId];
- bone.localMatrix_ = animMatrix;
- }
- // bones' local matrices were updated, so we update the global matrix
- // after that, we got the skeleton state at time t
- UpdateGlobalMatrix(skeleton,numJoints);
- }
下面讨论position_interpolate和rotation_interpolate两个插值函数的实现,插值方法有很多;如线性插值,hermite(埃尔米特)插值,还有球面插值;我们对平移选择hermite插值,对旋转采用四元数球面插值,因为线性插值存在过渡不平滑的问题;具体的实现因为篇幅问题就不深入讨论了。蒙皮动画不能少了皮,我们现在只有骨架的动画,下面介绍软件和硬件蒙皮。
5、软件蒙皮的实现
当骨架的绑定pose调整好之后,就可以给它绑上一层“皮”了,皮就是一个三维网格,如一个人,一只动物等。绑定一般是通过建模软件如maya,3dmax来做的,当然现在也有自动化的绑定工具,有兴趣可以看看07年siggraph上的一篇论文:Automatic
rigging and animation of 3d characters。SPE是通过建模工具绑定的,网格被绑之后,网格上的每个顶点都会绑定到一个或者多个(一般不多于4个)对该顶点影响最大的关节上,这些关节的状态变化会按照权重共同影响该顶点的位置变化,总体效果就是皮随着骨架运动。蒙皮的任务就是根据当前骨架状态以及各顶点的绑定信息计算出新的网格顶点坐标。软件蒙皮的伪码如下:
- void DrawSkinnedMesh(cObject &object, sJoint *skeleton)
- {
- for each vertex v in object
- {
- int *boneIds = v.bones;
- float *weights = v.weights;
- VECTOR4D vert = v.pos;
- VECTOR4D norm = v.norm;
- VECTOR4D animedVertex(0,0,0,0),animedNorm(0,0,0,0);
- for each bone boneId in boneIds
- {
- sJoint &joint = skeleton[boneId];
- animedVertex += joint.globalMatrix_*joint.offsetMatrix_*vert*weight;
- animedNorm += joint.globalMatrix_*joint.offsetMatrix_*norm*weight;
- }
- v.animedPos = animedVertex;
- v.animedNorm = animedNorm;
- }
- // draw animated skin here....
- }
offsetMatrix_是sJoint中的新成员,表示在绑定姿势下由建模空间或者世界空间变换到关节空间的矩阵,在动画过程中,它也是固定不变的,如何计算见第2节。
- struct sJoint
- {
- std::string name_;
- int parent_;
- MATRIX4X4 offsetMatrix_; // new added
- MATRIX4X4 localMatrix_;
- MATRIX4X4 globalMatrix_;
- };
6、硬件蒙皮的实现
软件蒙皮工作得很好,但是它在某些情况下比较没有效率,特别是在同一场景中骨骼蒙皮模型很多的时候,例如游戏中一群怪物围着角色攻击,系统既要处理碰撞、AI等其他游戏逻辑又要做动画插值和蒙皮,而蒙皮又是骨骼动画中最耗时的步骤,因此它应该尽可能地被优化。优化最有效的方式就是让GPU处理蒙皮,因为对各顶点的变换是互不相关的,因此完全可以用GPU做并行计算,用顶点着色器(vertex shader)实现,下面是vs的代码:
- #version 140
- varying vec3 viewPos;
- varying vec3 viewNorm;
- ///////////////
- uniform mat4 globalMats[60];
- attribute vec4 weights;
- attribute vec4 bones;
- attribute int numBones;
- ///////////////
- void main()
- {
- vec4 vert4 = gl_Vertex;
- vec4 norm4 = vec4(gl_Normal,0.0);
- ///////////////
- int boneId;
- float weight;
- vec4 pos = vec4(0.0,0.0,0.0,0.0);
- vec4 norm = vec4(0.0,0.0,0.0,0.0);
- for (int i=0; i<numBones; ++i)
- {
- boneId = int(bones.x);
- weight = weights.x;
- mat4 globalMat = globalMats[boneId];
- pos += globalMat*vert4*weight;
- norm += globalMat*norm4*weight;
- bones = vec4(bones.yzw,bones.x);
- weights = vec4(weights.yzw,weights.x);
- }
- pos.w = 1.0;
- norm.w = 0.0;
- ///////////////
- viewNorm = normalize(gl_NormalMatrix * vec3(norm));
- viewPos = vec3(gl_ModelViewMatrix * pos);
- gl_TexCoord[0] = gl_MultiTexCoord0;
- gl_Position = gl_ModelViewProjectionMatrix * pos;
- }
在绘制每个骨骼蒙皮模型时,需要传递一个mat4类型的uniform数组globalMats到shader中,这个数组也被称为matrix pallete,另外每个顶点也关联三个属性变量,bones、weights和numBones,分别表示该顶点绑定到的关节索引、对应的权重以及关节数,接下来的实现就跟软件蒙皮差不多了,记得既要变换顶点坐标也要变换相应的顶点法线,另外这里限定每个关节最多同时绑定到4个关节。
7、结果
图3:Wuson动画,软件蒙皮
图4:跟上图一样的场景,硬件蒙皮,通过帧率可以看到要快5倍
骨骼蒙皮动画(SkinnedMesh Animation)的实现的更多相关文章
- 骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
http://blog.csdn.net/jimoshuicao/article/details/9253999 一)3D模型动画基本原理和分类 3D模型动画的基本原理是让模型中各顶点的位置随时间变化 ...
- 骨骼蒙皮动画(SkinnedMesh)
骨骼蒙皮动画也就是SkinnedMesh,应该是目前用的最多的3D模型动画了.因为他可以解决关节动画的裂缝问题,而且原理简单,效果出色,所以今天详细的谈一下骨骼蒙皮动画的相关知识. 关节动画中使用的是 ...
- [3dmax教程] 人物+骨骼+蒙皮+动画教程
人物+骨骼+蒙皮+动画完整教程选准备好一个人,做人的方法我在这里就不做了,大家可以学都用poser来做一个. 在大腿里建立4根骨骼! 在前视图中移动如图所示位置! 做一段简单的骨骼向前伸的动画,做4 ...
- 骨骼蒙皮动画(Skinned Mesh)的原理解析(二)
http://blog.csdn.net/jimoshuicao/article/details/9283071 2)蒙皮信息和蒙皮过程 2-1)Skin info的定义 上文曾讨论过,Skinned ...
- 骨骼蒙皮动画算法(Linear Blending Skinning)
交互式变形是编辑几何模型的重要手段,目前出现了许多实时.直观的交互式变形方法.本文介绍一种利用线性混合蒙皮(Linear Blending Skinning,LBS)技术来实现网格变形的方法,线性混合 ...
- Unity3d动画脚本 Animation Scripting(深入了解游戏引擎中的动画处理原理)
也许这一篇文章的内容有点枯燥,但我要说的是如果你想深入的了解游戏引擎是如何处理动画片断或者素材并 让玩家操控的角色动起来栩栩如生,那么这真是一篇好文章(当然我仅仅是翻译了一下) 动画脚本 Anim ...
- android 补间动画和Animation
介绍: 补间动画是一种设定动画开始状态.结束状态,其中间的变化由系统计算补充.这也是他叫做补间动画的原因. 补间动画由Animation类来实现具体效果,包括平移(TranslateAnimation ...
- CSS3动画属性animation的用法
转载: 赞生博客 高端订制web开发工作组 » CSS3动画属性animation的用法 CSS3提供了一个令人心动的动画属性:animation,尽管利用animation做出来的动画没有flash ...
- Android动画主要包含补间动画(Tween)View Animation、帧动画(Frame)Drawable Animation、以及属性动画Property Animation
程序运行效果图: Android动画主要包含补间动画(Tween)View Animation.帧动画(Frame)Drawable Animation.以及属性动画Property Animatio ...
随机推荐
- js如何获取手机的屏幕尺寸
var width = $(document.body).outerWidth();//手机的屏幕宽 var height = $(window).innerHeight(); //手机的屏幕高
- Ahead-of-time compilation
https://en.wikipedia.org/wiki/Ahead-of-time_compilation
- [P2769] 猴子上树
题目描述 在猴村有一条笔直的山路,这条山路很窄,宽度忽略不计.有 n只猴子正站在山路上静静地观望今天来参加比赛的各位同学.用一个正整数Xi表示第i只猴子所站位置,任意两只猴子的所站位置互不相同.在这条 ...
- HDU 2444 The Accomodation of Students (二分图最大匹配+二分图染色)
[题目链接]:pid=2444">click here~~ [题目大意]: 给出N个人和M对关系,表示a和b认识,把N个人分成两组,同组间随意俩人互不认识.若不能分成两组输出No,否则 ...
- Windows10搭建FTP服务器
配置FTP服务器步骤: 第一步: 打开控制面板--->选择程序--->启动或关闭Windows功能--->勾选FTP服务器等.如下图: 第二步: 右键此电脑--->点击管理-- ...
- 设置port转发来訪问Virtualbox里linux中的站点
上一篇中我们讲到怎么设置virtuabox来通过SSH登录机器. 相同.我们也能够依照上一篇内容中的介绍,设置port转发,来訪问虚拟linux系统已经搭建的站点: 1.设置port转发: water ...
- 20170314 OO ALV 出现双滚动条
1.出现双进度条,用户改变屏幕大小操作出现问题: 解决方法: [园童]BJ-ABAP-可乐(708925365) 16:08:55240 * 200改为240 200,然后将滚动条的步进改为1即可 ...
- java 传参方式--值传递还是引用传递
java 传参方式--值传递还是引用传递 参数是按值而不是按引用传递的说明 Java 应用程序有且仅有的一种参数传递机制,即按值传递.写它是为了揭穿普遍存在的一种神话,即认为 Java 应用程序按引用 ...
- 解密阿里云Redis助力双十一背后的技术
摘要: Redis是一个使用范围很广的NOSQL数据库,阿里云Redis同时在公有云和阿里集团内部进行服务,本文介绍了阿里云Redis双11的一些业务场景:微淘社区之亿级关系链存储.天猫直播之评论商品 ...
- 对于pod导入第三方库文件终端语言记录
//换成 pod install --verbose --no-repo-update //生成Podfile文件 touch Podfile 加上--verbose --no-repo-update ...