骨骼动画入门----BVH文件的载入和播放

阅读本文的基础

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;
}



 

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Unity是一款流行的游戏引擎,支持多种文件格式的导入,其中包括bvh文件格式。bvh文件格式是一种用于记录人体骨骼动作的文件格式,通常用于动画制作。 在Unity中导入bvh文件非常简单。首先,在Unity中创建一个新项目,并导入需要的人体模型。其次,打开导入人物模型的编辑器,在编辑器中选择导入bvh文件的选项。接下来,选择需要导入的bvh文件,并按照提示完成导入过程。在导入过程中,Unity会自动将bvh文件中的骨骼动作信息应用到导入的人体模型上。 在导入完毕后,可以使用Unity的动画编辑器来查看和编辑导入的骨骼动作。除了默认的动画编辑器,还可以使用一些第三方插件来更好地控制和编辑动画。此外,如果觉得需要,还可以利用Unity"动作匹配器"和"动作融合器"的功能,来组合和混合多个动画。 总之,Unity支持bvh文件的导入,使得人物动画制作变得更加容易和高效。通过导入和编辑bvh文件,人物动画制作者可以创造出更加生动、精彩的人物动作,从而提高游戏的质量和用户体验。 ### 回答2: Unity是一款非常强大的游戏引擎,可以实现3D场景的构建和游戏物体的操作。而导入bvh文件也是其中的一个非常重要的功能,这可以非常方便地实现角色动作的导入和应用。 首先,在Unity的资源管理器中选择需要导入bvh文件的对象,在其属性面板中选择“导入”的选项。随后选择bvh文件,并进行导入操作。 接下来,在导入后的动画对象上,我们可以进行各种不同的操作,比如修改动画的播放速度、添加新的动画片段、或者对动画进行差值和编辑。 需要注意的是,在导入bvh文件时,Unity会尝试将其重新调整为匹配当前场景的大小和比例。因此,有时候可能需要手动对动画进行一些微调和编辑,以实现最佳效果。 总之,使用Unity导入bvh文件是一项非常重要且实用的功能,可以让我们更加轻松和高效地创建精美的动画效果。 \end{cn} ### 回答3: Unity是一款流行的游戏引擎,可以用于开发各种类型的游戏。导入BVH文件是Unity中常见的任务,这种文件格式通常用于描述人体运动数据。以下是关于如何在Unity中导入BVH文件的一些提示。 首先,Unity可以通过使用第三方插件来导入BVH文件。有许多免费或付费的插件可供选择,例如"BVH Importer"或"FinalIK"。在查找和选择插件之前,要确保已经下载并安装了最新的Unity版本。 安装插件后,需要准备BVH文件。可以使用3D建模软件如Maya、Blender或MotionBuilder来创建或修改此文件。通常需要确保文件符合常见的BVH标准格式,例如正确的帧速率、帧数量、层次结构和骨骼命名。 导入过程与导入其他文件类型相似: 打开Unity,创建一个新项目或打开现有项目,然后从文件选项中选择导入功能。选择正确的插件和BVH文件后,将需要进行一些设置,例如指定是否移动,旋转或缩放人物模型。 成功导入BVH文件后,Unity会生成一个包含BVH数据的动画片段。可以通过将这段动画片段附加到人物模型上,使模型运动引擎和动画数据配合工作。 总之,在Unity中导入BVH文件需要使用适当的插件,并通过创建和配置人物模型来准备文件。成功导入后,BVH数据可用于实现复杂的人体动作和其他交互式游戏元素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值