骨骼蒙皮动画的一个简单版本实现(转载)

转载:https://www.cnblogs.com/tupx/articles/3006051.html

一、基本原理

骨骼动画的基本原理就是首先控制各个骨骼和关节,再使附在上面的蒙皮(Mesh)与其相匹配。

一个角色由作为皮肤的单一网格模型和按照一定层次组织起来的骨骼组成。

骨骼层次描述了角色的结构:相邻的骨骼通过关节连接,并且可以做出相对的运动。这里要注意的是,骨骼间是具有“父子”关系的,比如,右前臂是右上臂的子节点,同时又是右手的父节点。通过改变相邻骨骼间的位移、夹角,就可以做出不同的动作,实现不同的动画效果。

皮肤作为一个网格蒙在骨骼之上,规定角色的外观。这里的皮肤不死固定不变的刚性mesh,而是可以在骨骼影响下变化的一个可变性mesh。组成皮肤的每一个顶点都会受到一个或多个骨骼的影响(就人体来说,一个网格顶点最多受到4块骨骼的影响),不同的骨骼按照与顶点的几何、物理关系确定对该顶点的影响权重,通过计算影响该顶点的不同骨骼加权值之和就可以得到该顶点在世界坐标系中的正确位置。

动画文件中的关键帧一般保存着骨骼的位置、朝向等信息。通过在动画序列中相邻的两个关键帧之间插值可以确定某一时刻各个骨骼的新位置和新朝向(或者不插值,直接从一个关键帧变到另一个关键帧,作为简单版本实现,本例中就没有插值),这样就可以计算出一个骨骼变换矩阵。然后皮肤网格的每个顶点根据影响它的骨骼及相应的权重计算它们的加权和,就可以计算出这个顶点在世界坐标系(强调)中的新位置。如此,便实现了在骨骼驱动下的单一皮肤网格变形动画(这就是简单版本的骨骼蒙皮动画)。

二、实现

下面主要讨论技术细节。

在一个典型的骨骼蒙皮动画模型中,会保存如下信息:网格信息、骨骼信息和动画信息。

网格信息是角色的多边形模型。该多边形模型一般由三角形面片组成,每个三角形面片有三个指向模型的顶点表的索引(通过该索引,可以确定这个三角形的三个定点的坐标)。顶点表中的每一顶点除了带有位置、法向量、材质、纹理等基本信息外,还会指出有哪些骨骼影响了该顶点,影响权重又是多少。

骨骼信息包括全部去骨骼的数量和每个骨骼的具体信息。每一根骨骼包括该骨骼在父骨骼坐标系中的变换矩阵(可能为了方便会计算2个变换矩阵:绝对变换矩阵和相对变换矩阵),通过该变换矩阵可以在确定父骨骼位置的前提下,确定子骨骼的位置。另外,为了方便,常常计算出每根骨骼相对于世界坐标系的“显示变换矩阵”(它其实就是通过父骨骼的显示矩阵乘以子骨骼相对于父骨骼的平移+旋转变换矩阵得到的)总之,每一个关键帧指出了每一个骨骼在该时刻相对于父骨骼的变换矩阵或者说该骨骼相对于父骨骼的位移、旋转等操作。

关键的计算公式:

1. 子骨骼的位置=父骨骼的位置子骨骼相对于父骨骼的平移矩阵子骨骼相对于父骨骼的旋转矩阵

2. mesh顶点的位置=最初状态顶点的位置初始状态骨骼逆变换骨骼本地坐标系下的新变换

(有必要解释一下,“初始状态骨骼你变换”其实就是“世界坐标系到骨骼本地坐标系”的变换矩阵,之所以叫“逆矩阵”,是因为默认情况下,我们认为“骨骼本地坐标到世界坐标系”的变换为“正”–最常用的意思,更多解释见http://www.cnblogs.com/neoragex2002/archive/2007/09/13/Bone_Animation.html)

本例中的3个矩阵含义:

“显示变换矩阵”可以直接从世界坐标系变换到特定骨骼的本地坐标系(世界坐标系的标准矩阵乘以显示矩阵);“绝对变换矩阵”是相对于世界坐标系的原点而言的,不包含子骨骼相对于父骨骼的平移;“相对变换矩阵”是子骨骼相对于父骨骼的本地坐标系而言的,同样不包含平移,只含旋转操作。定义他们是有目的的:“绝对变换矩阵”是mesh顶点需要的,“相对变换矩阵”则是“绝对变换矩阵”需要的。

三、设计

一个模型类(BoneModel)包含一个骨骼类(Bone)和一个网格定点类(MeshVertex)。
  
骨骼类的属性:

/*骨骼类*/
class Bone
{
public:
    Bone();
    /*设置骨骼数据*/
    void SetBone(int parent,float length);
    void SetBone(int parent,float length,M3DMatrix44f rela,M3DMatrix44f abso);
 
    unsigned int m_parent; /*父节点索引*/
    float m_length;        /*骨骼长度*/
    /*变换矩阵:绝对变换矩阵和相对变换矩阵*/
    M3DMatrix44f m_abso;
    M3DMatrix44f m_rela;
};

网格顶点类的属性:

/*蒙皮顶点类*/
class MeshVertex
{
public:
    MeshVertex();
    /*设置蒙皮顶点数据*/
    void SetMeshVertex(M3DVector3f pos,M3DVector3f normal,int b1,int b2,int b3,int b4,
        float w1,float w2,float w3,float w4,float red,float green,float blue,float alpha,int nNumBone);
 
    M3DVector3f m_pos;           /*顶点位置*/
    M3DVector3f m_normal;        /*法线坐标*/
    int         m_arrBoneIdx[4]; /*影响顶点的骨骼索引,做多4个*/
    float       m_arrWeight[4];  /*各骨骼影响顶点的权重*/
    int         m_nNumBone;      /*实际影响顶点的骨骼数目*/
    float m_red;
    float m_green;         /*顶点颜色*/
    float m_blue;
    float m_alpha;
};

骨骼模型类的属性:

/*定义一些宏*/
#define MAX_BONES 2
#define MAX_MESHES 3
#define MAX_VERTICES_PER_MESH 4
 
class BoneModel
{
public:
    BoneModel();
 
    /*自定义初始化骨骼和蒙皮顶点数据*/
    void InitData();
 
    Bone         m_bones[MAX_BONES];            /*所有的骨骼索引*/  
    M3DMatrix44f m_DispMat[MAX_BONES];  /*各个骨骼的显示矩阵*/
    MeshVertex   m_ModelPoints[MAX_MESHES*MAX_VERTICES_PER_MESH];
};

更新骨骼位置的函数:

/*更新所有的骨骼变换矩阵*/
void UpdateBones()
{
    /*用于保存平移和旋转矩阵*/
    M3DMatrix44f XRotMat,zRotMat,InvTransMat;
 
    /*循环更新骨骼*/
    for(int i=0;i<MAX_BONES;++i)
    {
        //检查是否根骨骼
        if(pObjModel->m_bones[i].m_parent == -1)
        {
            /*初始化根骨骼相对 绝对 显示矩阵*/
            m3dTranslationMatrix44(pObjModel->m_bones[i].m_rela,0.0f,0.0f,1.0f);//根骨骼的位置在DrawModel()中指定
            m3dCopyMatrix44(pObjModel->m_bones[i].m_abso,pObjModel->m_bones[i].m_rela);
            m3dCopyMatrix44(pObjModel->m_DispMat[i],pObjModel->m_bones[i].m_rela);
        }
        else
        {
            //在父骨骼的基础上平移,这要求在显示的时候父子骨骼之间不能弹堆栈
            //移动到父骨骼的位置
            m3dTranslationMatrix44(pObjModel->m_bones[i].m_rela,0.0f,pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_length,0.0f);
            /*更新旋转矩阵*/
            m3dRotationMatrix44(XRotMat,m3dDegToRad(xRot),1.0f,0.0f,0.0f);
            m3dRotationMatrix44(zRotMat,m3dDegToRad(zRot),0.0f,0.0f,1.0f);
            /*保存相对矩阵的逆矩阵(此时的相对矩阵(平移矩阵)只包含平移操作,无旋转操作)*/
            m3dInvertMatrix44(InvTransMat,pObjModel->m_bones[i].m_rela);
            /*子骨骼显示矩阵=父骨骼绝对矩阵*子骨骼平移矩阵*子骨骼旋转矩阵*/
            m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_abso,pObjModel->m_bones[i].m_rela);
            m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_DispMat[i],XRotMat);
            m3dMatrixMultiply44(pObjModel->m_DispMat[i],pObjModel->m_DispMat[i],zRotMat);
            /*计算真正的相对变换矩阵(不含平移)*/
            m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,XRotMat);
            m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,zRotMat);
            m3dMatrixMultiply44(pObjModel->m_bones[i].m_rela,pObjModel->m_bones[i].m_rela,InvTransMat);
            /*计算绝对变换矩阵(不含平移--相对于原点)*/
            m3dMatrixMultiply44(pObjModel->m_bones[i].m_abso,pObjModel->m_bones[pObjModel->m_bones[i].m_parent].m_abso,pObjModel->m_bones[i].m_rela);
        }
    }
}

绘制网格顶点的函数(注意乘的是abso函数):

 

void DrawMesh()
{
    UpdateBones();
 
    M3DMatrix44f mat;
    int nIdx=0;
 
    //glPolygonMode(GL_FRONT,GL_LINE);
    /*渲染网格中的顶点*/
    for(int j=0;j<MAX_MESHES;++j)
    {
        glBegin(GL_QUADS);
         
        for(int i=0;i<MAX_VERTICES_PER_MESH;++i)
        {
            float vx=0.0f,vy=0.0f,vz=0.0f;       /*mesh顶点坐标*/
            float vnx=0.0f,vny=0.0f,vnz=0.0f;    /*mesh顶点法线坐标*/
            float tmp1=0.0f,tmp2=0.0f,tmp3=0.0f;/*在这里vnx,vny,vnz要初始化为0,因为在下层循环中vnx vny vnz要不断权重累加*/
 
            /*根据权值计算顶点的pos normal坐标*/
            nIdx=i+j*MAX_VERTICES_PER_MESH;
            for(int k=0;k < pObjModel->m_ModelPoints[nIdx].m_nNumBone;++k)
            {
                m3dCopyMatrix44(mat,pObjModel->m_bones[pObjModel->m_ModelPoints[nIdx].m_arrBoneIdx[k]].m_abso);//这里用的是绝对矩阵
                /*加权计算骨骼对顶点位置的影响*/
                 
                tmp1=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[0]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[4]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[8]+mat[12];
                tmp2=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[1]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[5]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[9]+mat[13];
                tmp3=(pObjModel->m_ModelPoints[nIdx].m_pos)[0]*mat[2]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[1]*mat[6]+
                    (pObjModel->m_ModelPoints[nIdx].m_pos)[2]*mat[10]+mat[14];
                vx+=tmp1*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                vy+=tmp2*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                vz+=tmp3*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                 
                /*在这里我犯了一个愚蠢的错误(就这么一个简单的测试程序就花了2天时间来调试):用vx代替了tmp1,vy--tmp2,vz--tmp3,huh,FOOLISH.*/
                tmp1=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[0]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[4]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[8]+mat[12];
                tmp2=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[1]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[5]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[9]+mat[13];
                tmp3=(pObjModel->m_ModelPoints[nIdx].m_normal)[0]*mat[2]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[1]*mat[6]+
                    (pObjModel->m_ModelPoints[nIdx].m_normal)[2]*mat[10]+mat[14];
                vnx+=tmp1*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                vny+=tmp2*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                vnz+=tmp3*(pObjModel->m_ModelPoints[nIdx].m_arrWeight[k]);
                 
            }
            /*渲染网格顶点*/
            glColor4f(pObjModel->m_ModelPoints[nIdx].m_red,pObjModel->m_ModelPoints[nIdx].m_green,
                pObjModel->m_ModelPoints[nIdx].m_blue,pObjModel->m_ModelPoints[nIdx].m_alpha);
            glNormal3f(vnx,vny,vnz);
            glVertex3f(vx,vy,vz);
        }
        glEnd();
    }
}

绘制整个模型的函数:

/*根据骨骼的位置变化,绘制骨骼和蒙皮*/
void DrawModel()
{
    glLoadIdentity();//????
    glTranslatef(0.0f,-4.0f,-15.0f);
 
    DrawMesh();
     
    /*绘制骨骼*/
    for(int i=0;i<MAX_BONES;++i)
    {
        glPushMatrix();
 
        glMultMatrixf(pObjModel->m_DispMat[i]);
        glColor3f(1.0f,0.0f,0.0f);
 
        glBegin(GL_LINES);
        /*绘制线段组成的模拟骨骼*/
        glVertex3f(-0.4f, 0.0f, -0.4f);
        glVertex3f(0.4f, 0.0f, -0.4f);
        glVertex3f(0.4f, 0.0f, -0.4f);
        glVertex3f(0.4f, 0.0f, 0.4f);
        glVertex3f(0.4f, 0.0f, 0.4f);
        glVertex3f(-0.4f, 0.0f, 0.4f);
        glVertex3f(-0.4f, 0.0f, 0.4f);
        glVertex3f(-0.4f, 0.0f, -0.4f);
 
        glVertex3f(-0.4f, 0.0f, -0.4f);
        glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f);
        glVertex3f(0.4f, 0.0f, -0.4f);
        glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f);
        glVertex3f(0.4f, 0.0f, 0.4f);
        glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f);
        glVertex3f(-0.4f, 0.0f, 0.4f);
        glVertex3f(0.0f, pObjModel->m_bones[i].m_length, 0.0f);
 
        glEnd();
 
        glPopMatrix();
    }
}

运行效果:

在这里插入图片描述

图1 父骨骼旋转一定角度后的效果

在这里插入图片描述

图2 初始状态

ref:<<OpenGL编程精粹>>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用 JavaScript 编写的杀死幽灵游戏(附源代码) 杀死鬼魂游戏是使用 Vanilla JavaScript、CSS 和 HTML 画布开发的简单项目。这款游戏很有趣。玩家必须触摸/杀死游荡的鬼魂才能得分。您必须将鼠标悬停在鬼魂上 - 尽量得分。鬼魂在眨眼间不断从一个地方移动到另一个地方。您必须在 1 分钟内尽可能多地杀死鬼魂。 游戏制作 这个游戏项目只是用 HTML 画布、CSS 和 JavaScript 编写的。说到这个游戏的特点,用户必须触摸/杀死游荡的幽灵才能得分。游戏会根据你杀死的幽灵数量来记录你的总分。你必须将鼠标悬停在幽灵上——尽量得分。你必须在 1 分钟内尽可能多地杀死幽灵。游戏还会显示最高排名分数,如果你成功击败它,该分数会在游戏结束屏幕上更新。 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要玩游戏,首先,单击 index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值