阅读本文的基础
1 c++
2 OpenGL
3 图形学的基本概念
骨骼动画基本概念概括
传统的帧动画将模型中的点的坐标信息存取为一帧,播放下一帧时,就读取下一帧的全部点的新的坐标信息。这样的方式浪费了很大的空间,而且模型之间的层级关系无法体现,不利于和场景以及其他模型的交互。
骨骼动画也是一帧一帧的,但是每一帧存取的数据不是坐标信息,而是层级结构中,子节点相对父亲节点的平移和旋转信息。递归画出层级结构是很容易实现的。
这样存取克服了帧动画的缺点。骨骼动画的缺点是实现复杂,新数据生成需要一定的时间。
选取BVH文件格式作为入门是不错的选择,因为这个格式的简单,不含有其他信息,只含有动作数据,让你化繁为简,一目了然的理解骨骼动画的算法。
BVH文件格式
细节1 每行的最后有一个回车符和一个换行符,用16进制观察会很明显的发现
部分一 层级结构信息
用文本文件打开后,一目了然的会出现一棵树的结构。
offset 表示当前节点相对于父节点的位置信息
channels 表示对下面motion数据的解释 除父节点外 channels都有三个信息( 父节点为六个) 对应了子节点对父节点的旋转量 这些数据放在motion中
部分二 motion数据
有多少帧 就有多少行数据
在每一帧中,也就是每一行中,数据的前3项为根节点的位移信息
其余数据三个为一组代表旋转信息,和层级中的对应顺序为: 文本文件中从上到下channels出现的顺序
也就是这个层级树先序遍历的顺序
细节2 End节点没有这些旋转的信息 root中有这些信息
说明:一组的3个数据中 要根据channels中的信息来解析
例如 当channels 中为 Xrotation Zrotation Yrotation
操作时就要
glRotatef(pFrame[0],1.0f,0.0f,0.0f);
glRotatef(pFrame[1],0.0f,0.0f,1.0f);
glRotatef(pFrame[2],0.0f,1.0f,0.0f);
pFrame 指向当前的旋转信息组
细节3 offset 后的数据之间和channels后的解释信息间可能为空格也可能为tab
强烈建议用16进制查看器看多个文件 这样对文件的具体格式将会有清晰的认识 避免不必要的错误代码的编写
下面是代码和注释
#ifndef __BVH_H__
#define __BVH_H__
#include <stack>
using namespace std;
class BVHJoint;
class BVH
{
public:
BVH();
~BVH();
void clear();
bool loadFile(const char * pfile);
void print();
void setCurrentFrame(unsigned c);
void draw();
unsigned getFrameCount();
void addFrame();
private:
BVHJoint * root;//根节点
stack<BVHJoint*> father;//载入时用到的栈 根据前序构建树
BVHJoint* currentNode;//载入时当前的节点
unsigned char* p;//载入时读buffer的当前位置
unsigned jointCount;//节点的数量包括root
unsigned frameCount;//帧数
unsigned currentFrame;//当前帧
float **frameData;//存取motion数据的二维数组
float frameTime;//
float *pFrame;//绘制使用的当前motion信息位置
void drawRecursive(BVHJoint * r);
void roateSpace(unsigned char);
void deleteRecursive(BVHJoint* r);
bool processLeftBrace();
bool processRightBrace();
bool processJoint();
bool processOffset();
bool processChannels();
bool processEnd();
unsigned char getFlags();
BVHJoint * newNode();
void printRecursive(BVHJoint* r,int n);
};
#endif
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <gl/glut.h>
#include "BVH.h"
enum { CHILDSIZE = 16 ,NAMESIZE = 24};
enum { ZYX = 1, YZX = 2, ZXY = 3, XZY = 5, YXZ = 6, XYZ = 7};
//------------------------------------------------------
class BVHJoint
{
public:
friend class BVH;
BVHJoint():x(0),y(0),z(0),childNum(0),flags(0)
{
name[0] = 0;
}
bool addChild(BVHJoint* pc)
{
assert(childNum != NAMESIZE);
child[childNum] = pc;
childNum++;
return true;
}
BVHJoint* getChild(int i)
{
assert( i>=0 && i<CHILDSIZE);
return child[i];
}
private:
float x;
float y;
float z; //三个值表示距离父节点的偏移量
BVHJoint * child[CHILDSIZE];
int childNum;
unsigned char name[NAMESIZE];
unsigned char flags;//记录了channels 所代表的含义
};
//-------------------------------------------------------
BVH::BVH()
:currentNode(nullptr)
,p(nullptr)
,root(nullptr)
,frameCount(0)
,frameData(nullptr)
,pFrame(nullptr)
,frameTime(1)
,jointCount(1)
,currentFrame(0)
{
}
//------------------------------------------------------
BVH::~BVH()
{
clear();
}
//------------------------------------------------------
void BVH::deleteRecursive(BVHJoint* r)
{
for(int i = 0; i < r->childNum; i++)
{
deleteRecursive(r->getChild(i));
}
delete r;
}
//------------------------------------------------------
void BVH:: clear()
{
currentNode = nullptr;
p = nullptr;
while(!father.empty()) father.pop();
frameTime = 1;
for(unsigned int i = 0; i < frameCount; i++)
delete []frameData[i];
delete []frameData;
frameData = nullptr;
pFrame = nullptr;
frameCount = 0;
currentFrame = 0;
jointCount = 1;
if(root != nullptr)
{
deleteRecursive(root);
}
root = nullptr;
}
//------------------------------------------------------
//-------------------------------------------------------
//根据当前p指向的名字构建一个节点,读取文件的buffer时
//必须通过这个函数来产生新节点
BVHJoint* BVH:: newNode()
{
BVHJoint * node = new BVHJoint();
int i;
for(i = 0; i < NAMESIZE && *p != '\n';i++)
{
node->name[i] = *p;
p++;
}
if(i == NAMESIZE) --i;
node->name[i-1] = 0;//为了吃掉一个回车符!!
p++;
return node;
}
//------------------------------------------------------
//获得每个节点的通道信息,采用1,2,3,5,6,7表示6种可能的情况
unsigned char BVH::getFlags()
{
return (unsigned char)((*p - 'X' + 1)*1 + (*(p+10) - 'X' + 1)*2 + (*(p+20) - 'X' + 1)*4 - 10);
}
//------------------------------------------------------
//载入一个文件,生成BVH的一棵树形结构和motion旋转信息的二位数组
bool BVH::loadFile(const char* pfile)
{
if(pfile == 0)
return false;
FILE *f;
if(!(f = fopen(pfile,"rb")))
{
printf("file load failed!\n");
return false;
}
//获取文件长度
int iStart = ftell(f);
fseek(f,0,SEEK_END);
int iEnd = ftell(f);
rewind(f);
int iFileSize = iEnd -iStart;
//结束获取长度信息
//分配文件长的动态数组
unsigned char *buffer = new unsigned char[iFileSize];
if(!buffer)
{
printf("mem alloc failed!!\n");
return false;
}
//载入文件到buffer
if(fread(buffer,1,iFileSize,f)!=(unsigned)iFileSize)
{
printf("failed!!\n");
delete []buffer;
return false;
}
//验证文件是否为BVH
const char * fileheader = "HIERARCHY";
p = buffer;
for(int i = 0; i < 9 ; i++ )
{
if( *p != fileheader[i])
{
delete []buffer;
return false;
}
p++;
}
//验证文件结束
//载入根节点名字
p += 7;
root = newNode();
//保持栈顶元素和当前节点同步
father.push(root);
currentNode = root;
//载入根节点的偏移offset信息 这里的offset信息和joint不同需要特化处理
while(*p != 'O') p++;
p += 7;
root->x = (float)atof((char*)p);
p += 5;
if(*p == ' ') p++;
root->y = (float)atof((char*)p);
p += 5;
if(*p == ' ') p++;
root->z = (float)atof((char*)p);
p += 5;
//结束offset信息的载入
//跳到*rotation 位置 越过根节点的位移通道量 这里认为root是固定的XYZ位移模式
while(*p != 'r') p++;
p--;
//获得根节点的旋转通道信息
root->flags = getFlags();
p += 30;
//结束载入根节点
//根据字符流的首字符 构建状态处理逻辑 载入子节点
int counter = 1; //大括号的数量 初始化为1因为根节点之后已经有了一个
for(bool running = true; running ; )
{
//文件格式出错才会这样
if(*p == 0)
{
delete []buffer;
clear();
return 0;
}
//根据首字母 分发状态处理
switch(*p)
{
case 13://回车
case '\n'://换行
case ' ':
case ' ':
p++;
break;
case '{':
processLeftBrace();
counter++;
break;
case '}':
processRightBrace();
//判断层级数据载入是否结束
counter--;
if(counter == 0) running = false;
break;
case 'J':
jointCount++;
processJoint();
break;
case 'O':
processOffset();
break;
case 'C':
processChannels();
break;
case 'E':
processEnd();
break;
default:
printf("_%c_ _%d_ file format error!! \n",*p,*p);
delete []buffer;
clear();
return false;
}
}
while(*p!= 'F') p++;
p += 8;
frameCount = (unsigned)atoi((char *)p);
while(*p != 'F') p++;
p += 12;
frameTime = (float)atof((char*)p);
//结束载入 层级关系的数据
while(*p++ != '\n');
// 现在 p 指向了真正的motion数据
// 先分配空间
frameData = new float*[frameCount];
if(frameData == nullptr)
{
delete []buffer;
clear();
return false;
}
//每一帧的数据包括3个root节点的平移数据 和所有节点的旋转数据
if(jointCount == 1)
{
delete []buffer;
clear();
return false;
}
int dataCount = jointCount*3 + 3;
//printf("dataCount is : %d\n", dataCount );
for(unsigned int i = 0; i < frameCount; i++)
{
frameData[i] = new float[dataCount];
if(frameData == nullptr)
{
delete []buffer;
clear();
return false;
}
}
// 开始载入 motion 的数据
// 细节:每一帧的数据为一行 每个数据之间有一个空格或tab
//行最后有一个空格或tab然后是一个回车符和一个换行符
for(unsigned int i = 0; i < frameCount; i++)
{
for(int j = 0; j < dataCount; j++)
{
frameData[i][j] = (float)atof((char*)p);
p += 8;
while(*p != ' ' && *p != ' ') p++;
p++;//跳过空格或tab
}
//跳过回车和换行符
p+=2;
}
//载入motion数据结束
//载入全部数据结束
delete []buffer;
fclose(f);
return true;
}
//------------------------------------------------------
bool BVH::processLeftBrace()
{
//栈中存放了一系列的父节点 栈顶为当前节点,
father.push(currentNode);
p++;
return true;
}
//------------------------------------------------------
bool BVH::processRightBrace()
{
//栈顶为当前节点所以一定要先出栈(被debug1)
father.pop();
//保持当前节点和栈顶元素同步
if(!father.empty())
currentNode = father.top();
p++;
return true;
}
//------------------------------------------------------
bool BVH::processJoint()
{
p += 6;
BVHJoint *node = newNode();
//发现一个子节点添加父子关系
if(!currentNode->addChild(node))
return false;
currentNode = node;
//马上会处理‘{’ 使得栈顶与当前节点同步
return true;
}
//------------------------------------------------------
//由于offset在这里的文件格式不太统一 所以需要注意 因为root的offset格式特殊
bool BVH::processOffset()
{
p += 7;
currentNode->x = atof((const char *)p);
p += 8;
while(*p != ' ' && *p != ' ' ) p++;
p++;
currentNode->y = atof((const char *)p);
p += 8;
while(*p != ' ' && *p != ' ' ) p++;
p++;
currentNode->z = atof((const char *)p);
while(*p++ != '\n');
return true;
}
//------------------------------------------------------
//getFlags 将根据三个roation前面的三个大写字母判断通道的格式
bool BVH::processChannels()
{
p += 11;
currentNode->flags = getFlags();
p += 30;
return true;
}
//------------------------------------------------------
bool BVH::processEnd()
{
//其实和joint节点的处理是一样的 就是 p+=4 这里前进的不同
p += 4;
BVHJoint *node = newNode();
if(!currentNode->addChild(node))
return false;
currentNode = node;
return true;
}
//-----------------------------------------------------
//绘制骨骼 root节点还是多了处理
void BVH::draw()
{
pFrame = frameData[currentFrame];
root->x = pFrame[0];
root->y = pFrame[1];
root->z = pFrame[2];
pFrame += 3;
drawRecursive(root);
}
//-----------------------------------------------------
//绘制函数的核心 递归的绘制
void BVH::drawRecursive(BVHJoint* r)
{
glPushMatrix();
//要先平移后旋转(被debug3)
//平移 根据父节点空间
glTranslatef(r->x,r->y,r->z);
//根据motion中的信息和节点的通道类型进行旋转
roateSpace(r->flags);
//画出点
glutSolidSphere(1.0f,20,16);
//递归绘制子节点
for(int i = 0; i < r->childNum; i++)
{
drawRecursive(r->getChild(i));
}
glPopMatrix();
}
//-----------------------------------------------------
void BVH::roateSpace(unsigned char flags)
{
//enum {ZYX = 1, YZX = 2,ZXY = 3,XZY = 5,YXZ = 6,XYZ = 7};
switch (flags)
{
case ZYX:
glRotatef(pFrame[0],0.0f,0.0f,1.0f);
glRotatef(pFrame[1],0.0f,1.0f,0.0f);
glRotatef(pFrame[2],1.0f,0.0f,0.0f);
break;
case YZX:
glRotatef(pFrame[0],0.0f,1.0f,0.0f);
glRotatef(pFrame[1],0.0f,0.0f,1.0f);
glRotatef(pFrame[2],1.0f,0.0f,0.0f);
break;
case ZXY:
glRotatef(pFrame[0],0.0f,0.0f,1.0f);
glRotatef(pFrame[1],1.0f,0.0f,0.0f);
glRotatef(pFrame[2],0.0f,1.0f,0.0f);
break;
case XZY:
glRotatef(pFrame[0],1.0f,0.0f,0.0f);
glRotatef(pFrame[1],0.0f,0.0f,1.0f);
glRotatef(pFrame[2],0.0f,1.0f,0.0f);
break;
case YXZ:
glRotatef(pFrame[0],0.0f,1.0f,0.0f);
glRotatef(pFrame[1],1.0f,0.0f,0.0f);
glRotatef(pFrame[2],0.0f,0.0f,1.0f);
break;
case XYZ:
glRotatef(pFrame[0],1.0f,0.0f,0.0f);
glRotatef(pFrame[1],0.0f,1.0f,0.0f);
glRotatef(pFrame[2],0.0f,0.0f,1.0f);
break;
default:
break;
}
//End叶节点时不能前进 因为它没有旋转信息(被debug2)
//pFrame初始化为当前帧root节点的旋转信息
//每旋转一个节点的空间后 自动+3跳到下一个节点的旋转信息
//说明: BVH 存放旋转的信息按照文件中从上到下channel出现的顺序存储
//就是递归遍历时的前序周游顺序
if(flags != 0) pFrame+=3;
}
//----------------------------------------------------
unsigned BVH::getFrameCount()
{
return frameCount;
}
//-----------------------------------------------------
void BVH::addFrame()
{
currentFrame ++;
if(currentFrame == frameCount) currentFrame = 0;
}
//-----------------------------------------------------
void BVH::setCurrentFrame(unsigned c)
{
assert(c>=0&&c<frameCount);
currentFrame = c;
}
//------------------------------------------------------
//将载入好的层级信息打印出来
void BVH::printRecursive(BVHJoint* r,int n)
{
for(int i = 0; i < n; i++) printf(" -");
printf("%s",r->name);
printf(" : %f,%f,%f -%d- ",r->x,r->y,r->z,r->flags);
switch(r->flags)
{
case 1:
printf("zyx");
break;
case 2:
printf("yzx");
break;
case 3:
printf("zxy");
break;
case 5:
printf("xzy");
break;
case 6:
printf("yxz");
break;
case 7:
printf("xyz");
break;
default:
break;
}
printf("\n");
for(int i = 0; i < r->childNum ; i++)
{
printRecursive(r->getChild(i) , n+1);
}
}
//------------------------------------------------------
void BVH::print()
{
printRecursive(root , 0);
//int datacount = jointCount*3+3;
//for(int j= 0 ; j < frameCount; j++)
//{
// printf("frame %d:\n",j);
// for(int i = 0; i < datacount; i++)
// printf("%f ",frameData[j][i]);
// printf("\n");
//}
}
//------------------------------------------------------
#include <cstdio>
#include <cstdlib>
#include <gl/glut.h>
#include "BVH.h"
BVH model;
float R = 200.0f;
float angle = 10;
void init()
{
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
glEnable(GL_DEPTH_TEST);
GLfloat position[] = {0.0f,0.0f,1.0f,1.0f};
glLightfv(GL_LIGHT0,GL_POSITION,position);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glLineWidth(3.0f);
glColor3f(0.0f,1.0f,0.0f);
model.loadFile("sexy.bvh");
model.print();
}
void uninit()
{
}
void display()
{
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
glLoadIdentity();
gluLookAt( R*cos(angle), 20, R*sin(angle), 0, 20, 0, 0, 1, 0 );
model.draw();
glBegin(GL_QUADS);
glVertex3f(-100,-10,100);
glVertex3f(100,-10,100);
glVertex3f(100,-10,-100);
glVertex3f(-100,-10,-100);
glEnd();
glutSwapBuffers();
}
void reshape( int w, int h )
{
glViewport( 0, 0, GLsizei( w ), GLsizei( h ) );
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
gluPerspective( 45, ( GLdouble ) w / ( GLdouble ) h, 1.0f, 1000.0f );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
gluLookAt( R*cos(angle), 0, R*sin(angle), 0, 0, 0, 0, 1, 0 );
}
void keyboard( unsigned char key, int x, int y )
{
switch( key )
{
case 27:
exit( 0 );
case 'a':
case 'A':
angle += 0.1;
if(angle >= 360.0f) angle = 0;
break;
case 'd':
case 'D':
angle -= 0.1;
if(angle <=0.0f) angle = 359.9f;
break;
case 'w':
case 'W':
model.addFrame();
}
glutPostRedisplay();
}
int main( int argc, char *argv[] )
{
glutInit( &argc, argv );
glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH );
glutInitWindowPosition( 300, 75 );
glutInitWindowSize( 600, 600 );
glutCreateWindow( "OpenGL Test" );
init();
glutReshapeFunc( reshape );
glutKeyboardFunc( keyboard );
glutDisplayFunc( display );
// glutIdleFunc(display);
glutMainLoop();
uninit();
return 0;
}