Load file Quake 2 (.MD2) di delphi

pendahuluan

dalam membuat game khususnya dengan grafik 3 dimensi, salah satu komponennya adalah objek 3d dan animasinya. salah satu format objek 3 dimensi yang juga sudah mengandung animasi yang cukup populer (banyak didukung oleh engine baik yang sifatnya komersial maupun gratisan) adalah file model dari game Quake 2 (id Software). format file objek yang digunakan dalam md2 sudah mengandung rekaman animasi sehingga jika kita ingin menggunakan model tersebut cukup memutar ulang animasi dari frame f1 ke frame f2. keuntungan dari menggunakan model dengan animasi seperti format md2 ini (vertex morphing) adalah komputasi yang digunakan hanya interpolasi/kombinasi linier. sedangkan kelemahannya adalah penggunaan model seperti ini tentunya akan menghabiskan lebih banyak memori karena posisi titik(vertex) pada tiap frame disimpan. dibandingkan dengan teknik animasi lainnya (skeletal animation) yang posisi titik(vertex) pada tiap frame dihitung berdasarkan posisi tulang acuan. untuk pembahasan lebih lanjut tentang vertex morphing bisa dibaca di bukunya Jim AdamsAdvanced Animation With DirectX. walaupun di buku itu menggunakan DirectX, tapi di tulisan ini saya akan menggunakan openGL untuk mempermudah pemahaman dalam proses penggambaran dan penampilan di layar (subjektif memang..). dalam tulisan ini saya tidak akan menceritakan bagaimana menginisialisasi OpenGL untuk digunakan dalam Form VCL di delphi. tulisan tentang hal itu sudah pernah saya tulis untuk tutorial di situs delphi-id, jadi silakan cari disana saja karena saya belum sempat mengimpor ke dalam blog ini.😉 .

deskripsi format file md2

header file

file md2 merupakan file dalam format biner (non-teks). file md2 diidentifikasi dari 4 byte pertama file tersebut yang merupakan string karakter ‘IDP2’. jika kita menggunakan integer dalam delphi yang juga berukuran 4 byte maka akan dihasilkan ‘magic number’ yaitu 844121161. identitas ini akan kita gunakan untuk memeriksa apakah file yang kita buka merupakan file md2 yang kita maksud atau bukan. sebagai langkah awal, mari kita coba buat struktur record untuk header file md2.

type
TMD2Header = record
  ID                     : integer;
  Version              : integer;{ 8 }
  SkinWidth           : integer;
  SkinHeight          : integer;
  FrameSize          : integer;
  NumSkins           : integer;
  NumVertices       : integer;
  NumTexCoords    : integer;
  NumTris             : integer;
  NumGLCmd         : integer;
  NumFrames        : integer;
  OffsetSkins        : integer;
  OffsetTexCoords : integer;
  OffsetTris          : integer;
  OffsetFrames     : integer;
  OffsetGLCmd      : integer;
  FileSize             : integer;
end;

ID adalah identitas yang sudah diceritakan di atas (IDP2), Version adalah versi file MD2 (bisa diabaikan, biasanya bernilai 8). SkinWidth adalah lebar tekstur yang digunakan, SkinHeight adalah tinggi tekstur. FrameSize adalah banyak frame dalam animasi. NumSkins menyatakan jumlah skin/tekstur. NumVertices menyatakan jumlah vertex objek yang dikandung. NumTexCoords menyatakan jumlah koordinat tekstur. NumTris menyatakan jumlah segitiga yang membentuk objek. NumGLCmd (saya abaikan) jika anda membaca deskripsi format dari tempat lain elemen ini maksudnya untuk menyatakan penggambaran dengan cara lain (Triangle Strips/Fan) yang saya juga belum mengerti. untuk sementara bisa saya abaikan dan tidak terlalu berpengaruh dalam keberhasilan melakukan loading😀. OffsetSkins adalah offset(posisi) nama file tekstur. OffsetTexCoords adalah posisi awal data koordinat tekstur di dalam file. OffsetTris menyatakan posisi awal data segitiga. OffsetFrames adalah posisi awal data Frame. tiap frame berisi data kumpulan vertex objek pada frame tersebut. offsetGLCmd..? yah, karena GLCmd akan diabaikan maka yang ini juga saya abaikan. FileSize berisi ukuran file, elemen ini bisa digunakan untuk melakukan verifikasi file yang kita buka apakah corrupt atau tidak dengan membandingkan nilai elemen ini dengan ukuran file yang kita buka.

struktur data lainnya

struktur data utama yang lainnya yang perlu dibuat struktur record khusus adalah data frame dan segitiga. data frame berisi kumpulan vertex pada frame tersebut. sedangkan data segitiga berisi indeks ke koleksi vertex dan indeks ke koordinat tekstur segitiga tersebut.

TMD2Frame = record
  Scale         : TAffineVector;
  { terdefinisi sebagai array[0..2] of single 
  di Geometry.pas dari mike Lischke }
  Translation   : TAffineVector;
  Name          : string[16];
  Vertices      : array of TAffineVector;
  Normals       : array of TAffineVector;
end;

TMD2Tri = packed record
  VertexID  : array [0..2] of Word;
  TexID     : array [0..2] of Word;
end;

TMD2Vertex = TVector4b; { array[0..3] of byte }

tiap vertex disimpan dalam file md2 sebagai integer/byte. pemampatan seperti ini sangat perlu mengingat md2 menyimpan setiap titik untuk tiap frame. bayangkan saja kalau tiap vertex disimpan sebagai floating-point maka ukurannya dengan asumsi file md2 berisi 100 frame dan 1000 vertex maka dibutuhkan 100 * 1000 * 12 byte (tanpa kompresi) ~ 1,2 MB. bandingkan dengan kompresi seperti di atas hanya 100 * 1000 * 4 byte + 24 (overhead untuk faktor skala dan pergeseran) ~ 400KB. rasio kompresinya sampai 70%!. proses dekompresi tiap vertex yaitu dengan mengalikan dengan faktor skala (TMD2Frame.Scale) dan pergeseran/translasi (TMD2Frame.Translation). cara kompresinya adalah menghitung bounding box dari setiap vertex pada bounding box tersebut kemudian tiap vertex dibuat relatif terhadap titik pusat bounding box tersebut, lalu dibagi dengan panjang sumbu maksimum bounding box tsb untuk masing-masing elemen. saya ceritakan umumnya saja, nanti saya jabarkan lebih lanjut setelah posting ini.

kelas TMD2Mesh

TMD2Mesh = class(TInterfacedObject, IAnimatedMesh)
protected
  FMD2Header : TMD2Header;
  FSkinName : string;

  FTris : array of TMD2Tri;
  FFrames : array of TMD2Frame;
  FTexCoords : array of TVector2f; { array [0..1] of single }
  FVertex : array of TAffineVector;
  FTextureID : cardinal; { GLUint; }

  FMaxTime, FFrameDelay : single;
public
  constructor Create;
  destructor Free;

  procedure Draw(CurrentTime: single);{ draw mesh at specified time }

  procedure LoadFromFile(Filename:string);
  procedure LoadFromStream(Stream:TStream);

  procedure SetSpeed(AFPS : single);
end;

variabel-variabel di bawah ini digunakan untuk menyimpan data mesh yang akan kita

proses loading

dalam kelas TMD2Mesh, proses loading dibagi menjadi dua prosedur yaitu TMD2Mesh.LoadFromFile dan TMD2Mesh.LoadFromStream. yang akan digunakan adalah LoadFromFile tetapi kode proses loading akan ditempatkan di LoadFromStream (sedikit abstraksi).

procedure TMD2Mesh.LoadFromFile(Filename: string);
var fs : TFileStream;
begin
  fs := TFileStream.Create(Filename, fmOpenRead);
  LoadFromStream(fs);
  fs.Free;
end;

procedure TMD2Mesh.LoadFromStream(Stream: TStream);
var
  Buffer : array [1..64] of char;
  i, j : integer;
  MD2UV : TVector2w;{ array [0..1] of Word }
  MD2Vertex : TVector4b;
  V1, V2, V3 : TAffineVector;
begin

hal pertama yang dilakukan adalah membaca informasi header dari file

Stream.Read(FMD2Header, sizeof(FMD2Header));

lalu dilanjutkan dengan melakukan verifikasi terhadap informasi yang ada di header file berdasarkan ID dan ukuran file.

{ verifikasi }
if FMD2Header.ID <> IDP2 then begin
  MessageBox(0, PChar('Couldn''t load Mesh - Not an MD2 Format')
  , PChar('uMeshMD2 Unit'), MB_OK);
 exit;
end;

if FMD2Header.FileSize <> Stream.Size then begin
  MessageBox(0, PChar('Couldn''t load Mesh - File Corrupted')
  , PChar('uMeshMD2 Unit'), MB_OK);
  exit;
end;

jika verifikasi berhasil maka bisa dilanjutkan dengan melakukan alokasi sejumlah frame sebesar nilai FMD2Header.NumFrames dan mengalokasikan vertex

{ alloc frames }
SetLength(FFrames, FMD2Header.NumFrames);
for i := 0 to FMD2Header.NumFrames-1 do begin
  SetLength(FFrames[i].Vertices, FMD2Header.NumVertices);
  SetLength(FFrames[i].Normals, FMD2Header.NumTris);
end;

baca himpunan segitiga ke dalam array FTris

{ read tris }
SetLength(FTris, FMD2Header.NumTris);
Stream.Seek(FMD2Header.OffsetTris, soFromBeginning);
Stream.Read(FTris[0], FMD2Header.NumTris * sizeof(TMD2Tri));

baca tiap frame

{ read frames }
Stream.Seek(FMD2Header.OffsetFrames, soFromBeginning);
for i := 0 to FMD2Header.NumFrames-1 do begin
  { header pemampatan }
  Stream.Read(FFrames[i].Scale, Sizeof(TAffineVector));
  Stream.Read(FFrames[i].Translation, Sizeof(TAffineVector));

  { nama frame }
  FillChar(Buffer, Sizeof(Buffer), 0);
  Stream.Read(Buffer, 16);
  FFrames[i].Name := Buffer;

  { vertex }
  for j := 0 to FMD2Header.NumVertices-1 do begin
    { baca vertex dalam kondisi mampat }
    Stream.Read(MD2Vertex, Sizeof(MD2Vertex));

    { lakukan nirmampat/dekompresi }
    FFrames[i].Vertices[j][0] := (MD2Vertex[0] * FFrames[i].Scale[0] 
    + FFrames[i].Translation[0]);
    FFrames[i].Vertices[j][1] := (MD2Vertex[1] * FFrames[i].Scale[1] 
    + FFrames[i].Translation[1]);
    FFrames[i].Vertices[j][2] := (MD2Vertex[2] * FFrames[i].Scale[2] 
    + FFrames[i].Translation[2]);
  end;

  { hitung face normal }
  for j := 0 to FMD2Header.NumTris-1 do begin
    V1 := FFrames[i].Vertices[FTris[j].VertexID[0]];
    V2 := FFrames[i].Vertices[FTris[j].VertexID[1]];
    V3 := FFrames[i].Vertices[FTris[j].VertexID[2]];
    FFrames[i].Normals[j] := VectorCrossProduct(VectorAffineSubtract(V3, V1), 
    VectorAffineSubtract(V2, V1));
  end;
end;

baca koordinat tekstur. tiap elemen dalam koordinat tekstur juga disimpan sebagai bilangan bulat berukuran 2 byte (Word) untuk menghemat tempat. pemampatan ini

{ read Tex Coord }
SetLength(FTexCoords, FMD2Header.NumTexCoords);
Stream.Seek(FMD2Header.OffsetTexCoords, soFromBeginning);
for i := 0 to FMD2Header.NumTexCoords-1 do begin
  Stream.Read(MD2UV, sizeof(MD2UV));
  FTexCoords[i] := Point2f((MD2UV[0] / FMD2Header.SkinWidth), 
  1-(MD2UV[1] / FMD2Header.SkinHeight));
end;

selanjutnya baca nama file tekstur

if FMD2Header.NumSkins > 0 then begin
  Stream.Seek(FMD2Header.OffsetSkins, soFromBeginning);
  FillChar(Buffer, Sizeof(Buffer), 0);
  Stream.Read(Buffer, sizeOf(Buffer));
  FSkinName := Buffer;
  FLog.Add('Skin File Name : '+FSkinName);

  FMaterial.Ambient := Point4f(1, 1, 1, 1);
  FMaterial.Diffuse := Point4f(1, 0.2, 0.2, 1);
 
  if (FSkinName  <> '') and (FileExists(FSkinName)) then begin
    { LoadTexture ada di Textures.pas buatan Jan Horn }
    { Email       : jhorn@global.co.za }
    { Website     : http://home.global.co.za/~jhorn }
    LoadTexture(FSkinName, FMaterial.TextureID);
  end;
end;

proses loading selesai.😉

end; { LoadFromStream }

inisialisasi

sebelum digunakan ditentukan terlebih dahulu kecepatan pemutarannya.

procedure TMD2Mesh.SetSpeed (AFPS : single);
begin
  FMaxTime := High(FFrames) / AFPS;
  FFrameDelay := 1 / AFPS;
end;

penggambaran

procedure TMD2Mesh.Draw(CurrentTime : single);
var
  i, j : integer;
  v : TAffineVector;
  currTime : single;
  prevFrame, nextFrame : integer;
  fraction : single;
begin

hal pertama yang harus dilakukan adalah menentukan frame mana yang akan digambar. jika waktu saat ini berada di antara dua frame maka dilakukan interpolasi terhadap posisi titik di kedua frame tersebut.

if CurrentTime > FMaxTime then 
  currTime := FMaxTime;

prevFrame := Trunc(CurrentTime / FFrameDelay);
fraction := Frac(CurrentTime / FFrameDelay);
nextFrame := prevFrame+1;

if nextFrame>High(FFrames) then begin
  nextFrame := prevFrame;
  fraction := 0;
end;

set tekstur jika ada (hasil operasi LoadTexture akan menghasilkan handle tekstur dari openGL > 0)

if FTextureID <> 0 then begin
  glEnable(GL_TEXTURE_2D);  { Enable Texture Mapping }
  glBindTexture(GL_TEXTURE_2D, FTextureID);
end else
  glDisable(GL_TEXTURE_2D);

ini adalah kode utama yang melakukan penggambaran objek. setiap objek disusun atas himpunan segitiga tersambung (triangle mesh)

glPushMatrix;
glRotatef(-90, XVector[0], XVector[1], XVector[2]);
{ entah kenapa posisi vertex agak terputar.. ada yang tahu kenapa? }

for i := 0 to FMD2Header.NumTris-1 do begin
  glBegin(GL_TRIANGLES);

  { LERP ~ Linear Interpolation v1, v2, f -> v1 + (v2-v1)*(f) }
  v := VectorAffineLERP(FFrames[prevFrame].Normals[i], 
  FFrames[nextFrame].Normals[i], Fraction);
  glNormal3fv( @v );

  for j := 0 to 2 do begin
    { set koordinat tekstur }
    glTexCoord2fv( @FTexCoords[FTris[i].TexID[j]] );

    { set koordinat vertex }
    v := VectorAffineLERP(FFrames[prevFrame].Vertices[FTris[i].VertexID[j]],
    FFrames[nextFrame].Vertices[FTris[i].VertexID[j]], fraction); 
    glVertex3fv( @v );
  end;

  glEnd();
end;

glPopMatrix;

penutup
hah.. akhirnya selesai juga artikelnya. semoga bermanfaat untuk siapapun yang membaca

One comment

  1. birlin · Juni 27, 2012

    Tanks Gan… Ini Berguna…,Tapi ane Newbie Yang lagi mendalami Dhelpi Gan…,Mohon Bimbingan nya wat pengerjaan Game 3D menggunakan bahasa pemrograman dhelpi…,Ada Tutorial Awal nya gak gan.. wat bikin game 3D berbasis dhelpi.. Tanks gan….

Tinggalkan Balasan

Isikan data di bawah atau klik salah satu ikon untuk log in:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s