번역: 전병관 ( 2007년 9월 1일 ) - 계속 수정중입니다.
참조: http://tfc.duke.free.fr/

MD2 file format
(Quake 2's models)

Written by David Henry, 19th December of 2004

소개

MD2 model file format 은 id 소프트웨어가 Quake2를 1997년 11월에 출품하면서 소개 되었다. 그것은 이해하기에 정말 쉬운 파일 포멧이다. MD2 모델의 케릭터는 :

  • Model의 기하학적 도형 데이타(삼각형) ( Model's geometric data (triangles) )
  • 연결된 프레임의 활동만화 ( Frame-by-frame animations )
  • 구조화된 데이타 ( GL_TRIANGLE_FAN, GL_TRIANGLE_STRIP 원시데이타 를 그린다 : OpenGL 명령어라고 불린다 )

모델의 표면입히기(texture)는 분리된 파일로 존재한다.  한개의 MD2 모델은 동시에 단 하나의 표면입히기(texture)를 갖는다.

MD2 모델 파일의 확장자는 “md2”이다. MD2 파일은 이진파일의 두 부분으로 나뉜다; header, data. 헤더(머리)는 데이타를 조작하고 사용하기에 필요한 모든 정보를 포함한다.

Header
Data

변수 크기

변수 유형과 크기:

  • char: 1 byte
  • short: 2 bytes
  • int: 4 bytes
  • float: 4 bytes

위 자료형은 C언어의 x86 구조와 일치한다. 다른 곳에서 컴파일 할 때 자료형의 크기에 신경을 써야 한다. OS 및 언어별 또는 버젼에 따라서 지원되는 자료형의 크기가 다르기 때문이다.

Endianess issues

MD2파일이 이진 포멧이어서, 반드시 엔디안(endianess)을 다뤄야만 한다. MD2 파일은 리틀-엔디안(x86)로 저장된다. 만약 빅-엔디안 구조물(PowerPC, SPARC, ...)을 타켓으로 한다거나, 또는 간단하게 이식성있는 프로그램을 만들고 싶다면, 파일을 읽을 때 각 단어 또는 더블(2배)-단어에 대해서 적절한 전환을 수행해야만 한다.

The header

헤더는 파일의 시작점에 있는 구조이다.:

/* MD2 header */
struct md2_header_t
{
int ident; /* magic number: "IDP2" */
int version; /* version: must be 8 */

int skinwidth; /* texture width */
int skinheight; /* texture height */

int framesize; /* size in bytes of a frame */

int num_skins; /* number of skins */
int num_vertices; /* number of vertices per frame(프레임별 꼭지점들)*/
int num_st; /* number of 표면입히기(texture) coordinates */
int num_tris; /* number of 삼격형(triangles) */
int num_glcmds; /* number of opengl commands */
int num_frames; /* number of frames(활동만화 동작들) */

int offset_skins; /* offset skin data */
int offset_st; /* offset texture coordinate data */
int offset_tris; /* offset triangle data */
int offset_frames; /* offset frame data */
int offset_glcmds; /* offset OpenGL command data */
int offset_end; /* offset end of file */
};

ident(식별)은 파일의 마법의 숫자이다. 파일 유형을 식별하는데 사용한다. ident(식별)은 반드시 844121161 과 동일하거나 또는 문자열 “IDP2”와 동일해야 한다. 우리는 (('2'<<24) + ('P'<<16) + ('D'<<8) + 'I') 표현식을 사용하면 얻을 수 있다.

버젼은 파일 포멧 버젼 숫자이며 틀림없이 8이어야 한다..

skinwidth 와 skinheight는 각 모델의 표면입히기(texture) 넓이와 높이이다.

framesize는 프레임과 그 모든 데이타의 바이트(bytes) 크기이다.

num_skins은 모델에 조합된 표면입히기(texture)의 숫자이다.
num_vertices 은 하나의 프레임에 대한 꼭지점의 숫자이다.
num_st 은 표면입히기(texture) 좌표계의 숫자이다.
num_tris 는 삼각형(triangles)의 숫자이다.
num_glcmds 은 OpenGL명령어의 숫자이다.
num_frames 은 모델이 갖는 프레임의 숫자이다.

offset_skins 은  파일 시작에서 표면입히기 데이타에 연결하는 것을 바이트로 위치를 지시한다.
offset_st 는 표면입히기(texture) 좌표계 데이타의 위치를 가리킨다.
offset_tris 는 삼각형(triangle) 데이타의 위치를 가리킨다.
offset_frames 는 프레임 데이타의 위치를 기리킨다.
offset_glcmds 는 OpenGL 명령들의 위치를 가리킨다.
offset_end 는 파일의 끝 위치를 가리킨다.

자료형

Vector

벡터, 실수형 좌표계(x, y, z)로 구성된다:

/* Vector */
typedef float vec3_t[3];

표면입히기(Texture) 정보

Texture 정보는 모델을 연결하는 표면입히기 이름의 리스트이다:

/* 표면입히기(Texture) 이름 */
struct md2_skin_t
{
char name[64]; /* texture file name */
};

표면입히기 좌표계(Texture coordinates)

Texture 좌표계는 short 정수형으로 구조화되어 저장된다. 올바른 표면입히기(texture)좌표계를 획득하기 위해서, 반다시 skinwidth 나누기 s, skinheight 나누기 t를 해줘야만 한다:

/* 표면입히기 좌표계(Texture coords) */
struct md2_texCoord_t
{
short s;
short t;
};

삼각형(Triangles)

각 삼각형은 꼭지점을 지시하는 배열과 표면입히기(texture) 좌표계를 지시하는 배열을 갖는다.

/* 삼각형 정보(Triangle info) */
struct md2_triangle_t
{
unsigned short vertex[3]; /* 삼각형의 꼭지점 인덱스 */
unsigned short st[3]; /* tex. coord. indices : 표면입히기. 좌표계. 인덱스 */
};

꼭지점(Vertices)

꼭지점은 “압축된” 3D 좌표계로 구성되어진다, 그것은 1 바이트 좌표로 저장되고, 보통 벡터 인덱스로 관리된다. 보통 벡터 배열은  Quake 2의  anorms.h 파일에 저장되고 실수형 좌표(3 float)의 162 개의 벡터 를 갖는다.

/* Compressed vertex */
struct md2_vertex_t
{
unsigned char v[3]; /* position */
unsigned char normalIndex; /* normal vector index */
};

Frames

프레임들은 그자신과 꼭지점 좌표 리스트에 대한 구체적인 정보들을 갖는데. 정보는 압축된 꼭지점을 풀고, 실좌표를 얻는데 사용되어 진다.

/* Model frame */
struct md2_frame_t
{
vec3_t scale; /* scale factor */
vec3_t translate; /* translation vector */
char name[16]; /* frame name */
struct md2_vertex_t *verts; /* list of frame's vertices */
};

꼭지점 좌표계의 압축을 풀기 위해서, 크기로 각 컴포넌트를 곱하고 각각의 이동 컴포넌트를  더할 필요가 있다:

vec3_t v;                     /* real vertex coords. */
struct md2_vertex_t vtx; /* compressed vertex */
struct md2_frame_t frame; /* a model frame */

v[i] = (vtx.v[i] * frame.scale[i]) + frame.translate[i];

OpenGL 명령어들

OpenGL 명령어는 정수형(int) 배열에 저장된다. 그것은 문서의 말미에 다룰 것이다.

MD2 파일 렌더링(그리기)

md2_model_t이 모든 당신의 모델 데이타(md2_model_t 객체에 붙어 있는 포인터 *mdl)를 잡고 있는 구조라고 가정하자, 이 코드는 MD2 모델 파일을 어떻게 읽어 오는지를 보여준다:

int
ReadMD2Model (const char *filename, struct md2_model_t *mdl)
{
FILE *fp;
int i;

fp = fopen (filename, "rb");
if (!fp)
{
fprintf (stderr, "Error: couldn't open \"%s\"!\n", filename);
return 0;
}

/* Read header */
fread (&mdl->header, 1, sizeof (struct md2_header_t), fp);

if ((mdl->header.ident != 844121161) ||
(mdl->header.version != 8))
{
/* Error! */
fprintf (stderr, "Error: bad version or identifier\n");
fclose (fp);
return 0;
}

/* Memory allocations */
mdl->skins = (struct md2_skin_t *)
malloc (sizeof (struct md2_skin_t) * mdl->header.num_skins);
mdl->texcoords = (struct md2_texCoord_t *)
malloc (sizeof (struct md2_texCoord_t) * mdl->header.num_st);
mdl->triangles = (struct md2_triangle_t *)
malloc (sizeof (struct md2_triangle_t) * mdl->header.num_tris);
mdl->frames = (struct md2_frame_t *)
malloc (sizeof (struct md2_frame_t) * mdl->header.num_frames);
mdl->glcmds = (int *)malloc (sizeof (int) * mdl->header.num_glcmds);

/* Read model data */
fseek (fp, mdl->header.offset_skins, SEEK_SET);
fread (mdl->skins, sizeof (struct md2_skin_t),
mdl->header.num_skins, fp);

fseek (fp, mdl->header.offset_st, SEEK_SET);
fread (mdl->texcoords, sizeof (struct md2_texCoord_t),
mdl->header.num_st, fp);

fseek (fp, mdl->header.offset_tris, SEEK_SET);
fread (mdl->triangles, sizeof (struct md2_triangle_t),
mdl->header.num_tris, fp);

fseek (fp, mdl->header.offset_glcmds, SEEK_SET);
fread (mdl->glcmds, sizeof (int), mdl->header.num_glcmds, fp);

/* Read frames */
fseek (fp, mdl->header.offset_frames, SEEK_SET);
for (i = 0; i < mdl->header.num_frames; ++i)
{
/* Memory allocation for vertices of this frame */
mdl->frames[i].verts = (struct md2_vertex_t *)
malloc (sizeof (struct md2_vertex_t) * mdl->header.num_vertices);

/* Read frame data */
fread (mdl->frames[i].scale, sizeof (vec3_t), 1, fp);
fread (mdl->frames[i].translate, sizeof (vec3_t), 1, fp);
fread (mdl->frames[i].name, sizeof (char), 16, fp);
fread (mdl->frames[i].verts, sizeof (struct md2_vertex_t),
mdl->header.num_vertices, fp);
}

fclose (fp);
return 1;
}

모델 렌더링(그리기)

여기, 모델 mdl의 프레임 n을 어떻게 그릴 것인가 하는 예제가 있다.:

void
RenderFrame (int n, const struct md2_model_t *mdl)
{
int i, j;
GLfloat s, t;
vec3_t v;
struct md2_frame_t *pframe;
struct md2_vertex_t *pvert;

/* n이 가능 영역에 있는지 확인한다. */
if ((n < 0) || (n > mdl->header.num_frames - 1))
return;

/* 모델의 표면입히기(texture)를 가능케 한다. */
glBindTexture (GL_TEXTURE_2D, mdl->tex_id);

/* 모델을 그린다. */
glBegin (GL_TRIANGLES);
/* 각 삼각형(triangle)을 그린다. */
for (i = 0; i < mdl->header.num_tris; ++i)
{
/* 각 꼭지점을 그린다. */
for (j = 0; j < 3; ++j)
{
pframe = &mdl->frames[n];
pvert = &pframe->verts[mdl->triangles[i].vertex[j]];

/* 표면입히기(texture) 좌표계를 계산한다. */
s = (GLfloat)mdl->texcoords[mdl->triangles[i].st[j]].s / mdl->header.skinwidth;
t = (GLfloat)mdl->texcoords[mdl->triangles[i].st[j]].t / mdl->header.skinheight;

/* OpenGL에 표면입히기(texture)좌표계를 통과시킨다. */
glTexCoord2f (s, t);

/* Normal vector */
glNormal3fv (anorms_table[pvert->normalIndex]);

/* 꼭지점의 실좌표를 계산한다. */
v[0] = (pframe->scale[0] * pvert->v[0]) + pframe->translate[0];
v[1] = (pframe->scale[1] * pvert->v[1]) + pframe->translate[1];
v[2] = (pframe->scale[2] * pvert->v[2]) + pframe->translate[2];

glVertex3fv (v);
}
}
glEnd ();
}

Animation(활동만화)

MD2 모델은 프레임에서 프레임으로 연사되어 애니메이션화 된다. 한 프레임은 한 애니메이션의 스크린샷이다.  덜덜거리고 추한 애니메이션을 회피하려면, 두개의 연속된 프레임의 꼭지점 좌표 사이에 선형 보간법(linear interplocation)을 사용한다(우리가 그리는 현재 프레임은 다음에 그리는 프레임이다). 표준 벡터를 위해 같은 것을 한다:

struct md2_frame_t *pframe1, *pframe2;
struct md2_vertex_t *pvert1, *pvert2;
vec3_t v_curr, v_next, v;

for (/* ... */)
{
pframe1 = &mdl->frames[current];
pframe2 = &mdl->frames[current + 1];
pvert1 = &pframe1->verts[mdl->triangles[i].vertex[j]];
pvert2 = &pframe2->verts[mdl->triangles[i].vertex[j]];

/* ... */

v_curr[0] = (pframe1->scale[0] * pvert1->v[0]) + pframe1->translate[0];
v_curr[1] = (pframe1->scale[1] * pvert1->v[1]) + pframe1->translate[1];
v_curr[2] = (pframe1->scale[2] * pvert1->v[2]) + pframe1->translate[2];

v_next[0] = (pframe2->scale[0] * pvert2->v[0]) + pframe2->translate[0];
v_next[1] = (pframe2->scale[1] * pvert2->v[1]) + pframe2->translate[1];
v_next[2] = (pframe2->scale[2] * pvert2->v[2]) + pframe2->translate[2];

v[0] = v_curr[0] + interp * (v_next[0] - v_curr[0]);
v[1] = v_curr[1] + interp * (v_next[1] - v_curr[1]);
v[2] = v_curr[2] + interp * (v_next[2] - v_curr[2]);

/* ... */
}

v 는 그리기 위한 마지막 꼭지점이다. interp 는 두 프레임 사이에 통계-보간(interpolation:중간값 삽입) 백분율이다. 그것은 0.0~1.0 구간의 float 이다. 그것이 1.0 과 동일 할 때, 현재의 값은 1씩 증가하고 interp 는 0.0으로 다시 초기화 된다.  표면입히기(texture) 좌표계에 삽입할 필요가 없는데  이유는 그것들이 모든 모델 프레임들이 동일 하기 때문이다. interp가 초당 렌더링되는 프레임의 프로그램의 수(fps)와 관련되어지는 것이 더 바람직하다.

void
Animate (int start, int end, int *frame, float *interp)
{
if ((*frame < start) || (*frame > end))
*frame = start;

if (*interp >= 1.0f)
{
/* Move to next frame */
*interp = 0.0f;
(*frame)++;

if (*frame >= end)
*frame = start;
}
}

OpenGL 명령어 사용하기

OpenGL 명령어는 단지 GL_TRIANGLE_FAN and GL_TRIANGLE_STRIP primitives를 사용하는 모델을 위한 구조화된 데이타이다. 그것은 꾸러미내에서 읽혀지는 정수(int)형 배열이다:

  • 처음 정수형은 신규 primitive를 그리는 꼭지점들의 숫자이다. 그것이 양수의 값이면, primitive는 GL_TRIANGLE_STRIP이다. 그렇지 않으면 GL_TRIANGLE_FAN 이다.
  • 다음 정수형들은 거기에 그리기 위한 많은 꼭지점들을 3개의 꾸러미로 가져갈 수 있다. 두 개의 처음은 실수형 좌표에 존재하는 표면입히기(texture) 좌표계이고 세번째는 그리기 위한 꼭지점 인덱스이다.
  • 그리기 위한 꼭지점들의 수가 0 일 때, 모델을 그리기는 끝난다.

우리는 그 데이타들의 꾸러미를 구조화해서 생성할 수 있다:

/* GL 명령 꾸러미들 */
struct md2_glcmd_t
{
float s; /* s texture coord. */
float t; /* t texture coord. */
int index; /* vertex index */
};

여기 렌더링(그리기) 알고리즘을 사용하는 것은 옛날 기능보다 낳은 프레임율을 포함한다. 이유는 우리가 GL_TRIANGLES primitieves를 사용않고 GL_TRIANGLE_FAN 과 GL_TRIANGLE_STRIP primitives 를 사용하고(어떤 것은 GPU 시간보다 덜 사용한다) 표면입히기(texture) 좌표계는 더이상 계산되지 않개 때문이다(skinwidth와 skinheight로 나눌 필요가 없다). 여기에 그것들을 사용하는 예제가 있다:

void
RenderFrameWithGLCmds (int n, const struct md2_model_t *mdl)
{
int i, *pglcmds;
vec3_t v;
struct md2_frame_t *pframe;
struct md2_vertex_t *pvert;
struct md2_glcmd_t *packet;

/* n이 가능 영역에 있는지 확인한다. */
if ((n < 0) || (n > mdl->header.num_frames - 1))
return;

/* 모델의 표면입히기(texture)를 가능케 한다. */
glBindTexture (GL_TEXTURE_2D, mdl->tex_id);

/* 명령 리스트의 시적점 pglcmds 좌표들 */
pglcmds = mdl->glcmds;

/* 모델을 그린다. */
while ((i = *(pglcmds++)) != 0)
{
if (i < 0)
{
glBegin (GL_TRIANGLE_FAN);
i = -i;
}
else
{
glBegin (GL_TRIANGLE_STRIP);
}

/* 이 그룹의 각 꼭지점을 그린다. */
for (/* Nothing */; i > 0; --i, pglcmds += 3)
{
packet = (struct md2_glcmd_t *)pglcmds;
pframe = &mdl->frames[n];
pvert = &pframe->verts[packet->index];

/* OpenGL의 표면입히기(texture) 좌표계를 통과 시킨다. */
glTexCoord2f (packet->s, packet->t);

/* Normal vector */
glNormal3fv (anorms_table[pvert->normalIndex]);

/* 꼭지점의 실좌표를 계산한다.( Calculate vertex real position ) */
v[0] = (pframe->scale[0] * pvert->v[0]) + pframe->translate[0];
v[1] = (pframe->scale[1] * pvert->v[1]) + pframe->translate[1];
v[2] = (pframe->scale[2] * pvert->v[2]) + pframe->translate[2];

glVertex3fv (v);
}

glEnd ();
}
}

상수

여기 최대 치수들을 정의하는 상수값이 있다:

  • Maximum number of triangles: 4096
  • Maximum number of vertices: 2048
  • Maximum number of texture coordinates: 2048
  • Maximum number of frames: 512
  • Maximum number of skins: 32
  • Number of precalculated normal vectors: 162

Sample code: md2.c (16 KB), anorms.h (6.7 KB). No texture mapping.


This document is available under the terms of the GNU Free Documentation License (GFDL)
© David Henry – contact : tfc_duke (AT) club-internet (DOT) fr

Posted by stekilove
,