从Sprite3D理解3D骨骼动画原理

来源:互联网 发布:海岛旅游小镇数据 编辑:程序博客网 时间:2024/06/11 06:55

为了能够更好的使用cocos为我们提供的Sprite3D,我和大家分享一下Sprite3D中关于骨骼动画原理的部分,本文使用cocos2d-x 3.2版本,这是cocos首次出现3D骨骼动画的版本,相对与本文写出来时候最新的3.5版本,由于没有其他比如灯光等功能,3D骨骼动画模块读起来要更加的清晰。如果文章有纰漏或者错误的地方,也请大家指教。 目前引擎支持3种动画格式,分别是.obj,.c3b,.c3t,由于.obj没有骨骼,.c3b是二进制,而.c3t是json格式,所以本文就用官方test中自带的orc.c3t的创建以及渲染作为例子。

第一步:读取文件,得到数个struct,以下说明是对这些struct的说明:

复制代码
// D:\phoneclient\cocos2dxlib\cocos\3d\CCBundle3DData.h/**mesh vertex attribute*/// 顶点属性(位置属性、颜色属性、纹理属性等)struct MeshVertexAttrib{    //attribute size    // 描述该属性所需要的元素个数,比如描述一个Vec3的位置信息,需要x,y,z这么3个变量    GLint size;    //GL_FLOAT    // 元素的类型,如GL_FLOAT    GLenum type;    //VERTEX_ATTRIB_POSITION,VERTEX_ATTRIB_COLOR,VERTEX_ATTRIB_TEX_COORD,VERTEX_ATTRIB_NORMAL, VERTEX_ATTRIB_BLEND_WEIGHT, VERTEX_ATTRIB_BLEND_INDEX, GLProgram for detail    // 描述改属性类型的值,使用的值是GLProgram类的枚举,如GLProgram::VERTEX_ATTRIB_POSITION    int  vertexAttrib;    //size in bytes    // 存储该属性所需要的字节数,等于size * sizeof(type)    int attribSizeBytes;};/**mesh data*/// 模型蒙皮数据struct MeshData{    // 每个顶点拥有的属性类型数目    int attribCount;    // Vertex数组元素个数    int vertexSizeInFloat;    // indices数组元素个数    int numIndex;    // 每个顶点的属性信息    std::vector<MeshVertexAttrib> attribs;    std::vector<float> vertex;    std::vector<unsigned short> indices;};
复制代码

注释:拿orc.c3t来举例说明一下,得到的attribs数组分别是代表:
1、顶点在模型坐标系下的位置信息(VERTEX_ATTRIB_POSITION)
2、顶点的法线(VERTEX_ATTRIB_NORMAL)
3、顶点的纹理坐标(VERTEX_ATTRIB_TEX_COOD)
4、作用于该顶点的某骨骼,对该顶点最终位置的权重(VERTEX_ATTRIB_BLEND_WEIGHT)
5、影响该顶点的骨骼在骨骼数组中的索引(VERTEX_ATTRIB_BLEND_INDEX)
所以一个顶点共计要16个float变量来表示,所以vertex数组中的顶点数目就是数组大小除以16,而每个面需要3个顶点,所以面的数目我们也能确定了。至于indices数组,就是这些个顶点的索引了。

复制代码
/**skin data*/// 模型骨骼信息struct SkinData{    // 影响到模型蒙皮的骨骼名字数组,称之为skinBone的数组    std::vector<std::string> skinBoneNames; //skin bones affect skin    // 未影响到模型蒙皮的骨骼名字数组,称之为nodeBone的数组    std::vector<std::string> nodeBoneNames; //node bones don't affect skin, all bones [skinBone, nodeBone]    // 从对应的skinBone坐标系到模型坐标系变换的逆变换,可实现将该骨骼影响的蒙皮顶点从模型坐标系的坐标,转换至该骨骼坐标系的坐标    std::vector<Mat4>        inverseBindPoseMatrices; //bind pose of skin bone, only for skin bone    // skinBone到其父骨骼坐标系的初始矩阵    std::vector<Mat4>        skinBoneOriginMatrices; // original bone transform, for skin bone    // nodeBone到其父骨骼坐标系的初始矩阵    std::vector<Mat4>        nodeBoneOriginMatrices; // original bone transform, for node bone        //bone child info, both skinbone and node bone    // 所有骨骼与其子骨骼索引的map,值得说明的是这个索引是对skinBone和nodeBone两个数组而言的    std::map<int, std::vector<int> > boneChild;//key parent, value child    // 根骨骼索引,同样是相对两个数组而言    int                              rootBoneIndex;}/**material data*/// 材质数据struct MaterialData{    // 3.2 版本的材质数据结构很简单,只有纹理信息    std::string texturePath;};
复制代码

第二步:根据以上解析出来的数据结构,在Sprite3D中创建Mesh类对象和MeshSkin类对象,下面说明一下这两个类的作用:

复制代码
// D:\phoneclient\cocos2dxlib\cocos\3d\CCMesh.h/**  * Mesh: Geometry with a collection of vertex.  * Supporting various vertex formats. */class Mesh : public Ref{public:    /**build buffer*/    // 将MeshData中的vertex和indices数组传给GPU并得到对应的_vertexBuffer和_indexBuffer    void buildBuffer();protected:    // 顶点数据缓冲    GLuint _vertexBuffer;    // 顶点索引缓冲    GLuint _indexBuffer;    // 复制了MeshData中的数据    RenderMeshData _renderdata;};// D:\phoneclient\cocos2dxlib\cocos\3d\CCMeshSkin.h/** * Defines a basic hierachial structure of transformation spaces. */class Bone3D : public Ref{protected:    // 复制对应SkinData::inverseBindPoseMatrices而来,只对skinBone有意义    Mat4 _invBindPose;    // 复制对应SkinData::skinBoneOriginMatrices 或 Skin::nodeBoneOriginMatrices而来    Mat4 _oriPose; //original bone pose    // 其父骨骼指针    Bone3D* _parent; //parent bone    // 子骨骼指针    Vector<Bone3D*> _children;    // 到模型空间的变换矩阵    Mat4          _world;    // 到父骨骼坐标系的变换矩阵    Mat4          _local;    };/** * MeshSkin, A class maintain a collection of bones that affect Mesh vertex. * And it is responsible for computing matrix palletes that used by skin mesh rendering. */class MeshSkin: public Ref{protected:    // 由SkinData::skinBoneNames, inverseBindPoseMatrices, skinBoneOriginMatrices这些数据创建出相应Bone3D,得到该数组    Vector<Bone3D*> _skinBones; // bones with skin    // 由SkinData::skinBoneNames, nodeBoneOriginMatrices这些数据创建出相应Bone3D,得到该数组    Vector<Bone3D*> _nodeBones; //bones without skin, only used to compute transform of children    // 根骨骼    Bone3D* _rootBone;        // Pointer to the array of palette matrices.    // This array is passed to the vertex shader as a uniform.    // Each 4x3 row-wise matrix is represented as 3 Vec4's.    // The number of Vec4's is (_skinBones.size() * 3).    Vec4* _matrixPalette;};
复制代码

注释:这里说一下_matrixPalette这个成员变量。在Mesh::buildBuffer()后,已经将所有的顶点传递给了GPU,所以接下来的问题就是如何将这些顶点坐标,由其初始状态的模型坐标系下的坐标,跟随对应骨骼,变换至当前模型坐标系下的坐标。而这些变换的矩阵,就是使用_matrixPalette来传递的。引擎使用3个Vec4来表示这样一个矩阵,而_matrixPalette就表示了所有_skinBones的这个变换矩阵。而具体每个skinBone的这个4x3矩阵的计算则在MeshSkin::updateJointMatrix函数中:

复制代码
// D:\phoneclient\cocos2dxlib\cocos\3d\CCMeshSkin.cppvoid Bone3D::updateJointMatrix(Vec4* matrixPalette){    {        static Mat4 t;        // 得到我们需要的矩阵,但这是4*4 的。        Mat4::multiply(_world, getInverseBindPose(), &t);        // 将矩阵最后一行去掉,得到4*3的Vec4向量数组        matrixPalette[0].set(t.m[0], t.m[4], t.m[8], t.m[12]);        matrixPalette[1].set(t.m[1], t.m[5], t.m[9], t.m[13]);        matrixPalette[2].set(t.m[2], t.m[6], t.m[10], t.m[14]);    }}
复制代码

第三步:构建渲染指令,传递相应数据至GPU进行绘制:
我们来分析一下对应的ccShader_3D_PositionTex.vert文件,看看经过这些步骤以后,shader是如何使用这些数据的:

复制代码
const char* cc3D_PositionTex_vert = STRINGIFY(attribute vec4 a_position;attribute vec2 a_texCoord;varying vec2 TextureCoordOut;void main(void){    gl_Position = CC_MVPMatrix * a_position;    TextureCoordOut = a_texCoord;    TextureCoordOut.y = 1.0 - TextureCoordOut.y;});const char* cc3D_SkinPositionTex_vert = STRINGIFY(attribute vec3 a_position;attribute vec4 a_blendWeight;attribute vec4 a_blendIndex;attribute vec2 a_texCoord;const int SKINNING_JOINT_COUNT = 60;// Uniformsuniform vec4 u_matrixPalette[SKINNING_JOINT_COUNT * 3];// Varyingsvarying vec2 TextureCoordOut;vec4 getPosition(){       // 对该顶点产生作用的第一个块骨骼,所占的权重    float blendWeight = a_blendWeight[0];    int matrixIndex = int (a_blendIndex[0]) * 3;    // 对传递进来的matriPalette矩阵乘以该骨骼的权重    vec4 matrixPalette1 = u_matrixPalette[matrixIndex] * blendWeight;    vec4 matrixPalette2 = u_matrixPalette[matrixIndex + 1] * blendWeight;    vec4 matrixPalette3 = u_matrixPalette[matrixIndex + 2] * blendWeight;            blendWeight = a_blendWeight[1];    if (blendWeight > 0.0)    {        // 若还有别的骨骼对该顶点产生影响,则进行混合        matrixIndex = int(a_blendIndex[1]) * 3;        matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;        matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;        matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;    }            blendWeight = a_blendWeight[2];    if (blendWeight > 0.0)    {        matrixIndex = int(a_blendIndex[2]) * 3;        matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;        matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;        matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;    }            blendWeight = a_blendWeight[3];    if (blendWeight > 0.0)    {        matrixIndex = int(a_blendIndex[3]) * 3;        matrixPalette1 += u_matrixPalette[matrixIndex] * blendWeight;        matrixPalette2 += u_matrixPalette[matrixIndex + 1] * blendWeight;        matrixPalette3 += u_matrixPalette[matrixIndex + 2] * blendWeight;    }        vec4 _skinnedPosition;    vec4 postion = vec4(a_position, 1.0);    // 使用这个混合后的矩阵,对顶点进行变换,得到该顶点在模型坐标系下的坐标    _skinnedPosition.x = dot(postion, matrixPalette1);    _skinnedPosition.y = dot(postion, matrixPalette2);    _skinnedPosition.z = dot(postion, matrixPalette3);    _skinnedPosition.w = postion.w;        return _skinnedPosition;}void main(){    // 得到顶点在模型坐标系下的坐标    vec4 position = getPosition();    // 使用MVP矩阵进行变换得到最终坐标    gl_Position = CC_MVPMatrix * position;        TextureCoordOut = a_texCoord;    TextureCoordOut.y = 1.0 - TextureCoordOut.y;});
复制代码

总结:本文忽略了很多细节,只重点讲解了三个关于骨骼动画原理的部分,希望能对关于这部分有兴趣的朋友有所帮助,也特别希望有前辈能指出问题,或者进行补充。下次有时间我会加上animation部分,谢谢。

【上文转自】http://www.cocoachina.com/bbs/read.php?tid-295864-page-1.html

http://www.cocoachina.com 全球最大苹果开发者中文社区

-----------------------------------------------------------------------------------------------------

cocos2d-x 开发者文档

【下文转自】http://cn.cocos2d-x.org/article/index?type=cocos2d-x&url=/doc/cocos-docs-master/manual/framework/native/v3/spine/zh.md

骨骼动画详解-Spine

游戏中人物的走动,跑动,攻击等动作是必不可少,实现它们的方法一般采用帧动画或者骨骼动画。

帧动画与骨骼动画的区别在于:帧动画的每一帧都是角色特定姿势的一个快照,动画的流畅性和平滑效果都取决于帧数的多少。而骨骼动画则是把角色的各部分身体部件图片绑定到一根根互相作用连接的“骨头”上,通过控制这些骨骼的位置、旋转方向和放大缩小而生成的动画。

它们需要的图片资源各不相同,如下分别是帧动画和骨骼动画所需的资源图:   

骨骼动画比传统的逐帧动画要求更高的处理器性能,但同时它也具有更多的优势,比如:

  • 更少的美术资源: 骨骼动画的资源是一块块小的角色部件(比如:头、手、胳膊、腰等等),美术再也不用提供每一帧完整的图片了,这无疑节省了资源大小,能为您节省出更多的人力物力更好的投入到游戏开发中去。
  • 更小的体积: 帧动画需要提供每一帧图片。而骨骼动画只需要少量的图片资源,并把骨骼的动画数据保存在一个 json 文件里面(后文会提到),它所占用的空间非常小,并能为你的游戏提供独一无二的动画。
  • 更好的流畅性: 骨骼动画使用差值算法计算中间帧,这能让你的动画总是保持流畅的效果。
  • 装备附件: 图片绑定在骨骼上来实现动画。如果你需要可以方便的更换角色的装备满足不同的需求。甚至改变角色的样貌来达到动画重用的效果。
  • 不同动画可混合使用: 不同的骨骼动画可以被结合到一起。比如一个角色可以转动头部、射击并且同时也在走路。
  • 程序动画: 可以通过代码控制骨骼,比如可以实现跟随鼠标的射击,注视敌人,或者上坡时的身体前倾等效果。

骨骼动画编辑器——Spine

Spine是一款针对游戏的2D骨骼动画编辑工具,它具有良好的UI设计和完整的功能,是一个比较成熟的骨骼动画编辑器。Spine旨在提供更高效和简洁的工作流程,以创建游戏所需的动画。

使用Spine创建骨骼动画分两大步骤:

  1. 在SETUP模式下,组装角色部件,为其绑定骨骼;
  2. 在ANIMATE模式下,基于绑定好的骨骼创建动画。

下面简单介绍下具体步骤,更多详细内容请查看官方网站教程:Spine快速入门教程。

1)在SETUP模式下,选中Images属性,导入所需图片资源所在文件夹,其中路径名和资源名中不能出现中文,否则解析不了; 

2)拖动Images下的图片到场景,对角色进行组装(把各个身体部位拼在一起),可通过Draw Order属性调整图片所在层的顺序; 

3)创建骨骼,并绑定图片到骨骼上,要注意各骨骼的父子关系。 

4)切换到ANIMATE模式,选中要“动”的骨骼,对其进行旋转、移动、缩放等操作,每次改动后要记得打关键帧。

5)在菜单栏找到Texture Packer项,对角色纹理进行打包,资源文件后缀为atlas(而非Cocos2d-x常用的plist)。打包后将生成两个文件,即:png 和 atlas。

   

6)导出动画文件Json。

Spine动画的使用

Cocos2d-x程序中,使用Spine动画首先需要包含spine的相关头文件。

1
2
3
#include <spine/spine-cocos2dx.h>
#include "spine/spine.h"
usingnamespace spine;

其常用方法如下:

创建一个Spine动画对象,将动画文件和资源文件导入。

1
auto skeletonNode = newSkeletonAnimation("enemy.json","enemy.atlas");

骨骼动画往往是不止一个动画的,例如:当人物需要行走时,就设置播放动画为行走;当要发动攻击时,就设置播放动画为攻击。下面方法可以设置当前播放动画,其中参数false表示不循环播放,true表示循环播放。

1
skeletonNode->setAnimation(0,"walk",true);

setAnimation方法只能播放一种动画,所以当要连续播放不同的动画时,需要使用addAnimation方法来实现,它可以一条一条的播放不同的动画。

1
2
skeletonNode->addAnimation(0,"walk",true);
skeletonNode->addAnimation(0,"attack",false);

对于一般情况下,动画的切换要求两个动画完全能衔接上,不然会出现跳跃感,这个对于美术来说要求很高,而Spine加了个动画混合的功能来解决这个问题。使得不要求两个动画能完全的衔接上,比如上面的walk和attack动画, 就是衔接不上的,直接按上面的办法播放,会出现跳跃,但是加了混合后,看起来就很自然了。哪怕放慢10倍速度观察,也完美无缺。这个功能在序列帧动画时是无法实现的,也是最体现Spine价值的一个功能。

1
2
skeletonNode->setMix("walk","attack", 0.2f);
skeletonNode->setMix("attack","walk", 0.4f);

设置动画的播放快慢可通过设置它的timeScale值来实现。

1
skeletonNode->timeScale = 0.6f;

设置是否显示骨骼通过设置debugBones,true表示显示,false表示隐藏。

1
skeletonNode->debugBones = true;

例子:创建一个player行走和攻击的动画, 并且循环播放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto skeletonNode = newSkeletonAnimation("enemy.json","enemy.atlas");
skeletonNode->setMix("walk","attack", 0.2f);
skeletonNode->setMix("attack","walk", 0.4f);
 
skeletonNode->setAnimation(0,"walk",false);
skeletonNode->setAnimation(0,"attact",false);
skeletonNode->addAnimation(0,"walk",false);
skeletonNode->addAnimation(0,"attact",true);
 
skeletonNode->debugBones = true;
 
Size windowSize = Director::getInstance()->getWinSize();
skeletonNode->setPosition(Point(windowSize.width / 2, windowSize.height / 2));
addChild(skeletonNode);
效果图: 
0 0