Marketing

DirectX tutorial za Delphi i Lazarus

Izaberi

DirectX tutorial za Delphi i Lazarus

Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Microsoft DirectX je vrlo mocan API ugradjen u Microsoft Windows operativni sistem koji nam omogucava lako (bolje receno relativno lako) pravljenje aplikacija koje koriste 3D grafiku, ulazne uredjaje kao sto su tastatura, mis, dzojstik, volan..., zvuk, mrezno igranje...

Vise o DirectX-u mozete procitati na Microsoft-ovom sajtu

Jedina mana DirectX-u je sto moze da radi samo na Windows operativnim sistemima, ali ako ste opredeljeni samo za Windows onda to nije nikakav problem.

Da biste koristili DirectX u Delphi-u potrebni su vam prevedeni header fajlovi i pomocni dll fajlovi.
Instalacija ovih fajlova je vrlo laka. Sve sto treba je da kopirate dll fajlove u Windows\System32 folder, a prevedene fajlove raspakujete u bilo koji folder i taj folder dodate u Library path u Delphi-u. To je sve Smile

U ovim tutorialima necemo koristiti VCL (forme, dugmice, labele i slicno) nego cist Win32 API i DirectX API. Da kasnije ne bi bilo problema sa Win32 API kodom prvo cemo nauciti kako se on koristi za kreiranje prozora.

Prvo sto treba da uradimo je da kreiramo prazan projekat (samo *.dpr fajl... sve *.pas fajlove izbacite iz projekta ako ih Delphi sam ubaci, a iz *.dpr fajla obrisite sav kod). Posto cemo koristiti Windows funkcije i poruke dodacemo sledeci kod
uses
  Windows, Messages;

Windows unit sadrzi procedure, funkcije, tipove, konstante... vezane za Windows, a u Messages sve sto je potrebno za rad sa Win porukama.

Da bi kreirali prozor moramo prvo da registrujemo klasu naseg prozora. Ova registracija omogucava Windows-u da zna kakav prozor zelimo da kreiramo (kakve ivice da ima, da li ima minimize, maximize i close dugme i slicno). Funkcija koja nam omogucuje registrovanje klase prozora je RegisterClassEx. Ova funkcija uzima samo jedan parametar tipa TWNDClassEx. Ovaj tip sadrzi sve potrebne podatke da Windows zna kako da radi sa prozorom koji zelimo da registrujemo.
TWNDClassEx = packed record
  cbSize: UINT;
  style: UINT;
  lpfnWndProc: TFNWndProc;
  cbClsExtra: Integer;
  cbWndExtra: Integer;
  hInstance: HINST;
  hIcon: HICON;
  hCursor: HCURSOR;
  hbrBackground: HBRUSH;
  lpszMenuName: PAnsiChar;
  lpszClassName: PAnsiChar;
  hIconSm: HICON;
end;


cbSize treba da popunimo velicinom ovog tipa pre nego sto prosledimo podatak funkciji. Ovo se vrlo lako radi i najbolje je da se uradi pre unosa ostalih podataka (da se ne zaboravi posle)
WndClass.cbSize := SizeOf(WndClass);

style definise neke od opcija koje odredjuju kako je klasa registrovana, kada prozor iscrtava svoj deo ekrana i slicno. Za sve flagove koji mogu biti u ovom podatku pogledajet Win32 API Help. Podatke koje cemo mi postaviti ce uticati na prozor tako da se iscrtava svaki put kada mu se promeni velicina... ako deo prozora bude zaklonjen nekim drugim prozorom, a zatim ponovo otkriven i tada ce, naravno, doci do crtanja (ovo ne treba da se definise nekim specijalnim flagom)
WndClass.style := CS_HREDRAW or CS_VREDRAW;

lpfnWndProc je procedura koja ce se pozivati svaki put kada Windows prosledi neku poruku nasem prozoru. Postavicemo je na WndProc (kasnije cemo napisati WndProc proceduru).
WndClass.lpfnWndProc := @WndProc;

cbClsExtra dozvoljava aplikaciji da kreira klasu sa dodatnim prostorom za cuvanje podataka. Ova mogucnost nam nece biti potrebna pa je necu posebno objasnjavati.

cbWndExtra dozvoljava aplikaciji da za svaki kreiran prozor ove klase ima dodatni prostor u kojem moze da upise podatke. I ova mogucnost na nece biti potrebna pa je necemo dalje objasnjavati. Kome su ova dva podatka bitna moze pogledati Win32 API Help.
Postavimo ova dva podatka na 0
WndClass.cbClsExtra := 0;
WndClass.cbWndExtra := 0;


hInstance podatak treba da sadrzi vrednost HInstance promenljive koja je definisana u System unit-u.
WndClass.hInstance := HInstance;

hIcon sadrzi ikonicu koja se vidi u gornjem levom uglu prozora. Ikona se ucitava funkcijom LoadIcon. Windows u sebi ima i neke specijalne ikone pa cemo iskoristiti jednu od njih (standardnu).
WndClass.hIcon := LoadIcon(0, IDI_APPLICATION);

hCursor sadrzi kursor koji ce se videti kad je mis preko prozora. Ucitava se funkcijom LoadCursor. Windows takodje ima i par standardnih kursora.
WndClass.hCursor := LoadCursor(0, IDC_ARROW);

hbrBackground je boja kojom ce prozor biti ispunjen... koristicemo istu kao i Delphi.
WndClass.hbrBackground := COLOR_BTNFACE + 1;

lpszMenuName je ime menija u *.res fajlu koji zelimo da nas prozor ima kada se kreira. Posto nam meni sada nije potreban stavicemo nil.
WndClass.lpszMenuName := nil;

lpszClassName sadrzi ime klase koju kreiramo... neka bude MojProzor.
WndClass.lpszClassName := 'MojProzor';

hIconSm je ikona koja se koristi za prikaz kada je potrebno iscrtati malu ikonicu. Ako ovaj parametar nije definisan onda se ikona iz hIcon koristi i za prikaz male ikonice sto je nama sasvim uredu.
WndClass.hIconSm := 0;

Sada nam jos samo preostaje da registrujemo klasu.
RegisterClassEx(WndClass);

Kada imamo registrovanu klasu mozemo da kreiramo prozor. Za to cemo koristiti funkciju CreateWindowEx.
function CreateWindowEx(
  dwExStyle: DWORD;
  lpClassName: PChar;
  lpWindowName: PChar;
  dwStyle: DWORD;
  X, Y, nWidth, nHeight: Integer;
  hWndParent: HWND;
  hMenu: HMENU;
  hInstance: HINST;
  lpParam: Pointer): HWND;


dwExStyle kontrolise kakav cemo prozor napraviti (da li ce imati kontrole u sebi, da li prihvata fajlove kada se puste iznad njega, da li je transparentan...). Za sve flagove pogledajte Win32 API Help.
Mi cemo koristiti:
WS_EX_OVERLAPPEDWINDOW

lpClassName ime klase koju zelimo da koristimo za nas prozor.
Koristimo nasu klasu:
'MojProzor'

lpWindowName text koji ce se videti u title bar-u.
Stavicemo:
'Prozor bez VCL'

dwStyle je slicno kao i dwExStyle. Mozemo definisati da li ce prozor bidi minimiziran po defaultu, disable-ovan, da li ce imati scroll bar (vertikalni, horizontalni),... Za sve flagove pogledati Win32 API Help.
Mi cemo postaviti ove flagove na:
WS_OVERLAPPEDWINDOW

X, Y, nWidth, nHeight predstavljaju poziciju i velicinu prozora.
Postavicemo prozor na 100, 100 sa sirinom 400 i visinom 300:
100, 100, 400, 300

hWndParent predstavlja prozor u koji cemo postaviti nas nov prozor. Posto ce nas prozor biti glavni ovaj parametar je 0.
0

hMenu predstavlja meni koji zelimo da prozor ima ako ne zelimo da koristimo onaj koji smo definisali u klasi prozora. Nama ne treba meni pa cemo i ovaj parametar postaviti na 0.
0

hInstance parametar je isti kao i kod registaracije klase. Pokazuje na HInstance promenljivu.
HInstance

lpParam parametar se prosledjuje prozoru kada se kreira. Moze sluziti za prenosenje bilo kojeg podatka, ali nama to nije bitno pa cemo ovaj parametar postaviti na nil.
nil
Na kraju dobijamo ovakav poziv:
Wnd := CreateWindowEx(
  WS_EX_OVERLAPPEDWINDOW,
  'MojProzor',
  'Prozor bez VCL',
  WS_OVERLAPPEDWINDOW,
  100, 100, 400, 300,
  0,
  0,
  HInstance,
  nil
);


Kada smo kreirali prozor, mozemo i da ga prikazemo
ShowWindow(Wnd, SW_SHOWDEFAULT);
UpdateWindow(Wnd);

ShowWindow uzima dva parametra. Prvi je prozor koji zelimo da prikazemo/sakrijemo, a drugi kaze sta zelimo da radimo.
UpdateWindow tera prozor da se iscrta.

Lepo nam ide za sad. Sada cemo napisati kod koji ce citati poruke koje Windows salje nasem prozoru i zatim objasniti taj deo koda.
while GetMessage(Msg, 0, 0, 0) do
  DispatchMessage(Msg);

Msg je promenljiva tipa TMsg koja ce sadrzati poruku koju nam Windows salje.
GetMessage uzima 4 parametra. Prvi je TMsg promenljiva u kojoj ce poruka biti smestena, drugi parametar je prozor za koji zelimo da uzmemo poruku (ako je ovaj parametar 0 tada primamo poruke svih prozora koje smo kreirali), a treci i cetvrti oznacavaju koje poruke zelimo da primimo. Ta dva parametra mozemo koristiti da uzmemo recimo samo poruke sa tastature ili misa, samo poruke za crtanje, itd... Kada su i jedan i drugi parametar 0 tada primamo sve poruke. Ova funkcija vraca True sve dok prozor ne kaze da je vreme za gasenje (pokazacemo kako se to radi).
DispatchMessage poziva proceduru koju smo definisali za klasu prozora i prosledjuje joj poruku.

Skoro da smo zavrsili. Kada se prozor zatvori ostaje nam da izbrisemo klasu iz memorije. To se radi funkcijom UnregisterClass.
UnregisterClass('MojProzor',HInstance);
Dva parametra koje uzima su ime klase koju zelimo da izbrisemo iz memorije (ne mozemo izbrisati klasu koju nismo registrovali) i HInstance promenljiva.

Sve sto nam ostaje je jos da napisemo WndProc funkciju. Ona mora biti posebnog oblika:
function WndProc(
  Wnd: HWnd;
  Msg: UINT;
  wParam: WPARAM;
  lParam: LPARAM
): LRESULT; stdcall;

Prvi parametar koji ce primiti je prozor za koji je funkcija pozvana, drugi je poruka koja se prosledjuje, a treci i cetvrti su podaci vezani za poruku i za svaku poruku imaju neko drugo znacenje.
Rezultat javlja Windows-u da li smo obradili poruku ili ne.
U ovom primeru cemo obraditi samo jednu poruku. Ona koju prozor dobija kada treba da se ugasi.
function WndProc(Wnd: HWnd; Msg: UINT;
  wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  case Msg of
    WM_DESTROY:
    begin
      PostQuitMessage(0);
      Result:= 0;
    end
  else
    Result:= DefWindowProc(Wnd, Msg, wParam, lParam);
  end;
end;

Poruka koja se dobija kada prozor treba da se zatvori je WM_DESTROY i tada nas prozor poziva PostQuitMessage funkciju. Ona oznacave da GetMessage treba da vrati False i time prekine petlju citanja poruka. Vrednost koju dajemo u PostQuitMessage funkciji se pojavljuje kao wParam u WM_QUIT poruci. Kada obradimo poruku rezultat postavljamo na 0 sto znaci da smo obradili poruku.
Pravilo je da se sve poruke koje ne obradimo proslede funkciji DefWindowProc koja ih obradjuje kako je to u windows-u po default-u definisano.

Iiiii... to je to... ovako se kreira prozor u cistom Win32 API Smile

Kreiranje prozora radimo ovako jer ce nam izvrsni fajl biti manji, a takodje i brzi sto je, uglavnom, bitno za aplikacije koje koriste DirectX (to su... igre Smile ).

Source (Delphi) | Source (Lazarus) | Bin

Registruj se da bi učestvovao u diskusiji. Registrovanim korisnicima se NE prikazuju reklame unutar poruka.
Idi na vrh
offline
  • Passwd 
  • Novi MyCity građanin
  • Pridružio: 15 Jan 2006
  • Poruke: 1
Uloguj se preko Facebook-a i postavi pitanje:
Hehe, odlican tutorial..bas mi se svidio! (cak sam se registrirao rad njega)
Dal se moze ocekivat nastavak tutoriala kako dodat objekte (button,edit...) na takav prozor i kako im napravit evente itd..

-sam prozor u delphiu (VCL) iznosi oko 400kb
-ovako napravljen prozor-15kb
c00l Wink

Samo tako, p0z Smile

Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
@Passwd
Nije ovo Win32 API tutorial nego DirectX tutorial. Win32 API cemo koristiti samo za kreiranje prozora i to je to. Kasnije cemo koristiti DirectX za kreiranje GUI (dugmici, labeli, liste...)... to je nesto najblize onoga sto zelis... ali lepse izgleda Smile

--------------------

Kada znamo kako da kreiramo prozor pocecemo sa osnovnim stvarima sa DirectX-om. Prvo cemo se okrenuti Direct3D delu. Ovaj deo DirectX-a je zaduzen za prezentaciju 3D i 2D scena. Sadrzi funkcije za kreiranje objekata, shadera, za formiranje matrica, itd...

Krenucemo sa tutorialima koji idu sa DirectX SDK. Prvi tutorial pokazuje kako se Direct3D inicijalizuje.

Klasa prozora kada je u pitanju prozor u kojm ce se prikazivati sadrazj koji crta Direct3D treba da izgleda ovako:
FillChar(WndClass, SizeOf(WndClass), 0);
WndClass.cbSize := SizeOf(WndClass);
WndClass.style := CS_CLASSDC;
WndClass.lpfnWndProc := @WndProc;
WndClass.hInstance := HInstance;
WndClass.hCursor := LoadCursor(0, IDC_ARROW);
WndClass.lpszClassName := 'MojD3DProzor';

style smo postavili na CS_CLASSDC sto omogucava pravilno crtanje kada vise thread-ova pokusa nesto da iscrta. Posto je DirectX multithread-ed onda je ova opcija ono sto mu treba.
Modifikovacemo kod iz prvog tutoriala tako da klasa bude definisana kao sto sam objasnio.

Da bi smo mogli da koristimo D3D funkcije moramo u uses deo dodati i Direct3D9 unit.

Bice nam potrebne jos 2 globalne promenljive:
pD3D: IDirect3D9;
pd3dDevice: IDirect3DDevice9;

Ove dve promenljive ce drzati podatke o Direct3D objektu i on objektu koji ce biti zaduzen za iscrtavanje svega.

Sada cemo dodati funkciju koja ce pokrenuti Direct3D. Nazvacemo je SetupD3D i kao rezultat ce vracati podatak tipa HRESULT.
function SetupD3D: HRESULT;

Direct3D objekat dobijamo vrlo lako... pozivom samo jedne funkcije:
pD3D := Direct3DCreate9(D3D_SDK_VERSION);

Sledece sto treba da uradimo je da kreiramo i objekat koji ce sve crtati (taj objekat se zove Direct3D Device ili skraceno D3DDevice). Da bi smo mogli da kreiramo D3DDevice potrebno je da prosledimo Direct3D objektu sta tacno zelimo. To prosledjujemo jednom promenljivom tipa TD3DPresentParameters.
TD3DPresentParameters = packed record
  BackBufferWidth: LongWord;
  BackBufferHeight: LongWord;
  BackBufferFormat: TD3DFormat;
  BackBufferCount: LongWord;
  MultiSampleType: TD3DMultiSampleType;
  MultiSampleQuality: DWORD;
  SwapEffect: TD3DSwapEffect;
  hDeviceWindow: HWND;
  Windowed: Bool;
  EnableAutoDepthStencil: Bool;
  AutoDepthStencilFormat: TD3DFormat;
  Flags: LongInt;
  FullScreen_RefreshRateInHz: LongWord;
  PresentationInterval: LongWord;
end;

BackBufferWidth i BackBufferHeight predstavljaju zeljenu sirinu i visinu backbuffer-a (backbuffer je deo memorije koji se koristi za pripremanje slike za prikaz).

BackBufferFormat oznacava format backbuffera.

BackBufferCount je broj backbuffera koje zelimo.

MultiSampleType oznacava nacin na koji ce slika da bude "ulepsana" (da se ne vide ostro ivice objekata).

MultiSampleQuality je kvalitet ulepsavanja slike.

SwapEffect odredjuje nacin na koji ce se backbufferi menjati.

hDeviceWindow je prozor koji ce prikazivati Direct3D sadrzaj.

Windowed oznacava da li ce prikaz biti fullscrene ili u prozoru.

EnableAutoDepthStencil govori Direct3D-u da li da automatski brine kako se iscrtavaju objekti u zavisnosti od udaljenosti od kamere.

AutoDepthStencilFormat odredjuje preciznost odredjivanja kada je EnableAutoDepthStencil = True.

Flags su razni flag-ovi koji uticu na unutrasnju organizaciju memorije u Direct3D.

FullScreen_RefreshRateInHz mora biti 0 ako se sve prikazuje u prozoru, a ako se prikazuje u fullscreen-u onda predstavlja zenjeni refresh.

PresentationInterval oznacava koliko brzo se iscrtava backbufer (da li se ceka refresh ili ne).

Za vise informacija pogledati ovde.

Mi cemo podesiti podatke tako da se skoro sve uzima od prozora u kojem ce se crtati:
  FillChar(d3dpp, SizeOf(d3dpp), 0);
  d3dpp.Windowed := True;
  d3dpp.SwapEffect := D3DSWAPEFFECT_DISCARD;
  d3dpp.BackBufferFormat := D3DFMT_UNKNOWN;


Sada kreiramo D3DDevice funkcijom pD3D.CreateDevice. Ona uzima 6 parametara.
Prvi parametar predstavlja graficku karticu koju zelimo da koristimo. Za ovaj tutorial cemo koristiti vrednost D3DADAPTER_DEFAULT sto znaci da koristimo primarnu graficku karticu.
Drugi parametar govori da li zelimo da koristimo hardversko ubranje ili ne ili da koristimo software-ski emulator koji podrzava sve mogucnosti Direct3D-a (jaaaaako sporo). Postavicemo ovu vrednost na D3DDEVTYPE_HAL sto znaci da cemo koristiti hardversko ubrzanje ako je moguce.
Sledeci parametar je prozor u kojem ce se crtati.
Cetvrti parametar predstavlja nacin na koji ce podaci biti obradjeni. Postavicemo ga na D3DCREATE_SOFTWARE_VERTEXPROCESSING za slucaj da neko ima malo stariju karticu tako da ce se obradjivanje vrsiti software-ski.
Poslednja dva parametra predstavljaju promenljivu u kojoj smo rekli kakav D3DDevice zelimo i promenljivu u kojoj ce D3DDevice biti smesten.
Za vise informacija pogledajte ovde.
pD3D.CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, Wnd,
  D3DCREATE_SOFTWARE_VERTEXPROCESSING,
  @D3DPP, pd3dDevice);


Uvek je dobra praksa proveravati rezultat funkcija DirectX-a jer je moguce da ne uspeju da izvrse ono sto se zahteva tako da ce nasa konacna funkcija izgledati ovako:
function SetupD3D: HRESULT;
var
  D3DPP: TD3DPresentParameters;
begin
  Result := E_FAIL;

  pD3D := Direct3DCreate9(D3D_SDK_VERSION);
  if pD3D = nil then
    Exit;

  FillChar(D3DPP, SizeOf(D3DPP), 0);
  D3DPP.hDeviceWindow := Wnd;
  D3DPP.Windowed := True;
  D3DPP.SwapEffect := D3DSWAPEFFECT_DISCARD;
  D3DPP.BackBufferFormat := D3DFMT_UNKNOWN;

  Result :=
    pD3D.CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, Wnd,
      D3DCREATE_SOFTWARE_VERTEXPROCESSING,
      @D3DPP, pd3dDevice);

  if FAILED(Result) then
  begin
    Result := E_FAIL;
    Exit;
  end;

  Result := S_OK;
end;


Ovu funkciju pozivamo odmah posle kreiranja prozora i ako se sve zavrsi kako treba obradjujemo poruke prozora:
if SUCCEEDED(SetupD3D) then
begin
  ShowWindow(Wnd, SW_SHOWDEFAULT);
  UpdateWindow(Wnd);

  while GetMessage(Msg, 0, 0, 0) do
    DispatchMessage(Msg);
end;


Kada imamo D3DDevice mozemo da pocnemo i da crtamo. Za sada cemo samo da obojimo ceo prozor jednom bojom... recimo crvenom. Napisacemo funkciju Oboji koja ce to raditi i zatim je objasniti.
procedure Oboji;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,0,0), 1.0, 0);
  pd3dDevice.Present(nil, nil, 0, nil);
end;

Funkcija pd3dDevice.Clear sluzi za popunjavanje buffer-a odredjenom vrednoscu.
Drugi parametar predstavlja niz TRect promenljivih koje oznacavaju koji deo buffer-a treba da se obrise. Ako je ovaj parametar nil tada se brise ceo buffer.
Prvi parametar oznacava koliko TRect objekata ima u drugom parametru.
Treci parametar oznacava sta treba da se obrise (pozadina, z-buffer ili stencil-buffer).
Cetvrti parametar je bitan samo ako se birse pozadina i odredjuje kojom bojom da se pozadina obrise.
Peti je bitan samo kada se brise z-buffer i odredjuje verdnost koja treba da se upise u z-buffer.
Sesti je bitan samo kada se brise stencil-buffer.
Za vise informacija pogledajte ovde.

pd3dDevice.Present funkcija prikazuje buffer na ekran ili u prozor.
Prvi, drugi i cetvrti parametar moraju da budu nil osim ako je D3DDevice kreiran tako da koristi D3DSWAPEFFECT_COPY nacin menjanja backbuffer-a i tada oznacavaju delove koji trebaju da budu iscrtani.
Treci parametar moze predstavljati novi prozor u koji zelimo iscrtati ono sto je Direct3D iscrtao.
Za vise informacija pogledajte ovde.

Da bi se iscrtavalo ono sto DirectX crta moramo dodati i obradu poruke WM_PAINT:
WM_PAINT:
begin
  Oboji;
  ValidateRect(Wnd, nil);
  Result := 0;
end;

Pozivamo Oboji i time se sve iscrtava, ali Windows ne zna koji smo deo iscrtali pa mu funkcijom ValidateRect to saopstavamo.

Ostalo je jos samo da oslobodimo memoriju kada zavrsimo sa radom.
procedure CloseD3D;
begin
  pd3dDevice:= nil;
  pD3D:= nil;
end;

Ovo je sasvim dovoljno da se Direct3D i D3DDevice objekti izbrisu iz memorije. Ovu funkciju bi trebalo dodati pre gasenja prozora:
WM_DESTROY:
begin
  CloseD3D;
  PostQuitMessage(0);
  Result:= 0;
end;


To je to Smile Kada pokrenemo program imamo crvenu pozadinu koju je iscrtao Direct3D Smile



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 16 Jan 2006 19:33


Ovog puta cemo koristiti Vertex Buffer za iscrtavanje poligona. Pixel je tacka u 2D prostoru (X i Y koordinata), a Vertex je tacka u 3D prostoru (X, Y i Z koordinata). U sustini vertex buffer je niz vertex-a koje ce Direct3D da iscrta na neki od mogucih nacina (kao tacke, kao linije (spaja 2 tacke) ili kao trougao(spaja 3 tacke)).

Menjacemo kod koji smo imali u proslom tutorialu. Promenljiva koja ce sadrzati vertex buffer ce biti tipa IDirect3DVertexBuffer9 i postavicemo je kao globalnu promenljivu.
var
  pVB: IDirect3DVertexBuffer9;


Direct3D koristi Flexible Vertex Format (FVF). To omogucava da u vertex-u prosledimo samo podatke koje zelimo. Za vise informacija o FVF pogledajte ovde. U ovom primeru cemo koristiti transformisane vertex-e sto znaci da se u obzir uzimaju X i Y koordinate i predstavljaju prave koordinate (Direct3D ih nece menjati). Vertex ce takodje sadrzati i podatak o boji. Napravicemo tip podatka koji ce cuvati te podatke:
type
  TCustomVertex = packed record
    X, Y, Z, RHW: Single;
    Color: DWORD;
  end;

Svi podaci su jasni... X, Y su koordinate na kojima ce vertex biti na ekranu, Z i RHW parametri ne uticu na poziciju vertex-a i Color je boja. (Ne znam sta je tacno RHW parametar, ali je obavezan i skoro uvek se postavlja na 1).

Imamo spreman tip podatka koji cemo ubaciti u vertex buffer... treba nam samo jos i nacin da objasnimo buffer-u koje mu podatke dajemo. To se radi tako sto u jednoj promenljivoj stavljamo sta ce sve vertex sadrzati:
const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZRHW or D3DFVF_DIFFUSE);

D3DFVF_XYZRHW konstantom oznacavamo koje cemo koordinate koristiti, a D3DFVF_DIFFUSE govori da uz koordinatu ide i boja. To je dovoljno da vertex buffer zna sta mu dajemo.

Sada imamo sve sto nam je potrebno da popunimo vertex buffer podacima koje zelimo. Podatke cemo spremiti u jednu promenljivu:
const
  Vertices: array[0..2] of TCustomVertex = (
    (X : 200.0; Y : 20.0; Z : 1; RHW : 1; Color: $ffffff00),
    (X : 350.0; Y : 250.0; Z : 1; RHW : 1; Color: $ff00ff00),
    (X : 50.0; Y : 250.0; Z : 1; RHW : 1; Color: $ff0000ff)
  );
Napisacemo funkciju PopuniBuffer koja ce to uraditi:
function PopuniBuffer: HRESULT;
var
  pVertices: Pointer;
begin
  Result:= E_FAIL;

  if FAILED(
    pd3dDevice.CreateVertexBuffer(
      3 * SizeOf(TCustomVertex),
      0, D3DFVF_CUSTOMVERTEX,
      D3DPOOL_DEFAULT, pVB, nil)) then
    Exit;

  if FAILED(
    pVB.Lock(0, SizeOf(Vertices), pVertices, 0)) then
  Exit;

  CopyMemory(pVertices, @Vertices, SizeOf(Vertices));
  pVB.Unlock;

  Result:= S_OK;
end;

Vertex buffer kreiramo funkcijom pd3dDevice.CreateVertexBuffer.
Prvi parametar je velicina buffer-a. Zelimo da u nasem buffer-u stane bar 3 vertexa i zato rezervisemo memoriju velicine 3 puta vecu od jednog vertex-a.
Drugi parametar predstavlja nacin na koji ce Direct3D postupati sa buffer-om.
Treci parametar je promenljiva koja objasnjava nas vertex format.
Cetvrti parametar oznacava gde ce buffer biti kreiran.
Sledeci parametar je promenljiva gde ce biti smesten buffer kada se kreira.
Poslednji mora biti nil.
Za vise informacija o parametrima pogledati ovde.
Kada se buffer kreira uzimamo pointer koji pokazuje na memoriju koji buffer koristi da bi mogli da je popunimo nasim podacima. To se radi funkcijom pVB.Lock.
Prvi parametar odredjuje na koji element u nizu ce pointer da pokazuje. Tako mozemo da popunimo samo elemente od recimo 3 do 6 vertex-a (pod uslovom da vertex buffer ima mesta za bar 6 vertex-a).
Drugi parametar govori koliko elemenata zelimo da uzmemo.
Treci je pointer u kojem ce biti adresa gde se nalaze podaci.
Poslednji odredjuje kako ce podaci biti dostupni korisniku (read-only, automatski se brisu...).
Za vise informacija pogledati ovde.

Kada imamo pointer tada smo kopiramo podatke na tu memorijsku lokaciju. Kada zavrsimo sa tim pozivamo funkciju pVB.Unlock i tako javljamo da smo zavrsili.

Ovu funkciju cemo pozvati odmah posle kreiranja D3DDevice.

Na red konacno dolazi crtanje. Promenucemo funkciju Oboji da izgleda ovako:
procedure Oboji;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,0,0), 1.0, 0);

  if (SUCCEEDED(pd3dDevice.BeginScene)) then
  begin
    pd3dDevice.SetStreamSource(0, pVB, 0, SizeOf(TCustomVertex));
    pd3dDevice.SetFVF(D3DFVF_CUSTOMVERTEX);
    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
    pd3dDevice.EndScene;
  end;

  pd3dDevice.Present(nil, nil, 0, nil);
end;

Pre nego sto pocnemo sa crtanjem potrebno je da pozovemo pd3dDevice.BeginScene, a kada zavrsimo pd3dDevice.EndScene.
Da bi iscrtali buffer prvo moramo da ga selektijemo. To se radi SetStreamSource funkcijom.
Posto Direct3D podrzava rad sa vise buffer-a prvi parametar je mesto gde ce buffer biti.
Drugi parametar je buffer.
Treci govori od kog elementa u nizu da se pripremi buffer tako da ne moramo ako necemo da uzimamo ceo buffer.
Cetvrti je velicina jednog vertexa.
Vise informacija mozete naci ovde.
Sledece je izabir tipa vertexa. To radi funkcija pd3dDevice.SetFVF koju ne treba posebno objasnjavati.
Poslednje je, konacno, crtanje Smile pd3dDevice.DrawPrimitive uzima 3 parametra:
Prvi je nacin crtanja vertex-a.
Drugi predstavlja od kog elementa se pocinje sa crtanjem.
Poslednji odredjuje koliko se elemenata (ne vertex-a) iscrtava (ovaj broj zavisi od prvog parametra).
Za vise informacija pogledati ovde.

Ostaje nam jos da na kraju obrisemo buffer.
procedure CloseD3D;
begin
  pVB := nil;
  pd3dDevice := nil;
  pD3D := nil;
end;


Evo prvog trougla u Direct3D Smile



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 17 Jan 2006 21:10


Radili smo sa 2D koordinatama, a sad cemo raditi sa 3D koordinatama i matricama. Kod 3D koordinata je potrebno izvrsiti transformacije kako bi se one prevele u 2D koordinate. Za to nam sluze matrice. Postoji nekoliko vrsta matrica u Direct3D-u. Matrica koja postavlja objekat koji zelimo da crtamo na odredjenu poziciju se zove World matrica. Na osnovu nje se zna gde, u kom polozaju i koje velicine objekat treba da se nalazi u 3D svetu. Sledeca matrica je Camera ili View matrica. Ona odredjuje gde se nalazi posmatrac i tako odredjuje gde se objekat nalazi u odnosu na posmatraca. Poslednja matrica koju cemo koristiti je Projection matrica i njen zadatak je da do sada obradjenu 3D koordinatu prevede u 2D koordinatu za prikaz na ekranu. Mozda sada deluje komplikovano, ali na Direct3D olaksava posao jer sve sam racuna, a za popunjavanje matrica nam na raspolaganju stoji Direct3DX sa svojim pomocnim funkcijama i tipovima.

Pocnimo sa radom. Prvo sto cemo uraditi je izmena FVF definicije vertex-a. Koristicemo 3D koordinatu i boju:
type
  TCustomVertex = packed record
    X, Y, Z: Single;
    Color: DWORD;
  end;

const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZ or D3DFVF_DIFFUSE);

  Vertices: array[0..2] of TCustomVertex = (
    (X : -1; Y : -1; Z : 0; Color: $ffffff00),
    (X : 1; Y : -1; Z : 0; Color: $ff00ff00),
    (X : 0; Y : 1; Z : 0; Color: $ff0000ff)
  );


Ok... to smo resili. Sledeca stvar koju treba znati je da Direct3D koristi pravilo desne ruke da odredi koja strana poligona je gornja i po default-u iscrtava samo nju. Posto cemo mi rotirati nas trougao doci ce i trenutak kada ce on biti okrenut tako da vidimo njegovu donju stranu. Da bi i nju Direct3D iscrtao moramo podesiti jedan parametar:
pd3dDevice.SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
Ovo radimo kada kreiramo D3DDevice. D3DDevice ima dosta parametara koji odredjuju kako se radi sa svetlima, da li se koristi transparentnost i slicno... medju njima je i kontrola da li se prikazuje poligon kada je gornja strana prema kameri (moze da se bira da li se koristi pravilo leve ili desne ruke) ili da se crtaju i jedna i druga strana. Pozivom funkcije smo podesili da se prikazuju obe strane.
Jos jedna od default vrednosti je da boja 3D vertex-a zavisi od svetla. Posto jos necemo da koristimo svetla, iskljucicemo i tu opciju tako da se koristi boja koju smo mi postavili bez modifikovanja:
pd3dDevice.SetRenderState(D3DRS_LIGHTING, iFalse);
Za spisak svih parametara pogledati ovde.

Sada cemo napisati proceduru koja ce podesavati matrice, ali pre nego sto pocnemo ubacicemo u uses listu jos 2 unit-a.
uses
  Windows, Messages, MMSystem, Direct3D9, D3DX9;

MMSystem cemo koristiti za merenje vremena, a D3DX9 nam daje funkcije i tipove uz pomoc kojih ce nam biti lakse da radimo sa matricama.
Funkcija koja ce ih kreirati ide ovako:
procedure Rotiraj;
var
  matWorld: TD3DMatrix;
  iTime: LongWord;
  fAngle: Single;

  vEyePt, vLookatPt, vUpVec: TD3DVector;
  matView: TD3DMatrix;
  matProj: TD3DMatrix;
begin
  iTime := timeGetTime mod 10000;
  fAngle := iTime * (2 * D3DX_PI) / 10000;
  D3DXMatrixRotationY(matWorld, fAngle);
  pd3dDevice.SetTransform(D3DTS_WORLD, matWorld);

  vEyePt:= D3DXVector3(0, 3, -5);
  vLookatPt:= D3DXVector3(0, 0, 0);
  vUpVec:= D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEyePt, vLookatPt, vUpVec);
  pd3dDevice.SetTransform(D3DTS_VIEW, matView);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    400 / 300, 1, 100);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, matProj);
end;

matWorld, matView i matProj ce sadrzati matrice o kojima smo pricali na pocetku.
matWorld u ovom slucaju postavljamo tako da ona rotira nas objekat (trougao) oko Y ose. Posto smo vertex-e trougla postavili tako da su donja leva i donja desna tacka jednako udaljene od centra, a gornja tacka se nalazi tacno na sredini, izgledace kao da se trougao okrece oko sopstvene ose (ako zelite, mozete pomeriti koordinate vertex-a za, recimo, 0.2 i videcete kako ce se onda trougao rotirati).
timeGetTime vraca vreme u milisekundama od kada je Windows pokrenut. Na osnovu tog vremena izracunavamo ugao rotacije.
D3DXMatrixRotationY funkcija puni matricu podacima za rotaciju oko Y ose za zeljeni ugao. To je to za matWorld.

Sada postavljamo kameru. Trebaju nam tacka u kojoj se nalazi kamera, tacka u koju kamera gleda i vektor koji nam oznacava gde je "gore" (kamera moze da je okrenuta naopako pa je "gore" ustvari dole, ili da je kamera oborena na neku stranu pa je "gore" ustvari ta strana i slicno).
vEyePt ce sadrzati podatak o poziciji kamere i postavicemo je horiznotalno na sredini (3), vertikalno malo gore (3) i malo nazad (-5) jer nam se trougao nalazi na udaljenosti 0 po Z osi, a World matrica ga nece pomerati odatle nego cemo ga samo rotirati.
vLookatPt pokazuje na centar (0, 0, 0) trougla i tu ce kamera gledati.
vUpVec je postavljen na 0, 1, 0 sto znaci da je gore u pozitivnom smeru Y ose.
Kada imamo sve te podatke mozemo popuniti matricu funkcijom D3DXMatrixLookAtLH.
Za matricu projekcije cemo koristiti perspektivu. D3DXMatrixPerspectiveFovLH funkcija vraca rezultat u prvom parametru. Drugi parametar oznacava koliko siroko kamera vidi u Y pravcu u radijanima. Standardna vrednost je D3DX_PI / 4. Treci parametar je odnos sirine i visine dela ekrana u kojem se prikazuje ono sto kamera vidi. Poslednja dva parametra oznacavaju od koliko blizu ispred sebe do koliko daleko ispred sebe kamera moze da vidi. Sve sto je blize od minimalne daljine ili je iza kamere, ili je dalje od maximalne daljine se nece videti. Bitno je da (minimalna daljina) / (maximalna daljina) ne bude previse blizu nule jer ce se mozda pojaviti greska u racunanju pozicije vertex-a zbog preciznosti realnih brojeva u racunaru.
Matrice koje smo kreirali se postavljaju funkcijom pd3dDevice.SetTransform. Na osnovu prvog parametra Direct3D zna koju mu matricu prosledjujemo u drugom parametru. Ova funkcija ne mora posebno da se objasnjava.
Funkciju Rotiraj cemo pozivati odmah pre crtanja buffer-a tako da ce matrice biti spremne i uticace na izgled 3D scene.

Ovaj primer, za razliku od prethodnih, konstantno treba da se iscrtava. Posto GetMessage funkcija koja uzima poruke zaustavlja program dok ceka da joj Windows nesto posalje, crtanje nece moci da se odvija neometano. Zbog toga cemo malo modifikovati deo koda za citanje poruka:
FillChar(Msg, SizeOf(Msg), 0);
while (Msg.message <> WM_QUIT) do
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
    DispatchMessage(Msg)
  else
    Oboji;
end;

Koristicemo PeekMessage funkciju koja ce da procita poruku i smesti je u Msg promenljivu i vratiti True. Tako da ce poruka da se obradi ako je ima, a ako je nema iscrtace se nasa 3D scena. Posto ce iscrtavanje da ovde da se vrsi, nema vise potrebe da imamo obradu WM_PAINT poruke pa mozemo da je izbrisemo iz koda.

To je sve za ovaj put. Pokrenite program... iii... ipak se okrece Smile



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 22 Jan 2006 16:06


U ovom tutorialu cemo raditi sa svetlima i materijalima. Postoje nekoliko vrsta svtela.
Ambijentalno (ambient) svetlo osvetljava svaki vertex bez obzira gde se nalazi.
Svetlo koje dolazi iz jednog pravca (directional). Ono je kao Sunceva svetlost... dolazi negde iz daleka pod nekim uglom i osvetljava samo vertexe koji su okrenuti prema svetlu.
Svetlo koje obasjava sve oko sebe (point).
Svetlo koje se ponasa kao baterijska lampa (spot).

Za koriscenje svetla potrebno je da svaki verex pored informacije o koordinatama ima jos jedan podatak. To je podatak koji govori na koju stranu je vertex okrenut. Na osnovu tog podatka moze da se izracuna koliko svetlosti pada na njega i koja ce biti njegova konacna boja.

Materijal sadrzi informacije o boji kojom ce objekat biti iscrtan. Moze sadrzati boji koja zavisi od ambijentalnog svetla, koja zavisi od difuznog svetla (to su sva svetla koja nisu ambijentalna), odsjaj na objektu, svetlost koja izgleda kao da izlazi iz objekta.
Koristicemo materijale i zbog toga nece biti potrebno da definisemo boju za svaki vertex posebno kako smo do sada radili.

Posto smo do sada presli osnovne stvari, necu se vise na njima toliko detaljno zadrzavati i pisacu samo onaj deo koda koji je bitan za objasnjavanje. Konacan kod ce biti prikacen uz tutorial.

Da krenemo sa kodom. U ovom primeru cemo crtati malu cev u kojoj ce se nalaziti izvor svetlosti. Posto ce u ovom slucaju doci do slucajeva kada ce jedan verex biti iza drugog i zbog toga ne bi trebalo da se vidi ukljucicemo upotrebu ZBuffer-a koji ce vrsiti proveru vidljivosti tacaka koje se iscrtavaju.
D3DPP.EnableAutoDepthStencil := True;
D3DPP.AutoDepthStencilFormat := D3DFMT_D16;

pd3dDevice.SetRenderState(D3DRS_ZENABLE, iTrue);


Format vertex-a ce sada biti ovakav:
type
  PCustomVertex = ^TCustomVertex;
  TCustomVertex = packed record
    Position: TD3DVector;
    Normal: TD3DVector;
  end;

  PCustomVertexArray = ^TCustomVertexArray;
  TCustomVertexArray = array [0..MaxInt div SizeOf(TCustomVertex)-1] of TCustomVertex;

const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZ or D3DFVF_NORMAL);


Sada, kao sto vidite, pored pozicije, imamo i normalu za svaki vertex (TD3DVector tip sadrzi 3 podatka, X, Y i Z koordinatu).
Dodali smo i jos par tipova da nam omoguce lakse popunjavanje podataka. Sluzice nam da buffer mozemo da gledamo kao niz podataka tipa TCustomVertex.

Buffer cemo popuniti ovako:
function PopuniBuffer: HRESULT;
var
  I: Integer;
  Ugao: Single;
  pVertices: PCustomVertexArray;
begin
  Result:= E_FAIL;

  if FAILED(
    pd3dDevice.CreateVertexBuffer(
      50 * 2 * SizeOf(TCustomVertex),
      0, D3DFVF_CUSTOMVERTEX,
      D3DPOOL_DEFAULT, pVB, nil)) then
    Exit;

  if FAILED(
    pVB.Lock(0, SizeOf(Vertices), pVertices, 0)) then
  Exit;

  for I := 0 to 49 do
  begin
    Ugao := (2 * D3DX_PI * I) / (50 - 1);
    pVertices[2 * I].Position := D3DXVector3(Sin(Ugao), -1, Cos(Ugao));
    pVertices[2 * I].Normal := D3DXVector3(Sin(Ugao), 0, Cos(Ugao));
    pVertices[2 * I + 1].Position := D3DXVector3(Sin(Ugao), 1, Cos(Ugao));
    pVertices[2 * I + 1].Normal := D3DXVector3(Sin(Ugao), 0, Cos(Ugao));
  end;

  pVB.Unlock;

  Result:= S_OK;
end;

Popunjavamo buffer tako sto postavljamo jedan vertex na dnu cevi i zatim jedan na vrhu. Tako idemo u krug sve dok ne stignemo na pocetak. Normalu postavljamo tako da je svaki vertex okrenut vertikalno prema centru objekta tako da ce svaki gledati u svetlo i bice obasjan.

Imamo objekat koji cemo da crtamo... sada da dodamo materijal i svetlo.
procedure Svetlo;
var
  Mtrl: TD3DMaterial9;
  vecDir: TD3DXVector3;
  Light: TD3DLight9;
begin
  ZeroMemory(@Mtrl, SizeOf(TD3DMaterial9));
  Mtrl.Diffuse.r := 1; Mtrl.Ambient.r := 1;
  Mtrl.Diffuse.g := 1; Mtrl.Ambient.g := 1;
  Mtrl.Diffuse.b := 0; Mtrl.Ambient.b := 0;
  Mtrl.Diffuse.a := 1; Mtrl.Ambient.a := 1;
  pd3dDevice.SetMaterial(Mtrl);

  ZeroMemory(@Light, SizeOf(TD3DLight9));
  Light._Type := D3DLIGHT_DIRECTIONAL;
  Light.Diffuse.r := 1;
  Light.Diffuse.g := 1;
  Light.Diffuse.b := 1;
  vecDir :=
    D3DXVector3(Cos(timeGetTime / 350),
      1,
      Sin(timeGetTime / 350)
    );
  D3DXVec3Normalize(Light.Direction, vecDir);
  Light.Range := 1000;
  pd3dDevice.SetLight(0, Light);
  pd3dDevice.LightEnable(0, True);
end;

TD3DMaterial9 tip sadrzi podatke o materijalu. Ovog puta cemo koristiti samo boju koja se vidi kada se svetli na objekat (nema odsjaja i slicnih stvari). Skoro uvek se ambijentala i difuzna boja postavljaju na istu i ona predstavlja boju zeljenog objekta. Nas ce biti zut.
Kada kreiramo materijal postavljamo ga funkcijom pd3dDevice.SetMaterial i svi objekti koji se budu crtali ce imati taj materijal sve dok se on ne promeni.

TD3DLight9 sadrzi informacije o svetlu. Mi cemo kreirati DIRECTIONAL svetlo (dolazi iz daljeine i ima neki odredjeni ugao pod kojim pada na objekte). Posto podesimo tip i boju svetla (postavili smo je na belu) postavljamo i ugao pod kojim pada na scenu. Iskoristicemo Sin i Cos funkciju da simuliramo okretanje svetla. D3DXVec3Normalize funkcijom vektor koji smo izracunali svodimo na vektor intenziteta 1 i postavljamo ga. Ovaj tip svetla ima i svojstvo Range koje oznacava koliko daleko svetlost osvetljava predmete. Postavili mo ga na veliku vrednost tako da se svi vertex-i osvetle.
Posto Direct3D podrzava vise izvora svetlosti kada postavljamo svetlo moramo navesti i koje je to svetlo po redu. Funkcijom pd3dDevice.SetLight bas to radimo. Kada postavimo svetlo ostaje nam jos samo da ga ukljucimo funkcijom pd3dDevice.LightEnable.

Da bi se svetlos uzela u obzir kada se iscrtava scena mora se ukljuciti jos par opcija u Direct3D-u
pd3dDevice.SetRenderState(D3DRS_LIGHTING, iTrue);
pd3dDevice.SetRenderState(D3DRS_AMBIENT, $00202020);

Prva opcija kaze da se svetlo uzima u obzir, a druga postavlja boju ambijentalnog svetla.

Sada imamo sve sto je potrebno da bi iscrtali scenu.
procedure Oboji;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,255), 1.0, 0);

  if (SUCCEEDED(pd3dDevice.BeginScene)) then
  begin
    Svetlo;
    Rotiraj;
    pd3dDevice.SetStreamSource(0, pVB, 0, SizeOf(TCustomVertex));
    pd3dDevice.SetFVF(D3DFVF_CUSTOMVERTEX);
    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2 * 50 - 2);
    pd3dDevice.EndScene;
  end;

  pd3dDevice.Present(nil, nil, 0, nil);
end;

Prvo brisemo pozadinu i ZBuffer... postavljamo svetlo, rotiramo scenu i iscratavamo nas objekat.

To je to Smile



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 22 Jan 2006 18:05


Nas sledeci cilj je da postavimo sliku na objekat koji crtamo. Iskoristicemo kod iz primera gde smo rotirali trougao. Izmenucemo ga tako da se umesto trougla iscrta pravougaonik (dodacemo jos jedan verex) i na njega cemo nalepiti sliku (MyCity... valjda to nije protiv pravila Smile ).

Hajde prvo da sredimo laksi deo... da podesimo da se umesto trougla iscrtava pravougaonik.
const

  Vertices: array[0..3] of TCustomVertex = (
    (X : -2; Y : -1; Z : 0; Color: $ffffffff),
    (X : 2; Y : -1; Z : 0; Color: $ffffffff),
    (X : -2; Y : 1; Z : 0; Color: $ffffffff),
    (X : 2; Y : 1; Z : 0; Color: $ffffffff)
  );

Ovo su podaci za vertex-e. Popunjavanje ostaje isto osim sto se ne popunjava 3 nego 4 vertex-a.

Za iscrtavanje cemo napisati kod ovako:
pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
sto znaci da ce se u buffer-u nalaziti lista trouglova koji se nadovezuju jedan na drugi i iscrtacemo 2 trougla.

To je to... sad da se bacimo na dodavanje texture (malo cemo izmenuti i Vertices konstantu).
Tip vertexa ce biti ovakav:
type
  TCustomVertex = packed record
    X, Y, Z: Single;
    Color: DWORD;
    tU, tV: Single;
  end;

const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZ or D3DFVF_DIFFUSE or D3DFVF_TEX1);

tU i tV ce sadrzati koordinate textura. tU je koordinata po X osi, a tV po Y osi na texturi. Kada je vrednost tU = 0 to je leva ivica texture, kada je tU = 1 to je desna ivica texture. Isto vazi i za tV. tV = 0 je gornja ivica, tV = 1 je donja.
Kada ubacimo i koordinate textura u Vertices dobijamo konacnu verziju podataka za buffer
Vertices: array[0..3] of TCustomVertex = (
  (X : -2; Y : -1; Z : 0; Color: $ffffffff; tU : 0; tV : 1),
  (X : -2; Y : 1; Z : 0; Color: $ffffffff; tU : 0; tV : 0),
  (X : 2; Y : -1; Z : 0; Color: $ffffffff; tU : 1; tV : 1),
  (X : 2; Y : 1; Z : 0; Color: $ffffffff; tU : 1; tV : 0)
);


Potrebna nam je promenljiva u kojoj ce biti sacuvana textura... dodacemo je i odmah napisati i kod koji ce je na kraju osloboditi.
var
  pTexture: IDirect3DTexture9;

procedure CloseD3D;
begin
  pTexture := nil;
  pVB := nil;
  pd3dDevice := nil;
  pD3D := nil;
end;


Kod za ucitavanje texture cemo postaviti u PopuniBuffer funkciju. Kod se svodi na pozivanje jedne funkcije zahvaljujuci Direct3DX funkcijama:
function PopuniBuffer: HRESULT;
var
  pVertices: Pointer;
begin
  Result:= E_FAIL;

  if FAILED(D3DXCreateTextureFromFile(
    pd3dDevice,
    'MyCity.jpg',
    pTexture)
  ) then
  begin
    MessageBox(0, 'Ne mogu da nadjem MyCity.jpg', 'D3DTut5', MB_OK);
    Exit;
  end;

  if FAILED(
    pd3dDevice.CreateVertexBuffer(
      4 * SizeOf(TCustomVertex),
      0, D3DFVF_CUSTOMVERTEX,
      D3DPOOL_DEFAULT, pVB, nil)) then
    Exit;

  if FAILED(
    pVB.Lock(0, SizeOf(Vertices), pVertices, 0)) then
  Exit;

  CopyMemory(pVertices, @Vertices, SizeOf(Vertices));
  pVB.Unlock;

  Result:= S_OK;
end;


D3DXCreateTextureFromFile funkcija uzima D3DDevice za koji se kreira textura, putanju do texture i promenljivu u kojoj ce textura biti kreirana. To je sve. Imamo texturu, imamo objekat... ostaje nam da sve iscrtamo.
procedure Oboji;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET, D3DCOLOR_XRGB(255,0,0), 1.0, 0);

  if (SUCCEEDED(pd3dDevice.BeginScene)) then
  begin
    Rotiraj;
   
    pd3dDevice.SetTexture(0, pTexture);

    pd3dDevice.SetTextureStageState(0, D3DTSS_COLOROP,   D3DTOP_MODULATE);
    pd3dDevice.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
    pd3dDevice.SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
    pd3dDevice.SetTextureStageState(0, D3DTSS_ALPHAOP,   D3DTOP_DISABLE);

    pd3dDevice.SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );
    pd3dDevice.SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR );
    pd3dDevice.SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR );
    pd3dDevice.SetSamplerState(0, D3DSAMP_ADDRESSU,  D3DTADDRESS_CLAMP );
    pd3dDevice.SetSamplerState(0, D3DSAMP_ADDRESSV,  D3DTADDRESS_CLAMP );

    pd3dDevice.SetStreamSource(0, pVB, 0, SizeOf(TCustomVertex));
    pd3dDevice.SetFVF(D3DFVF_CUSTOMVERTEX);
    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
    pd3dDevice.EndScene;
  end;

  pd3dDevice.Present(nil, nil, 0, nil);
end;


pd3dDevice.SetTexture funkcija postavlja texturu. Moguce je postaviti vise textura od jednom i prvi parametar govori koju texturu postavljamo, a drugi parametar je buffer u kojoj je textura. Kada postavimo texturu potrebno je objasniti Dirrect3D-u kako zelimo da je postavi na objekat. To se vrsi funkcijom pd3dDevice.SetTextureStageState. Prvi parametar oznacava za koju texturu podesavamo opcije. Drugi je opcija koju zelimo da menjamo i treci predstavlja vrednost koju zelimo da postavimo.
U ovom slucaju kazemo da se boja texture stapa sa bojom vertex-a. To znaci da ako je boja vertex-a bela videce se prava boja texture. ako je vertex crven i textura ce biti crvenkasta.
Za vise informacija o ovoj funkciji i opcijama koje mogu da se postave pogledajte ovde.

Sledece sto radimo je postavljanje nacina na koji ce se kreirati dodatne manje texture texture. Kada se objekat dovoljno udalji od kamere Direct3D ce automatski iskoristiti texturu koju je kreirao.
Za vise informacija o ovoj funkciji i opcijama koje mogu da se postave pogledajte ovde.

To bi bilo to... kada se program pokrene, videce se pravougaonik na kojem ce se nalaziti textura Smile



Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 22 Jan 2006 20:20


Poslednji od tutoriala koji idu sa SDK je ucitavanje i prikazivanje objekta iz *.x fajla. U tom fajlu se nalaze podaci o vertex-im, materijalima, texturama, animacijama... Opet cemo koristiti funkcije iz Direct3DX da bismo lakse ucitali sve te podatke.

Pocecemo definisanjem tipova i postavljanjem globalnih promenljivih.
type
  PD3DMaterial9Array = ^TD3DMaterial9Array;
  TD3DMaterial9Array = array [0..MaxInt div SizeOf(TD3DMaterial9) - 1] of TD3DMaterial9;

  PIDirect3DTexture9Array = ^IDirect3DTexture9Array;
  IDirect3DTexture9Array = array [0..MaxInt div SizeOf(IDirect3DTexture9) - 1] of IDirect3DTexture9;

var
  WndClass: TWNDClassEx;
  Wnd: HWnd;
  Msg: TMsg;
  pD3D: IDirect3D9;
  pd3dDevice: IDirect3DDevice9;
  pMesh: ID3DXMesh;
  pMeshMaterials: PD3DMaterial9Array;
  pMeshTextures: PIDirect3DTexture9Array;
  NumMaterials: Integer;


Tipove koje smo definisali ce nam omoguciti da lako mozemo da radimo sa nizovima materijala i nizovima textura jer jedan 3D objekad (mesh) moze u svom sastavu imati vise materijala i textura.
pMesh promenljiva ce sadrzati podatke o mesh-u, a u pMeshMaterials i pMeshTextures podaci o materijalima i texturama koje mesh koristi.
NumMaterials ce sadrzati broj materijala koje koristi mesh.

Hajde da ucitamo mesh iz fajla.
function PopuniBuffer: HRESULT;
type
  PD3DXMaterialArray = ^TD3DXMaterialArray;
  TD3DXMaterialArray = array [0..MaxInt div SizeOf(TD3DXMaterial) - 1] of TD3DXMaterial;

var
  pD3DXMtrlBuffer: ID3DXBuffer;
  D3DXMaterials: PD3DXMaterialArray;
  I: Integer;
begin
  Result:= E_FAIL;

  if FAILED(D3DXLoadMeshFromX('Tiger.x', D3DXMESH_SYSTEMMEM,
                              pd3dDevice, nil,
                              @pD3DXMtrlBuffer, nil, @NumMaterials,
                              pMesh)) then
  begin
    MessageBox(0, 'Ne mogu da nadjem Tiger.x', 'D3DTut6', MB_OK);
    Exit;
  end;

  D3DXMaterials := pD3DXMtrlBuffer.GetBufferPointer;
  try
    GetMem(pMeshMaterials, SizeOf(TD3DMaterial9) * NumMaterials);
    GetMem(pMeshTextures, SizeOf(IDirect3DTexture9) * NumMaterials);
  except
    Result:= E_OUTOFMEMORY;
    Exit;
  end;
  ZeroMemory(pMeshTextures, SizeOf(IDirect3DTexture9) * NumMaterials);

  I := 0;
  while (I < NumMaterials) do
  begin
    pMeshMaterials[I] := D3DXMaterials[I].MatD3D;
    pMeshMaterials[I].Ambient := pMeshMaterials[I].Diffuse;
    pMeshTextures[I] := nil;
    if (D3DXMaterials[I].pTextureFilename <> nil) and
       (StrLen(D3DXMaterials[I].pTextureFilename) > 0) then
    begin
      if FAILED(D3DXCreateTextureFromFile(pd3dDevice,
                                          D3DXMaterials[I].pTextureFilename,
                                          pMeshTextures[I])) then
      begin
        MessageBox(0, 'Ne mogu da nadjem texturu', 'D3DTut6', MB_OK);
      end;
    end;
    Inc(I);
  end;

  pD3DXMtrlBuffer := nil;

  Result := S_OK;
end;


Funkcija D3DXLoadMeshFromX ucitava mesh iz fajla koji je naveden u prvom parametru. Od drugog zavisi kako ce se u memoriji mesh kreirati. Treci je D3DDevice za koji se mesh kreira. Sledeci parametar predstavlja informaciju o poligonima koji su jedan pored drugog. Ovaj podatak se koristi u nekim drugim funkcijama pa nam sad nije potreban. Sledeca dva parametra predstavljaju niz materijala i efekata koje mesh koristi. Noviji programi uopste ne koriste materijale nego se oslanjaju samo na efekte, ali cemo se mi zasada zadrzati samo na materijalima. Naredni parametar je promenljiva u kojojo ce biti smesten broj materijala, a poslednji je promenljiva u kojoj ce biti 3D objekat.

Kada smo ucitali mesh treba da uzmemo materijale i texture kako bi kasnije mogli da ih koristimo. U sustini ovde nema vise niceg novog. Kopiramo podatke o materijalima (u *.X fajlu se ne nalazi podatak o ambijentalnoj boji pa je sami postavljamo na verdnost difuzne boje) i ucitavamo texture za svaki materijal koji je sadrzi.

Imamo sve potrebne podatke i zato cemo sad da iscrtamo mesh:
procedure Oboji;
var
  I: Integer;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER,
                     D3DCOLOR_XRGB(0,0,255), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    Rotiraj;

    I := 0;
    while (I < NumMaterials) do
    begin
      pd3dDevice.SetMaterial(pMeshMaterials[I]);
      pd3dDevice.SetTexture(0, pMeshTextures[I]);

      pMesh.DrawSubset(I);

      Inc(I);
    end;

    pd3dDevice.EndScene;
  end;

  pd3dDevice.Present(nil, nil, 0, nil);
end;


Prvo brisemo sadrzaj ekrana i postavljamo matrice kao i uvek. Zatim idemo redom i iscrtavamo deo po deo mesh-a. Postavljamo materijal i texturu za odredjeni deo i pozivom funkcije pMesh.DrawSubset iscrtavamo zeljeni deo. Kada sve iscrtamo zavrsavamo scenu i prikazujemo gotovu sliku. Nista komplikovano.

Sve sto nam ostaje je da napisemo kod za osobadanje memorije.
procedure CloseD3D;
var
  I: Integer;
begin
  if (pMeshMaterials <> nil) then
    FreeMem(pMeshMaterials);

  if (pMeshTextures <> nil)  then
  begin
    I := 0;
    while (I < NumMaterials) do
    begin
      pMeshTextures[I] := nil;
      Inc(I);
    end;
    FreeMem(pMeshTextures);
  end;

  pMesh := nil;
  pd3dDevice := nil;
  pD3D := nil;
end;


To je kraj tutoriala koji idu sa SDK... sledece cemo sami da smisljamo Smile



Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 23 Jan 2006 22:00


Ovog puta cemo pokazati kako je moguce saznati koje sve graficke adaptere ima korisnik na racunaru i koje modove prikaza podrzavaju. Koristicemo VCL komponente u ovom primeru da bismo lakse prikazali podatke.

Pocecemo stavljanjem jednog combo box-a (cmbKartice) u kojem ce se videti sve graficke, i jednog list box-a (lstMod) gde ce se prikazati svi modovi koje graficka podrzava.

Bice nam potrebno da kreiramo Direct3D objekat i da ga kasnije obrisemo pa cemo napisati procedure i dodati promenljive:
pD3D: IDirect3D9;

procedure TfrmGlavna.Init3D;
begin
  pD3D := Direct3DCreate9(D3D_SDK_VERSION);
  if pD3D = nil then
    Application.Terminate;
end;

procedure TfrmGlavna.Done3D;
begin
  pD3D := nil;
end;


Prelazimo na glavni deo... da bi smo saznali koje graficke korisnik ima iskoristicemo sledeci kod:
procedure TfrmGlavna.EnumAdapters;
var
  D3DAI: TD3DAdapterIdentifier9;
  Num, I: Integer;

begin
  cmbKartice.Clear;
  Num := pD3D.GetAdapterCount;

  for I := 0 to Num - 1 do
  begin
    pD3D.GetAdapterIdentifier(I, 0, D3DAI);
    cmbKartice.AddItem(D3DAI.Description, nil);
  end
end;

TD3DAdapterIdentifier9 tip sluzi za cuvanje svih podataka koji su potrebni za oznacavanje gravicke kartice i njen opis.
Uz pomoc pD3D.GetAdapterCount funkcije saznajemo koliko postoji grafickih kartica na racunaru.
Da bismo dosli do podataka pozivamo pD3D.GetAdapterIdentifier funkciju. Prvi parametar je redni broj graficke (prva kartica je po rednim brojem 0). Drugi oznacava sta zelimo da uzmemo od podataka (0 znaci da uzimamo sve) i poslednji parametar je promenljiva u kojoj ce biti smesten rezultat... prilicno jednostavno.
Taj rezultat smestamo u combo box i time zavrsavamo ovu proceduru.

Procedura za pronalazenje modova u kojima graficka moze da radi je malo komplikovanija, ali ne previse:
procedure TfrmGlavna.EnumModes(Adapter: Integer);
var
  D3DDM: TD3DDisplayMode;
  ModeText: String;
  Num, I, M: Integer;

const
  FormatArray: array [0..5] of TD3DFormat = (
    D3DFMT_A1R5G5B5,
    D3DFMT_A2R10G10B10,
    D3DFMT_A8R8G8B8,
    D3DFMT_R5G6B5,
    D3DFMT_X1R5G5B5,
    D3DFMT_X8R8G8B8
  );

begin
  lstMod.Clear;

  for M := 0 to 5 do
  begin
    Num := pD3D.GetAdapterModeCount(Adapter, FormatArray[M]);

    for I := 0 to Num - 1 do
    begin
      pD3D.EnumAdapterModes(Adapter, FormatArray[M], I, D3DDM);
      ModeText := IntToStr(D3DDM.Width) + 'x' +
        IntToStr(D3DDM.Height) + 'x';
      case D3DDM.Format of
        D3DFMT_R3G3B2,
        D3DFMT_A8,
        D3DFMT_P8,
        D3DFMT_L8,
        D3DFMT_A4L4: ModeText := ModeText + '8';

        D3DFMT_R5G6B5,
        D3DFMT_X1R5G5B5,
        D3DFMT_A1R5G5B5,
        D3DFMT_A4R4G4B4,
        D3DFMT_A8R3G3B2,
        D3DFMT_X4R4G4B4,
        D3DFMT_A8P8,
        D3DFMT_L16,
        D3DFMT_A8L8: ModeText := ModeText + '16';

        D3DFMT_R8G8B8: ModeText := ModeText + '24';

        D3DFMT_A8R8G8B8,
        D3DFMT_X8R8G8B8,
        D3DFMT_A2B10G10R10,
        D3DFMT_A8B8G8R8,
        D3DFMT_X8B8G8R8,
        D3DFMT_G16R16,
        D3DFMT_A2R10G10B10: ModeText := ModeText + '32';

        D3DFMT_A16B16G16R16: ModeText := ModeText + '64';
      end;
      ModeText := ModeText + '@' + IntToStr(D3DDM.RefreshRate);
      lstMod.AddItem(ModeText, nil);
    end;
  end;
end;

Krenimo od pocetka... tip TD3DDisplayMode sluzi za cuvanje svih potrebnih podataka o modu za prikaz kao sto su sirina, visina, osvezavanje ekrana...
pD3D.GetAdapterModeCount funkcija vraca broj razlicitih modova prikaza za zeljenu karticu. Drugi parametar odredjuje koji se modovi prebrojavaju tako da ne moramo da prebrojavamo i one koji nas ne interesuju. Posto u ovom primeru zelimo da prikazemo sve moguce modove koristicemo i sve moguce formate koje ova funkcija prihvata. Oni se nalaze u konstanti FormatArray.
Kada dobijemo broj modova za odredjeni format koristimo pD3D.EnumAdapterModes da bismo dobili podatke o odredjenom modu. Prva dva parametra su ista kao i pri pozivu pD3D.GetAdapterModeCount funkcije. Druga dva predstavljaju redni broj moda koji zelimo da uzmemo i promenljivu u kojoj ce biti smesten rezultat.
Ostatak koda je formatiranje texta kako bi bio laksi za citanje. Koristimo sirinu i visinu moda za dobijanje texta kao sto je npr 800x600, zatim na osnovu formata odredjujemo koliko se bitova koristi za definisanje jedne boje i na kraju dodajemo i frekvenciju osvezavanja ekrana. Na kraju dobijamo text kao sto je 800x600x16@75 gde je prvi broj sirina, drugi visina, treci broj bitova za jednu boju i poslednji frekvencija osvezavanja ekrana.

Glavni deo programa je gotov. Sada jos samo treba dodati kod koji ce sve ovo da poziva kada je potrebno:
procedure TfrmGlavna.FormCreate(Sender: TObject);
begin
  Init3D;
  EnumAdapters;
end;

procedure TfrmGlavna.FormDestroy(Sender: TObject);
begin
  Done3D;
end;

procedure TfrmGlavna.cmbKarticeChange(Sender: TObject);
begin
  if cmbKartice.ItemIndex <> -1 then
    EnumModes(cmbKartice.ItemIndex);
end;


Na osnovu ovog koda se moze napraviti program koji dozvoljava korisniku izabir moda za full screen aplikaciju.



Source (Delphi) | Source (Lazarus) | Bin
Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Ovog puta cemo pokazati kako se kreira DirectX Common Framework, fullscreen aplikacija i kako se koristi alpha blending.

DirectX Common Framework je skup klasa, tipova, funkcija, konstanti, ... koji nam pomazu u radu sa DirectX-om. Videcete koliko ce biti lakse raditi uz pomoc Common Framework-a Smile

FullScreen aplikacija zauzima ceo ekran, cesto promeni i rezoluciju na onu koja joj odgovara i samo se ona iscrtava. Jedini problem sa ovakvim aplikacijama je kada korisnik na neki nacin promeni aktivnu aplikaciju i time prikaze neki drugi prozor. FullScreen aplikacija se tada minimizuje i svi podaci koje je ona smestila u memoriju graficke kartice (texture, koordinate, podesavanje iscrtavanja, ...) bivaju obrisani i prilikom ponovnog prikazivanja je potrebno ponovo se to vratiti u memoriju graficke kartice. Osim ove "sitnice" sve ostalo je isto kao da ne redite u fullscreen-u.

Alpha blending je tehnika uz pomoc koje je moguce napraviti da objekti izgledaju transparentno tj. da se provide. Za kontrolu providnosti se koristi Alpha kanal u bojama (imamo Red, Green, Blue i Alpha kanal). Moguce je na razlicite nacine kombinovati vrednosti boja pixel-a koji se poklapaju i time se dobiti razliciti efekti. Bitno je da se objekti koji su providni iscrtaju posle objekata koji su iza njih tako da Direct3D moze da uzme u obzir boje koje su iza objekta i da na osnovu njih izracuna konacnu boju. Zbog toga je najbolji nacin crtanje prvo daljih pa tek onda blizih objekata.

Common Framework mozete skinuti odavde... nacicete folder Common i tamo se nalaze svi potrebi unit-i za rad. Da bi Delphi mogao da pronadje te unit-e potrebno je u Library Path dodati i putanju do Common folder-a. Kada ste to uradili mozemo poceti da pisemo program.

Radi bolje preglednosti programa radicemo sa jednim *.dpr fajlom (u njemu ce se nalaziti instrukcije za pokretanje framework-a) i jednog *.pas fajla (u kome ce se nalaziti funkcije za pripremanje buffer-a, iscrtavanje, ...).

Common Framework radi na principu event-a. Kada se kreira D3DDevice framework ce pozvati funkciju koju smo mu zadali, kada je potrebno da se inicijalizuju vrednosti za D3DDevice opet ce pozvati funkciju koju mu zadamo, kada treba da se iscrtava poziva funkciju koju smo mu zadali, itd... svaki korak od kreiranja do unistavanja D3DDevice-a ima svoj event za koji mozemo da postavimo funkciju koju zelimo da framework pozove.

Funkcije koje pozivamo za postavljanje funkcija koje zelimo da framework pozove u odredjenom trenutku su:

DXUTSetCallbackDeviceChanging: postavljanje funkcije koja se poziva kada se menja D3DDevice (prelazak iz jednog moda u drugi i slicno)

DXUTSetCallbackDeviceCreated: postavljanje funkcije koja se poziva kada se kreira D3DDevice (prilikom poziva nase funkcije D3DDevice je kreiran i mozemo da pocnemo sa kreiranjem drugih objekata)

DXUTSetCallbackDeviceDestroyed: postavljanje funkcije koja se poziva kada se D3DDevice izbrise iz memorije (D3DDevice je jos uvek u memoriji za vreme poziva funkcije i ovde je poslednji trenutak kada treba da obrisemo objekte koji su vezani za D3DDevice)

DXUTSetCallbackDeviceLost: postavljanje funkcije koja se poziva kada aplikacija izgubi kontrolu nad grafickom karticom (neki drugi program je zatrazio da bude u fullscreen-u, nas program je bio u fullscreen-u i izgubio je fokus, ili nesto slicno... ovde treba da se oslobode objekti koji su kreirani u memoriji graficke kartice)

DXUTSetCallbackDeviceReset: postavljanje funkcije koja se poziva kada aplikacija uspostavi kontrolu nad grafickom karticom (ovde se kreiraju objekti koji treba da se nalaze u memoriji graficke kartice)

DXUTSetCallbackFrameMove: postavljanje funkcije koja se poziva kada je vreme da se objekti pomeraju (ovde mozemo da vrsimo rotacije, promenu boje, velicine i slicno za objekte)

DXUTSetCallbackFrameRender: postavljanje funkcije koja se poziva kada je potrebno da se iscrta sadrzaj

DXUTSetCallbackKeyboard: postavljanje funkcije koja ce reagovati na pritiske tastera na tastaturi

DXUTSetCallbackMouse: postavljanje funkcije koja ce reagovati na promenu pozicije misa ili pritisak na neki njegov taster

DXUTSetCallbackMsgProc: postavljanje funkcije koja se poziva kada god je potrebno da se obradi neka windows poruka

Postoje jos funkcija koje framework poziva, ali cemo ih spomenuti kada dodjemo do njih.

Hajde da pocnemo sa pisanjem koda... krenucemo od *.dpr fajla. Prvo sto radimo je inicijalizacija framework-a:
DXUTInit(True, True, True);
Prvi parametar oznacava da li zelimo da framework obradi ulazne parametre prilikom pozivanja izvrsnog fajla. Na ovaj nacin korisnik putem komandne linije moze da izabere da li ce raditi u fullscreen-u ili ne, koje ce biti dimenzije prozor u kojem ce se izvrsavati aplikacija, posle koliko frejmova da se aplikacija sama zatvori i slicno.

Drugi parametar oznacava da li ce framework da reaguje na standardne kombinacije tastera (ESC izlaz iz aplikacije, ALT + ENTER prelazak iz fullscreen u window mod i slicno).

Treci parametar oznacava da li ce framework da izbacuje poruku o gresci u MessageBox kada na nju naidje ili ne.

Sledece sto radimo je podesavanje kursora:
DXUTSetCursorSettings(False, True);
Prvi parametar oznacava da li ce kursor biti vidljiv u fullscreen-u, a drugi da li ce pozicija kursora biti ogranicena na velicinu koja je definisana za fullscreen.

Sada kreiramo prozor... nece vise biti potrebe za pisanjem onog dugog koda koji smo pisali u proslim primerima... sve sto je potrebno je ovo:
DXUTCreateWindow('D3DTut8');
Ova funkcija ima 6 parametara, ali su svi osim prvog neobavezni.
Prvi parametar je naziv prozora koji zelimo da kreiramo.

Drugi je HInstance vrednost i ako nista ne navedemo, framework ce uzeti vrednost iz HInstance promenljive.

Treci parametar je ikona koju zelimo da koristimo.

Cetvrti je meni ako zelimo da ga prozor ima.

Poslednja dva su koordinate gde zelimo da se prozor nalazi.

Naredna stvar je kreiranje D3DDevice-a:
DXUTCreateDevice(D3DADAPTER_DEFAULT, False, 800, 600, IsDeviceAcceptable);
Prvi parametar oznacava koju graficku karticu cemo koristiti. D3DADAPTER_DEFAULT oznacava da koristimo default karticu.

Drugi parametar oznacava da li ce biti window ili fullscreen aplikacija. True znaci window, a False fullscreen.

Sledeca dva parametra oznacavaju zeljenu sirinu i visinu aplikacije. Kada je aplikacija u window modu tada ce se koristiti tcne vrednosti koje smo naveli, a ako je aplikacija u fullscreen-u tada ce se koristiti najpribliznija moguca vrednost.

Naredna dva predstavljaju funkcije koje ce framework pozivati da bi znao kakav D3DDevice nam tacno odgovara. Prva funkcija (za sad cemo samo nju koristiti) se poziva za svaki moguci mod koji se slaze sa vrednostima koje smo do sad postavili i ako funkcija vrati True znaci da nam mod odgovara. Na ovaj nacin cemo reci framework-u da zelimo D3DDevice koji podrzava alpha blending.
Druga funkcija se poziva kada framework vec izabere mod za prikaz, ali ga jos ne postavi. Ovde mozemo da vidimo sta je tacno izabrani i da promenimo nesto u poslednji cas ako je potrebno.

Poslednji parametar je pointer na bilo koju vrednost. Nesto kao Tag u VCL klasama. Ova vrednost se prosledjuje svim nasim funkcijama koje framework poziva.

Kada je kreiran D3DDevice ostaje nam da pokrenemo glavnu petlju i da zaustavimo program kada se ona zavrsi:
DXUTMainLoop;

ExitCode:= DXUTGetExitCode;

DXUTFreeState;


DXUTMainLoop moze uzeti jedan parametar koji predstavlja tabelu precica koje aplikacija treba da koristi ako za to ima potrebe.

DXUTGetExitCode daje rezultat koji oznacava kako se aplikacija zavrsila. Ako je vrednost 0 znaci da je sve proslo kako treba, a ako je razlicito od 0 tada se neka greska pojavila.

DXUTFreeState funkcija se poziva na kraju da bi se oslobodili podaci koji su bili kreirani prilikom pozivanja ostalih funkcija.

Ovo je deo koda koji ce kreirati sve sto je potrebno za rad framework-a, ali mi cemo dodati 3 funkcije koje zlimo da framework poziva:
DXUTSetCallbackDeviceReset(OnResetDevice);
DXUTSetCallbackDeviceLost(OnLostDevice);
DXUTSetCallbackFrameRender(OnFrameRender);


Na ovaj nacin bicemo obavesteni kada je potrebno da kreiramo i oslobodimo objekte koji se nalaze u memoriji graficke kartice i kada je potrebno iscrtavati scenu. Ovo nam je sasvim dovoljno za ovaj primer.

Zavrsili smo sa *.dpr fajlom i prelazimo da definisanje funkcija u *.pas fajlu. U interface delu cemo definisati funkcije koje su potrebne u *.dpr fajlu:
function IsDeviceAcceptable(const pCaps: TD3DCaps9; AdapterFormat, BackBufferFormat: TD3DFormat; bWindowed: Boolean; pUserContext: Pointer): Boolean; stdcall;

function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;

procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;

procedure OnLostDevice(pUserContext: Pointer); stdcall;


Prva funkcija ce se pozivati kada je potrebno da se odluci da li nam parametri za kreiranje D3DDevice odgovaraju. pCaps parametar nam daje podatke o mogucnosti koje ce imati device kada se kreira, formati predstavljaju koji ce se formati za iscrtavanje koristiti, bWindowed oznacava da li je mod za window ili fullscreen prikaz i poslednji parametar je vrednost koju smo naveli prilikom pozivanja funkcije za kreiranje D3DDevice.

Druga funkcija se poziva kada je potrebno kreirati objekte u memoriji graficke kartice. Kao parametre dobijamo D3DDevice i podatke o backbuffer-u koji ce se koristiti kao i vrednost koju smo naveli prilikom kreiranja device-a.

Procedura za crtanje scene kao parametre dobija device, vreme koliko aplikacija radi kao i vreme koje je proslo od prethodnog iscrtavanja.

Poslednja procedura ce se pozivati kada izgubimo kontrolu nad grafickom i kada treba da obrisemo objekte koji su vezani za nju.

U implementation delu cemo definisati podatke koji ce nam biti potrebni za iscrtavanje objekata:
type
  TCustomVertex = packed record
    X, Y, Z, RHW: Single;
    Color: DWORD;
  end;

const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZRHW or D3DFVF_DIFFUSE);

  Vertices: array[0..7] of TCustomVertex = (
    (X : 100.0; Y : 100.0; Z : 1; RHW : 1; Color: $ffff0000),
    (X : 700.0; Y : 100.0; Z : 1; RHW : 1; Color: $ffff0000),
    (X : 100.0; Y : 500.0; Z : 1; RHW : 1; Color: $ffff0000),
    (X : 700.0; Y : 500.0; Z : 1; RHW : 1; Color: $ffff0000),
    (X : 50.0; Y : 150.0; Z : 1; RHW : 1; Color: $a00000ff),
    (X : 750.0; Y : 150.0; Z : 1; RHW : 1; Color: $a00000ff),
    (X : 50.0; Y : 550.0; Z : 1; RHW : 1; Color: $a00000ff),
    (X : 750.0; Y : 550.0; Z : 1; RHW : 1; Color: $a00000ff)
  );

var
  pVB: IDirect3DVertexBuffer9;


U ovom delu koda nema niceg novog... Kreiramo tip za vertex format, imamo podatke koje cemo koristiti (prva 4 vertexa oznacavaju prvi pravougaonik, a druga cetiri drugi) i promenljivu za vertex buffer.

Sada cemo poceti sa pisanjem funkcija koje ce uticati na rad framework-a.
function IsDeviceAcceptable(const pCaps: TD3DCaps9; AdapterFormat, BackBufferFormat: TD3DFormat; bWindowed: Boolean; pUserContext: Pointer): Boolean; stdcall;
begin
  Result:= False;

  if FAILED(DXUTGetD3DObject.CheckDeviceFormat(pCaps.AdapterOrdinal, pCaps.DeviceType,
                   AdapterFormat, D3DUSAGE_QUERY_POSTPIXELSHADER_BLENDING,
                   D3DRTYPE_TEXTURE, BackBufferFormat))
  then Exit;

  Result:= True;
end;

DXUTGetD3DObject funkcija nam vraca IDirect3D objekat uz pomoc kojeg mozemo da proverimo da li format prikaza podrzava alpha blending i na osnovu toga vracamo True ako podrzava ili False ako ne podrzava. Ovim cemo dobiti D3DDevice koji ce podrzavati alpha blending.

Zatim nam treba pripremanje objekata.
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  pVertices: Pointer;
begin
  Result:= E_FAIL;

  if FAILED(
    pd3dDevice.CreateVertexBuffer(
      8 * SizeOf(TCustomVertex),
      0, D3DFVF_CUSTOMVERTEX,
      D3DPOOL_DEFAULT, pVB, nil)) then
    Exit;

  if FAILED(
    pVB.Lock(0, SizeOf(Vertices), pVertices, 0)) then
  Exit;

  CopyMemory(pVertices, @Vertices, SizeOf(Vertices));
  pVB.Unlock;

  pd3dDevice.SetRenderState(D3DRS_ZENABLE, iFalse);
  pd3dDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, iTrue);
  pd3dDevice.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
  pd3dDevice.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

  Result:= S_OK;
end;


Ovde je sve vec vidjeno osim par poziva pri kraju:
pd3dDevice.SetRenderState(D3DRS_ZENABLE, iFalse);
Ovaj poziv ce iskljuciti Z-buffer jer nam on u ovom slucaju nece trebati.

pd3dDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, iTrue);
pd3dDevice.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
pd3dDevice.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);

Ovi pozivi ukljucuju alpha blending i postavljaju nacin na koji ce mesanje boja biti izvrseno. U ovom slucaju kazemo da se koristi procenat boje koja se iscrtava onoliki kolika je Alpha vrednost i procenat boje pozatine onoliki koliko je 1 - Alpha vrednost boje koja se iscrtava. Mozda se nisam bas najbolje izrazio pa cu to napisati ovako:

Konacna Boja = (Boja * Alpha) + (Boja Pozadine * (1 - Alpha))

Sada je sve spremno za iscrtavanje.
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    pd3dDevice.SetStreamSource(0, pVB, 0, SizeOf(TCustomVertex));
    pd3dDevice.SetFVF(D3DFVF_CUSTOMVERTEX);
    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 4, 2);

    pd3dDevice.EndScene;
  end;
end;

Iscrtavanje se vrsi isto kao sto smo i ranije radili. U ovom slucaju se vidi kako je moguce koristiti jedan vertex buffer za prikazivanje 2 objekta. Uvek je dobro koristiti sto manje buffer-a kako bi se memorija sto manje kopirala iz ram-a u graficku memoriju i time usporavao rad programa.
Primecujete da nigde ne pozivamo pd3dDevice.Present funkciju. To nije potrebno jer framework to radi umesto nas.

Poslednje sto ostaje je brisanje objekata.
procedure OnLostDevice; stdcall;
begin
  pVB := nil;
end;


I to je sve. Prilicno mali kod u odnosu na one koje smo do sada pisali, a time laksi za odrzavanje. Jedina mana je sto je izvrsni fajl malo veci, ali s obzirom na sve olaksice koje dobijamo koriscenjem Common Framework-a vredi zrtvovati stotinak kilobajta.



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 31 Jan 2006 20:27


Nasao sam malo vremena da napisem jedan mali wizard za Delphi. On ce dodati novu vrstu aplikacije koju mozemo da kreiramo.

Aplikacija ce se nalaziti u File->New->Other->DirectX->DirectX 9 Application Wizard

Kada izaberete taj wizard kreirace se aplikacija koja ce sadrzadi jedan *.dpr fajl u kojem ce se nalaziti funkcije za pokretanje Common Framework-a, i jedan *.pas fajl u kojem ce se nalaziti callback funkcije.

Wizard

P.S.
Wizard je pisan za Delphi 7... moguce je da ce raditi i u Delphi 6, ali nisam siguran. Ako neko ima problema sa instalacijom wizard-a neka mi posalje PP.

Dopuna: 04 Feb 2006 13:24


Skoro svaki program treba da ispisuje neki tekst koji opisuje stanje programa, objasnjava korisniku sta da radi ili nesto trece. Direct3DX ima tip podatka ID3DXFONT koji pisanje teksta cini ekstremno lakim. Ako ste radili sa Win API funkcijama za ispis teksta, videcete da se i ovde sve radi skoro isto... cak se koriste i iste konstante za odredjivanje nacina ispisa.

Prvo sto cemo da uradimo je postavljanje funkcije tako da uvek imamo 800x600 prikaz (i u windows i u fullscreen modu):
function ModifyDeviceSettings(var pDeviceSettings: TDXUTDeviceSettings;
  const pCaps: TD3DCaps9; pUserContext: Pointer): Boolean; stdcall;
begin
  Result:= True;
  pDeviceSettings.pp.BackBufferWidth := 800;
  pDeviceSettings.pp.BackBufferHeight := 600;
end;


OK... kreiracemo 3 vrste fonta i trebace nam 3 promenljive u koje cemo ih smestiti:
var
  TahomaFont: ID3DXFont;
  TahomaBoldFont: ID3DXFont;
  TimesNewRomanFont: ID3DXFont;


Imamo sve sto je potrebno da bi smo mogli da kreiramo fontove:
function OnCreateDevice(const pd3dDevice: IDirect3DDevice9;
  const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  Result := D3DXCreateFont
    (pd3dDevice, -12, 0, FW_DONTCARE, 1, FALSE,
     DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
     DEFAULT_PITCH or FF_DONTCARE, 'Tahoma', TahomaFont);

  if FAILED(RESULT) then
    Exit;

  Result := D3DXCreateFont
    (pd3dDevice, -40, 0, FW_BOLD, 1, FALSE,
     DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
     DEFAULT_PITCH or FF_DONTCARE, 'Tahoma', TahomaBoldFont);

  if FAILED(RESULT) then
    Exit;

  Result := D3DXCreateFont
    (pd3dDevice, -20, 0, FW_DONTCARE, 1, FALSE,
     DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
     DEFAULT_PITCH or FF_DONTCARE, 'Times New Roman', TimesNewRomanFont);
end;

Funkcija D3DXCreateFont kreira podatke o fontu i smesta ih u promenljive koje smo ranije definisali.
Prvi parametar je device za koji kreiramo font.
Sledeci su visina i sirina fonta kojeg zelimo da kreiramo. Ako zelite font visine 12 (kao kad podesite u Word-u velicinu fonta na 12) postavite vrednost -12. Svaki font ima malo razmaka kako se slova ne bi dodirivala kada se pisu jedno ispod drugog. Kada napisemo -12 to znaci da ce slova biti velicine 12, a prostor za ispisivanje fonta 12 + razmak. Da smo ovaj parametar postavili na 12 tada bi prostor za ispis teksta bio velicine 12, a font bi bilo velicine prostor za ispis teksta - razmak. Nadam se da ste me razumeli.
Sledeci parametar predstavlja prosecnu sirinu karaktera. Ovaj parametar se uglavonm uvek postavlja na 0.
Cetvrti parametar predstavlja debljinu karaktera. FW_DONTCARE je normalna debljina, FW_BOLD je boldovano i slicno... spisak mogucih vrednosti su:
FW_DONTCARE
FW_THIN
FW_EXTRALIGHT
FW_ULTRALIGHT
FW_LIGHT
FW_NORMAL
FW_REGULAR
FW_MEDIUM
FW_SEMIBOLD
FW_DEMIBOLD
FW_BOLD
FW_EXTRABOLD
FW_ULTRABOLD
FW_HEAVY
FW_BLACK
Naredni parametar predstavlja koliko nivoa velicine da se automatski kreiraju. Nama je potreban samo jedan nivo (slike fontova za vece udaljenosti nam nisu potrebne jer font nece biti iscrtavan negde daleko od kamere).
Sledeci parametra je True ako font treba da bude italic.
Sedmi parametar oznacava koji set karaktera da se koristi. Ako, recimo, zelimo da koristimo nasa slova, mozemo ovaj parametar postaviti na EASTEUROPE_CHARSET. Kada se koriste Unicode pozivi funkcijama za ispis teksta dovoljno je postaviti set karaktera na DEFAULT_CHARSET i svi karakteri ce moci da se ispisuju.
Osmi parametra predstavlja koji font ce biti izabran ako postoje vise fontova istog naziva. OUT_DEFAULT_PRECIS daje dobre rezultate. Ako uvek zelite TrueType preciznost onda ovaj parametar mozete postaviti na OUT_TT_PRECIS.
Sledeci parametar oznacava preciznost kojom ce slike slova biti generisane. DEFAULT_QUALITY oznacava da nam je nebitno kako ce izgledati font. DRAFT_QUALITY znaci da nam ne treba lep izgled, a PROOF_QUALITY da ocekujemo lep izgled fonta.
Naredni parametar odredjuje kakav font zelimo. Da li da su slova promenljive ili fiksne sirine, kao i da li ce slova biti kao rukom pisana, sa malim crticama na kraju fonta i slicno. Ova opcija ce pronaci font najblizi onom koji zelimo i njega ce koristiti i zato nije zagarantovano da cete dobiti bas ono sto ste trazili.
Poslednja dva parametra oznacavaju ime fonta koji zelimo i promenljivu u kojoj ce kreirani font biti smesten.

Imamo kreirane fontove... sada cemo napisati i funkcije koje ce reagovati kada izgubimo i dobijemo kontrolu nad grafickom, kao i za brisanje objekata iz memorije.
function OnResetDevice(const pd3dDevice: IDirect3DDevice9;
  const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  TahomaFont.OnResetDevice;
  TahomaBoldFont.OnResetDevice;
  TimesNewRomanFont.OnResetDevice;
  Result := S_OK;
end;

procedure OnLostDevice; stdcall;
begin
  TahomaFont.OnLostDevice;
  TahomaBoldFont.OnLostDevice;
  TimesNewRomanFont.OnLostDevice;
end;

procedure OnDestroyDevice; stdcall;
begin
  TahomaFont := nil;
  TahomaBoldFont := nil;
  TimesNewRomanFont := nil;
end;


Sada... kod za iscrtavanje:
var
  XPos: Single;

procedure OnFrameMove(const pd3dDevice: IDirect3DDevice9;
  fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  XPos := Sin(fTime) * 300;
end;

procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9;
  fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
var
  R: TRect;
  S: String;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER,
    D3DCOLOR_XRGB(0, 0, 0), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    R := Rect(8, 8, 0, 0);
    S := 'Jednostavan poziv funkciji DrawText... iz Rect parametra se uzima samo' +
      'pocetna tacka.' + #13#10 +
      'DT_NOCLIP kaze da se ne obraca paznja na desnu i donju ivicu teksta... ' +
      'ispisuje se koliko ga ima.';
    TahomaFont.DrawTextA(nil, PChar(S), Length(S), @R, DT_NOCLIP, $FF00FF00);

    R := Rect(8, 50, 400, 100);
    S := 'Sada se u obzir uzimaju svi podaci iz Rect parametra. Oni ce ograniciti tekst ' +
      'i videce se samo ono sto staje u naznacen prostor.' + #13#10 +
      'Drugi red' + #13#10 +
      'Treci red' + #13#10 +
      'Cetvrti red' + #13#10 +
      'Peti red';
    TahomaFont.DrawTextA(nil, PChar(S), Length(S), @R, 0, $FF00FF00);

    R := Rect(0, 0, 800, 600);
    S := 'Ovaj tekst je napisan na sredini!';
    TahomaBoldFont.DrawTextA(nil, PChar(S), Length(S), @R,
      DT_CENTER or DT_VCENTER, $FF0000FF);

    R := Rect(0 + Round(XPos), 500, 800 + Round(XPos), 600);
    S := 'Ovaj tekst se krece';
    TimesNewRomanFont.DrawTextA(nil, PChar(S), Length(S), @R, DT_CENTER, $FFFF0000);

    pd3dDevice.EndScene;
  end;
end;


U OnFrameMove samo podesavamo XPos promenljivu tako da ce zbog nje u iscrtavanju jedan deo teksta da se krece.

DrawTextA ili DrawTextW (Unicode) funkcije sluze za ispis teksta. Prilicno su jednostavne za pozivanje.
Prvi parametar, koji nismo koristili u ovom primeru, moze predstavljati objekat tipa ID3DXSPRITE. Kada se ovaj parametar navede, sprajt ce primiti sve pozive za iscrtavanje teksta i tako ih sortirati da se prvo iscrtavaju svi tekstovi koji koriste isti font, pa tek onda sledeci cime se ubrzava ispis.
Drugi parametar je tekst koji treba da se ispise. Kod DrawTextA je to tpi PChar, a kod DrawTextW je PWideChar.
Sledeci parametar je duzina stringa.
Zatim sledi parametar tipa PRect... odnosno, pointer na tip TRect. U ovom parametru se nalazi prevougaonik u koji ce se smestiti tekst. Tekst je moguce smestiti u centar vertikalno ili horiznotalno, uz levu ili desnu ivicu, gore, dole, pravougaonik moze da ogranici ispis teksta ili da dozvoli ispis i van, itd...
Sledeci parametar predstavlja gde ce se tekst ispisati u pravougaoniku koji smo dali.
Poslednji parametar predstavlja boju ispisa.

To je sve... za sve moguce vrednosti nacina ispisa teksta u pravougaoniku pogledajte ovde.

I to je to... kada budemo radili sa mesh-evima, pokazacemo kako se iscrtava 3D tekst.



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 13 Feb 2006 23:22


Pokazali smo kako se koristi textura da bi se dobio lepsi izgled odjekata koje iscrtavamo. Direct3D ima mogucnost stapanja 2 ili vise textura. Ova tehnika se zove multitexturing. Direct3D ima ogranicenje od 8 textura koje se mogu spojiti, ali postoje nacini koji dovzoljavaju spavanje neogranicenog broja textura (multipass, shader-i). Ogranicenje u broju textura zavisi i od graficke kartice. Starije mogu da rade sa samo 2 texture pa ako pravite program koji ce raditi na njima, nemojte pokusavati odjednom da spajate vise textura. Maximalni broj textura koje istovremeno mogu da se koriste moze da se sazna analizirajuci prvi parametar (pCaps) funkcije IsDeviceAcceptable.
MaxTextureBlendStages oznacava koliko textura mogu da se spoje odjednom, a MaxSimultaneousTextures oznacava koliko textura mogu da se istovremeno postave za koriscenje (jedan objekat moze koristiti prvu texturu, drugi drugu, itd... a texture ne moraju da se mesaju).

U ovom primeru cemo pokazati najjednostavniji nacin kombinovanja textura. Koristicemo dve texture na jednom pravougaoniku. Prva textura ce biti ona koja ce definisati izgled objekta, a druga ce simulirati svetlo. Kada se te dve texture spoje, dobicemo objekat koji je vidljiv samo tamo gde "svetli svetlo" Smile

Postvicemo par tipova, konstanti i promenljivih:
type
  TCustomVertex = packed record
    X, Y, Z: Single;
    tU1, tV1: Single;
    tU2, tV2: Single;
  end;

const
  D3DFVF_CUSTOMVERTEX = (D3DFVF_XYZ or D3DFVF_TEX1 or D3DFVF_TEX2);

  Vertices: array[0..3] of TCustomVertex = (
    (X : -2; Y : -2; Z : 0; tU1 : 0; tV1 : 1; tU2 : 0; tV2 : 1),
    (X : -2; Y : 2; Z : 0; tU1 : 0; tV1 : 0; tU2 : 0; tV2 : 0),
    (X : 2; Y : -2; Z : 0; tU1 : 1; tV1 : 1; tU2 : 1; tV2 : 1),
    (X : 2; Y : 2; Z : 0; tU1 : 1; tV1 : 0; tU2 : 1; tV2 : 0)
  );

var
  DetailTexture, LightTexture: IDirect3DTexture9;
  vBuffer: IDirect3DVertexBuffer9;


Sve je do sad vec vidjeno jedino je tip vertex-a malo izmenjen. Dodata je koordinata za drugu texturu. U ovom slucaju se koordinate textura poklapaju, ali je moguce da jedan vertex ima jednu koordinatu za jednu, drugu za drugu, trecu za trecnu texturu, itd...

Ovog puta cemo koristiti flag D3DPOOL_MANAGED kada budemo kreirali vertex buffer pa necemo morati da ga brisemo i ponovo kreiramo kad god izgubimo kontrolu nad grafickom karticom. Kreiracemo ga na pocetku i obrisati na kraju, a Direct3D ce se brinuti o ostalom. D3DXCreateTextureFromFile ce takodje kreirati texturu koristeci D3DPOOL_MANAGED flag tako da cemo i ucitvanje textura postaviti ovde, a brisanje ostaviti za kraj:
function OnCreateDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  pVertices: Pointer;
begin
  Result:= E_FAIL;

  if FAILED(
    pd3dDevice.CreateVertexBuffer(
      4 * SizeOf(TCustomVertex),
      0, D3DFVF_CUSTOMVERTEX,
      D3DPOOL_MANAGED, vBuffer, nil)) then
    Exit;

  if FAILED(
    vBuffer.Lock(0, SizeOf(Vertices), pVertices, 0)) then
  Exit;

  CopyMemory(pVertices, @Vertices, SizeOf(Vertices));
  vBuffer.Unlock;

  if Failed(
    D3DXCreateTextureFromFile(pd3dDevice, 'ashwood.jpg',
      DetailTexture)) then
  Exit;

  if Failed(
    D3DXCreateTextureFromFile(pd3dDevice, 'flare.bmp',
      LightTexture)) then
  Exit;

  Result:= S_OK;
end;

procedure OnDestroyDevice; stdcall;
begin
  vBuffer := nil;
  DetailTexture := nil;
  LightTexture := nil;
end;


Da bi se pravougaonik lepo video i pri promeni velicine prozora dodacemo sledeci kod:
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  vEyePt, vLookatPt, vUpVec: TD3DVector;

  matView: TD3DMatrix;
  matProj: TD3DMatrix;
begin
  vEyePt := D3DXVector3(0, 0, -5);
  vLookatPt := D3DXVector3(0, 0, 0);
  vUpVec := D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEyePt, vLookatPt, vUpVec);
  pd3dDevice.SetTransform(D3DTS_VIEW, matView);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    1, 100);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, matProj);

  Result := S_OK;
end;


Sad, konacno, iscrtavanje Smile
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    pd3dDevice.SetTexture(0, DetailTexture);
    pd3dDevice.SetTexture(1, LightTexture);

    pd3dDevice.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
    pd3dDevice.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

    pd3dDevice.SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_MODULATE);
    pd3dDevice.SetTextureStageState(1, D3DTSS_COLORARG1, D3DTA_TEXTURE);
    pd3dDevice.SetTextureStageState(1, D3DTSS_COLORARG2, D3DTA_CURRENT);

    pd3dDevice.SetStreamSource(0, vBuffer, 0, SizeOf(TCustomVertex));
    pd3dDevice.SetFVF(D3DFVF_CUSTOMVERTEX);
    pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

    pd3dDevice.EndScene;
  end;
end;


Prvo smo postavili texture za 0 i 1 stage (stage se moze protumaciti kao korak). U stage 0 se nalazi textura koju koristimo za izgled objekta, a u stage 1 koja ce nam posluziti umesto svetla.

Zatim podesavamo kako ce koja textura uticati na izgled objekta. Prva textura ce jednostavno da predstavlja boju objekta. Da smo ga iscrtali samo sa prvom texturom, lepo bismo videli pravougaonik koji izgleda kao prva textura.

D3DTSS_COLOROP flag govori kako da se boje mesaju posto za svaki stage postoje dva parametra. Mi smo naveli da se za prvi stage jednostavno uzima prvi parametar (D3DTOP_SELECTARG1), a za prvi parametar smo postavili texturu (D3DTSS_COLORARG1 smo postavili na D3DTA_TEXTURE).

Drugi stage ce raditi malo drugacije. Kao nacin stapanja boja stoji D3DTOP_MODULATE sto znaci da ce boje biti pomnozene (maximalna vrednost za svaku komponentu boje je 1). Jedan parametar je textura, a drugi je D3DTA_CURRENT sto znaci da se uzima boja koja je vec iscrtana na zadatom mestu. Na taj nacin ce druga textura da izmeni sadrzaj slike koja ce se na kraju prikazati.

Kada je sve to podeseno, mozemo iscrtati objekta i videti rezultat.



U sledecem primeru cemo pokazati kako se vrsi spajanje textura kada zelimo da spojimo vise nego sto je dozvoljeno. Radicemo isto sto smo radili u ovom primeru, ali cemo sami sebi postaviti ogranicenje... samo jedna textura moze da se iscrtava. Ovaj nacin ce dozvoljavati spajanje textura i na vrlo starim karticama.

Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 14 Feb 2006 21:24


Nadovezacemo se na prethodni primer i prikazati kako je moguce spojiti texture multipass tehnikom.
Uradicemo to ovako... iscrtacemo pravougaonik i na njemu ce biti osnovna textura kao sto smo vec radili, a zatim preko njega ponovo nacrtati isti taj pravougaonik, ali koristeci drugu texturu i alphablending kako bismo spojili te dve texture.

Objasnjavacu samo delove koda koji su promenjeni... prva promena se nalazi u IsDeviceAcceptable funkciji jer moramo da zatrazimo da device podrzava alphablending:
function IsDeviceAcceptable(const pCaps: TD3DCaps9; AdapterFormat, BackBufferFormat: TD3DFormat; bWindowed: Boolean; pUserContext: Pointer): Boolean; stdcall;
begin
  Result := False;

  if FAILED(DXUTGetD3DObject.CheckDeviceFormat(pCaps.AdapterOrdinal, pCaps.DeviceType,
                   AdapterFormat, D3DUSAGE_QUERY_POSTPIXELSHADER_BLENDING,
                   D3DRTYPE_TEXTURE, BackBufferFormat))
  then Exit;

  Result := True;
end;

Ovo smo ranije vec koristili.

Sledeca izmena je ukljucivanje alphablendinga pre iscrtavanja objekata:
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  vEyePt, vLookatPt, vUpVec: TD3DVector;

  matView: TD3DMatrix;
  matProj: TD3DMatrix;
begin
  vEyePt := D3DXVector3(0, 0, -5);
  vLookatPt := D3DXVector3(0, 0, 0);
  vUpVec := D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEyePt, vLookatPt, vUpVec);
  pd3dDevice.SetTransform(D3DTS_VIEW, matView);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    1, 100);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, matProj);

  pd3dDevice.SetRenderState(D3DRS_ALPHABLENDENABLE, iTrue);

  Result := S_OK;
end;


Kada je sve to podeseno, mozemo preci na crtanje... idemo sa prvim iscrtavanjem:
pd3dDevice.SetTexture(0, DetailTexture);

pd3dDevice.SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
pd3dDevice.SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);

pd3dDevice.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
pd3dDevice.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

Postavljamo texturu i posle toga podesavamo da se objekat iscrtava bojom texture.
Pre samog iscrtavanja podesavamo nacin na koji ce se vrsiti stapanje boje. D3DRS_SRCBLEND i D3DRS_DESTBLEND odredjuju nacin na koji ce boje biti stopljene na sledeci nacin:

BojaIscrtavanja * D3DRS_SRCBLEND + BojaPozadine * D3DRS_DESTBLEND

Posto nas prilikom prvog iscrtavanja ne interesuje sta se vec nalazi iscrtano kazemo da zelimo boju iscrtavanja da pomnozimo jedinicom i time ostaje ne promenjena, a boju pozadine mnozimo nulom tako da ni ona ne utice na boju iscrtavanja.

Posle toga iscrtavamo pravougaonik i sada na mestu gde cemo ponovo iscrtati pravouganoik pozadina vise nije crna nego ima boju texture koju smo upravo koristili.

Drugi korak:
pd3dDevice.SetTexture(0, LightTexture);

pd3dDevice.SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
pd3dDevice.SetRenderState(D3DRS_DESTBLEND, D3DBLEND_SRCCOLOR);

pd3dDevice.DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);

Uzimamo drugu texturu. Nema potrebe da ponovo postavljamo D3DTSS_COLOROP i D3DTSS_COLORARG1 jer i dalje zelimo da crtamo bojom izabrane texture, ali zato menjamo nacin na koji ce boje biti stopljene.

Sada imamo sledecu situaciju... boju iscrtavanja mnozimo nulom tako da i ona postaje 0, a boju pozadine mnozimo bojom iscrtavanja i time dobijamo boju koja je identicna kao kada smo u prethodnom primeru koristili D3DTOP_MODULATE nacin spajanja textura:

BojaIscrtavanja * 0 + BojaPozadine * BojaIscrtavanja

Iscrtavamo pravougaonik ponovo kako bi se boje pomesale i dobijamo sliku koja je ista kao i kada smo koristili samo jedno iscrtavanje.



Ova tehnika se koristi kada je potrebno da aplikacija radi na starijim grafickim karticama. Naravno, moguce je koristiti razne kombinacije tehnika... npr... 2 texture se spajaju u jednom iscrtavanju, zatim se na njih u drugom iscrtavanju dodaje neka treca textura, a u trecem iscrtavanju se spajaju jos 2 texture i tako stopljene se dodaju, itd... Smile

Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 15 Feb 2006 1:23


Dobio sam molbu da ove primere pocnem da pisem i za Lazarus IDE. Bio sam vredan pa sam sve primere napisane do sad napisao i u Lazarus-u.

Da biste mogli da koristite DirectX morate, kao i u Delphi-u, podesiti putanje do fajlova u kojima se nalaze unit-i. Svi programi koje cu pisati ce ih traziti u folderu:
$(LazarusDir)\components\directx\headers (ovde se nalaze i include fajlovi)
$(LazarusDir)\components\directx\common


Slobodno mozete koristiti unit-e koje ste koristili i za Delphi jer su kompatibilni i sa Lazarus-om.

Jedina sitnica koja je potrebna da bi Common Framework radio je Imm.pas fajl koji ne ide sa Lazarus-om. Samo i ovaj fajl kopirajte u folder u kojem je Common Framework ($(LazarusDir)\components\directx\common) i sve ce raditi savrseno.

U prethodnim postovima su dodati linkovi za Lazarus Smile

Dopuna: 18 Feb 2006 17:08


U ovom primeru cemo pokazati kako je moguce saznati da li se kursor nalazi iznad nekog objekta na sceni. Direct3DX ima funkciju koja taj deo posla cini vrlo jednostavnim. Funkcija se zove D3DXIntersect. Ona ima mogucnost da vrati podatak da li neka prava sece 3D objekat (mesh), sve tacke preseka, poligone na kojima je nadjen presek, koordinate texture... u ovom slucaju cemo funkciju koristiti najjednostavnije. Samo ce nam vretiti da li prava sece mesh ili ne. Pravac prave cemo dobiti na osnovu matrica za transofrmaciju (WORLD, VIEW i PROJECTION) i koordinate misa na ekranu.

Ovaj program ce iscrtati mesh (tigra kojeg smo vec koristili) i okretati ga kada se mis bude nasao iznad njega. Koristicemo CDXUTMesh tip podatka koji je definisan u DXUTmesh unit-u. On ce nam olaksati kreiranje i rad sa 3D objektom (necemo morati vise da brinemo o materijalima, texturama i slicnim stvarima... prosto morate da volite Common Framework Wink ). Uz ovaj podatak, trebace nam i podatak o tome koliko je trenutno tigar okrenut.
var
  fAngle: Single;
  Mesh: CDXUTMesh;


Idemo na kreiranje objektata... sada cete videti kako je lako koristiti CDXUTMesh (secate se da smo jedan ceo primer posvetili ucitavanju 3d objekta... ovde se sve radi u samo par linija koda):
function OnCreateDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  fAngle := 0;
  Mesh := CDXUTMesh.Create('Tigar');
  Result := Mesh.CreateMesh(pd3dDevice, 'tiger.x');
end;

To je sve sto je potrebno... kada kreiramo objekta mozemo da mu damo ime (Tigar) i zatim ga uzitavamo funkcijom CreateMesh. Sada su ucitani i pocaci o obliku, materijalu i texturama... jedino sto nije podeseno je ambijentalna boja pa ambijentalno svetlo nece imati uticaja na izgled objekta.

Ok... sada da podesimo matrice i sve sto je potrebno kada dobijemo kontrolu nad grafickom:
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  vEyePt, vLookatPt, vUpVec: TD3DVector;

  matView: TD3DMatrix;
  matProj: TD3DMatrix;
  matWorld: TD3DMatrix;
begin
  D3DXMatrixIdentity(matWorld);
  pd3dDevice.SetTransform(D3DTS_WORLD, matWorld);

  vEyePt:= D3DXVector3(0, 3, -5);
  vLookatPt:= D3DXVector3(0, 0, 0);
  vUpVec:= D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEyePt, vLookatPt, vUpVec);
  pd3dDevice.SetTransform(D3DTS_VIEW, matView);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    1, 100);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, matProj);

  Mesh.RestoreDeviceObjects(pd3dDevice);

  pd3dDevice.SetRenderState(D3DRS_LIGHTING, iFalse);

  Result:= S_OK;
end;

Iskljucili smo osvetljenje tako da ce se boje na objektu videti kao sto su na texturi bez ikakve promene.

Oslobadjanje memorije kada izgubimo kontrolu nad grafickom i kada program treeba da se zavrsi:
procedure OnLostDevice; stdcall;
begin
  Mesh.InvalidateDeviceObjects;
end;

procedure OnDestroyDevice; stdcall;
begin
  Mesh.Free;
end;


Idemo sad na odredjivanje da li se korsor nalazi iznad objekta:
function Pick: Boolean;
var
  vPickRayDir: TD3DXVector3;
  vPickRayOrig: TD3DXVector3;
  pd3dDevice: IDirect3DDevice9;
  pd3dsdBackBuffer: PD3DSurfaceDesc;
  matProj: TD3DXMatrix;
  ptCursor: TPoint;
  v: TD3DXVector3;
  matView: TD3DXMatrix;
  matWorld: TD3DXMatrix;
  mWorldView: TD3DXMatrix;
  m: TD3DXMatrix;
  bHit: BOOL;
begin
  pd3dDevice := DXUTGetD3DDevice;
  pd3dsdBackBuffer := DXUTGetBackBufferSurfaceDesc;

  pd3dDevice.GetTransform(D3DTS_PROJECTION, matProj);
  pd3dDevice.GetTransform(D3DTS_VIEW, matView);
  pd3dDevice.GetTransform(D3DTS_WORLD, matWorld);

  D3DXMatrixMultiply(mWorldView, matWorld, matView);
  D3DXMatrixInverse(m, nil, mWorldView);

  GetCursorPos(ptCursor);
  ScreenToClient(DXUTGetHWND, ptCursor);

  v.x :=  ( ( ( 2.0 * ptCursor.x ) / pd3dsdBackBuffer.Width  ) - 1 ) / matProj._11;
  v.y := -( ( ( 2.0 * ptCursor.y ) / pd3dsdBackBuffer.Height ) - 1 ) / matProj._22;
  v.z :=  1.0;

  vPickRayDir.x  := v.x*m._11 + v.y*m._21 + v.z*m._31;
  vPickRayDir.y  := v.x*m._12 + v.y*m._22 + v.z*m._32;
  vPickRayDir.z  := v.x*m._13 + v.y*m._23 + v.z*m._33;
  vPickRayOrig.x := m._41;
  vPickRayOrig.y := m._42;
  vPickRayOrig.z := m._43;

  D3DXIntersect(Mesh.Mesh, vPickRayOrig, vPickRayDir, bHit, nil, nil, nil, nil, nil, nil);
  Result := bHit;
end;


Kao sto vidite... veci deo ove funkcije je cista matematika Smile Prvo uzimamo sve matrice i koordinatu na kojoj se nalazi mis.
Koordinatu misa u prozoru se zatim koristi za racunanje vektora zraka za koji cemo proveravati da li prolazi kroz mesh. Vektor zraka se u omo trenutku nalazi u "screen space" tj. u koordinatom sistemu ekrana.
Ostaje nam jos samo da na osnovu ovog podatka odredimo pocetak i pravac zraka u 3d prostoru u kojem se nalazi nas mesh.
Posle ove silne matematike (casove matematike ce neko drugi da drzi) ostaje jos samo jedan poziv fukciji D3DXIntersect. Za objasnjenje svih mogucnosti ove funkcije pogledajte ovde, a mi koristimo samo prva 4 parametra koji predstavljaju mesh, pocetak i smer zraka, i promenljivu koja ce biti True ili False u zavisnosti od toga da li zrak prolazi kroz mesh.

Sad da iskoristimo ovu funkciju da bismo znali kad da rotiramo mesh:
procedure OnFrameMove(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
var
  matWorld: TD3DMatrix;
begin
  if Pick then
    fAngle := fAngle + fElapsedTime;

  D3DXMatrixRotationY(matWorld, fAngle);
  pd3dDevice.SetTransform(D3DTS_WORLD, matWorld);
end;

Ako je mis iznad objekta povecava se ugao rotacije i na osnovu njega se racuna WORLD matrica koja ce postaviti objekta tamo gde ga zelimo.

Ostaje jos samo iscrtavanje... koje je prilicno lako Smile
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(55, 0, 200), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    Mesh.Render(pd3dDevice);
    pd3dDevice.EndScene;
  end;
end;

Dovoljno je samo pozvati Render funkciju i objekta ce se iscrtati Smile

To je to za sad Smile



Source (Delphi) | Source (Lazarus) | Bin | Media


Idi na vrh
offline
  • Pridružio: 10 Nov 2004
  • Poruke: 960
  • Gde živiš: Kucura
Uloguj se preko Facebook-a i postavi pitanje:
Evo ti jedna veeeelika pohvala od mene!!!
Stvarno si se potrudio!

POZDRAV!!!


Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Hvala Smile
Trudim se da pisem sto razumljivije i sto jednostavnije. Ja sam se patio dok sam ucio, ali drugi ne moraju Smile Pomocice ljudima ako se otvori deo foruma za pravljenje igara Very Happy

Dopuna: 19 Feb 2006 22:05


Za sve sto smo do sada radili smo koristili Fixed Function Pipeline. Zbog Fixed Function Pipeline su vertex-i dobijali boju, svetlo ih je obasjavalo, WORLD, VIEW i PROJ. matrice su uticale na njihovu poziciju, pixel-i su dobijali boje na osnovu podataka o vertex-ima, idt...
Sve ove funkcije mozemo sami da radimo i na taj nacin da oderdjujemo kako ce koji parametar uticati na krajnji rezultat. Mozemo postici da se osvetljeni predmeti vide jednom, a neosvetljeni drugom bojom, da menjamo pozicije objekata, da uticemo na njihov oblik, da crtamo senke, itd... Za to sluze Vertex i Pixel Shader-i. Vertex Shader je zaduzen za poreacije nad vertex-ima, a Pixel Shader za oderdjivanje konacne boje pixel-a na ekranu. Ranije su Vertex i Pixel Shader-i pisani kao posebni programi koji su licili na assembler, ali sada Direct3D podrzava HLSL (High Level Shader Language) jezik koji je vrlo slican C jeziku. Prednost shader-a je u tome sto se izvrsavaju u GPU i time rasterecuju CPU.

U ovom primeru cemo raditi sa efektima. Efekat omogucava spajanje Vertex i Pixel Shader-a... efekti su sledeci korak u razvitku shadera. Takodje je moguce navesti nekoliko nacina obrade podataka i tako podrzati kartice od vrlo losih do najkvalitetnijih. Najbolje od svega je sto cak i ako se pojavi nova graficka kartica sa nekim novim mogucnostima, necete morati da pisete program iz pocetka nego samo u efekt fajlu dodate nov nacin obrade. Vise o efektima mozete pronaci ovde.

Za informacije o HLSL jeziku (sintaksa, tipovi podataka, mogucnosti,...) pogledajte ovde.

Primer cemo poceti pisanjem efekta. Idemo od definisanja promenljivih koje ce shaderi koristiti:
float g_fTime;
float4x4 g_mWorldViewProjection;
texture g_MeshTexture;

sampler MeshTextureSampler =
sampler_state
{
  Texture = <g_MeshTexture>;
  MipFilter = LINEAR;
  MinFilter = LINEAR;
  MagFilter = LINEAR;
};

Ko je radio sa C/C++ vec primecuje slicnost prilikom pisanja koda. Za raliku od Delphi-a i Lazarus-a gde se promenljiva definise tako sto prvo pisemo njeno ime, a zatim tip, kod efekata se prvo pise tip pa tek onda naziv.

g_fTime je tipa float (decimalni broj) i u toj promenljivoj ce se nalaziti vrele koliko dugo program radi.

g_mWorldViewProjection je tipa float4x4 (matrica velicine 4x4 decimalnih brojeva) i u njoj ce se nalaziti matrica koja ce koordinatu vertex-a prevesti u 2D koordinatu za prikaz na ekranu.

g_MeshTexture ce sadrzati texturu objekta koji budemo iscrtavali.

MeshTextureSampler ce sadrzati podatke o texturi koja se iscrtava.

Vrste podataka kao i njihova imena mogu biti drugaciji nego u ovom primeru... sve zavisi od toga sta shader-i treba da rade.

Sada cemo napisati Vertex Shader koji ce nas objekat rotirati oko Y ose:
float4x4 RotationY(float angle)
{
  return float4x4(cos(angle), 0, -sin(angle), 0, 0, 1, 0, 0,
           sin(angle), 0, cos(angle), 0, 0, 0, 0, 1 );
}

struct VS_OUTPUT
{
  float4 Position : POSITION;
  float2 TextureUV : TEXCOORD0;
};

VS_OUTPUT RenderSceneVS(float4 vPos : POSITION,
                        float2 vTexCoord0 : TEXCOORD0)
{
  VS_OUTPUT Output;
  float4 vRotPos;

  vRotPos = mul(vPos, RotationY(g_fTime));
   
  Output.Position = mul(vRotPos, g_mWorldViewProjection);
   
  Output.TextureUV = vTexCoord0;
   
  return Output;
}

Idemo od pocetka... prvo smo definisali funkciju RotationY. Ona ce vratiti matricu rotacije oko Y ose za dati ugao. Na osnovu rezultata ove funkcije cemo dobiti rotiran polozaj vertex-a.

Zatim definisemo tip podatka koji ce Vertex Shader da vrati. Vrati ce poziciju pixel-a koji treba da se iscrta kao i koordinatu na texturi koja odgovara tom pixel-u.

OK... idemo sad na shader... ulazni podaci su pozicija vertex-a i koordinata na texturi... to je sve sto nam je potrebno za ovaj shader. Primetili ste da pored definicije promenljive vPos stoji i POSITION. To oznacava da ta promenljiva treba da se popuni podatkom o poziciji vertex-a... za prvu texturu tekst iza promenljive je TEXCOORD0, za drugu je TEXCOORD1, za normalu vertex-a je NORMAL, itd... sve to imate u opisu HLSL jezika.

Na pocetku shader-a definisemo promenljivu Output u kojoj ce biti rezultat obrade i vRotPos u kojoj ce se nalaziti rotirana koordinata vertex-a.

Koordinatu vertex-a rotiramo tako sto je mnozimo matricom rotacije oko Y ose za koju smo vec napisali funkciju. Tako dobijenu koorindau zatim mnozimo matricom g_mWorldViewProjection (ona je proizvod WORLD, VIEW i PROJECTION matrice) i time dobijamo koordinatu tacke na ekranu.

Posto koordinatu texture necemo menjati jednostavno je kopiramo u Output promenljivu i time zavrsavamo Vertex Shader.

Dakle... shader ce da na osnovu vremena (rekli smo da ce g_fTime da sadrzi vreme od pocetka rada aplikacije) da izrotira objekat i to je to. Prilicno jednostavan shader.

Sledeci na redu je pixel shader:
struct PS_OUTPUT
{
  float4 RGBColor : COLOR0;
};

PS_OUTPUT RenderScenePS(float2 TextureUV : TEXCOORD0)
{
  PS_OUTPUT Output;

  Output.RGBColor = tex2D(MeshTextureSampler, TextureUV);

  return Output;
}

Pixel Shader ce vratiti samo zeljenu boju pixel-a... a za ulaz uzima koordinatu texture koja treba da odredi boju tacke na ekranu.

Sve sto shader radi je izvlacenje boje tacke na texturi koja se nalazi na koordinati smestenoj u TextureUV promenljivoj. Na ovaj nacin nista osim texture ne utice na boju tacke (mozemo slobodno da ukljucimo ili iskljucimo svetlo, da menjamo difuse ili ambient boju... shader jednostavno ne obraca paznju na te podatke).

Sve sto nam ostaje je da ove shader-e spojimo u jednu celinu koju ce program moci da poziva:
technique RenderScene
{
  pass P0
  {
    VertexShader = compile vs_1_1 RenderSceneVS();
    PixelShader  = compile ps_1_1 RenderScenePS();
  }
}

Ovaj nacin crtanja smo nazvali RenderScene i pod tim imenom ga moramo pozivati u programu. Ovaj nacin tj. tehnika koristi singlepass (sve se iscrtava u jednom prolazu), ali moguce je dodati jos prolaza ako je to potrebno i tada dobijamo multipass nacin iscrtavanja (kao u primeru gde su u dva prolaza stapali texture).

Effect fajl je zavrsen... sada se prebacujemo na glavni program. Potrebne su nam sledece globalne promenljive:
var
  Mesh: CDXUTMesh;
  Effect: ID3DXEffect;
  matView: TD3DMatrix;
  matProj: TD3DMatrix;
  matWorld: TD3DMatrix;


Posto u efektu koristimo Pixel Shader verzije 1.1 potrebno je i da kartica podrzava tu verziju shader-a i zato cemo ubaciti proveru prilikom odabiranja moda prikaza:
function IsDeviceAcceptable(const pCaps: TD3DCaps9; AdapterFormat, BackBufferFormat: TD3DFormat; bWindowed: Boolean; pUserContext: Pointer): Boolean; stdcall;
begin
  Result := False;

  if (pCaps.PixelShaderVersion < D3DPS_VERSION(1, 1)) then
    Exit;

  Result := True;
end;

Mogli smo napraviti i tehniku koja ne bi koristila Pixel Shader i da na osnovu podataka o kartici izaberemo najbolju tehniku, ali nisam hteo da komplikujem kod. Primeri sa vise tehnika ce biti pokazani kasnije.

Sada kreiramo mesh (nas tigar kojeg smo i do sad koristili) i efekat koji cemo koristiti da iscrtamo tigra:
function OnCreateDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  Result := E_FAIL;
  try
    Mesh := CDXUTMesh.Create('Tigar');
  except
    Mesh := nil;
    Exit;
  end;

  if Failed(Mesh.CreateMesh(pd3dDevice, 'tiger.x')) then
    Exit;

  if Failed(D3DXCreateEffectFromFile(pd3dDevice, 'Effect.fx',
      nil, nil, 0, nil, Effect, nil)) then
    Exit;

  Result := S_OK;
end;

Efekat kreiramo funkcijom D3DXCreateEffectFromFile.
Kao prvi parametar uzima devoce za koji se efekat kreira.
Drugi je putanja do effect fajla.
Treci parametar predstavlja pointer na niz promenljivih tipa D3DXMACRO. Svaki podatak sadrzi po jednu preprocessor macro definiciju (kao {$define} u Delphi-u).
Cetvrti predstavlja ID3DXInclude koji sluzi za ucitavanje #include fajlova. Ako je ovaj parametar nil tada ce Direct3D da se pobrine za to.
Sledeci parametar su razni flagovi koji uticu na kreiranje efekta.
Zatim ide objekat tipa ID3DXEffectPool. Kada postoje vise efekata navedeni ID3DXEffectPool ce sluziti za cuvanje zajednickih promenljivih.
Poslednja dva parametra su promenljiva u kojoj ce biti efekat i buffer u kojem ce biti smesteni podaci o kompajliranju efekta (greske i slicno).
Za kompletno objasnjenje ove funkcije pogledajte ovde.

Ovog puta kad budemo dobili kontrolu nad grafickom necemo postavljati matrice... samo cemo ih izracunati. Ne bi bilo nikakvih problema i da smo podesili matrice, ali sam samo hteo da pokazem da efekat ne uzima vrednosti koje postavljamo za device nego samo one koje mu dajemo:
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  vEyePt, vLookatPt, vUpVec: TD3DVector;

begin
  Result := E_FAIL;

  D3DXMatrixIdentity(matWorld);

  vEyePt:= D3DXVector3(0, 3, -5);
  vLookatPt:= D3DXVector3(0, 0, 0);
  vUpVec:= D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEyePt, vLookatPt, vUpVec);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    1, 100);

  if Failed(Mesh.RestoreDeviceObjects(pd3dDevice)) then
    Exit;

  if Failed(Effect.OnResetDevice) then
    Exit;

  Result := S_OK;
end;


Idemo sad na iscrtavanje:
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
var
  mat: TD3DXMatrix;
  matWorldViewProjection: TD3DXMatrix;
  cPasses: LongWord;
  P, M: Integer;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(55, 0, 200), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    D3DXMatrixMultiply(mat, matView, matProj);
    D3DXMatrixMultiply(matWorldViewProjection, matWorld, mat);

    Effect.SetMatrix('g_mWorldViewProjection', matWorldViewProjection);
    Effect.SetFloat('g_fTime', fTime);
    Effect.SetTechnique('RenderScene');

    Effect._Begin(@cPasses, 0);
    for P := 0 to cPasses - 1 do
    begin
      Effect.BeginPass(p);
      for M := 0 to Mesh.m_dwNumMaterials - 1 do
      begin
        Effect.SetTexture('g_MeshTexture', Mesh.m_pTextures[M]);
        Effect.CommitChanges;
        Mesh.Mesh.DrawSubset(M);
      end;
      Effect.EndPass;
    end;
    Effect._End;

    pd3dDevice.EndScene;
  end;
end;

Prvo sto radimo je racunanje WORLD * VIEW * PROJ. matrice... kada ja imamo mozemo da postavimo g_mWorldViewProjection parametar u efektu.
Zatim postavljamo g_fTime parametar na trenutno vreme iz programa.
Kada to uradimo, biramo tehniku i pocinjemo sa iscrtavanjem.

Effect._Begin funkcija zapocinje izabranu tehniku. Prvi parametar ce posle ove funkcije sadrzati broj prolaza koji je potreban da bi se sve iscrtalo, a drugi parametar je flag koji oznacava da li ce efekat sacuvati stanje device-a i po zavrsetku iscrtavanja vratiti ili ne. Kada je flag 0 tada se podaci cuvaju.

Effect.BeginPass zapocenjie izabrani prolaz i sada se mogu iscrtavati vertex-i. Proci cemo kroz sve materijale u mesh-u kao sto smo i ranije radili i prilikom svakog prolaza cemo postaviti texturu za efekat u g_MeshTexture promenljivu.

Posle poziva funkciji Effect.CommitChanges, sve promene u efektu postaju aktivne i tada iscrtavamo objekat.

Po zavrsetku prolaza pozivamo Effect.EndPass i na kraju efekta pozivamo Effect._End... i time zavrsavamo iscrtavanje.

Ovo je najjednostavniji nacin koriscenja efekta... ima jos mnogo funkcija koje se mogu koristiti za odredjivanje tehnike koja najvise odgovara racunaru na kojem se program izvrsava, funkcije za postavljanje parametara, funkcije za rad sa vise efekata, itd... kako budemo nailazili na nove funkcije za rad sa efektima tako cemo ih objasnjavati. Do tada mozete probati da malo izmenite effect fajl i da pokusate da dobijete neki drugaciji prikaz (recimo promena velicine celog objekta u zavisnosti od vremena).



Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 22 Feb 2006 22:38


U ovom primeru cemo pokazati neke od mogucnosti koje Common Framework nudi za lakse kreiranje GUI-a. Kontrole nisu vrhunske (edit box ne reaguje na strelice, combo box ne radi kako treba sa scroll tockicem...), ali za neki jednostavan GUI su dovoljno dobre.

Kontrole koje mogu da se kreiraju su:
Static (text kontrola kao label)
Button
CheckBox
RadioButton
ComboBox
Slider (kao scroll bar)
EditBox
IMEEditBox (edit kontrola za koju je moguce menjati jezik unosa (unicode))
ListBox

Da pocnemo... idemo od konstanti i promenljivih koje ce nam trebati:
const
  IDC_STATICURL = 1;
  IDC_EDITURL = 2;
  IDC_BUTTONGO = 3;
  IDC_CHBOXOPEN = 4;
  IDC_STATICRED = 5;
  IDC_STATICGREEN = 6;
  IDC_STATICBLUE = 7;
  IDC_SLIDERRED = 5;
  IDC_SLIDERGREEN = 6;
  IDC_SLIDERBLUE = 7;
  IDC_RADIOLIST = 8;
  IDC_RADIOCOMBO = 9;
  IDC_LISTBOX = 10;
  IDC_COMBOBOX = 11;

var
  DialogResourceManager: CDXUTDialogResourceManager;
  dlgMain: CDXUTDialog;


IDC_* konstante ce nam sluziti kao ID broj za kontrole koje cemo kreirali... to je, recimo, kao Name property kod standardnih kontrola koje se koriste u Delphi-u i Lazarus-u.

DialogResourceManager je objekat koji ce cuvati sve potrebne podatke za iscrtavanje kontrola (texture i fontove). Ovaj objekat skoro da necemo ni koristiti... dialozi ce komunicirati sa ovim objektom kad god to bude potrebno.
dlgMain je objekat koji cemo koristiti za kreiranje kontrola i koji ce se brinuti o njima.

Prvo da kreiramo sve objekte i kontrole koje cemo koristiti:
procedure InitDialog;
var
  I: Integer;
  S: WideString;
begin
  DialogResourceManager := CDXUTDialogResourceManager.Create;
  dlgMain := CDXUTDialog.Create;
  dlgMain.Init(DialogResourceManager);
  dlgMain.SetCallback(OnGUIEvent);

  dlgMain.AddStatic(IDC_STATICURL, 'Putanja do fajla, URL adresa... sve što ShellExecute može da otvori komandom OPEN:', 8, 8, 450, 20);
  dlgMain.Static[IDC_STATICURL].Element[0].dwTextFormat := DT_LEFT or DT_TOP;

  dlgMain.AddEditBox(IDC_EDITURL, 'http://www.mycity.rs/', 8, 24, 200, 32);

  dlgMain.AddButton(IDC_BUTTONGO, 'Otvori', 300, 24, 40, 32);

  dlgMain.AddCheckBox(IDC_CHBOXOPEN, 'Otvori pritiskom na Enter', 8, 64, 150, 16, True);

  dlgMain.AddStatic(IDC_STATICRED, 'Red:', 8, 90, 40, 30);
  dlgMain.AddSlider(IDC_SLIDERRED, 60, 90, 200, 30, 0, 255, 100);

  dlgMain.AddStatic(IDC_STATICGREEN, 'Green:', 8, 130, 40, 30);
  dlgMain.AddSlider(IDC_SLIDERGREEN, 60, 130, 200, 30, 0, 255, 100);

  dlgMain.AddStatic(IDC_STATICBLUE, 'Blue:', 8, 170, 40, 30);
  dlgMain.AddSlider(IDC_SLIDERBLUE, 60, 170, 200, 30, 0, 255, 100);

  dlgMain.AddRadioButton(IDC_RADIOLIST, 0, 'Prikaži List Box', 8, 250, 110, 16, True);
  dlgMain.AddRadioButton(IDC_RADIOCOMBO, 0, 'Prikaži Combo Box', 8, 274, 110, 16);

  dlgMain.AddListBox(IDC_LISTBOX, 150, 250, 200, 200);
  dlgMain.AddComboBox(IDC_COMBOBOX, 150, 250, 200, 32);

  for I := 1 to 1000 do
  begin
    S := UTF8Decode('List Box Item: ' + IntToStr(I));
    dlgMain.ListBox[IDC_LISTBOX].AddItem(PWideChar(S), nil);

    S := UTF8Decode('Combo Box Item: ' + IntToStr(I));
    dlgMain.ComboBox[IDC_COMBOBOX].AddItem(PWideChar(S), nil);
  end;
  dlgMain.ComboBox[IDC_COMBOBOX].Visible := False;

  dlgMain.RequestFocus(dlgMain.EditBox[IDC_EDITURL]);
end;

procedure DoneDialog;
begin
  FreeAndNil(DialogResourceManager);
  FreeAndNil(dlgMain);
end;


Pre nego sto pocnemo da da radimo sa kontrolama potrebno je da inicijalizujemo doalog pozivom funkcije dlgMain.Init. Ona uzima nekoliko parametara, ali dovoljno je samo da prosledimo objekat koji ce se brinuti o resursima. Ostali parametri odredjuju koja ce se textura koristiti za iscrtavanje kontrola. Posto nismo nista naveli, koristice se default textura.

Dodavanje kontrola se vrsi pozivanjem dlgMain.Add* funkcija. Prvi parametar je uvek ID broj koji zelimo da kontrola ima. Na osnovu tog ID broja kasnije mozemo da pronadjemo kontrolu ili da reagujemo na evente koji su vezani za nju. Naredni parametri su specificni za svaku kontrolu... uglavnom su to text, pozicija i velicina kontrole i slicno. Svaki tip kontrola je koristen u ovom primeru pa mozete videti parametre koji su bitni.

Primetili ste da smo postavili callback funkciju... za to se koristi dlgMain.SetCallback. Callback funkcija se poziva svaki put kada se neki event dogodi. Uskoro cemo reci nesto i o toj funkciji.

Kada nam dialog vise nije potreban osobadjamo ga... ovo se radi na kraju programa zbog nacina na koji ovi objekti organizuju pristup memoriji.

Posto velicina prozora moze da se menja, moramo definisati nacin na koji ce se kontrole siriti i skupljati u zavisnosti od velicine. Ovo se najlakse radi u toku OnResetDevice funkcije:
function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  Result := E_FAIL;

  if Failed(DialogResourceManager.OnResetDevice) then
    Exit;

  dlgMain.SetLocation(0, 0);
  dlgMain.SetSize(pBackBufferSurfaceDesc.Width, pBackBufferSurfaceDesc.Height);

  dlgMain.EditBox[IDC_EDITURL].SetSize(pBackBufferSurfaceDesc.Width - 60, 32);
  dlgMain.Button[IDC_BUTTONGO].SetLocation(pBackBufferSurfaceDesc.Width - 48, 24);
  dlgMain.Slider[IDC_SLIDERRED].SetSize(pBackBufferSurfaceDesc.Width - 88, 30);
  dlgMain.Slider[IDC_SLIDERGREEN].SetSize(pBackBufferSurfaceDesc.Width - 88, 30);
  dlgMain.Slider[IDC_SLIDERBLUE].SetSize(pBackBufferSurfaceDesc.Width - 88, 30);
  dlgMain.ListBox[IDC_LISTBOX].SetSize(pBackBufferSurfaceDesc.Width - 158,
    pBackBufferSurfaceDesc.Height - 258);
  dlgMain.ComboBox[IDC_COMBOBOX].SetSize(pBackBufferSurfaceDesc.Width - 158,
    32);
  dlgMain.ComboBox[IDC_COMBOBOX].SetDropHeight(pBackBufferSurfaceDesc.Height - 318);

  Result := S_OK;
end;


Na ovaj nacin, kad god se velicina promeni, izracunacemo novu poziciju i velicinu kontrola. SetLocation postavlja koordinatu na kojoj se nalazi kontrola, a SetSize sirina i visina kontrole. Combo box kontrole imaju mogucnost da definisu koliko ce se otvoriti kada kliknemo na nju. To se podesava pozivom funkcije SetDropHeight.

Uredu... hajde i da iscrtamo GUI... ovo ce biti veoma kratko Smile
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER,
    D3DCOLOR_XRGB(
      dlgMain.Slider[IDC_SLIDERRED].Value,
      dlgMain.Slider[IDC_SLIDERGREEN].Value,
      dlgMain.Slider[IDC_SLIDERBLUE].Value
    ), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    dlgMain.OnRender(fElapsedTime);
    pd3dDevice.EndScene;
  end;
end;


Koristili smo slider komponente i na osnovu njihove pozicije racunamo boju pozadine. Samo iscrtavanje GUI-a se vrsi pozivom funkcije dlgMain.OnRender. To je sve sto je potrebno da bi se kontrole iscrtale.

Da se vratimo na callback funkciju... ona ce izgledati ovako:
procedure OnGUIEvent(nEvent: LongWord; nControlID: Integer; pControl: CDXUTControl; pUserContext: Pointer); stdcall;
var
  S: WideString;
begin
  case nControlID of
    IDC_BUTTONGO:
    begin
      if nEvent = EVENT_BUTTON_CLICKED then
      begin
        S := dlgMain.EditBox[IDC_EDITURL].Text;
        ShellExecuteW(DXUTGetHWND, 'open', PWideChar(S), nil, nil, SW_SHOW);
      end;
    end;

    IDC_EDITURL:
    begin
      if (nEvent = EVENT_EDITBOX_STRING) and
         (dlgMain.CheckBox[IDC_CHBOXOPEN].Checked) then
         dlgMain.SendEvent(EVENT_BUTTON_CLICKED, True,
           dlgMain.Button[IDC_BUTTONGO]);
    end;

    IDC_RADIOLIST, IDC_RADIOCOMBO:
    begin
      if nEvent = EVENT_RADIOBUTTON_CHANGED then
      begin
        dlgMain.ListBox[IDC_LISTBOX].Visible :=
          dlgMain.RadioButton[IDC_RADIOLIST].Checked;
        dlgMain.ComboBox[IDC_COMBOBOX].Visible :=
          dlgMain.RadioButton[IDC_RADIOCOMBO].Checked;
      end;
    end;
  end;
end;


Podaci koje primamo su event koji je nasato, ID kontrole i kontrola za koju se desio event. Ovo je sasvim dovoljno da mozemo da znamo sta se desava u nasem GUI-u.
Kod koji je napisan u ovoj funckiji reaguje na klik dugmeta tako sto otvara fajl iz edit box-a. Takodje... ako se pritisne Enter u edit box-u, a check box je cekiran izazvacemo event koji simulira klik na dugme i time opet otvaramo fajl iz edit box-a. I, za kraj, kada se obelezeni radio button promeni, odlucujemo da li ce list box ili combo box biti vidljivi. Prilicno je slicno kao obrada windows poruka koju smo vrsili u prvih nekoliko primera.

I... to je to... nista komplikovano... jedino je, za neki malo komplikovaniji GUI, potrebno pisati dosta koda Smile



Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 26 Feb 2006 0:46


U ovom primeru cemo pokazati kako je moguce animirati objekte uz pomoc skinning tehnike... ovo ce biti najduzi text do sada Smile U ovoj tehnici objekat se sastoji od vertexa i kostiju. Kosti objekta uticu na polozaj vertexa koji su vezani na njih. Kosti su jednostavno matrice koje postavljaju vertex-e na zeljenu poziciju.

Postoji vise tehnika koje se mogu koristiti za ovakvu vrstu animacije... fixed-function non-indexed, fixed-function indexed, software, HLSL shader... svaka od ovih tehnika ima svoje prednosti i mane i na programeru je da odluci koja tehnika najvise odgovara specifikacijama racunara na kojem ce se program izvrsavati.

Da bi lakse razumeli kod, prvo cu ukratko objasniti kako program radi.
Prvo cemo ucitati mesh funkcijom D3DXLoadMeshHierarchyFromX. Ova funkcija ce ucitati sve frejmove (u frejmu se nalaze informacije o kostima tj. matricama koje uticu na polozaj vertex-a) i objekte koje se nalaze u *.x fajlu.

Postojace jedan glavni frejm i u njemu ce se nalaziti vise podfrejmova... svaki podfrejm moze imati jos svojih podfrejmova... nesto kao sto je struktura foldera na racunaru. Na ovaj nacin, ako se glafni frejm zarotira to ce uticati na sve njegove podfrejmove.

Hijerarhija frejmova ce se nalaziti u podatku tipa TD3DXFrame:
PD3DXFrame = ^TD3DXFrame;
TD3DXFrame = packed record
  Name: PAnsiChar;
  TransformationMatrix: TD3DXMatrix;
  pMeshContainer: PD3DXMeshContainer;
  pFrameSibling: PD3DXFrame;
  pFrameFirstChild: PD3DXFrame;
end;


Aplikacija moze prosiriti ovu strukturu da bi sacivala i neke specificne podatke vezane za frejm. U ovom tipu se nalazi minimum podataka koji su potrebni za rad sa fremovima... naziv frejma, matrica transofrmacije (kost), mesh container (objekti vezani za ovaj frejm), frejmovi koji se nalaze na istom nivou (sibling) i frejmovi koji se nalaze ispod (child).
Mi cemo u nasem primeru dodati jos jedan clan koji ce sadrzati apsolutnu matricu za dati frejm. Racunacemo je tako sto cemo pomnoziti sve matrice od vrha do trenutnog frejma.

Neki od frejmova ce sadrzati objekte... oni ce biti smesteni u podacima tipa TD3DXMeshContainer:
PD3DXMeshContainer = ^TD3DXMeshContainer;
TD3DXMeshContainer = packed record
  Name: PAnsiChar;
  MeshData: TD3DXMeshData;
  pMaterials: PD3DXMaterial;
  pEffects: PD3DXEffectInstance;
  NumMaterials: DWORD;
  pAdjacency: PDWORD;
  pSkinInfo: ID3DXSkinInfo;
  pNextMeshContainer: PD3DXMeshContainer;
end;


Svi podaci iz ove strukture postoje i u obicnom mesh-u osim Name i pSkinInfo promenljivih. Name ce sadrzati ime koje oznacava objekat, a pSkinInfo podatke o tome kako su vertex-i vezani za kosti.


Osim sto D3DXLoadMeshHierarchyFromX ucitava celu hijerarhiju mesh-a, takodje nam vraca objekta tipa ID3DXAnimationController koji nam omogucava da lako na osnovu vremena menjamo polozaj kostiju, a time i vertex-a.

Posto se ova dva tipa skoro uvek dopunjuju podacima vezanim za aplikaciju, DirectX ne moze da zna kako da kreira takve objekte i da ih oslobadja i zbog toga aplikacija treba da postavi funkcije koje ce to uraditi. ID3DXAllocateHierarchy je interfejs na osnovu kojeg treba da se napisu funkcije za kreiranje i oslobadnjanje frejma i mesh container-a: CreateFrame, DestroyFrame, CreateMeshContainer i DestroyMeshContainer.

Najkomlpikovanija je CreateMeshContainer funkcija... ona ce na osnovu primljenih podataka da popuni polja u strukturi i zatim da pozove funkciju GenerateSkinnedMesh (mi cemo je napisati). Ova funkcija ce na osnovu podataka iz pSkinInfo da podesi hijerarhiju za animaciju i iscrtavanje.

Posle ucitavanja i postavljanja podataka ostaje nam jos da samo izracunamo matrice... to ce raditi funkcija SetupBoneMatrixPointers... ona ce ici kroz sve frejmove u potrazi za validnim mesh container objektima. Kada ih nadje pozvace SetupBoneMatrixPointersOnMesh koja ce postaviti matrice kostiju za svaki mesh.

Renderovanje se sastoji iz dva dela... postavljanje aktivnih matrica i iscrtavanja vertex-a.
Da bi smo mogli da postavimo matrice moramo prvo pozvati AdvanceTime funkciju ID3DXAnimationController objekta. Ova funkcija ce proci kroz sve frejmove i podesiti matrice tako da se nalaze u poziiji za zadato vreme. Zatim pozivamo UpdateFrameMatrices funkciju koja ce izracunati apsolutnu matricu za svaki frejm kako smo to ranije vec objasnili.
Iscrtavanje objekta zavisi od tehnike koja se oristi:

Kada se koristi Fixed-Function Non-Indexed Skinning tehnika device moze postaviti najvise 4 matrice i da na osnovu njih iscrta sve vertex-e koji su vezani za njih. Posle toga se uzimaju sledece 4 matrice i tako dok se ne iscrtaju svi vertex-i.

Kada se koristi Fixed-Function Indexed Skinning tada device radi sa paletama matrica. Velicina palete se moze pronaci u osobinama device-a. Aplikacija ucitava sve matrice koje mogu da stanu u paletu i zatim se iscrtavaju svi vertex-i. svaki vertex ima index-a koji odredjuju za koju je matricu vezan taj vertex.

Renderovanje uz pomoc shader-a je u sustini isto kao i Indexed Skinning samo sto se umesto matrica paleta koriste shader konstante. Da bi se ustedelo na prostoru matrica se koristi u formatu 4x3 jer je cetvrta kolona uvek 1 i ne mora da se navodi.

Software Skinning je tehnika u kojoj se sve vrsi u aplikaciji. Na osnovu matrica se kreira potpuno novi mesh u kojem ce vertex-i imati polozaj koji je uslovljen matricama kostiju. Ovaj nacin je ubedljivo najsporiji, ali je takodje i koristan jer se, kada imamo nov mesh, moze koristiti za detekciju sudara, iscrtavanje se moze vrsiti na losijim karticama, deformisani mesh se moze snimiti u fajl kao novi mesh,...

Da bi sto vise ljudi moglo da pokrene primer, koristicemo Software Skinning.

Posle malo teorije idemo na program (provera gresaka u programu je svedena na minimum kako bi kod bio sto jednostavniji) Smile

Prvo cemo definisati tipove i promenljive koje ce nam trebati:
var
  g_pBoneMatrices: array of TD3DXMatrix;
  g_NumBoneMatricesMax: LongWord = 0;
  g_pAnimController: ID3DXAnimationController;
  g_matView: TD3DXMatrix;
  g_matProj: TD3DXMatrix;
  g_matWorld: TD3DXMatrix;
  g_pFrameRoot: PD3DXFrame;
  g_vObjectCenter: TD3DXVector3;
  g_fObjectRadius: Single;

type
  PD3DXFrameDerived = ^TD3DXFrameDerived;
  TD3DXFrameDerived = packed record
    Name: PAnsiChar;
    TransformationMatrix: TD3DXMatrix;
    pMeshContainer: PD3DXMeshContainer;
    pFrameSibling: PD3DXFrame;
    pFrameFirstChild: PD3DXFrame;
    CombinedTransformationMatrix: TD3DXMatrix;
  end;

  TD3DXMaterialArray = array of TD3DXMaterial;

  TIDirect3DTexture9Array = array of IDirect3DTexture9;

  TD3DXMatrixPointerArray = array of PD3DXMatrix;

  TD3DXMatrixArray = array of TD3DXMatrix;

  PD3DXMeshContainerDerived = ^TD3DXMeshContainerDerived;
  TD3DXMeshContainerDerived = packed record
    Name: PAnsiChar;
    MeshData: TD3DXMeshData;
    pMaterials: TD3DXMaterialArray;
    pEffects: PD3DXEffectInstance;
    NumMaterials: DWORD;
    pAdjacency: PDWORD;
    pSkinInfo: ID3DXSkinInfo;
    pNextMeshContainer: PD3DXMeshContainer;

    ppTextures: TIDirect3DTexture9Array;
    pOrigMesh: ID3DXMesh;
    pAttributeTable: array of TD3DXAttributeRange;
    NumAttributeGroups: DWORD;
    ppBoneMatrixPtrs: TD3DXMatrixPointerArray;
    pBoneOffsetMatrices: TD3DXMatrixArray;
  end;


g_pBoneMatrices ce sadrzati matrice svih kostiju na osnovu kojih cemo kreirati nov, deformisani mesh.

g_NumBoneMatricesMax ce sadrzati broj kostiju (za svaki objekat u fajlu ce se naci broj kostiju i onaj koji ima najvise ce verdnost upisati u ovu promenljivu).

g_pAnimController ce nam sluziti za kontrolu animacije.

g_pFrameRoot ce sadrzati osnovni frejm... koristi cemo ovu promenljivu za pocetak kretanja kroz hijerarhiju frejmova.

g_vObjectCenter ce sadrzati koordinatu centra objekta, g_fObjectRadius precni lopte opisane oko objekta... koristicemo ih da bismo prikazali objekat tacno na sredini ekrana bez obzira koje je velicine i na kojim koordinatama je.

TD3DXFrameDerived tip je samo prosiren TD3DXFrame tip podatka. Jedino sto je dodato je CombinedTransformationMatrix polje koje ce sadrzati proizvod matrica od vrha do trenutnog frejma.

TD3DXMeshContainerDerived je prosiren TD3DXMeshContainer tip. Ovde smo dodali malo vise polja... jedno u kojem ce se cuvati texture, jedno u kojem ce se nalaziti originalni mesh (njega cemo menjati svakog frejma pa je potrebno da ga imamo), dva polja koja ce oznacavati atribute svakog dela mesha i koliko tih delova ima (kad budemo kreirali animirani mesh, vertex-i ce biti rasporedjeni po grupama u zavisnosti od kostiju koje na njih uticu... svaka grupa ce imati atribute koji ih opisuju) i dva polja u kojoj ce se nalaziti pokazivac na matricu frejma u kojem se mesh nalazi kao i matrica koja oznacava poziciju mesh-a.

Kada imamo sve to mozemo preci na kreiranje klase koja ce se brinuti o kreiranju i unistavanju frejmova i mesh container-a:
type
  CAllocateHierarchy = class(ID3DXAllocateHierarchy)
  public
    function CreateFrame(Name: PAnsiChar; out ppNewFrame: PD3DXFrame): HResult; override;
    function CreateMeshContainer(Name: PAnsiChar; const pMeshData: TD3DXMeshData;
        pMaterials: PD3DXMaterial; pEffectInstances: PD3DXEffectInstance;
        NumMaterials: DWORD; pAdjacency: PDWORD; pSkinInfo: ID3DXSkinInfo;
        out ppMeshContainerOut: PD3DXMeshContainer): HResult; override;
    function DestroyFrame(pFrameToFree: PD3DXFrame): HResult; override;
    function DestroyMeshContainer(pMeshContainerToFree: PD3DXMeshContainer): HResult; override;
  end;


Prvo da se pozabavimo kodom za frejmove... koji je vrlo lak:
function CAllocateHierarchy.CreateFrame(Name: PAnsiChar;
  out ppNewFrame: PD3DXFrame): HResult;
var
  pFrame: PD3DXFrameDerived;
begin
  New(pFrame);
  ZeroMemory(pFrame, SizeOf(TD3DXFrameDerived));

  pFrame.Name:= StrNew(Name);

  D3DXMatrixIdentity(pFrame.TransformationMatrix);
  D3DXMatrixIdentity(pFrame.CombinedTransformationMatrix);

  ppNewFrame := PD3DXFrame(pFrame);

  Result := S_OK;
end;

function CAllocateHierarchy.DestroyFrame(pFrameToFree: PD3DXFrame): HResult;
begin
  StrDispose(pFrameToFree.Name);
  Dispose(pFrameToFree);
  Result := S_OK;
end;


Sve sto radimo je postavljanje imena i resetovanje matrica.

Kod za kreiranje mesh container-a je malo komplikovaniji:
function CAllocateHierarchy.CreateMeshContainer(Name: PAnsiChar;
  const pMeshData: TD3DXMeshData; pMaterials: PD3DXMaterial;
  pEffectInstances: PD3DXEffectInstance; NumMaterials: DWORD;
  pAdjacency: PDWORD; pSkinInfo: ID3DXSkinInfo;
  out ppMeshContainerOut: PD3DXMeshContainer): HResult;
var
  pNewMeshContainer: PD3DXMeshContainerDerived;
  pNewBoneOffsetMatrices: TD3DXMatrixArray;

  pd3dDevice: IDirect3DDevice9;

  pMesh: ID3DXMesh;
  NumFaces: LongWord;
  iMaterial: Integer;
  NumBones: DWORD;
  iBone: Integer;

begin
  New(pNewMeshContainer);
  FillChar(pNewMeshContainer^, 0, SizeOf(TD3DXMeshContainerDerived));

  pNewMeshContainer.Name := StrNew(Name);

  pMesh := pMeshData.pMesh as ID3DXMesh;
  NumFaces := pMesh.GetNumFaces;

  pNewMeshContainer.MeshData.pMesh := pMesh;
  pNewMeshContainer.MeshData._Type := pMeshData._Type;

  pNewMeshContainer.NumMaterials := NumMaterials;
  SetLength(pNewMeshContainer.pMaterials, pNewMeshContainer.NumMaterials);
  SetLength(pNewMeshContainer.ppTextures, pNewMeshContainer.NumMaterials);

  GetMem(pNewMeshContainer.pAdjacency, SizeOf(DWORD) * NumFaces * 3);
  CopyMemory(pNewMeshContainer.pAdjacency, pAdjacency, SizeOf(DWORD) * NumFaces * 3);

  pMesh.GetDevice(pd3dDevice);
  CopyMemory(pNewMeshContainer.pMaterials, pMaterials, SizeOf(TD3DXMaterial) * NumMaterials);
  for iMaterial := 0 to NumMaterials - 1 do
  begin
    if (pNewMeshContainer.pMaterials[iMaterial].pTextureFilename <> nil) then
    begin
      if FAILED(D3DXCreateTextureFromFile(pd3dDevice, pNewMeshContainer.pMaterials[iMaterial].pTextureFilename,
                                           pNewMeshContainer.ppTextures[iMaterial])) then
        pNewMeshContainer.ppTextures[iMaterial] := nil;

      pNewMeshContainer.pMaterials[iMaterial].pTextureFilename := nil;
    end;
  end;

  if (pSkinInfo <> nil) then
  begin
    pNewMeshContainer.pSkinInfo := pSkinInfo;
    pNewMeshContainer.pOrigMesh := pMesh;
    NumBones := pSkinInfo.GetNumBones;
    SetLength(pNewBoneOffsetMatrices, NumBones);
    for iBone := 0 to NumBones - 1 do
      pNewBoneOffsetMatrices[iBone] := pSkinInfo.GetBoneOffsetMatrix(iBone)^;

    pNewMeshContainer.pBoneOffsetMatrices := pNewBoneOffsetMatrices;

    GenerateSkinnedMesh(pd3dDevice, pNewMeshContainer);
  end;

  ppMeshContainerOut := PD3DXMeshContainer(pNewMeshContainer);

  pd3dDevice := nil;

  Result := S_OK;
end;

function CAllocateHierarchy.DestroyMeshContainer(pMeshContainerToFree: PD3DXMeshContainer): HResult;
var
  iMaterial: LongWord;
  pMeshContainer: PD3DXMeshContainerDerived;
begin
  pMeshContainer := PD3DXMeshContainerDerived(pMeshContainerToFree);

  StrDispose(pMeshContainer.Name);
  FreeMem(pMeshContainer.pAdjacency);
  pMeshContainer.pMaterials := nil;
  pMeshContainer.pBoneOffsetMatrices := nil;

  if (pMeshContainer.ppTextures <> nil) then
    for iMaterial := 0 to pMeshContainer.NumMaterials - 1 do
      pMeshContainer.ppTextures[iMaterial] := nil;

  pMeshContainer.ppTextures := nil;
  pMeshContainer.ppBoneMatrixPtrs := nil;
  SAFE_RELEASE(pMeshContainer.MeshData.pMesh);
  SAFE_RELEASE(pMeshContainer.pSkinInfo);
  SAFE_RELEASE(pMeshContainer.pOrigMesh);
  Dispose(pMeshContainer);
  Result := S_OK;
end;


Hehe... koliko koda odmah na pocetku... neki primeri nisu ni celi imali ovoliko koda Smile
Kreimo od pocetka. Kreiramo nov objekat i postavljamo mu ime.
Nakon toga uzimamo mesh koji je ucitan, pronalazimo od koliko poligona se sastoji i podatke o mesh-u kopiramo u objekat koji smo kreirali.
Zatim na osnovu broja materijala odrdjujemo velicinu niza za materijale i texture.
pAdjacency polje u objektu sadrzi podatke o poligonima... za svaki poligon je naznaceno koji ga poligoni okruzuju. Nama to i nije bitno, ali je bitno Direct3D-u pa i taj podatak kopiramo u objekat.
Na red je doslo i podesavanje materijala. Materijale jednostavno mozemo kopirati, ali za texture moramo napraviti petlju u kojoj ce se svaka ako postoji da se ucita.
To je bilo sve skoro isto kao i kad smo ucitavali obican mesh (ovaj kod u sustini i moze da ucita obican mesh i da ga prikaze), a sada ide deo koji je specifican za animirane objekta.
Ako promenljiva pSkinInfo nije nil znaci da u sebi sadrzi informacije o animaciji objekta. Te podatke kopisramo u objekat i originalni mesh cuvamo na sigurnom mestu.
Uzimamo proj kostiju za zeljeni mesh i njihove matrice prebacujemo u objekat da bismo im lakse pristupali.
Kada imamo sve ove podatke mozemo da kreiramo mesh koji ce biti spreman za deformacije pozivanjem funkcije GenerateSkinnedMesh (nju sledecu objasnjavamo).
Na kraju kopiramo kreiran objekat u promenljivu koju ce preuzeti Direct3D.

GenerateSkinnedMesh, kao sto smo rekli, kreira mesh koji cemo deformisati na osnovu matrica kostiju:
procedure GenerateSkinnedMesh(const pd3dDevice: IDirect3DDevice9;
  pMeshContainer: PD3DXMeshContainerDerived);
begin
  pMeshContainer.MeshData.pMesh := nil;

  pMeshContainer.pOrigMesh.CloneMeshFVF(D3DXMESH_MANAGED, pMeshContainer.pOrigMesh.GetFVF,
                                          pd3dDevice, ID3DXMesh(pMeshContainer.MeshData.pMesh));

  ID3DXMesh(pMeshContainer.MeshData.pMesh).GetAttributeTable(nil, @pMeshContainer.NumAttributeGroups);
  pMeshContainer.pAttributeTable := nil;
  SetLength(pMeshContainer.pAttributeTable, pMeshContainer.NumAttributeGroups);

  ID3DXMesh(pMeshContainer.MeshData.pMesh).GetAttributeTable(@pMeshContainer.pAttributeTable[0], nil);

  if (g_NumBoneMatricesMax < pMeshContainer.pSkinInfo.GetNumBones) then
  begin
    g_NumBoneMatricesMax := pMeshContainer.pSkinInfo.GetNumBones;
    g_pBoneMatrices := nil;
    SetLength(g_pBoneMatrices, g_NumBoneMatricesMax);
  end;
end;


Pozivom funkcije CloneMeshFVF kreiramo nov mesh koji je isti kao i originalni i zatim uzimamo atribute za grupe vertex-a koje cemo kasnije koristiti prilikom renderovanja.

Na kraju proveravamo da li ovaj mesh ima vise kostiju od onih koji su se ranije pojavili i ako ima povecavamo g_pBoneMatrices matricu da bi mogla da primi sve matrice.

Kada se sve ucita potrebno je jos da povezemo mesh sa matricama iz frejmova kako bismo im kasnije lakse pristupali:
procedure SetupBoneMatrixPointers(pFrame: PD3DXFrame);
begin
  if (pFrame.pMeshContainer <> nil) then
    SetupBoneMatrixPointersOnMesh(pFrame.pMeshContainer);

  if (pFrame.pFrameSibling <> nil) then
    SetupBoneMatrixPointers(pFrame.pFrameSibling);

  if (pFrame.pFrameFirstChild <> nil) then
    SetupBoneMatrixPointers(pFrame.pFrameFirstChild);
end;

procedure SetupBoneMatrixPointersOnMesh(pMeshContainerBase: PD3DXMeshContainer);
var
  iBone, cBones: Integer;
  pFrame: PD3DXFrameDerived;
  pMeshContainer: PD3DXMeshContainerDerived;
begin
  pMeshContainer := PD3DXMeshContainerDerived(pMeshContainerBase);

  if (pMeshContainer.pSkinInfo <> nil) then
  begin
    cBones := pMeshContainer.pSkinInfo.GetNumBones;
    SetLength(pMeshContainer.ppBoneMatrixPtrs, cBones);

    for iBone := 0 to cBones - 1 do
    begin
      pFrame := PD3DXFrameDerived(D3DXFrameFind(g_pFrameRoot, pMeshContainer.pSkinInfo.GetBoneName(iBone)));
      pMeshContainer.ppBoneMatrixPtrs[iBone] := @pFrame.CombinedTransformationMatrix;
    end;
  end;

  if pMeshContainer.pNextMeshContainer <> nil then
    SetupBoneMatrixPointersOnMesh(pMeshContainer.pNextMeshContainer);
end;


SetupBoneMatrixPointers funkcija ide kriz hijerarhiju frejmova trazeci mesh container-e. Kad god se neki pojavi poziva se SetupBoneMatrixPointersOnMesh funkcija koja postavlja ppBoneMatrixPtrs da pokazuje na matrice iz frejmova koje uticu na na mesh.
Ove funkcije su rekurzivne i na taj nacin se pronalaze svi mesh container-i.

Funkcije i podaci koje do sada imamo su dovoljne da ispisemo sav kod osim dela za renderovanje:
function IsDeviceAcceptable(const pCaps: TD3DCaps9; AdapterFormat, BackBufferFormat: TD3DFormat; bWindowed: Boolean; pUserContext: Pointer): Boolean; stdcall;
begin
  Result:= False;

  if FAILED(DXUTGetD3DObject.CheckDeviceFormat(pCaps.AdapterOrdinal, pCaps.DeviceType,
                   AdapterFormat, D3DUSAGE_QUERY_POSTPIXELSHADER_BLENDING,
                   D3DRTYPE_TEXTURE, BackBufferFormat))
  then Exit;

  Result:= True;
end;

function ModifyDeviceSettings(var pDeviceSettings: TDXUTDeviceSettings; const pCaps: TD3DCaps9; pUserContext: Pointer): Boolean; stdcall;
begin
  Result:= True;
end;

function OnCreateDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  Alloc: CAllocateHierarchy;
begin
  Alloc:= CAllocateHierarchy.Create;

  D3DXLoadMeshHierarchyFromX('tiny.x', D3DXMESH_MANAGED, pd3dDevice,
                                       Alloc, nil, g_pFrameRoot, g_pAnimController);
  SetupBoneMatrixPointers(g_pFrameRoot);
  D3DXFrameCalculateBoundingSphere(g_pFrameRoot, g_vObjectCenter, g_fObjectRadius);

  FreeAndNil(Alloc);

  Result:= S_OK;
end;

function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  vEye, vAt, vUp: TD3DXVector3;
  L: TD3DLight9;
begin
  D3DXMatrixTranslation(g_matWorld, -g_vObjectCenter.x,
    -g_vObjectCenter.y, -g_vObjectCenter.z);
  pd3dDevice.SetTransform(D3DTS_WORLD, g_matWorld);

  vEye := D3DXVector3(0, 0, -2 * g_fObjectRadius);
  vAt := D3DXVector3(0, 0, 0);
  vUp := D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(g_matView, vEye, vAt, vUp);

  pd3dDevice.SetTransform(D3DTS_VIEW,  g_matView);

  D3DXMatrixPerspectiveFovLH(g_matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    g_fObjectRadius / 64.0, g_fObjectRadius * 200.0);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, g_matProj);

  ZeroMemory(@L, SizeOf(TD3DLIGHT9));
  L._Type := D3DLIGHT_DIRECTIONAL;
  L.Diffuse.r := 1.0;
  L.Diffuse.g := 1.0;
  L.Diffuse.b := 1.0;
  D3DXVec3Normalize(L.Direction, D3DXVector3(0.0, -1.0, 1.0));
  L.Position.x := 0.0;
  L.Position.y := -1.0;
  L.Position.z := 1.0;
  L.Range := 1000.0;
  pd3dDevice.SetLight(0, L);
  pd3dDevice.LightEnable(0, True);

  Result:= S_OK;
end;

function MsgProc(hWnd: HWND; uMsg: LongWord; wParam: WPARAM; lParam: LPARAM; out pbNoFurtherProcessing: Boolean; pUserContext: Pointer): LRESULT; stdcall;
begin
  Result := 0;
end;

procedure OnLostDevice; stdcall;
begin

end;

procedure ReleaseAttributeTable(pFrameBase: PD3DXFrame);
var
  pFrame: PD3DXFrameDerived;
  pMeshContainer: PD3DXMeshContainerDerived;
begin
  pFrame := PD3DXFrameDerived(pFrameBase);

  pMeshContainer := PD3DXMeshContainerDerived(pFrame.pMeshContainer);

  while (pMeshContainer <> nil) do
  begin
    pMeshContainer.pAttributeTable:= nil;
    pMeshContainer := PD3DXMeshContainerDerived(pMeshContainer.pNextMeshContainer);
  end;

  if (pFrame.pFrameSibling <> nil) then
    ReleaseAttributeTable(pFrame.pFrameSibling);

  if (pFrame.pFrameFirstChild <> nil) then
    ReleaseAttributeTable(pFrame.pFrameFirstChild);
end;

procedure OnDestroyDevice; stdcall;
var
  Alloc: CAllocateHierarchy;
begin
  ReleaseAttributeTable(g_pFrameRoot);
  g_pBoneMatrices := nil;

  Alloc:= CAllocateHierarchy.Create;
  D3DXFrameDestroy(g_pFrameRoot, Alloc);
  g_pAnimController := nil;
  Alloc.Free;
end;


Jedino sto je ovde novo je D3DXFrameCalculateBoundingSphere funkcija koja racuna cenatr i precnik lopte oko objekta i D3DXFrameDestroy funkcije koja brise frejmove iz memorije. Ove funkcije su prilicno jednostavne... obe kao prvi parametar uzimaju osnovni frejm. Drugi i treci parametar kod D3DXFrameCalculateBoundingSphere su centar i precnik, a drugi parametar kod D3DXFrameDestroy je objekat u kojem smo definisali nase funkcije za unistavanje frejma. Ostatak koda je vec vidjen Smile

Ostalo nam je jos samo iscrtavanje... (hehe... samo Wink ). Definisacemo jednu funkciju koja ce proci kroz sve frejmove i izracunati apsolutne matrice:
procedure UpdateFrameMatrices(pFrameBase: PD3DXFrame; pParentMatrix: PD3DXMatrix);
var
  pFrame: PD3DXFrameDerived;
begin
  pFrame := PD3DXFrameDerived(pFrameBase);

  if (pParentMatrix <> nil) then
    D3DXMatrixMultiply(pFrame.CombinedTransformationMatrix, pFrame.TransformationMatrix, pParentMatrix^)
  else
    pFrame.CombinedTransformationMatrix := pFrame.TransformationMatrix;

  if Assigned(pFrame.pFrameSibling) then
    UpdateFrameMatrices(pFrame.pFrameSibling, pParentMatrix);

  if Assigned(pFrame.pFrameFirstChild) then
    UpdateFrameMatrices(pFrame.pFrameFirstChild, @pFrame.CombinedTransformationMatrix);
end;


Funkcija ce rekurzivno da se poziva i tako ce obici sve frejmove u hijerarhiji mnozeci matricu transofrmacije trenutnog frejma sa apsolutnom matricom frejma iznad trenutnog. Rekurzija pocinje postavljanjem frejma na osnovi i matrice na Identity matricu (to je matrica koja nista ne menja).

Uz pomoc ove funkcije cemo moci da napisemo OnFrameMove proceduru u kojoj cemo samo izmenuti matrice da odgovaraju trenutnom vremenu:
procedure OnFrameMove(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  if (g_pAnimController <> nil) then
    g_pAnimController.AdvanceTime(fElapsedTime, nil);

  UpdateFrameMatrices(g_pFrameRoot, @g_matWorld);
end;

Jednostavno pozivamo AdvanceTime kako bi se matrice u frejmovima promenule i zatim racunamo apsolutne matrice.

Sada imamo sve potrebne informacije da deformisemo mesh i da ga iscrtamo:
procedure DrawFrame(const pd3dDevice: IDirect3DDevice9; pFrame: PD3DXFrame);
var
  pMeshContainer: PD3DXMeshContainer;
begin
  pMeshContainer := pFrame.pMeshContainer;
  while (pMeshContainer <> nil) do
  begin
    DrawMeshContainer(pd3dDevice, pMeshContainer, pFrame);
    pMeshContainer := pMeshContainer.pNextMeshContainer;
  end;

  if (pFrame.pFrameSibling <> nil) then
    DrawFrame(pd3dDevice, pFrame.pFrameSibling);
  if (pFrame.pFrameFirstChild <> nil) then
    DrawFrame(pd3dDevice, pFrame.pFrameFirstChild);
end;

procedure DrawMeshContainer(const pd3dDevice: IDirect3DDevice9; pMeshContainerBase: PD3DXMeshContainer; pFrameBase: PD3DXFrame);
var
  pMeshContainer: PD3DXMeshContainerDerived;
  pFrame: PD3DXFrameDerived;
  iMaterial, iAttrib: Integer;
  Identity: TD3DXMatrix;
  cBones: DWORD;
  iBone: DWORD;
  pbVerticesSrc: PByte;
  pbVerticesDest: PByte;
begin
  pMeshContainer := PD3DXMeshContainerDerived(pMeshContainerBase);
  pFrame := PD3DXFrameDerived(pFrameBase);

  if (pMeshContainer.pSkinInfo <> nil) then
  begin
    cBones := pMeshContainer.pSkinInfo.GetNumBones;
    for iBone := 0 to cBones - 1 do
    begin
      D3DXMatrixMultiply(
        g_pBoneMatrices[iBone],
        pMeshContainer.pBoneOffsetMatrices[iBone],
        pMeshContainer.ppBoneMatrixPtrs[iBone]^
      );
    end;

    D3DXMatrixIdentity(Identity);
    pd3dDevice.SetTransform(D3DTS_WORLD, Identity);

    pMeshContainer.pOrigMesh.LockVertexBuffer(D3DLOCK_READONLY, Pointer(pbVerticesSrc));
    ID3DXMesh(pMeshContainer.MeshData.pMesh).LockVertexBuffer(0, Pointer(pbVerticesDest));

    pMeshContainer.pSkinInfo.UpdateSkinnedMesh(@g_pBoneMatrices[0], nil, pbVerticesSrc, pbVerticesDest);

    pMeshContainer.pOrigMesh.UnlockVertexBuffer;
    ID3DXMesh(pMeshContainer.MeshData.pMesh).UnlockVertexBuffer;

    for iAttrib := 0 to pMeshContainer.NumAttributeGroups - 1 do
    begin
      pd3dDevice.SetMaterial((pMeshContainer.pMaterials[
        pMeshContainer.pAttributeTable[iAttrib].AttribId].MatD3D));
      pd3dDevice.SetTexture(0, pMeshContainer.ppTextures[
        pMeshContainer.pAttributeTable[iAttrib].AttribId]);
      ID3DXMesh(pMeshContainer.MeshData.pMesh).
        DrawSubset(pMeshContainer.pAttributeTable[iAttrib].AttribId);
    end;
  end
  else
  begin
    pd3dDevice.SetTransform(D3DTS_WORLD, pFrame.CombinedTransformationMatrix);

    for iMaterial := 0 to pMeshContainer.NumMaterials - 1 do
    begin
      pd3dDevice.SetMaterial(pMeshContainer.pMaterials[iMaterial].MatD3D);
      pd3dDevice.SetTexture(0, pMeshContainer.ppTextures[iMaterial]);
      ID3DXMesh(pMeshContainer.MeshData.pMesh).DrawSubset(iMaterial);
    end;
  end;
end;

DrawFrame prolazi kroz svaki frejm i trazi mesh container-e. Kada se neki pronadje poziva se DrawMeshContainer procedura. Prvo sto radimo u ovoj funkciji je racunanje konacne matrice za vertex-e. Posto smo ranije nasli koliko je maximalno kostiju potrebno za jedan mesh mozemo slobodno sve matrice staviti u g_pBoneMatrices. Posto ce ove matrice da postave vertex-e na njihovu tacnu poziciju u svetu, World matrica mora biti postavljenja tako da nista ne menja i zato je postavljamo na Identity.
Sledece sto radimo je kreiranje novog mesh-a. Uzimamo vertex buffer originalnog mesh-a (postavljamo flag na READONLY... nije obavezno, ali tako ce brze raditi) i vertex buffer novog mesh-a i zatim pozivamo funkciju UpdateSkinnedMesh koja ce u novom mesh-u na osnovu matrica da izracuna nove pozicije vertex-a.
Kada to uradimo ostaje jos samo postavljanje materijala, textura i konacno iscrtavanje.
Za slucaj da smo ucitali mesh koji nije animiran tada postavljamo World matricu na apsolutnu matricu frejma i iscrtavamo mesh kao i ranije.

Konacno, OnFrameRender procedura izgleda ovako:
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    DrawFrame(pd3dDevice, g_pFrameRoot);
    pd3dDevice.EndScene;
  end;
end;

Prosto k'o pasulj Smile

Mozda vam ovaj primer nece biti bas najjasniji (i ja sam se u pocetku malo patio da ukapiram kako sve to radi), ali ako se ide korak po korak, videcete da ipak nije. Ako se kasnije ukaze potreba, potrudicu se da napravim klasu slicnu CDXUTMesh klasi koja ce moci da radi sa animiranim fajlovima.



Source (Delphi) | Source (Lazarus) | Bin | Media


Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Pre nego sto predjemo na zvukove pokazacemo kako se koristi billboarding tehnika. Ovom tehnikom je moguce iscrtavati pravougaonike koji su uvek okrenuti prema kameri. Napravicemo i jedan vrlo jednostavan particle system koji ce, koliko-toliko, simulirati vatru.

Pocecemo pravljenjem particle system-a. Bice nam potrebna klasa za cesticu... ona ce sadrzati podatke potrebne za iscrtavanje jednog pravougaonika:
type
  TParticle = class
  private
    X, Y, Z: Single;
    R: Byte;
    Alpha: Single;
    Life: Single;
    Speed: Single;
    function GetColor: D3DCOLOR;
    function GetMatrix: TD3DXMatrix;
    function GetIsAlive: Boolean;
  public
    constructor Create;
    procedure Reset;
    procedure OnFrameMove(fElapsedTime: Single);
    property Color: D3DCOLOR read GetColor;
    property Matrix: TD3DXMatrix read GetMatrix;
    property IsAlive: Boolean read GetIsAlive;
  end;

X, Y i Z ce sadrzati koordinatu cestice.
Posto cemo simulirati vatru dovoljna nam je samo crvena komponenta boje i nju cemo cuvati u R promenljivoj.
Alpha ce sadrzati alpha vrednost boje... takodje ce i oznacavati da li je cestica "ziva". Ako Alpha padne ispod 0 znaci da je cestica gotova. Life ce oznacavati koliko ce se Alpha smanjivati.
Speed je brzina kojom ce se cestice kretati u vis.
Procedura Reset ce postaviti sve parametre za pocetak zivota cestice.
OnFrameMove treba da pozivamo i na osnovu vremena ce se promenuti pozicija i alpha vrednost boje.
Color ce vratiti boju na osnovu R i alpha vrednosti.
Matrix ce vratiti matricu na osnovu koordinata cestice.
IsAlive ce vratiti da li je cestica "ziva".

Da ispisemo funkcije:
constructor TParticle.Create;
begin
  Reset;
end;

function TParticle.GetColor: D3DCOLOR;
begin
  Result := D3DCOLOR_ARGB(Round($FF * Alpha), R,
    Round($FF * (1 - Alpha)), 0);
end;

function TParticle.GetIsAlive: Boolean;
begin
  Result := Alpha > 0;
end;

function TParticle.GetMatrix: TD3DXMatrix;
begin
  D3DXMatrixTranslation(Result, X, Y, Z);
end;

procedure TParticle.OnFrameMove(fElapsedTime: Single);
begin
  Y := Y + Speed * fElapsedTime;
  Alpha := Alpha - Life * fElapsedTime;
end;

procedure TParticle.Reset;
begin
  X := 0.1 * (Random(1000) / 1000);
  if Random(2) = 0 then
    X := -X;
  Y := 0.05 * (Random(1000) / 1000);
  Z := 0.1 * (Random(1000) / 1000);
  if Random(2) = 0 then
    Z := -Z;

  R := 128 + Random(128);

  Alpha := 1;
  Life := 1 + 0.5 * (Random(1000) / 1000);
  Speed := 1 * (Random(1000) / 1000);
end;

Pa... ovde nista, osim matematike, nema novo Smile

Jedna cestica nije neki lep prizor... zato cemo napraviti i klasu koja ce u sebi sadrzati vise cestica:
const
  PARTICLE_COUNT = 200;

type
  TParticleSystem = class
  private
    Particles: array[0..PARTICLE_COUNT - 1] of TParticle;
    function GetCount: Integer;
    function GetParticle(Index: Integer): TParticle;
  public
    constructor Create;
    destructor Destroy; override;
    procedure OnFrameMove(fElapsedTime: Single);
    property Count: Integer read GetCount;
    property Particle[Index: Integer]: TParticle read GetParticle; default;
  end;

Ova klasa ima niz cestica i jednostavno se samo brine o njihovom kreiranju, resetovanju i unistavanju.

constructor TParticleSystem.Create;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    Particles[I] := TParticle.Create;
end;

destructor TParticleSystem.Destroy;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    Particles[I].Free;
  inherited;
end;

function TParticleSystem.GetCount: Integer;
begin
  Result := PARTICLE_COUNT;
end;

function TParticleSystem.GetParticle(Index: Integer): TParticle;
begin
  Result := Particles[Index];
end;

procedure TParticleSystem.OnFrameMove(fElapsedTime: Single);
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
  begin
    Particles[I].OnFrameMove(fElapsedTime);
    if not(Particles[I].IsAlive) then
      Particles[I].Reset;
  end;
end;


Kada imamo klase za rad sa ovim jednostavnim cesticama, mozemo poceti sa pisanjem glavnog dela programa. Bice nam potrebne sledece promenljive:
var
  Mesh: CDXUTMesh;
  Sprite: ID3DXSprite;
  Texture: IDirect3DTexture9;
  TexInfo: TD3DXImageInfo;
  matView: TD3DXMatrix;
  matWorld: TD3DXMatrix;
  ParticleSys: TParticleSystem;


Kreiracemo scenu u kojoj na maloj poljani gori vatra (hmmm... nesto sto mozda podseca na vatru Wink ). Mesh ce sadrzati objekat koji predstavlja poljanu.
U Texture se nalazi slika koju cemo koristiti za iscrtavanje cestice, a u TexInfo podaci o toj slici (bice nam potrebna sirina i visina).
Sprite ce se patiti sa iscrtavanjem cestica i izracunavanjem koje je potrebno da bi se postigao billboard efekat.
matView i matWorld smo definisali ovde jer ce nam biti potrebne za billboarding.
Na kraju, ParticleSys ce biti zaduzen za cuvanje cestica.

function OnCreateDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
begin
  Mesh := CDXUTMesh.Create;
  Mesh.CreateMesh(pd3dDevice, 'teren.x');

  D3DXCreateTextureFromFileEx(pd3dDevice, 'vatra.dds',
  D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0,
  D3DFMT_UNKNOWN, D3DPOOL_MANAGED,
  D3DX_DEFAULT, D3DX_DEFAULT, 0, @TexInfo, nil, Texture);

  D3DXCreateSprite(pd3dDevice, Sprite);

  ParticleSys := TParticleSystem.Create;

  Result:= S_OK;
end;

Za ucitavanje texture koristimo D3DXCreateTextureFromFileEx da bismo mogli da izvucemo i podatke o texturi.

function OnResetDevice(const pd3dDevice: IDirect3DDevice9; const pBackBufferSurfaceDesc: TD3DSurfaceDesc; pUserContext: Pointer): HRESULT; stdcall;
var
  matProj: TD3DXMatrix;
  vEye, vAt, vUp: TD3DXVector3;
  L: TD3DLight9;
begin
  D3DXMatrixIdentity(matWorld);
  pd3dDevice.SetTransform(D3DTS_WORLD, matWorld);

  vEye := D3DXVector3(0, 2, -2);
  vAt := D3DXVector3(0, 0, 0);
  vUp := D3DXVector3(0, 1, 0);
  D3DXMatrixLookAtLH(matView, vEye, vAt, vUp);
  pd3dDevice.SetTransform(D3DTS_VIEW,  matView);

  D3DXMatrixPerspectiveFovLH(matProj, D3DX_PI / 4,
    pBackBufferSurfaceDesc.Width / pBackBufferSurfaceDesc.Height,
    0.1, 100);
  pd3dDevice.SetTransform(D3DTS_PROJECTION, matProj);

  ZeroMemory(@L, SizeOf(TD3DLIGHT9));
  L._Type := D3DLIGHT_DIRECTIONAL;
  L.Diffuse.r := 0.8;
  L.Diffuse.g := 0.8;
  L.Diffuse.b := 0.8;
  D3DXVec3Normalize(L.Direction, D3DXVector3(0.0, -1.0, 1.0));
  L.Position.x := 0.0;
  L.Position.y := -1.0;
  L.Position.z := 1.0;
  L.Range := 1000.0;
  pd3dDevice.SetLight(0, L);
  pd3dDevice.LightEnable(0, True);

  Mesh.RestoreDeviceObjects(pd3dDevice);
  Sprite.OnResetDevice;

  pd3dDevice.SetRenderState(D3DRS_ALPHATESTENABLE, iTrue);

  Result:= S_OK;
end;

Ovde podesavamo matrice, ukljucujemo alphablending i postavljamo osnovno svetlo (ono ne svetli maximalnim intenzitetom jer cemo na centru kreirati jos jedno svetlo koje ce simulirati svetlost vatre).

procedure OnFrameMove(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
var
  matRot: TD3DXMatrix;
  L: TD3DLight9;
begin
  D3DXMatrixRotationY(matRot, fElapsedTime);
  D3DXMatrixMultiply(matView, matRot, matView);
  pd3dDevice.SetTransform(D3DTS_VIEW, matView);

  ParticleSys.OnFrameMove(fElapsedTime);

  ZeroMemory(@L, SizeOf(TD3DLIGHT9));
  L._Type := D3DLIGHT_POINT;
  L.Diffuse.r := 1.0;
  L.Diffuse.g := 0.3;
  L.Diffuse.b := 0.3;
  L.Position.x := 0.0;
  L.Position.y := 0.2;
  L.Position.z := 0.0;
  L.Range := 0.4 + Abs(Sin(fTime * 100)) / 5;
  L.Attenuation0 := 1;
  pd3dDevice.SetLight(1, L);
  pd3dDevice.LightEnable(1, True);
end;

Prvo okrecemo kameru oko objekata da bismo videli da se svaka cestica sama okrece prema kameri.
Update-ujemo pozicije cestica i kreiramo jos jedno svetlo (tip point light) koje ce svetliti na sredini crvenom bojom. Daljina do koje svetlost obasjava objekte se menja u zavisnosti od vremena tako da se postize malo realniji izgled svetla od vatre (sve nam je realno Razz ).

Stigli smo do glavnog dela... iscrtavanje:
procedure OnFrameRender(const pd3dDevice: IDirect3DDevice9; fTime: Double; fElapsedTime: Single; pUserContext: Pointer); stdcall;
var
  matPos: TD3DXMatrix;
  I: Integer;
  C: TD3DXVector3;
begin
  pd3dDevice.Clear(0, nil, D3DCLEAR_TARGET or D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0, 0);

  if SUCCEEDED(pd3dDevice.BeginScene) then
  begin
    Mesh.Render(pd3dDevice);

    C := D3DXVector3(TexInfo.Width / 2,
      TexInfo.Height / 2, 1);

    Sprite.SetWorldViewLH(@matWorld, @matView);
    Sprite._Begin(D3DXSPRITE_ALPHABLEND or
      D3DXSPRITE_BILLBOARD or
      D3DXSPRITE_SORT_DEPTH_FRONTTOBACK);

    for I := 0 to ParticleSys.Count - 1 do
    begin
      D3DXMatrixScaling(matPos,
        0.08 / TexInfo.Width, 0.08 / TexInfo.Height, 1);
      D3DXMatrixMultiply(matPos, matPos, ParticleSys[I].Matrix);
      Sprite.SetTransform(matPos);

      Sprite.Draw(Texture, nil, @C, nil, ParticleSys[I].Color);
    end;

    Sprite._End;

    pd3dDevice.EndScene;
  end;
end;

Iscrtavamo mesh kao i obicno.
Posto ce pravougaonik koji Sprite bude kreirao biti velicine texture i bice nam potrebno da znamo gde je centar tog pravougaonika, racunamo ga i postavljamo u promenljivu C.

da bi billboarding tehnika mogla da se koristi, potrebno je da se pozove funkcija SetWorldViewLH kojoj prosledjujemo matrice World i View. Kada smo to uradili mozemo da pocnemo sa crtanjem. Pocetak se oznacava pozivanjem funkcije Sprite._Begin. D3DXSPRITE_ALPHABLEND, D3DXSPRITE_BILLBOARD, D3DXSPRITE_SORT_DEPTH_FRONTTOBACK flagovi oznacavaju da zelimo da koristimo billboarding tehniku, da treba da se koristi alphablending i da se objekti koje iscrtavamo sortiraju kako bi alphablending mogao lepo da radi.

Prolazimo kroz sve cestice koje imamo u sistemu... svaku od njih smanjujemo na 0.08 sirinu i 0.08 visinu i tu matricu mnozimo matricom pozicije koja je definisana u cestici. Matricu postavljamo funkcijom Sprite.SetTransform.

Sada je sve spremno za iscrtavanje... pozivanjem funkcije Sprite.Draw prosledjujemo texturu koju zelimo da koristimo, centar texture kako bi pozicija mogla tacno da se izracuna i boju koju zelimo da cestica ima.

To je to... Sprite ce se sam pobrinuti za sve ostalo. ID3DXSprite nije najbrze resenje za billboarding, ali je za pocetak dovoljno brzo i lako.



Sledece na listi je rad sa zvukovima.

Source (Delphi) | Source (Lazarus) | Bin | Media

Dopuna: 03 Mar 2006 22:43


Za zvucne efekte DirectX nam na raspolaganje daje DirectSound i DirectMusic. DirectSound ima bolju kontrolu nad hardware-om, koristi low-level funkcije, ali su i funkcije koje nudi prilicno low-level (moze da pusta jedino wav fajlove). DirectMusic koristi DirectSound, ali u sebi ima funkcije koje omogucavaju pustanje drugih vrsta fajlova kao i mnoge druge mogucnosti.

Za sad cemo raditi sa DirectSound-om, a kada se ukaze potreba objasnicemo i DirectMusic. Da bismo koristili DirectSound potrebno je da znamo da koristimo funkcije za rad sa wav fajlovima (bar najosnovnije kao sto su otvaranje, citanje zaglavlja i uzimanje podataka iz fajla) jer DirectSound nema mehanizam za ucitavanje. Potrebno je da format i podatke sami izvucemo i da ih predamo DirectSound-u kako bi mogao da ih pusti. Da se ne bi odmah mucili sa wav fajlovima koristicemo Common Framework koji nam nudi klase za jednostavno koriscenje DirectSound-a.

Koristicemo klasu CSoundManager koja je zaduzena za kreiranje DirectSound objekat, postavljanje parametara, kreiranje buffer-a za smestanje podataka o zvuku,... i CSound klasu koja ce preuzeti kreirani buffer objekat i omoguciti nam da lako pustamo zvuk.

Prvi primer ce biti vrlo jednostavan. Izabracemo fajl i reci SoundManager-u da kreira buffer dovoljno velik da ceo fajl stane u memoriju i onda cemo ga pustiti (nesto kao saound recorder, ali bez mogucnosti snimanja). Imajte na umu da ako otvorite neki wav koji ima 100Mb trebace vam toliko i memorije zbog buffer-a.

Koristicemo VCL/LCL da bi lakse radili sa formama i kontrolama. Postavicemo label, edit, i 3 digmeta kao na slici:



Da bismo se lakse snasli u kodu kontrole cemo nazvati ovako:

Label - lblFajl
Edit - txtFajl
Dugme pored edit-a - btnFajl
Play dugme - btnPlay
Stop dugme - btnStop
Open Dialog - dlgFajl

Gde cete postaviti kontrole i koja cete im svojstva postaviti nije mnogo bitno za ovaj primer (kad budete skinuli kod, videcete kako sam ih ja postavio, ali to ne znaci da i vi morate tako).

Kada imamo sve kontrole koje su nam potrebne mozemo preci na pisanje koda.

Promenljive koje cemo koristiti za pustanje zvuka su:
uses
  DirectSound, DXUTsound;

var
  pSoundManager: CSoundManager;
  pSound: CSound;


Prilikom kreiranja forme cemo kreirati i SoundManager:
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  pSound := nil;

  pSoundManager := CSoundManager.Create;
  if Failed(pSoundManager.Initialize(Handle, DSSCL_NORMAL)) then
    Application.Terminate;
end;


Kada kreiramo SoundManager potrebno je da pozovemo Initialize funkciju u kojoj dajemo handle prozora koji sadrzi ovaj objekat i nacin na koji zelimo da zazumemo memoriju za pustanje zvuka. NORMAL dozvoljava da vise programa pustaju zvuk istovremeno, dok recimo EXCLUSIVE ne dozvoljava ni jednom drugom programu da pusta zvuk dok je nas aktivan.

Imamo SoundManager i mozemo da izaberemo zvuk koji zelimo da pustimo (samo WAV fajlovi):
procedure TfrmMain.btnFajlClick(Sender: TObject);
var
  FileName: WideString;
begin
  if dlgFajl.Execute then
  begin
    if Assigned(pSound) then
      FreeAndNil(pSound);
    FileName := dlgFajl.FileName;

    if Failed(pSoundManager.Create(pSound, PWideChar(FileName), 0, DS3DALG_DEFAULT)) then
      ShowMessage('Greska pri ucitavanju fajla')
    else
    begin
      txtFajl.Text := dlgFajl.FileName;
      btnPlay.Enabled := True;
    end;
  end;
end;

Create procedura (u ovom slucaju ne pozivamo constructor Create nego funkciju) kreira CSound objekat koji je zaduzen za buffer u kojem su podaci o zvuku i o njegovom pustanju.
Prvi parametar je CSound objekat koji ce biti kreiran.
Drugi je putanja do WAV fajla.
Treci je flag koji oznacava kakav buffer zelimo da kreiramo, a cetvri koji algoritam za simulaciju 3D zvuka da ce se koristiti.
Za vise informacija o parametrima pogledajte ovde

Imamo zvuk... mozemo da ga pustimo:
procedure TfrmMain.btnPlayClick(Sender: TObject);
begin
  if Assigned(pSound) then
    pSound.Play;
end;

Kao sto vidite, sve sto je potrebno je da pozovemo Play funkciju. Ova funkcija moze uzimati nekoliko parametara, ali nisu obavezni.
Prvi je prioritet ovog zvuka. Kada pustamo mnogo zvukova istovremeno i DirectSound ne moze da ih sve pusti odjednom, na osnovu prioriteta se moze odrediti koji od njih su vazniji da se cuju.
Drugi je flag koji odredjuje da li zvuk da se ponavlja, kako da se odredjuje koji zvuk da se ugasi ako nema dovoljno mesta da se ovaj zvuk pusti i slicno.
Sledeca tri parametra predstavljaju glasnocu, frekvenciju i pan (balance... koliko glasno se cuje levi, a koliko desni zvucnik).

Da bismo zaustavili zvuk potrebno je da pozovemo 2 funkcije:
procedure TfrmMain.btnStopClick(Sender: TObject);
begin
  if Assigned(pSound) then
  begin
    pSound.Stop;
    pSound.Reset;
  end;
end;

Samo pozivanjem Stop funkcije se zaustavlja pustanje zvuka, ali se pozicija ne resetuje na pocetak tako da bi sledece pozivanje Play funkcije nastavilo tamo gde je pustanje zvuka stalo.
Reset funkcija vraca poziciju na pocetak i tako dobijamo zeljenu vrednost koju Stop dugme treba da ima.

Ostalo je jos samo oslobadjanje objekata:
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  btnStopClick(nil);
  if Assigned(pSound) then
    pSound.Free;
  if assigned(pSoundManager) then
    pSoundManager.Free;
end;


To je to... pokrenite program, izaberite wav fajl i pustajte ga Smile

Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 19 Mar 2006 18:45


Update-ovao sam header-e i tako da ce izvrsni fajlovi koje budem slao u narednim postovima zahtevati nov DLL. Da biste mogli da pokrecete te fajlove, morate i vi da update-ujete fajlove sa sajta koji sam dao u prvom postu.

Dopuna: 19 Mar 2006 18:49


Ovog puta je na red doslo pustanje zvuka u 3D prostoru. Da bi to bilo moguce potrebno je postaviti opcije koje ce definisati DirectSound-u sa koje pozicije slusamo i odakle dolazi zvuk. Naravno, postoje jos podesavanja koja ce odrediti kako ce se zvuk cuti na zvucnicima.

Pre nego sto pocnemo treba naglasiti da je potrbno imati bar 5.1 zvucnike da bi se sve culo kako treba, ali ce ipak raditi i na stereo zvucnicima.

Opet cemo koristiti VCL/LCL komponente za GUI... GUI necu posebno objasnjavati, vise cu se skoncentrisati na DirectSound i pustanje zvuka.

Forma ce izgledati ovako



TrackBar komponente cemo koristiti za menjanje opcija koje uticu na zvuk koji cemo cuti.
Prvi utice na ispoljavanje Doplerovog efekta. Doplerov evekat objasnjava zasto se frekvencija zvuka menja kada se izvor zvuka krece prema ili se udaljava od mesta sa kojeg se slusa. Kada se zvuk krece prema mestu sa kojeg se slusa, frekvencija zvuka se povecava i obrnuto. Ovaj parametar odredjuje koliko je jako izrazen Doplerov efekat. Kada je vrednost 1 tada je isto kao u stvarnom svetu, a kada je vrednost 0 tada se Doplerov efekat ne uzima u obzir.

Drugi parametar je Rolloff efekat odredjuje koliko naglo opada glasnoca zvuka u zavisnosti od udaljenosti izvora od mesta sa kojeg se slusa. Kada je ovaj prarmetar 1 tada je isti kao i u stvarnom svetu, a ako je parametar 0 tada se zvuk cuje isto glasno sa bilo koje pozicije.

Ova dva parametra su vezana za slusaoca. Kada se promene uticace na sve zvukove koje cuje.
Osim ovih koje mozemo menjati u programu na raspolaganju nam stoje i pozicija sa koje se slusa, orijentacija, brzina kretanja i svi ti parametri uticu na zvuk koji cemo cuti. U ovom programu se pozicija, orijentacija i kretanje ne menjaju i po defaultu je da slusamo sa koordinate 0, 0, 0, okrenuti smo prema pozitivnom smeru Z ose i ne krecemo se.

Naredna dva su specificna za svaki izvor zvuka.
Min udaljenost predstavlja vrednost na osnovu koje se odredjuje koliko brzo opada glasnoca zvuka. Ako je ova vrednost, recimo 10, tada ce zvuk na udaljenosti 20 biti 2 puta tisi.
Max udaljenost odredjuje udaljenost na kojoj zvuk vise ne treba da se stisava. Ovaj podatak moze sluziti da se zvuk kada predje maximalnu udaljenost utisa skroz. Ovo se postize navodjenjem DSBCAPS_MUTE3DATMAXDISTANCE prilikom kreiranja zvuka. Ako se ovaj flag ne navede tada maximalna udaljenost sluzi da se zvuk smanjuje sve do maximalne vrednosti i posle toga ostaje nepromenjen. Ovo moze sluziti ako recimo zvuk kada predje udaljenost od 100 postaje necujan, a potrebno je da se idalje bar tiho cuje. Tada mozemo postaviti maximalnu udaljenost na, recimo, 80 i tada ce se glasnoca smanjivati sve do udaljenosti 80, a posle toga ce zadrzavati glasnocu.

Pored ova dva parametra ce se u programu menjati pozicija i smer kretanja objekta.

Idemo sad na kod... dodacemo nekoliko promenljivih koje ce nam trebati u programu:
var
  AppTime: Single;
  Bitmap: TBitmap;
  SoundManager: CSoundManager;
  Sound: CSound;
  DS3DBuffer: IDirectSound3DBuffer;
  DSListener: IDirectSound3DListener;
  dsBufferParams: TDS3DBuffer;
  dsListenerParams: TDS3DListener;

AppTime ce nam sluziti za odredjivanje pozicije izvora zvuka.
U Bitmap cemo ucitati sliku grid-a po kojoj cemo crtati mesto na kojem je zvuk.
SoundManager i Sound ce biti isti kao i u proslom primeru.
DS3DBuffer je promenljiva kojom cemo moci da menjamo osobine izvora zvuka. dsBufferParams ce biti podaci za izvor zvuka.
DSListener je promenljiva kojom mozemo menjati osobine slusaoca, a u dsListenerParams ce se nalaziti te osobine.

Pocecemo od OnCreate eventa forme:
procedure TfrmMain.FormCreate(Sender: TObject);
var
  WavFile: WideString;
begin
  Bitmap := TBitmap.Create;
  Bitmap.LoadFromFile('grid.bmp');
  imgGrid.Picture.Assign(Bitmap);

  SoundManager := CSoundManager.Create;
  SoundManager.Initialize(Handle, DSSCL_NORMAL);
  WavFile := 'test.wav';
  SoundManager.Create(Sound, PWideChar(WavFile), DSBCAPS_CTRL3D, DS3DALG_DEFAULT);

  SoundManager.Get3DListenerInterface(DSListener);
  dsListenerParams.dwSize := SizeOf(dsListenerParams);
  DSListener.GetAllParameters(dsListenerParams);

  Sound.Get3DBufferInterface(0, DS3DBuffer);
  dsBufferParams.dwSize := SizeOf(dsBufferParams);
  DS3DBuffer.GetAllParameters(dsBufferParams);

  tbarDopplerFactor.Position := 0;
  tbarRolloffFactor.Position := 0;
  tbarMinDistance.Position := 500;
  tbarMaxDistance.Position := 1000;

  AppTime := 0;

  Sound.Play(0, DSBPLAY_LOOPING);
end;

Prvih par redova sluzi za ucitavanje slike.
Posle toga kreiramo SoundManager i Sound. Sound objekat kreiramo koristeci flag DSBCAPS_CTRL3D sto oznacava da ce zvuk biti u 3D okruzenju. Naredni parametar je vec opisan u proslom primeru i odredjuje kakva ce se matematika koristiti za racunanje oblika zvuka u 3D prostoru.

Posle toga uzimamo buffere i njihove podatke i postavljamo pocetne podatke. Koristili smo funkciju GetAllParameters za dobijanje svih parametara, ali je moguce koristiti i funkcije koje uzimaju pojedinacne parametre kao sto su npr. GetPosition, GetVelocity i slicne.

Na kraju pustamo zvuk.

Sledece sto cemo napraviti je menjanje parametara:
procedure TfrmMain.tbarDopplerFactorChange(Sender: TObject);
begin
  txtDopplerFactor.Text := Format('%.2f', [tbarDopplerFactor.Position / 100]);
  dsListenerParams.flDopplerFactor := tbarDopplerFactor.Position / 100;
  DSListener.SetAllParameters(dsListenerParams, DS3D_IMMEDIATE);
end;

procedure TfrmMain.tbarRolloffFactorChange(Sender: TObject);
begin
  txtRolloffFactor.Text := Format('%.2f', [tbarRolloffFactor.Position / 100]);
  dsListenerParams.flRolloffFactor := tbarRolloffFactor.Position / 100;
  DSListener.SetAllParameters(dsListenerParams, DS3D_IMMEDIATE);
end;

procedure TfrmMain.tbarMinDistanceChange(Sender: TObject);
begin
  txtMinDistance.Text := Format('%.2f', [tbarMinDistance.Position / 100]);
  dsBufferParams.flMinDistance := tbarMinDistance.Position / 100;
  DS3DBuffer.SetAllParameters(dsBufferParams, DS3D_IMMEDIATE);
end;

procedure TfrmMain.tbarMaxDistanceChange(Sender: TObject);
begin
  txtMaxDistance.Text := Format('%.2f', [tbarMaxDistance.Position / 100]);
  dsBufferParams.flMaxDistance := tbarMaxDistance.Position / 100;
  DS3DBuffer.SetAllParameters(dsBufferParams, DS3D_IMMEDIATE);
end;

Jednostavno promenimo vrednost u parametrima i pozovemo funkciju SetAllParameters. Moguce je postavljati jedan po jedan parametar funkcijama SetPosition, SetVelocity i slicne. DS3D_IMMEDIATE flag znaci da se promene odmah uzimaju u obzir. Umesto ovog flaga je moguce postaviti i DS3D_DEFERRED. Na ovaj nacin je moguce postaviti vise vrednosti i tek kada se pozove funkcija CommitDeferredSettings sve promene se uzimaju u obzir.

Ostalo nam je jos da menjamo poziciju izvora zvuka da bi culi kako se to manifestuje:
procedure TfrmMain.TimerTimer(Sender: TObject);
var
  vPosition, vVelocity: TD3DVector;
  X, Y: Integer;
begin
  AppTime := AppTime + 0.02;

  vPosition.x := 27.5 * Sin(AppTime);
  vPosition.y := 0;
  vPosition.z := 27.5 * Cos(AppTime);

  vVelocity.x := 27.5 * Sin(AppTime + 0.05);
  vVelocity.y := 0;
  vVelocity.z := 27.5 * Cos(AppTime + 0.05);

  X := Trunc((vPosition.x / 27.5 + 1) * 65 / 2);
  Y := Trunc((-vPosition.z / 27.5 + 1) * 65 / 2);
  with imgGrid.Picture.Bitmap.Canvas do
  begin
    Draw(0, 0, Bitmap);
    Pixels[X - 1, Y] := clRed;
    Pixels[X, Y - 1] := clRed;
    Pixels[X, Y] := clRed;
    Pixels[X, Y + 1] := clRed;
    Pixels[X + 1, Y] := clRed;
  end;

  dsBufferParams.vPosition := vPosition;
  dsBufferParams.vVelocity := vVelocity;
  DS3DBuffer.SetAllParameters(dsBufferParams, DS3D_IMMEDIATE);
end;

Ovde nema niceg komplikovanog. Na osnovu proteklog vremena racunamo poziciju i smer kretanja. Iscrtavamo grid na formu i na mestu gde se nalazi izvor zvuka postavljamo crveni krstic.
Kada izracunamo sve postavljamo nove parametre koje ce uticati na zvuk.

Sve sto je ostalo je zatvaranje forme koje nije potrebno posebno objasnjavati:
procedure TfrmMain.btnExitClick(Sender: TObject);
begin
  Application.Terminate;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  DS3DBuffer := nil;
  DSListener := nil;
  Sound := nil;
  SoundManager := nil;
  Bitmap.Free;
end;


To je to... kada budete pokrenuli program cucete kako izvor zvuka ide oko vas Smile

Source (Delphi) | Source (Lazarus) | Bin | Media


Idi na vrh
offline
  • Pridružio: 15 Feb 2006
  • Poruke: 17
  • Gde živiš: 1:442:5
Uloguj se preko Facebook-a i postavi pitanje:
Srki tebi treba podici spomenik odma posle Nikole Tesle, puno hvala :bow:


Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Hehe... sad kad sam dobio pohvalu mogu da nastavim sa pisanjem Very Happy

Da bi korisnik bilo sta mogao da radi u programu koji pravimo, moramo mu omoguciti da unosi podatke na neki nacin. Imali smo jedan primer kada smo pokazali GUI kontrole koje nam Common Framework nudi. Ovog puta cemo direktno da koristimo DirectInput za uzimanje ulaznih podataka.

U prvom primeru cemo raditi sa tastaturom. I ovog puta cemo koristiti VCL/LCL komponente kako bi mogli da se skoncetricemo na DirectInput.

U IDE ce nam forma izgledati ovako



Edit box ce sadrzati DirectInput kodove za pritisnute tastere, a Timer ce obezbediti da aplikacija cita podatke sa tastature.

Na pocetku cemo dodati unit-e i promenljive koje ce nam biti potrebene:
uses
  DirectInput;

var
  pDI: IDirectInput8;
  pKeyboard: IDirectInputDevice8;


pDI ce sadrzati DirectInput objekat koji ce nam sluziti da uzmemo uredjaj koji zelimo da pratimo. Ovog puta ga necemo bas mnogo objasnjavati... pokazacemo kasnije kako je moguce pronaci sve uredjaje ili samo one koje zelimo uz pomoc ovog objekta.

pKeyboard ce sadrzati objekat vezan za tastaturu... svi uredjaji koje mozemo da pratimo predstavljamo IDirectInputDevice8 objektom.

Hajde da pocnemo sa kodom... u OnCreate eventu cemo kreirati objekte:
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  DirectInput8Create(hInstance, DIRECTINPUT_VERSION,
    IID_IDirectInput8, pDI, nil);
  pDI.CreateDevice(GUID_SysKeyboard, pKeyboard, nil);
  pKeyboard.SetDataFormat(c_dfDIKeyboard);
  pKeyboard.SetCooperativeLevel(Handle, DISCL_EXCLUSIVE or DISCL_FOREGROUND);
  pKeyboard.Acquire;
end;


DirectInput objekat kreiramo funkcijom DirectInput8Create. Poziv ove funkcije je vrlo jednostavan... prvi parametar predstavlja promenljiva HInstance koju Delphi/Lazarus popuni pri pokretanju programa, drugi parametar je verzija DirectInput-a koja nam treba... i za to vec imamo gotovu promenljivu Smile Sledeci parametar je objekat koji zelimo... i za to imamo gotovu promenljivu... zadnja dva parametra su promenljiva u kojoj zelimo da nam se sacuva objekat. Prva od zadnje dve ce biti tipa koji smo naveli pre toga, a druga uvek bute tipa IUnknown. Posto nam ne treba taj podatak, poslednji parametar postavljamo na nil.

Kreiranje objekta koji se vezuje za ulazni uredjaj je slicno uradjeno. Pozivom CreateDevice funkcije dajemo GUID (broj koji jednoznacno oderdjuje uredjaj koji zelimo... GUID_SysKeyboard je glavna tastatura na kojoj kuckate) uredjaja koji zelimo i zadnja dva parametrea su promenljiva u kojoj ce objekat biti smesten i nil jer nem ne treba IUnknown interfejs.

Sledece sto je bitno da uradimo je da objasnimo DirectInput-u kako zelimo da nam vrati podatke. To radimo pozivanjem funkcije SetDataFormat. Moguce je podesiti da se, recimo, vracaju podaci samo za neke odredjene tastere, a za ostale ne... da se za neki taster vrati podatak dva ili vise puta i slicno. Postoji predefinisan nacin slanja podataka za tastaturu koji u noz od 256 bajtova vraca stanje svakog karaktera (pritisnut ili ne). Ovaj nacin je, uglavnom, najpogodniji za skoro sve slucajeve i zbog toga imamo promenljivu c_dfDIKeyboard koja nas oslobadja definisanja posebnog tipa vracanja podataka.
Ako imate zelju da saznate vise o parametru koji ova funkcija uzima pogledajte ovde:
http://msdn.microsoft.com/library/default.asp?url=.....Format.asp

Kao i kod pustanja zvuka, i ovde moramo definisati kako ce se uredjaj koji zelimo da pradimo, deliti izmedju naseg i ostalih programa.
DISCL_EXCLUSIVE znaci da samo nasa aplikacija ima pristup uredjaju. Svi tasteri i kombinacije tastera, osim CTRL+ALT+DEL i ALT+TAB, nece biti poslate Windows-u. Ovaj mod se, uglavnom, koristi u fullscreen aplikacijama.
DISCL_NONEXCLUSIVE je mod koji dozvoljava da vise aplikacija istovremeno pristupa istom uredjaju.
Ova dva moda nije moguce istovremeno koristiti.
Uz neki od ova dva moda se moze dodati i neki od ovih parametara
DISCL_BACKGROUND oznacava da ce aplikacija imati pristup uredjaju u svakom trenutku... cak i kada izgubi fokus.
DISCL_FOREGROUND oznacava da ce aplikacija, kada izgubi fokus, izgubiti pristup uredjaju. Na ovaj nacin, cak i kada je aplikacija zatrazila DISCL_EXCLUSIVE mod, prilikom promene aktivne aplikacije pritiskom na ALT+TAB ili klikom misem, druga aplikacija moze pristupiti tastaturi.
Postoji jos jedan parametar DISCL_NOWINKEY koji, kada se koristi u DISCL_NONEXCLUSIVE modu, ne dozvoljava upotrebu Windows tastera.

Posle svih podesavanja, ostaje nam jos samo da pocnemo sa pracenjem uredjaja pozivanjem Acquire funkcije.

Posto se pracenje prekida kada izgubimo fokus, potrebno ga je ponovo zapoceti u OnActivate eventu:
procedure TfrmMain.FormActivate(Sender: TObject);
begin
  pKeyboard.Acquire;
end;


Idemo sad na glavni deo... citanje podataka sa tastature:
procedure TfrmMain.GUIUpdaterTimer(Sender: TObject);
var
  KeyboardData: array[0..255] of Byte;
  EditText: String;
  I: Integer;
begin
  if Assigned(pKeyboard) then
  begin
    EditText := '';
    ZeroMemory(@KeyboardData, SizeOf(KeyboardData));
    if Failed(pKeyboard.GetDeviceState(SizeOf(KeyboardData), @KeyboardData)) then
      repeat
      until pKeyboard.Acquire <> DIERR_INPUTLOST;
    for I := 0 to 255 do
      if (KeyboardData[I] and $80) <> 0 then
        EditText := EditText + Format('$%x ', [I]);
    txtPressed.Text := EditText;
  end;
end;


GetDeviceState cita trenutne podatke sa uredjaja u formatu koji smo ranije definisali. Posto smo zatrazili da nam se podaci vrate u nizu od 256 bajtova treba nam jedan takav niz. Po zavrsetku ove funkcije u nizu imamo podatke za svaki taster na tastaturi-pritisnut ili ne.

Moguce je da je u toku ove funkcije izgubljena kontrola nad uredjajem pa zbog toga pozivamo Aquire funkciju sve dok ponovo ne dobijemo kontrolu.

Na svim mestima u nizu na kojima se nalazi vrednost $80 se nalazi taster koji je pritisnut i na osnovu toga mozemo popuniti edit box rednim brojevima pritisnutih tastera.

Na kraju... oslobadanje u par redova Smile
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  pKeyboard := nil;
  pDI := nil;
end;


To je to u najkracim crtama... ovaj primer ima jednu... manu... prednost... zavisi od slucaja do slucaja... kada proveravamo podatke o uredjaju, dobijamo samo trenutno stanje. Ako se izmedju dve provere taster pritisnuo nekoliko puta i opet na kraju otpustio, mi to necemo primetiti jer se sve izdesavalo izmedju provera. Ovo mozete videti tako sto cete povecati interval u timer komponenti. Moguc je i drugaciji nacin rada... DirectInput moze sve podatke cuvati u buffer-u koji mu dodelimo i on ce puniti buffer podacima sve dok ih ne iscitamo tako da necemo propustiti ni jednu promenu.
U sledecem primeru cemo pokazati kako se koristi ovakav pristup citanju podataka.

Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 22 Apr 2006 15:44


Rad sa misem je skoro isti kao i rad sa tastaturom... u tome i jeste lepota DirectInputa... nije bitno koji uredjaj koristimo... za program je sve to isto Smile Sada cemo prikazati kako je moguce uzimati podatke koji su prethodno buffer-ovani u DirectInput-u tako da ne gubimo podatke o promenama koje su se desile izmedju dva proveravanja. Kod ovog nacina je potrebno izabrati dovoljno velik buffer kako bi DirectInput imao dovoljno mesta za sve promene. U ovom slucaju sam podesio da se podaci sa misa proveravaju svakih 50ms, a buffer je dovoljno velik da primi 24 promene... to bi trebalo da je dovoljno da prihvati sve promene.

U IDE ce nam forma izgledati ovako



Objasnjavacemo samo kod koji je promenjen u odnosu na prosli primer. Kreiranje objekata tece na isti nacin. Jedino sto je novo je postavljanje velicie buffer-a:
const
  BUFFER_SIZE = 24;

procedure TfrmMain.FormCreate(Sender: TObject);
var
  DIProp: TDIPropDWord;
begin
  DirectInput8Create(hInstance, DIRECTINPUT_VERSION,
    IID_IDirectInput8, pDI, nil);
  pDI.CreateDevice(GUID_SysMouse, pMouse, nil);
  pMouse.SetDataFormat(c_dfDIMouse2);
  pMouse.SetCooperativeLevel(Handle, DISCL_NONEXCLUSIVE or DISCL_FOREGROUND);

  DIProp.diph.dwSize := SizeOf(DIProp);
  DIProp.diph.dwHeaderSize := SizeOf(TDIPropHeader);
  DIProp.diph.dwObj := 0;
  DIProp.diph.dwHow := DIPH_DEVICE;
  DIProp.dwData := BUFFER_SIZE;
  pMouse.SetProperty(DIPROP_BUFFERSIZE, DIProp.diph);

  pMouse.Acquire;
end;


DIProp promenljivu popunjavamo podacima tako da sadrzi velicinu buffer-a koju zelimo i prosledjujemo je funkciji SetProperty. Ovim smo definisali koliki buffer zelimo.

U kodu ima jos samo jedna izmena... u delu za uzimanje podataka:
procedure TfrmMain.GUIUpdaterTimer(Sender: TObject);
var
  MouseData: array[0..BUFFER_SIZE - 1] of TDIDeviceObjectData;
  dwElements: Cardinal;
  EditText: String;
  I: Integer;
begin
  if Assigned(pMouse) then
  begin
    EditText := '';
    ZeroMemory(@MouseData, SizeOf(MouseData));
    dwElements := BUFFER_SIZE;
    if Failed(pMouse.GetDeviceData(SizeOf(TDIDeviceObjectData), @MouseData, dwElements, 0)) then
      repeat
      until pMouse.Acquire <> DIERR_INPUTLOST;
    for I := 0 to dwElements - 1 do
    begin
      case MouseData[I].dwOfs of
        DIMOFS_BUTTON0: EditText := EditText + 'B0';
        DIMOFS_BUTTON1: EditText := EditText + 'B1';
        DIMOFS_BUTTON2: EditText := EditText + 'B2';
        DIMOFS_BUTTON3: EditText := EditText + 'B3';
        DIMOFS_BUTTON4: EditText := EditText + 'B4';
        DIMOFS_BUTTON5: EditText := EditText + 'B5';
        DIMOFS_BUTTON6: EditText := EditText + 'B6';
        DIMOFS_BUTTON7: EditText := EditText + 'B7';
        DIMOFS_X: EditText := EditText + 'X';
        DIMOFS_Y: EditText := EditText + 'Y';
        DIMOFS_Z: EditText := EditText + 'Z';
      end;

      case MouseData[I].dwOfs of
        DIMOFS_BUTTON0..DIMOFS_BUTTON7:
        begin
          if (MouseData[I].dwData and $80) <> 0 then
            EditText := EditText + 'D '
          else
            EditText := EditText + 'U ';
        end;
        DIMOFS_X, DIMOFS_Y, DIMOFS_Z:
        begin
          EditText := EditText + IntToStr(Integer(MouseData[I].dwData)) + ' ';
        end;
      end;

    end;

    txtData.Text := EditText;
  end;
end;


Za razliku od proslog nacina gde nam je DirectInput vracao trenutno stanje ulaznog uredjaja u jednoj strukturi, u ovom slucaju ce nam vratiti samo podatke o promenama u nizu tipa TDIDeviceObjectData. Ovaj tip podatak sadrzi podatke o svakoj promeni koja se desila (taster je pritisnut ili pusten, pozicija je promenjena i slicno). dwOfs predstavalja podatak koji je promenjen od poslednjeg citanja... to moze biti neko dugme ili osa po kojoj se pozicija pomerila. Ako zahtevamo od DirectInput-a da nam podatke vraca u standardnom formatu tada mozemo koristiti predefinisane konstante (kao u ovom programu) za odredjivanje podatka. Vrednost na koju je podatak promenjen se nalazi u dwData. Osim ove dve promenljive na raspolaganju su nam jos par koje nam mogu dati informacije o vremenu kada se promena dogodila i rednom broju promene.

To bi bile neke osnove DirectInput-a... ovo sto smo do sad objasnili je sasvim dovoljno da se napravi neka jednostavna igra (break out, sokoban, ili nesto slicno).

Mislio sam da sledece na redu bude DirectPlay, ali Microsoft kaze da je taj deo zastareo i da ne bi trebalo vise da se koristi:
Citat:DirectPlay is deprecated, and Microsoft strongly recommends against using it to develop new applications. Game developers should use Windows Sockets (see Windows Sockets) and the Windows Firewall APIs (see Games and Firewalls).


Zato ce sledece biti pravljenje jednostavne igre (jos ne znam koje Smile )od nule.

Source (Delphi) | Source (Lazarus) | Bin

Dopuna: 22 Apr 2006 20:32


Ima li neko ideju sta bi mogli praviti, a da ne bude previse komplikovano za objasnjavanje (kod je lako pisati, ali objasnjenjeeee Razz )?

Dopuna: 28 Maj 2006 21:00


Odlucih se za igru Pong. Lako je za napraviti i za objasniti. Modeli su malo losi jer niko nije hteo da mi pomogne (smrc) da ih napravim, ali ce i ovi posluziti svrsi.

Jos malo pa cu zavrsiti kod i poceti sa pisanjem objasnjenja. Dok cekate da zavrsim evo jednog screenshot-a:



Dopuna: 01 Jun 2006 20:23


I... zavrsih igru... ovako... u igri se koriste GUI komponente Common Framework-a za pocetni meni (nova igra i izlaz), u igri se iscrtavaju 3 mesh-a (po jedan za svaku plocicu i loptica). Svaki mesh ima svoj X fajl tako da je moguce imati levu plocicu koja je drugacija od desne. Koriste se 2 vrste fonta... jedna za ispis rezultata i jedna za ispis poruke na sredini ekrana. Koristi se samo jedan zvuk kada se loptica odbija od zida ili plocice. Nisam koristio DirectInput za uzimanje podataka sa tastature vec obican GetKeyState. Detekcija sudara se vrsi tako sto se odredjuje da li zrak koji ide iz centra lopte i ima pravac i smer kretanja lopte sece neku od plocica. Ako zrak sece mesh i loptica je dovoljno blizu, na osnovu normale vertex-a na plocici se racuna kako ce loptica biti odbijena.

Za kontrolu plocica je napravljena posebna klasa tako da je lako dodati novu koja ce sluziti da plocicu kontrolise racunar (napravio sam vec jednu takvu... pogledajte InputUnit.pas) ili, recimo, igrac sa nekog drugog racunara preko mreze.

Kontrole su:

Levi igrac: W - gore, A - dole
Desni igrac: strelice za gore i dole

Pobedio je onaj igrac koji prvi dodje do 10.

P.S.
Kod je napisan na brzinu pa je moguce da mi se negde provukao neki bug. Imajte u vidu da provere gresaka nisam stavljao tako da ako nesto krene naopako (nedostaju modeli, zvuk, slike...) program ce se ili srusiti ili jednostavno raditi pogresno. Bas sam lenj, zar ne? Smile

Ako nekom nije jasno kako nesto od koda radi, slobodno neka pita.

Source (Delphi) | Source (Lazarus) | Bin | Media


Idi na vrh
offline
  • Pridružio: 11 Apr 2005
  • Poruke: 22
Uloguj se preko Facebook-a i postavi pitanje:
I ako mi nikada verovatno nece trebati nista od ovoga, ja stvarno moram da ti odam priznje za ulozeni trud.SVAKA TI CAST!
Idi na vrh
offline
  • mtk206 
  • Novi MyCity građanin
  • Pridružio: 22 Jun 2006
  • Poruke: 1
Uloguj se preko Facebook-a i postavi pitanje:
Sva ti cast!!! Znaci, ekstra...
Idi na vrh
offline
Uloguj se preko Facebook-a i postavi pitanje:
Hvala vam, ali u buduce odgovore tipa "svaka cast" saljite kao privatne poruke.

Ovaj tutorial je zavrsen, ali ako bude zainteresovanih za neki dodatak neka mi posalje pm.

Dopuna: 28 Okt 2007 2:14


Sreo sam na netu pre nekog vremena jednog decka iz Brazila koji je procitao ovaj tutorial (gledao je samo kod posto ne razume srpski jezik) i poceo da pravi igru tipa sokoban. Igra je jos uvek alpha, ali moze da se igra.



Igru mozete skinuti odavde: http://www.wikifortio.com/710122/sokojam1.zip

Za pravljenje igre je koristio: Delphi 7 i DanJetX komponente.

Potreban je samo minut da se registrujete - da biste učestvovali u diskusiji:
Izaberite vaše korisničko ime [username] :
Vaša email adresa je [email] : Email adresa mora biti tačna!
Ukucajte željenu šifru [password] :
Ukucajte šifru ponovo [password again] :
Jezik [language] :




Ili se jednostavno uloguj preko Facebook-a:

Ko je trenutno na forumu

 

Ukupno su 270 korisnika na forumu :: 20 registrovanih, 3 sakrivenih i 247 gosta   ::   [ Administrator ] [ Supermoderator ] [ Moderator ] :: Detaljnije

Najviše korisnika na forumu ikad bilo je 1142 - dana 11 Mar 2012 19:06

Korisnici koji su trenutno na forumu:
Korisnici trenutno na forumu: angrist2, calvi, Chuck Norris, Dorcolac2, Dragan Zivkovic, dzony97, Goran Kalentic, janezek67, MARDUK, sastank, Sirius, Slobodan Kostic 4, Srki94, Sveto2, Tasha, topalovicdj, trenutni, Warhawk, Zorge, Žan Klod vam dam
VPS