骨骼动画原理与Cocos2d骨骼动画

QuickSDK | 2018-11-13 | 技术干货
蒙皮骨骼动画
蒙皮骨骼动画可以分为两部分,蒙皮Skinned Mesh和骨骼Bone。蒙皮指的是所有的点,骨骼可以控制点的位置,从而形成不同的“姿势”。如下图,图片是使用MeshViewer查看orc.c3b:




 
顶点与骨骼
顶点会跟随骨骼的移动而移动,对于手臂上的顶点会随手臂骨骼的移动而移动,对于肩膀处的顶点会随肩部骨骼的移动而移动,但对于肩膀手臂连接出的顶点会同时受到肩部骨骼和手臂骨骼的影响。所以顶点是可以同时受到多个骨骼影响时,这种情况下骨骼对顶点的影响会有一个权重比例,骨骼变化后,对点的影响还需要乘以影响的权重。

在*.c3t 类型3D模型文件如果有骨骼,顶点属性中会有VERTEX_ATTRIB_BLEND_WEIGHT和VERTEX_ATTRIB_BLEND_INDEX属性。VERTEX_ATTRIB_BLEND_INDEX指的是影响顶点骨骼的索引(骨骼会以数组形式存放),VERTEX_ATTRIB_BLEND_WEIGHT指的是骨骼的影响顶点权重。这两个属性size相等,一般都是4,也就是一个顶点最多同时收到4根骨骼影响,如下图:


 

骨骼与骨架
骨架Skeleton
以下是一副骨架


红点是两个骨骼的连接点,也就是关节点。骨骼有父子关系,一般有一个根骨骼然后其下面有子骨骼,子骨骼往下还有子骨骼的子骨骼,把骨骼按照父子关系连接起来就是一副骨架了。

骨骼的父子关系作用是当父骨骼变化时其所有子骨骼都会跟着有相同的变化。如下图:


这个类似于当手臂抬起时,整个手会抬起,和Cocos2d中场景树时一样的,当父节点移动时子节点都会跟着移动。

 

骨骼Bone
从骨架图上看整个骨架看上去和人的骨架很像,有手有脚有身体,很容易让我们联想其生物骨骼。骨架中画的每根骨头是为了更好理解骨骼而画的,实际的骨架只是一个骨骼的父子关系,实际的骨骼只是矩阵。在3D空间中,物体的移动缩放旋转都是可以使用矩阵进行变换,矩阵变换实际上是对模型中的各个点进行变换。骨骼的作用是控制模型中点的位置,使得模型有各种各样的姿势,这个功能完全可以用矩阵来实现,骨骼也只需要用矩阵来表示。

 
模型坐标系和骨骼坐标系
矩阵对顶点进行变换时,是对顶点坐标所在坐标系进行变换的。对于3D模型来说模型有一个全局坐标系,模型中顶点数据坐标都是在这个坐标系下的。因为骨骼需要控制点,对点进行变换,骨骼也有需要自己的坐标系。对于前面的骨架图,其中的红色的点实际上是骨骼坐标系的原点,绿色的线(看到的骨骼)是按照骨骼父子关系对骨骼原点进行连接。下图展示了模型的全局坐标系与骨骼坐标系:


 
骨骼与顶点变换
首先看一下下图,对于的代码SkeletonScene.cpp/SkeletonScene.h,对应程序菜单“Test Draw 3D”->“Skeleton Draw”:


当没有使用骨骼绘制模型时,顶点位置是没有使用骨骼矩阵进行变换的,坐标在模型的全局坐标下。实际绘制的时候不管用不用骨骼绘制,最终绘制使用的的顶点坐标都是模型全局坐标系下的坐标。因为要得到点在全局坐标系下坐标,所以骨骼一定要提供从骨骼坐标系到全局坐标的变换矩阵。前面骨架中说过,骨骼有父子关系,父骨骼改动会影响子骨骼。这个实现的方式是,子骨骼提供一个矩阵,该矩阵可以将子骨骼坐标系下坐标转到父骨骼,父骨骼提供继续往上转换的矩阵,最终根骨骼提供转换到全局坐标系的转换矩阵。这个矩阵也是用来控制点在模型不同“姿势”下的位置,变换这个矩阵可以得到不同的“姿势”。

对于模型的顶点数据,顶点坐标都是模型全局坐标系下的坐标,而我们骨骼在对顶点进行变换时要用顶点在骨骼坐标系下的坐标,所以骨骼还需要提供一个矩阵,这个矩阵可以将全局坐标系下的坐标转换到骨骼坐标系的坐标。

所以骨骼需要两个矩阵才能确定最终顶点坐标:一是顶点全局坐标系下到骨骼坐标系下的变换矩阵,二是骨骼坐标系到父骨骼坐标系下变换矩阵,对于根骨骼是根骨骼坐标系到全局坐标系下变换矩阵。对于模型中顶点数据绘制时不会改变,顶点坐标从全局坐标系到骨骼坐标系下的变换矩阵也不会改变,只有第二个子骨骼坐标系到父骨骼坐标系变换矩阵是可变的,通过变换这个矩形来实现模型的各种姿势。

使用骨骼绘制3D模型
以下是模型orc.c3t(json格式文本文件)的骨骼和骨架数据:



 

骨骼数据加载后会存放在NodeDatas中,使用Skeleton3D::create(nodeDatas.skeleton)可以创建模型的骨架,使用Skeleton3D对象和模块中的骨骼数据可创建最终MeshSkin,MeshSkin可以创建绘制时候要使用的骨骼矩阵。MeshSkin类数据结构如下:


 

MeshSkin::_skinBones和MeshSkin::_skeleton. _bones都是Bone3D*指针数组,指向相同的对象,不过MeshSkin::_skinBones是按照模型中骨骼数组顺序排列的。

 

由于多个矩阵对点的变换可以让矩阵相乘合并成一个变换矩阵,然后对点变换,所以实际绘制时,在Shader中每根骨骼只需要一个变换矩阵就可以了。模块中所有骨骼会以uniform数组形式传递给shader,如果使用了骨骼,shder中由如下定义:

const int SKINNING_JOINT_COUNT = 60;//骨骼矩阵的最大个数

uniform vec4 u_matrixPalette[SKINNING_JOINT_COUNT * 3];//使用3个vec4表示一个骨骼矩阵

这里使用的时mat4*4的矩阵,原本需要4个vec4才表示mat4*4,但矩阵第四行是齐次空间的变换,因为骨骼不涉及齐次空间,所以省去了。

 

u_matrixPalette的更新
programState->setUniformVec4v("u_matrixPalette", (GLsizei)_skin->getMatrixPaletteSize(), _skin->getMatrixPalette())

 

_skin->getMatrixPalette

Vec4* MeshSkin::getMatrixPalette()

{

    if (_matrixPalette == nullptr)

    {

        _matrixPalette = new (std::nothrow) Vec4[_skinBones.size() * PALETTE_ROWS];

    }

    int i = 0, paletteIndex = 0;

    static Mat4 t;

    for (auto it : _skinBones )

{

    //先用invBindPoses转换到骨骼坐标系下,再转换到全局坐标系下

        Mat4::multiply(it->getWorldMat(), _invBindPoses[i++], &t);

        _matrixPalette[paletteIndex++].set(t.m[0], t.m[4], t.m[8], t.m[12]);

        _matrixPalette[paletteIndex++].set(t.m[1], t.m[5], t.m[9], t.m[13]);

        _matrixPalette[paletteIndex++].set(t.m[2], t.m[6], t.m[10], t.m[14]);

    }

    return _matrixPalette;

}

 

it->getWorldMat:

const Mat4& Bone3D::getWorldMat()

{

    if (_worldDirty)

    {

        updateLocalMat();

        if (_parent)

        {

             //先转换到父坐标系下,在继续“往上转”

            _world = _parent->getWorldMat() * _local;

        }

        else

            _world = _local;

        _worldDirty = false;

    }

       return _world;

}

完整例子:SkeletonScene.cpp/SkeletonScene.h,对应程序菜单“Test Draw 3D”->“Skeleton Draw”。

动画
动画主要通过改变子骨骼坐标系到父骨骼坐标系矩阵来实现。在3D模型文件中会记录各个骨骼每一帧的矩阵,以下是orc.c3t(json格式文本文件)模型文件中动画





 

骨骼动画实际也就是根据时间进行切换子骨骼坐标系到父骨骼坐标系的转换矩阵,实际可能会使用插值(线性插值,平方插值等待),使得动画更平滑。Cocos2d中代码结构如下:



 

例子
以下例子从动画中抽取了十帧进行显示

完整代码:AnimationScene.cpp/AnimationScene.h,对应程序菜单“Test Draw 3D”->“Skeleton Animation”。



 

附件AttachNode
有这样一种情况,游戏中一个角色可拾取装备,拾取后要拿在手上,当角色手运动时,装备也需要跟着运动,如下图



在带骨骼的模型中这这种需要求很容易实现,我们只需要将装备的在手臂的骨骼坐标系下绘制就行,当手臂运动,实际上也就是手臂骨骼坐标系变换,这会带动坐标系下的装备运动。
--------------------- 
作者:KyleWlk 
原文:https://blog.csdn.net/wlk1229/article/details/83962923 

商务合作

李先生:13880511661

市场合作

赵先生:15390049857

技术支持

彭女士:18202818615

官方技术交流群

QQ群:608554925