.MD2文檔由美國id Software為其QuakeII開發3D模型.雖然它無骨架但它讀取簡單,而最重要是可以通過Internet穩到大量MD2文檔.根據模型規範.MD2格式最多含198動畫幀編號為0~197. 動畫幀將用於組成『步行』『功擊』『站立』『死亡』等動畫.MD2文檔 通常有下面幾個文檔組成:
文檔 | 簡述 |
TRIS.MD2 | 角色3D模型 |
WEAPON.MD2 | 武器3D模型 |
TRIS.PCX | 角色皮膚也就是紋理圖檔,通常為8Bit『256色』長寬為256*256保存為.PCX格式 |
WEAPON.PCX | 武器皮膚 |
MD2格式 | 簡述 |
Head | 文檔頭部格式3D模型描述 |
Data | 3D數據 |
MD2文檔頭部格式 MD2_HEADER | 簡述 |
int id; | MD2文檔標記’IDP2′,用於判斷是否MD2文檔 |
int version; | 版本號總為8 |
int skin_width | 皮膚紋理圖寬度.此值相對於紋理座標textcoord,通常為256 |
int skin_height; | 皮膚紋理圖高度.此值相對於紋理座標textcoord,通常為256 |
int frame_size; | 每幀字節總量 |
int skin_num; | 皮膚紋理總量,忽略. |
int vertex_num; | 單幀頂點量.每幀字節量均相同.有些教科書搞錯左此值. |
int textcoord_num; | 紋理座標總量 |
int polygon_num; | 多邊形總量 |
int command_num; | OpenGL命令總量,忽略 |
int frame_num; | 幀總量id softwarek規定為198幀編號為0~197.有些模型可能多於此值 |
int skin_offset; | 皮膚pcx偏移量每個64字節.因包含QuakeII專用路徑.忽略. |
int textcoord_offset; | 紋理座標偏移量 |
int polygon_offset; | 多邊形偏移量 |
int frame_offset; | 幀偏移量 |
int command_offset; | 命令偏移量忽略 |
int end_offset; | 結尾偏移量.相當於文檔長度 |
MD2紋理座標通過(data + textcoord_offset)得到,結構如下:
typedef struct MD2_TEXTCOORD_TYP {
WORD u, v;
}MD2_TEXTCOORD,*MD2_TEXTCOORD_PTR;
MD2多邊形通過(data + polygon_offset)得到,保存頂點與紋理索引.結構如下:
typedef struct MD2_POLYGON_TYP {
WORD vertex_index[3];// 頂點索引
WORD textcoord_index[3];// 紋理座標索引
}MD2_POLYGON,*MD2_POLYGON_PTR;
MD2關鍵幀通過(data + frame_offset)得到,幀頭包含對頂點進行縮放與平移因子.頂點數組長度由vertex_num確定.結構如下:
typedef struct MD2_FRAME_TYP{
float scale[3];// 縮放
float translate[3];// 平移
char name[16];// 動畫幀ASCII名
MD2_POINT list[1];// 頂點數組
}MD2_FRAME,*MD2_FRAME_PTR;
MD2頂點包含於關鍵幀裡,每個頂點由『xyz座標』與『法線索引』組成.『法線索引表』你需要自已構建.法線將在運行時計算所以忽略.結構如下:
typedef struct MD2_POINT_TYP {
BYTE v[3];// xyz頂點
BYTE noraml_index;// 頂點法線索引,此值忽略
}MD2_POINT,*MD2_POINT_PTR;
MD2動畫,用於控制模型動作速度,以秒為單位.結構如下:
typedef struct MD2_ANIMATION_TYP{
int start; // 起始幀索引
int end; // 結束幀索引
float irate; // 插幀頻率(0.0f~1.0f),1.0f表示不插幀
float speed; // 用於控制動畫播放時間,一般設為0~10秒
} MD2_ANIMATION, *MD2_ANIMATION_PTR;
動畫索引 | 動作名 | 幀索引 | 插幀頻率(幀) | 播放時間(秒) |
0 | STANDING_IDLE站立 | 0-39 | 0.5f | 4 |
1 | RUN奔跑 | 40-45 | 0.5f | 4 |
2 | ATTACK開火/攻擊 | 46-53 | 0.5f | 2 |
3 | PAIN1 | 54-57 | 0.5f | 4 |
4 | PAIN2 | 58-61 | 0.5f | 4 |
5 | PAIN3 | 62-65 | 0.5f | 4 |
6 | JUMP跳躍 | 66-71 | 0.5f | 5 |
7 | FLIP手勢 | 72-83 | 0.5f | 3 |
8 | SALUTE 敬禮 | 84-94 | 0.5f | 5 |
9 | TAUNT 嘲笑 | 95-111 | 0.5f | 5 |
10 | WAVE 揮手致意 | 112-122 | 0.5f | 5 |
11 | POINT 指向它人 | 123-134 | 0.5f | 5 |
12 | CROUCH STAND 蹲伏-不動 | 135-153 | 0.5f | 5 |
13 | CROUCH WALK 蹲伏-行走 | 154-159 | 0.5f | 5 |
14 | CROUCH ATTACK 蹲伏-開火 | 160-168 | 0.5f | 5 |
15 | CROUCH_PAIN 蹲伏-被擊打 | 169-172 | 0.5f | 5 |
16 | CROUCH_DEATH 蹲伏-死亡 | 173-177 | 0.25f | 5 |
17 | DEATH_BACK 死亡-後趴 | 178-183 | 0.25f | 5 |
18 | DEATH_FORWARD 死亡-前趴 | 184-189 | 0.25f | 5 |
19 | DEATH_SLOW 緩慢死亡 | 190-197 | 0.25f | 5 |
現在需要MD2結構用於保存3D模型數據
MD2模型數據結構 | 簡述 |
MD2_ANIMATION animation_array[20] | 動畫,MD2通常有20種不同動作 |
int animation_num; | 動畫總量 |
int frame_num; | 幀量id softwarek規定198幀 |
MD2_POLYGON_PTR polygon_array; | 多邊形列表 |
int polygon_num; | 多邊形總量 |
VECTOR3D_PTR vertex_array; | 頂點數組 長度=幀總量*單幀頂點量 |
int vertex_num; | 單幀頂點量 |
VECTOR2D_PTR textcoord_array; | 紋理座標數組,只有一組,由所有幀共享 |
int textcoord_num; | 紋理座標總量 |
TEXTURE texture_array[8] | 紋理skin |
int texture_num; | 皮膚紋理圖總量 |
float radius_avg; | 平均半徑 |
float radius_max; | 最大半徑 |
在遊戲中低級單位都一至如步兵.需要單獨保存位置、面方、動畫等狀態.需要一個新結構對MD2進行封裝.以共享數據節約空間.並且為讓動畫流暢需要對關鍵幀進行插值(插入其它幀).
3D模型結構定義(MODEL3D) | 簡述 |
int state; | 狀態, 死亡/存活 |
int attr; | 屬性 |
int color; | 沒有紋理時使用顏色 RGB(255,255,255) |
VECTOR3D pos; | 位置 |
VECTOR3D rot; | 旋轉 |
int anim; | 動畫索引 |
bool loop; | 循環播放 |
float speed; | 動畫速度,(0.0f~1.0f)數值越小速度越慢,數值越大速度越快 |
float frame; | 當前幀索引 |
float irate; | 插幀頻率(0.0f~1.0f),1.0f表示不插幀 |
float count; | 插幀計數器 |
bool complete; | 動畫完成播放標記 |
VECTOR3D_PTR vertex_array; | 單幀頂點量 |
VECTOR3D_PTR normal_array; | 法線數組 |
TEXTURE_PTR texture; | 指向紋理 |
MD2_PTR md2; | 指向MD2模型 |
在載入數據之前還需要定義幾個頂點控制
#define MD2_INVERT_X 0x0001// 反轉X軸
#define MD2_INVERT_Y 0x0002// 反轉Y軸
#define MD2_INVERT_Z 0x0004// 反轉Z軸
#define MD2_SWAP_XY 0x0010// 交換XY軸
#define MD2_SWAP_YZ 0x0020// 交換YZ軸
#define MD2_SWAP_XZ 0x0040// 交換XZ軸
#define MD2_INVERT_WINDING_ORDER 0x0100 // 反轉環繞順序
基本結構已定義可以讀取數據並分釋:
bool Load_Data_MD2(MD2_PTR md2, PBYTE data, int size,float scale,DWORD flag ){
1.讀取MD2頭部,data為文檔數據
MD2_HEADER_PTR header = (MD2_HEADER_PTR)data;
2.判斷是否MD2模型
if (header->id != ‘2PDI’) // MD2文檔標記
return false;
3.判斷版本號總為8
if (header->version != 8)
return false;
4.幀總量id softwarek規定198幀
md2->frame_num = header->frame_num;
5.皮膚紋理總量設為0
md2->texture_num = 0;
4.多邊形總量
md2->polygon_num = header->polygon_num;
5.讀取幀頂點量
md2->vertex_num = header->vertex_num;
6.分配多邊形記憶體
md2->polygon_array = (MD2_POLYGON_PTR)malloc(md2->polygon_num*sizeof(MD2_POLYGON));
7.分配頂點記憶體
md2->vertex_array = (VECTOR3D_PTR)malloc(md2->frame_num * md2->vertex_num * sizeof(VECTOR3D));
8.分配紋理座標記憶體.以繪畫三角形進行排列
md2->textcoord_array = (VECTOR2D_PTR)malloc(md2->polygon_num * 3 * sizeof(VECTOR2D));
9.遍歷每一幀
for (int findex = 0; findex < md2->frame_num; ++findex){
MD2_FRAME_PTR frame;// 讀取幀
frame = (MD2_FRAME_PTR)(data+header->frame_offset + header->frame_size * findex);
10.遍歷每一頂點
for (int vindex = 0; vindex < md2->vertex_num; ++vindex){
VECTOR3D v ;
11.對頂點座標進行縮放和平移
v.x = frame->list[vindex].v[0] * frame->scale[0] + frame->translate[0];
v.y = frame->list[vindex].v[1] * frame->scale[1] + frame->translate[1];
v.z = frame->list[vindex].v[2] * frame->scale[2] + frame->translate[2];
12.跟據傳入參數進行縮放
if (scale != NULL)
v = v * scale;
13.反轉座標軸
if (flag & MD2_INVERT_X)
v.x = -v.x;
if (flag & MD2_INVERT_Y)
v.y = -v.y;
if (flag & MD2_INVERT_Z)
v.z = -v.z;
14.交換座標軸
float temp;
if (flag & MD2_SWAP_YZ)
SWAP(v.y, v.z, temp);
if (flag & MD2_SWAP_XZ)
SWAP(v.x, v.z, temp);
if (flag & MD2_SWAP_XY)
SWAP(v.x, v.y, temp);
15.將頂點插入列表中
md2->vertex_array[findex*md2->vertex_num + vindex] = v;
}
}
16.讀取紋理座標
MD2_TEXTCOORD_PTR textcoord = (MD2_TEXTCOORD_PTR)(data + header->textcoord_offset);
17.讀取多邊形
MD2_POLYGON_PTR polygon = (MD2_POLYGON_PTR)(data+header->polygon_offset);
18.遍歷多邊形
for (int pindex = 0; pindex<header->polygon_num; ++pindex){
if (flag & MD2_INVERT_WINDING_ORDER) {
19.反轉頂點環繞順序
md2->polygon_array[pindex].vertex_index[0] = polygon[pindex].vertex_index[2];
md2->polygon_array[pindex].vertex_index[1] = polygon[pindex].vertex_index[1];
md2->polygon_array[pindex].vertex_index[2] = polygon[pindex].vertex_index[0];
// 反轉紋理座標環繞順序
md2->polygon_array[pindex].textcoord_index[0] = polygon[pindex].textcoord_index[2];
md2->polygon_array[pindex].textcoord_index[1] = polygon[pindex].textcoord_index[1];
md2->polygon_array[pindex].textcoord_index[2] = polygon[pindex].textcoord_index[0];
}
else
{// 不改變頂點環繞順序
md2->polygon_array[pindex].vertex_index[0] = polygon[pindex].vertex_index[0];
md2->polygon_array[pindex].vertex_index[1] = polygon[pindex].vertex_index[1];
md2->polygon_array[pindex].vertex_index[2] = polygon[pindex].vertex_index[2];
// 不改變紋理座標環繞順序
md2->polygon_array[pindex].textcoord_index[0] = polygon[pindex].textcoord_index[0];
md2->polygon_array[pindex].textcoord_index[1] = polygon[pindex].textcoord_index[1];
md2->polygon_array[pindex].textcoord_index[2] = polygon[pindex].textcoord_index[2];
}
20.以繪畫順序讀取三角形頂點紋理座標,無需在提取幀是在生成.
int tindex0 = md2->polygon_array[pindex].textcoord_index[0];
int tindex1 = md2->polygon_array[pindex].textcoord_index[1];
int tindex2 = md2->polygon_array[pindex].textcoord_index[2];
21.讀取紋理座標轉換為0.0f~1.0f
md2->textcoord_array[pindex*3+0].u = (float)textcoord[tindex0].u / (float)header->skin_width;
md2->textcoord_array[pindex*3+0].v = (float)textcoord[tindex0].v / (float)header->skin_height;
md2->textcoord_array[pindex*3+1].u = (float)textcoord[tindex1].u / (float)header->skin_width;
md2->textcoord_array[pindex*3+1].v = (float)textcoord[tindex1].v / (float)header->skin_height;
md2->textcoord_array[pindex*3+2].u = (float)textcoord[tindex2].u / (float)header->skin_width;
md2->textcoord_array[pindex*3+2].v = (float)textcoord[tindex2].v / (float)header->skin_height;
}
22.遍歷每個頂點計算模型半徑
md2->radius_avg = 0;// 平均半徑
md2->radius_max = 0;// 最大半徑
for (int vindex = 0; vindex < header->vertex_num; ++vindex){
float dist;
dist = (float)sqrt(md2->vertex_array[vindex].x * md2->vertex_array[vindex].x +
md2->vertex_array[vindex].y * md2->vertex_array[vindex].y +
md2->vertex_array[vindex].z * md2->vertex_array[vindex].z);
md2->radius_avg = md2->radius_avg + dist;// 累加半徑
if (dist > md2->radius_max)
md2->radius_max = dist;// 最大半徑
}
23.計算平均半徑
md2->radius_avg = md2->radius_avg / header->vertex_num;//
23.載入默認動畫序列. md2_animations[]數組跟據上面動畫列表定義
for (int aindex = 0; aindex < (sizeof(md2_animations) / sizeof(MD2_ANIMATION)); ++aindex)
md2->animation_array[aindex] = md2_animations[aindex];
return true;
}
紋理需要另外載入,紋理高寬需為2x2x.如256256、128*128
bool Load_Texture_MD2(MD2_PTR md2, const char * path){
int index = md2->texture_num; // 索引
++md2->texture_num;// 紋理數量
Load_File_Texture(&md2->texture_array[index], path);// 載入
Bind_Image_Texture(&md2->texture_array[index]);// 綁定
return true;
}
3D模型動畫平滑運動需要幀插值.『當前幀』frame_curr帶有小數在0~127之間.按權重插值公式如下:
vi=v0(1-value) + v1value
逐幀提取函式:
void Extract_Frame_MD2(MD2_PTR md2,VECTOR3D_PTR vertex_array,VECTOR3D_PTR normal_array,float frame_curr){
MD2_POLYGON_PTR polyon;
int vindex;
1.判斷幀是否插值得到
float ivalue = frame_curr – (int)frame_curr;
if (ivalue == 0.0f) {//判斷是否為整數
2.若為整數則直接讀取
int frame = (int)frame_curr;
if (frame >= md2->frame_num)
frame = md2->frame_num – 1;
3.計算當前幀索引頂點偏移
int base = md2->vertex_num * frame;
4.遍歷每個多邊形
for (int pindex = 0, index = 0; pindex < md2->polygon_num; ++pindex, index= index+3){
5.讀取多邊形每個頂點
polyon = &md2->polygon_array[pindex];
vindex = polyon->vertex_index[0];
vertex_array[index+0] = md2->vertex_array[base + vindex];
vindex = polyon->vertex_index[1];
vertex_array[index + 1] = md2->vertex_array[base + vindex];
vindex = polyon->vertex_index[2];
vertex_array[index + 2] = md2->vertex_array[base + vindex];
6.計算三角形法線
Normal_VECTOR3D(&normal_array[pindex],&vertex_array[index + 0],&vertex_array[index + 1],&vertex_array[index + 2]);
}
}
else{
2.若有小數數則進行幀插值,讓動畫平滑
int frame0 = (int)frame_curr;
int frame1 = (int)frame_curr + 1;
if (frame0 >= md2->frame_num)
frame0 = md2->frame_num – 1;
if (frame1 >= md2->frame_num)
frame1 = md2->frame_num – 1;
int base0 = md2->vertex_num * frame0;
int base1 = md2->vertex_num * frame1;
3.偏曆三角形在兩幀之間插值計算頂點
for (int pindex = 0, index = 0; pindex < md2->polygon_num; ++pindex, index = index + 3){
4.讀取兩個多邊形頂點並行權重插值
polyon = &md2->polygon_array[pindex];
vindex = polyon->vertex_index[0];
vertex_array[index+0] = md2->vertex_array[base0 + vindex] * (1 – ivalue) +md2->vertex_array[base1 + vindex] * (ivalue);
vindex = polyon->vertex_index[1];
vertex_array[index + 1] = md2->vertex_array[base0 + vindex] * (1 – ivalue) +md2->vertex_array[base1 + vindex] * (ivalue);
vindex = polyon->vertex_index[2];
vertex_array[index + 2] = md2->vertex_array[base0 + vindex] * (1 – ivalue) + md2->vertex_array[base1 + vindex] * (ivalue);
5.計算三角形法線
Normal_VECTOR3D(&normal_array[pindex],&vertex_array[index + 0], &vertex_array[index + 1], &vertex_array[index + 2]);
}
}
}
載入MD2模型.讓MODEL3D
void Load_MD2_MODEL3D(MODEL3D_PTR model3D,MD2_PTR md2,int texture_index){
1.清零
memset(model3D, 0, sizeof(MODEL3D));
2.指向md2模型
model3D->md2 = md2;
3.分配每幀多邊形頂點記憶體
model3D->vertex_array = (VECTOR3D_PTR)malloc(md2->polygon_num * 3*sizeof(VECTOR3D));
4.分配每幀多邊形法線記憶體
model3D->normal_array = (VECTOR3D_PTR)malloc(md2->polygon_num * 3 * sizeof(VECTOR3D));
5.指向紋理
if (texture_index < md2->texture_num){
texture_index = md2->texture_num – 1;
model3D->texture = &md2->texture_array[texture_index];
}
}
設置模型動畫MD2有20個不同動作
void Set_Animation_MODEL3D(MODEL3D_PTR model3D,int anim,bool loop){
1.讀取MD2模型
MD2_PTR md2= model3D->md2;
2.設定當前動畫索引
model3D->anim = anim;
3.動畫是否循環播放
model3D->loop = loop;
4.動畫播放標記設為未完成
model3D->complete = false;
5.動畫
MD2_ANIMATION_PTR animation = md2->animation_array;
6.插幀頻率(0.0f~1.0f),1.0f表示不插幀
model3D->irate = animation[anim].irate;
7.當前幀
model3D->frame = animation[anim].start;
8.速度
model3D->speed = animation[anim].speed;
9.插幀計數器
model3D->count = 0;
10.提取動畫幀
Extract_Frame_MD2(model3D->md2,model3D->vertex_array, model3D->normal_array,model3D->frame);
}
在遊戲引擎中你需要更新動畫, time為時間間隔通過Get_Counter_FPS(&fps);獲得
void Update_MODEL3D(MODEL3D_PTR model3D,float time){
MD2_PTR md2 = model3D->md2;
MD2_ANIMATION_PTR animation = md2->animation_array;
1.計算動畫有幾多幀
int frame_num = animation->end – animation->start + 1;
2.計算每幀速度
float frame_speed = (float)frame_num / model3D->speed ;
3.當前幀帶小數以進行插值
model3D->frame = model3D->frame + frame_speed * time;
4.幀計數器控制插值
model3D->count = model3D->count + frame_speed * time;
5.判斷動畫是否播放完畢
if (model3D->frame > animation[model3D->anim].end) {
if (model3D->loop == MD2_LOOP) {// 循環播放動畫
model3D->count = 0;
model3D->frame = animation[model3D->anim].start;// 啟動幀
}
else{// 單次播放動畫
model3D->frame = animation[model3D->anim].end;// 結束幀
model3D->complete = true;// 以完成動作
}
}
- irate為插幀頻率(0.0f~1.0f), 1.0f表示不插幀
if (model3D->count >= model3D->irate || model3D->count == 0){
model3D->count = 0;//清零
8.提取動畫幀
Extract_Frame_MD2(model3D->md2, model3D->vertex_array,model3D->normal_array,model3D->frame);
}
}
每幀都要對3D模型進行渲染.這裡使用頂點數組進行渲染.當前你可以逐個三角形進行渲染但是會較慢.
void Render_MODEL3D(MODEL3D_PTR model3D){
1.當前矩陣堆棧壓棧
glPushMatrix();
- 移動多邊形位置並旋旋
glTranslatef(model3D->pos.x, model3D->pos.y, model3D->pos.z);// 移動
glRotatef(model3D->rot.x, 1.0f, 0.0f, 0.0f); // 繞X軸旋轉
glRotatef(model3D->rot.y, 0.0f, 1.0f, 0.0f); // 繞Y軸旋轉
glRotatef(model3D->rot.z, 0.0f, 0.0f, 1.0f); // 繞Z軸旋轉
3.壓入當前屬性
glPushAttrib( GL_DEPTH_BUFFER_BIT | GL_LIGHTING_BIT | GL_TEXTURE_BIT);
4.提取MD2模型
MD2_PTR md2 = model3D->md2;
5.綁定紋理
TEXTURE_PTR texture = model3D->texture;
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, texture->ID);
5.啟用頂點數組
glEnableClientState(GL_VERTEX_ARRAY);
6.啟用紋理座標數組
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
7.啟用法線數組
glEnableClientState(GL_NORMAL_ARRAY);
8.指定頂點數組
glVertexPointer(3, GL_FLOAT, 0, model3D->vertex_array);
9.紋理座標
glTexCoordPointer(2, GL_FLOAT, 0, md2->textcoord_array);
10.三角形法線
glNormalPointer(GL_FLOAT,0, model3D->normal_array);
11.繪畫所有當前以啟用的頂點數組
glDrawArrays(GL_TRIANGLES, 0, md2->polygon_num * 3 );
12.啟用頂點數組
glDisableClientState(GL_VERTEX_ARRAY);
14.啟用紋理座標數組
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
15.啟用法線數組
glDisableClientState(GL_NORMAL_ARRAY);
16.彈出當前屬性
glPopAttrib();
17.當前矩陣堆棧出棧
glPopMatrix();
}
MD2載入器程式:下載
- 可分別載入『角色』與『武器』MD2模型
- 紋理載入支持 『.PCX』『.BMP』『.TGA』
- 按ALT鍵彈出『MENU』點『ANIMATION』選切換不同的動畫.