閱讀本文的基礎
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;//節點的數量包括rootunsigned 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;}//載入檔案到bufferif(fread(buffer,1,iFileSize,f)!=(unsigned)iFileSize){printf("failed!!\n");delete []buffer;return false;}//驗證檔案是否為BVHconst 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;}