본문 바로가기

컴퓨터 공학 자료(학부)/Irrlicht gameEngine

일리히트 게임엔진 케릭터 모델링할때 md2 파일구조

The .MD2 Model Format:

md2
파일은 4가지 다른 구조로 읽어 들여야 한다. 4가지 구조란 파일의 header, vertex information, frame, 그리고 또 다른 vertex structure 이다. 우선 헤더의 구조에 대해 알아보자.
typedef struct MD2_HEADER_TYP
{
	int magic;
	int version;
	int skinWidth;
	int skinHeight;
	int frameSize;
	int numSkins;
	int numVertices;
	int numTexcoords;
	int numTriangles;	
	int numGlCommands;
	int numFrames;
	int offsetSkins;
	int offsetTexcoords;
	int offsetTriangles;
	int offsetFrames;
	int offsetGlCommands;
	int offsetEnd;
} MD2_HEADER, *MD2_HEADER_PTR;
각각의 변수는 다음과 같은 의미를 지닌다.
  • magic - md2 파일임을 나타내는 숫자이다. 이 숫자가 844121161 이면 md2 파일임이 입증된다.
  • version - 파일의 버전을 나타낸다. 이 숫자는 항상 8이어야 한다..
  • skinWidth - 모델스킨의 너비를 알려준다. (not used in our code).
  • skinHeight - 모델스킨의 높이를 알려준다. (not used in our code).
  • frameSize - 모델을 구성하는 각각의 프레임의 크기를 알려준다.  
  • numSkins - 모델이 지니는 스킨의 개수를 알려준다. (not used in our code).
  • numVertices - 모델의 Vertex 개수를 알려준다. 
  • numTexcoords - 모델이 지니는 텍스쳐 좌표의 개수를 알려준다.
  • numTriangles - 모델이 지니는 트라이앵글의 개수를 알려준다 (we use this quite a bit).
  • numGlCommands - 모델이 triangle strips, 또는 fans 에 대해 최적화되어 있는지 알려준다. 보통 .md2 파일을 로딩할 때 많이 사용되지는 않지만 제대로 사용하면 실행할 때 상당한 이점이 있다.
  • numFrames - 모델이 지니는 animation keyframe 의 개수를 알려준다.
헤더의 다음 부분들은 우리가 찾는 것이 파일의 어떤 지점에 해당하는가에 대해 대응하는 값이다.
  • offsetSkins
  • offsetTexcoords
  • offsetTriangles
  • offsetFrames
  • offsetGlCommands
  • offsetEnd
 
typedef struct MD2_VERTEX_TYP
{
	unsigned char vertex[3];
	unsigned char lightNormalIndex;
} MD2_VERTEX, *MD2_VERTEX_PTR;
각각의 프레임에 대한 vertex 정보를 사용하게 되는데, 이중의 절반만이 유용하다. light normal index 는 퀘이크 2 에서 사용되는 normal table index 이다. .md2 파일은 내부적으로 198 프레임을 애니메이션을 위해 가지고 있고 그것들은 달리는 모습, 누운 모습, 죽은 모습, 쉬는 모습을 나타낸다.
typedef struct MD2_FRAME_TYP
{
	float	   scale[3];
	float	   translate[3];
	char	   name[16];
	MD2_VERTEX vertices[1];
} MD2_FRAME, *MD2_FRAME_PTR;

이 구조체는 모델을 scaling 하거나 traslating 하기 위한 정보를 포함하고 있다. 마지막에 포함된 구조체는 우리가 사용해야 할 진짜 vertex 구조체이다.

typedef struct MD2_MODELVERTEX_TYP
{
	float x,y,z;
	float u,v;
} MD2_MODELVERTEX, *MD2_MODELVERTEX_PTR;
보는 바와 같이 여기서는 OpenGL 을 위한 vertex 와 텍스쳐 정보를 저장한다.

그럼 이제 .md2 모델을 사용하기 위한 클래스를 보자.

class MD2
{
	private:	
		MD2_MODELVERTEX vertList[100];

		int numGlCommands;
		long* glCommands;

		int numTriangles;

	public:
		int stateStart;
		int stateEnd;

		int frameSize;
		int numFrames;
		char* frames;

		int currentFrame;
		int nextFrame;
		int endFrame;
		float interpolation;
	
	bool Load(char* filename);
	void Render(int numFrame);
	void Animate(int startFrame, int EndFrame, float Interpolation);
	void SetState(int state);

	MD2() : stateStart(IDLE1_START), stateEnd(IDLE1_END),
			numGlCommands(0), frameSize(0), numFrames(0), 
			currentFrame(IDLE1_START), nextFrame(currentFrame+1),
			endFrame(IDLE1_END), interpolation(0.0f)
	{	}

	~MD2()
	{	
		if(glCommands)
			delete [] glCommands;
		if(frames)
			delete [] frames;
	}
};
여기서 로딩과 렌더링 함수에 주의를 기울여라. 애니메이션과 스테이트 함수는 후에 언급할 것이다. 모든 변수가 포인터인 점에 당황할지도 모르지만 이는 동적 메모리 할당에 의해 초기에 최대의 메모리를 생성하지 않고 메모리를 절약하려는 의도이다. 이러한 사용을 위한 예를 들면 glCommands 란 변수를 로딩할 때 다음과 같이 한다.
glCommands= new long [header.numGlCommands*sizeof(long)];
여기에서 사용한 형태를 보다 일반화하면  variablePtr= new variableType [sizeYouWantToCreate]; 이고 glCommands 를 지우기 위해서는 다음과 같이 한다.
delete [] glCommands;


이제 로딩 코드를 보자.
bool MD2::
	Load(char* filename)
	{
	FILE* file;
	MD2_HEADER header;
동적으로 읽어 들이는 정보를 저장하기 위한 파일 포인터를 생성한다. 그리고 헤더 정보를 저장하기 위한 임시 변수로  MD2_HEADER 를 생성한다.
if((file= fopen(filename, "rb"))==NULL)
	{
	S3Dlog.Output("Could not load %s correctly", filename);
	return false;
	}
여기서는 filename 으로 정의된 파일을 읽어 들인다. 그리고 파일을 여는데 문제가 있으면 함수 밖으로 나간다. 이 때, ""illegal operations" 에 대한 메시지 박스가 나타난다.
fread(&header, sizeof(MD2_HEADER), 1, file);
이 문장은 이전에 생성한 헤더 구조체에 헤더를 저장하는 것을 나타낸다. 이 함수에서 여기에 저장한 헤더를 많이 사용하게 될 것이다. 그러나 오직 이 함수를 위해서만 필요하다. 이것이 함수를 특별하게 만드는 이유이다. 헤더의 몇몇 변수를 전역 변수로 만들 필요가 있으나 여전히 모든 것을 저장하기에는 공간이 부족하다.
if(header.magic!= 844121161)
{
	S3Dlog.Output("%s is not a valid .md2", filename);
	return false;
}

if(header.version!=8)
{
	S3Dlog.Output("%s is not a valid .md2", filename);
	return false;
}

이번에는 두 가지 헤더 변수와 읽어 들이는 파일의 타입과 비교하여 적절한 파일의 형태인지를 체크한다. 모든 .md2 파일은 같은 magic, version number 를 가져야 한다.

frames= new char[header.frameSize*header.numFrames];
파일에 포함된 프레임 수에 header.frameSize 를 곱하여 동적 메모리 할당에 의해 적절한 메모리를 할당한다.
fseek(file, header.offsetFrames, SEEK_SET);
fread(frames, header.frameSize*header.numFrames, 1, file);
일단 프레임 정보가 위치한 지점을 찾고 frames 변수에 그 정보를 저장한다.
glCommands= new long [header.numGlCommands*sizeof(long)];

if(glCommands==NULL)
	return false;

fseek(file, header.offsetGlCommands, SEEK_SET);
fread(glCommands, header.numGlCommands*sizeof(long), 1, file);
glCommands
정보를 로딩한다는 것만 다를 뿐 앞에서 했던 것과 동일한 경우이다.( 모델을 렌더링할 때 triangle strips 이나 fans 가 필요한지 알 필요는 없다. )
numFrames	= header.numFrames;
numGlCommands	= header.numGlCommands;
frameSize	= header.frameSize;
numTriangles	= header.numTriangles;
클래스 내부에서 렌더링하기 위해 알아야 하는 전역 헤더 정보를 여기서 저장한다. 
fclose(file);	

S3Dlog.Output("Loaded %s correctly", filename);
return true;
}
열었던 파일을 닫고 로그에 정보를 출력한 뒤 함수를 빠져나온다. 이제 렌더링 함수를 보자.
void MD2::
	Render(int numFrame)
	{ 	
static MD2_MODELVERTEX vertList[100];
	
MD2_FRAME_PTR	currentFrame;
	VERTEX	v1;
	long*	command;
	float	texcoord[2];
	int	loop;
	int	vertIndex;
	int	type;
	int	numVertex;
	int	index;
이 함수에선 렌더링에 필요한 변수들을 정의하고 있다. 현재의 프레임(각각의 모델은 한번에 하나의 프레임만을 렌더링할 수 있기 때문에)을 알려주는 하나의 프레임 변수가 있고 순서가 정해진 세개의 vertex 대신 벡터로서 vertex 를 출력하기 위한 4개의 임시 vertex 를 지닌다. 또한 함수가 호출될 때마다 생성되는 임시 변수로 vertList 를 가진다. 매 초당 10-15프레임 정도를 얻도록 하였는데 이는 너무 크지 않은 적절한 수준이다.
currentFrame= (MD2_FRAME*) ((char*)frames+frameSize*numFrame);
command      = glCommands;
현재의 OpenGL 에서 vertex 를 출력하는데 필요한 프레임 정보와 glCommand 를 설정한다.  
while((*command)!=0)
{
	if(*command>0)		// This Is A Triangle Strip
	{
		numVertex= *command; 
		command++; 
		type= 0;
	}
	else			// This Is A Triangle Fan
	{
		numVertex= - *command; 
		command++; 
		type= 1;
	}
렌더링을 위한 vertex vertextriangle strip 또는 fan 으로 렌더링하기 위한 command 변수를 설정한다. 
if(numVertex<0)
	numVertex= -numVertex;
결코 vertex 개수는 음수가 될 수 없으므로 vertex 개수가 음수가 되지 않도록 조정한다. 
for(loop=0; loop<numVertex; loop++)
{
	vertList[index].u= *((float*)command); 
	command++;
	vertList[index].v= *((float*)command); 
	command++;

	vertIndex= *command; 
	command++;

	vertList[index].x= ( (currentFrame->vertices[vertIndex].vertex[0]* 
			          currentFrame->scale[0])+ 
			          currentFrame->translate[0]);
	vertList[index].z= -((currentFrame-> vertices[vertIndex].vertex[1]* 
			          currentFrame->scale[1])+ 
			          currentFrame->translate[1]);
	vertList[index].y= ( (currentFrame->vertices[vertIndex].vertex[2]* 
			          currentFrame->scale[2])+ 
			          currentFrame->translate[2]);
	index++;
}
scaling, translating
한 후에 적절한 vertex position vertex list 를 채워 넣는다.
		if(type==0)
		{
			glBegin(GL_TRIANGLE_STRIP);
			for(loop=0; loop<index; loop++)
			{
				v1.vertex[0]=(vertList[loop].x);
				v1.vertex[1]=(vertList[loop].y);
				v1.vertex[2]=(vertList[loop].z);

				texcoord[0]= vertList[loop].u;
				texcoord[1]= vertList[loop].v;

				glTexCoord2fv(texcoord);
				glVertex3fv(v1.vertex);
			}
			glEnd();
		}
		else
		{
			glBegin(GL_TRIANGLE_FAN);
			for(loop=0; loop<index; loop++)
			{
				v1.vertex[0]=(vertList[loop].x);
				v1.vertex[1]=(vertList[loop].y);
				v1.vertex[2]=(vertList[loop].z);

				texcoord[0]= vertList[loop].u;
				texcoord[1]= vertList[loop].v;

				glTexCoord2fv(texcoord);
				v1.SendToOGL();
			}
			glEnd();
		}
	}
}
마지막으로 적절한 명령에 따라 vertex 들을 렌더링한다.

지금부터는 앞에서 언급한
state animation 함수들에 대해서 살펴보자. .md2
파일과 함께 사용할 미리 정의된 상수들의 리스트를 만들었는데 다음 테이블에 나타난 것과 같다.

IDLE1 4개의 정지 동작 중 첫번째
RUN 모델이 달리는 움직임
SHOT_STAND 선 채로 총 쏘는 움직임
SHOT_SHOULDER 어깨위에 총 들고 쏘는 움직임(여전히 선 채로)
JUMP 점프하는 움직임
IDLE2 4개의 정지 동작 중 두번째
SHOT_FALLDOWN 엎드리면서 총 쏘는 움직임(큰 무기를 가지고 공격할 경우 사용된다.)
IDLE3 4개의 정지 동작 중 세번째
IDLE4 4개의 정지 동작 중 네번째
CROUCH 누워있는 모델을 생성하는 움직임
CROUCH_CRAWL 누워서 기어가는 모델 생성
CROUCH_IDLE 누워서 쉬는 모델
CROUCH_DEATH 누운 상태에서 죽어가는 모델
DEATH_FALLBACK 뒤로 쓰러져 죽은 모델(정면에서 총 맞은 경우) 
DEATH_FALLFORWARD 앞으로 쓰러져 죽은 모델(뒤에서 총 맞은 경우)
DEATH_FALLBACKSLOW 뒤로 천천히 쓰러지며 죽는 모델


스테이트는 위와 같고 이를 통해 스테이트 함수를 만들었다. 자세한 소스코드는 Loaders.cpp 를 참조하여라. 모델 클래스 객체를 생성하고 그에 대한 달리는 스테이트를 얻고 싶다면 다음과 같이 하면 된다.
model.SetState(RUN);
이 명령이 어떤 눈에 보이는 결과를 생성하지 못하지만 달리는 움직임을 만들기 위해 필요한 모델 클래스 내부의 두 개의 변수를 설정하는 역할을 한다. 비주얼한 결과를 얻기 위해서는 Animate(...) 함수를 사용해야 한다. 이를 위한 명령은 다음과 같다.
model.Animate(model.stateStart, model.stateEnd, 0.02f);

클래스의 stateStart (starting state 의 프레임 수) 와 stateEnd 를 받아서 50프레임의 속도로 모델의 움직임을 보간한다. 다음은 모델의 무기를 포함한 또 다른 사용예이다.

glBindTexture(GL_TEXTURE_2D, hobgoblinSkin.ID);
glScalef(0.05f, 0.05f, 0.05f);
hobgoblin.Animate(hobgoblin.stateStart, hobgoblin.stateEnd, 0.02f);
glBindTexture(GL_TEXTURE_2D, hobgoblinWeaponSkin.ID);
hobgoblinWeapon.Animate(hobgoblin.stateStart, hobgoblin.stateEnd, 0.02f);
모델의 스킨을 위의 코드처럼 로딩한다. main.cpp 에서 많은 것을 체크하면 다음과 같은 결과를 볼 수 있다.

소스코드 (Visual C++) - 이 프로그램을 제대로 돌리기 위해서는 DirectX 8.0 SDK 를 설치해야 한다.

제임스 본드와 베지타 모델을 위한 .md2 파일은 FilePlanet 에서 받을 수 있다.