Voglio fare 2D
Guardando sugli scaffali dei negozi non si possono avere dubbi, il 3D domina
il mercato, rilegando i poveri titoli bidimensionali all’angolino meno
visitato. Tuttavia non tutto è perso, alcuni titoli, 2D da capo a piedi,
si confondono in mezzo agli antagonisti con una dimensione in più.
Eh si, perché non tutti i giochi sono adatti ad essere rappresentati
in tre dimensioni, soprattutto per problemi di giocabilità; i giochi
d’abilità, gli shotem’up, le simulazioni alla SimCity risultano
sicuramente più gradevoli in 2 dimensioni.
Riguardo a SimCity, c’è da dire che l’ultimo episodio (4),
è una miscela ben riuscita delle due tecnologie; al terreno tridimensionale
si affiancano delle costruzioni 2D, che possiedono un dettaglio veramente eccezionale,
giocare per credere…
Prima di andare sul tecnico, è d’obbligo aggiungere che il 2D torna
utile anche nei videogiochi 3D, non vorrete mica lasciare il giocatore senza
un misero pannello che lo informi sul suo stato, quello della sua nave, ecc..
Nel mondo del 3D
Poco meno di un mese fa (dall’inizio del tutorial…) decido di imparare
(finalmente) un po’ di C/C++, così, contento come uno scolaretto
al primo giorno di scuola (uhm, non è che fossi tanto contento al primo
giorno di elementari..), recupero la confezione del VC++ abbandonata nell’armadio
dall’università, scarico il mega SDK delle DirectX ed installo
il tutto.
Tra me e me penso, dopo tanti anni di Delphi e Java sarà uno scherzo
impararlo e fare il porting di qualche giochetto che ho da parte, niente di
complicato; un simil Tetris, un clone di Arkanoid, un altro giochetto stile
Worms, tutti accomunati dalla grafica 2D.
Istallato il tutto incomincio a familiarizzare con l’ambiente ed il linguaggio,
faccio le prime prove ed incomincio a cercare un esempio nel SDK, che mi dia
le basi per disegnare sullo schermo con DirectDraw, ma… come mai non ci
sono esempi ? Uhm, affiorano alla mente vaghi ricordi di una rivoluzione copernicana
iniziata con la versione 8 delle DirectX, mi fiondo su google ed incomincio
a cerca informazioni, giro un paio di siti, guardo quelli di settore e…
ecco !
La Microsoft ha detto addio al supporto nativo per il 2D, il simil-comunicato
in inglese che ho d’avanti dice più o meno “le schede grafiche
di oggi sono fatte per muovere 3D, il 2D te lo simuli”.
Iniziamo bene, mi hanno appena soffiato una facile conversione da sotto il naso, ma non mi arrendo per così poco. Torno un po’ in dietro con la mente, quando con un gruppo di amici conosciuti su IRC stavo realizzando il motore che doveva muovere un RPG; uno dei primissimi problemi che incontrammo riguardava la tecnologia da utilizzare, meglio il vecchio è consolidato 2D, nel vero senso del termine, o simulare il 2D appiccicando le “tile” a mo di “texture”, come avevano iniziato a fare in alcuni giochi di allora (Diablo II e NOD se non sbaglio).
All’epoca la scelta, per vari motivi cadde sul 2D, probabilmente non eravamo(ero) pronti ad un utilizzare una tecnologia simile, la documentazione era davvero nulla, e chi doveva fare la grafica impallidiva al pensiero di avere una dimensione in più, anche se alla fine non cambiava poi molto.
La soluzione
Come suddetto, il 2D può essere simulato via 3D, ed oggi, se sviluppate
con le DirectX, diventa pressoché obbligatorio, non vorrete mica usare
un’interfaccia (quella delle DirectDraw) deprecata ? Naaa…
Prima di spiegare come avviene la simulazione, ragioniamo un
momento su come funziona il 2D “classico”; il frame visualizzato
viene composto in un area di memoria non visibile (buffer), appiccicando delle
immagini o pezzi di esse, provenienti da file sul disco o meglio da altri buffer
(per questioni di velocità), alla fine il contenuto del buffer viene
“blittato" sull’area di memoria che la scheda video proietta
sullo schermo.
Insomma, il concetto “classico” di 2D è alla portata di tutti,
l’unica unità di misura con cui si combatte è il pixel (correggetemi
se sbaglio), e la materia trattata sono le immagini.
Utilizzando le tecnologie 3D moderne, tutti questi concetti
se ne vanno al creatore, primo fra tutti quello di “immagine”, che
viene soppiantato da quello di “texture”. Per spiegare ad un grafico
abituato al 2D il concetto di “texture”; direi che essa è
“una superficie dove si trovano una o più immagini affiancate tra
loro, ed eventualmente la relativa mappa Alpha”, ovvero qualcosa di simile
nell’utilizzo pratico ai buffer secondari che si utilizzavano nel 2D,
per caricare in memoria le immagini utilizzate nel gioco.
Avrete sicuramente notato nella descrizione di “texture”, l’accenno
alla mappa Alpha, contenuta nell’omonimo canale; questa serve per gestire
le trasparenze, permettendo di avere sino a 256 livelli (8 bit) diversi di trasparenza
su ogni pixel della texture. Per chiarire le idee, immaginate di avere sul canale
Alpha una copia in scala di grigi della texture, dove i pixel neri danno all’immagine
sottostante piena opacità e quelli bianchi piena trasparenza, ovviamente
le sfumature rappresentano i valori intermedi.
Volendo si potrebbe usare un singolo colore per rappresentare la trasparenza, nel 2D “classico” si utilizza solitamente il magenta (RGB 255,0,255) od il nero, ma volete mettere il vantaggio di avere a disposizione ben 256 livelli diversi di trasparenza ???
Se siete ancora vivi, andatevi a prendere un caffè, perché ora incominciamo ad entrare nella parte più consistente della questione.
Un po’ di codice
Finita la lunga introduzione teorica, possiamo incominciare a buttare giù
il codice necessario per costruire le basi del nostro videogioco 2D in D3D.
Tutti gli esempi che riporterò di seguito sono pensati per il Microsoft
VC++, per motivi di tempo (e di esperienza) non posso scrivere i cambiamenti
necessari per farli funzionare su altri ambienti/compilatori, comunque non è
un’operazione difficile da eseguire, anche per chi è alle prime
armi.
Se potete, scaricate il SDK delle DirectX 9 (o una versione più recente),
contiene la documentazione ufficiale, esempi utili per iniziare ed un framework
già pronto, che non fa sicuramente male a chi non può o non ha
il tempo per realizzarne uno proprio da zero.
Inizializzazione
Prima di tutto dobbiamo attivare una superficie D3D sulla quale poter disegnare,
nel SDK trovate un esempio pronto (Tut01_CreateDevice), ma vi consiglio di rifarlo
seguendo il prossimo paragrafo, in modo da capire passo per passo tutto il codice
in esso contenuto.
Includiamo il file con gli header di D3D e creiamo le variabili necessarie ad avviarlo:
- #include <d3d9.h>
- LPDIRECT3D9 g_pD3D = NULL; // Usata per creare il device D3D
- LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; // Device di rendering
Adesso la funzione di inizializzazione vera e propria:
- HRESULT InitD3D( HWND hWnd )
- {
- if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) return E_FAIL;
- D3DPRESENT_PARAMETERS d3dpp;
- ZeroMemory( &d3dpp, sizeof(d3dpp) );
- d3dpp.Windowed = TRUE;
- d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
- d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
- if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
- D3DCREATE_SOFTWARE_VERTEXPROCESSING,
- &d3dpp, &g_pd3dDevice ) ) )
- {
- return E_FAIL;
- }
- return S_OK;
- }
Il primo IF tenta di inizializzare il device D3D, se non vi riesce termina la funzione restituendo E_FAIL. La funzione Direct3DCreate9 restituisce NULL in caso di fallimento o un puntatore all’interfaccia IDirect3D9 in caso di successo. D3D_SDK_VERSION serve al compilatore per verificare se si stanno utilizzando gli header corretti, per ulteriori informazioni:
Di seguito troviamo i parametri che verranno passati alla funzione CreateDevice, per creare il device di rendering vero è proprio.
D3DPRESENT_PARAMETERS è una struttura capace di contenere tutte le informazioni riguardanti il device che andremo a creare, per conoscerla in dettaglio:
La funzione ZeroMemory serve a riempire di zeri l’area di memoria occupata da una variabile, in questo caso quella di d3dpp, per ulteriori informazioni:
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/memory/base/zeromemory.asp
Le tre linee di codice successivo settano dei parametri di d3dpp, che come detto contiene i dati (D3DPRESENT_PARAMETERS) dl nostro device, la creazione di un device in finestra, il comportamento sui buffer ed infine il formato del buffer, in questo caso con D3DFMT_UNKNOWN gli diciamo che non lo conosciamo, lasciando che se ne occupi D3D. In futuro potremo stabilire a priori il formato da utilizzare, per ulteriori informazioni:
Infine, passiamo tutto a CreateDevice che a scanso di errori, creerà finalmente un device sul quale poter renderizzare quello che più ci aggrada ! Velocemente i arametri non visti:
- D3DADAPTER_DEFAULT
utilizza la scheda video primaria (potrebbero esserci più schede video sul sistema) - D3DDEVTYPE_HAL
tipo di device, in questo caso quello “più hardware”, per informazioni:
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/enums/d3ddevtype.asp - hWnd
handle della finestra su cui creare il device. - D3DCREATE_SOFTWARE_VERTEXPROCESSING
una o più voci (come si traduce flag ? Bandiera ? naa…) che impostano il formato del device, in questo caso gli diciamo che i vertici verranno gestiti via software.
Per informazioni sulla funzione CreateDevice:
Rispettando la sequenza nell’esempio fornito da Microsoft, sequenza che tutto sommato mi sembra utile ai fini dell’apprendimento, ecco la procedura per “chiudere” il device che abbiamo creato con tanti sforzi:
- VOID Cleanup()
- {
- if( g_pd3dDevice != NULL)
- g_pd3dDevice->Release();
- if( g_pD3D != NULL)
- g_pD3D->Release();
- }
Mi sembra che non ci sia molto da dire, controlliamo se le due principali variabili (per il momento abbiamo solo quelle…) esistono ed eventualmente passiamo loro il metodo Release(); che si occupa per noi di compiere le operazioni necessarie per ripulire la memoria utilizzata.
- VOID Render()
- {
- if( NULL == g_pd3dDevice ) return;
- // Cancella il BackBuffer ed inposta il colore blue
- g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
- // Inizio della scena
- if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )
- {
- // Disegnamo allegramente
- // Fine della scena 9
- g_pd3dDevice->EndScene();
- }
- // Flippa il contenuto del BackBuffer sulla superficie primaria (va sullo schermo…)
- g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
- }
La funzione Render() controlla che esista un device inizializzato,
cancella il contenuto del BackBuffer, prova a creare una nuovo frame della scena
(in questo caso vuota) e quindi copia il contenuto del BackBuffer sulla superficie
primaria, che corrisponde all’immagine visibile sullo schermo.
Ci troviamo di fronte a quattro nuovi metodi del device (LPDIRECT3DDEVICE9),
esaminiamoli uno per uno, velocemente:
Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
Cancella l’intera area del buffer o una porzione rettangolare dello stesso,
attribuendogli un colore nel formato RGBA (Red Green Blue Alpha), per conoscere
meglio il metodo:
BeginScene() / EndScene();
rispettivamente aprono e chiudono l’area in cui inserire il codice per
disegnare la scena.
Present( NULL, NULL, NULL, NULL );
presenta (è educato) il contenuto del backbuffer sullo schermo, o meglio,
per essere precisi lo copia nella porzione di memoria della scheda video a cui
è associata l’area visibile sullo schermo.
I parametri sono nell’ordine:
- pSourceRect, rettangolo sorgente (come struttura RECT)
- pDestRect, rettangolo destinazione (come struttura RECT)
- hDestWindowOverride, destinazione, volendo potete copiare il buffer sul device di un’altra finestra
- pDirtyRegion, facciamo finta che non esista, siamo principianti no ?
Per approfondire:
Per ulteriori informazioni sull’interfaccia IDirect3DDevice9:
Il nostro videogioco, o meglio, la base da cui potremo svilupparlo, funziona dentro una “normale” finestra di Windows, di conseguenza è necessaria la funzione per decifrare i messaggi:
- LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
- {
- switch( msg )
- {
- case WM_DESTROY:
- Cleanup();
- PostQuitMessage( 0 );
- return 0;
- case WM_PAINT:
- Render();
- ValidateRect( hWnd, NULL );
- return 0;
- }
- return DefWindowProc( hWnd, msg, wParam, lParam );
- }
Per il momento consideriamo i messaggi di uscita e disegno, all’interno di questo ultimo troviamo un richiamo alla funzione Render() precedentemente esaminata. Dato che questo tutorial è rivolto all’uso di D3D9 non vado oltre nella spiegazione, se no diventa un libro sul C++ :-P
Concludiamo la nostra applicazione di prova con WinMain che, come saprete è il punto di inizio (entry point) per le applicazioni Windows, per ulteriori informazioni:
Ed ecco il codice:
- INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR, INT )
- {
- // Registro la classe
- WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
- GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
- \"D3D Tutorial\", NULL };
- RegisterClassEx( &wc );
- // Creo la finestra dell'applicazione
- HWND hWnd = CreateWindow( \"D3D Tutorial\", \"D3D Tutorial 01: CreateDevice\",
- WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,
- GetDesktopWindow(), NULL, wc.hInstance, NULL );
- // Provo ad inizializzare Direct3D
- if( SUCCEEDED( InitD3D( hWnd ) ) )
- {
- // Mostro la finestra
- ShowWindow( hWnd, SW_SHOWDEFAULT );
- UpdateWindow( hWnd );
- // Avvio il loop dei messaggi, bruttissimo per un videogame, poi lo miglioreremo…
- MSG msg;
- while( GetMessage( &msg, NULL, 0, 0 ) )
- {
- TranslateMessage( &msg );
- DispatchMessage( &msg );
- }
- }
- UnregisterClass( \"D3D Tutorial\", wc.hInstance );
- return 0;
- }
L’unica cosa interessante hai fini dell’articolo è InitD3D, che tenta di inizializzare le DirectX. Come da commento, il ciclo per la gestione dei messaggi non può andare bene per un videogioco, dato che non garantisce uno sviluppo lineare del tempo, vedremo in seguito come sostituirla con una più adeguata, per il momento possiamo accontentarci.
A questo punto, siamo pronti per provare ad avviare la nostra applicazione di prova. Avviamo il VC++, selezioniamo file->new e scegliamo di creare una nuova Win32 Application, andiamo avanti nella procedura con Empty Application.
Di nuovo selezioniamo file- new ed aggiungiamo un file sorgente C++ Source File alla nostra applicazione, su cui copieremo il codice sin ora esaminato.
Proviamo a compilare il tutto, se il SDK delle DirectX è stato istallato correttamente, il compilatore non dovrebbe restituire errori, possiamo quindi provare ad avviare la nostra applicazione e vedere che… il linker restituisce diversi errori.
Andiamo in Project->Settino clicchiamo su Link ed aggiungiamo d3d9.lib all’inizio della riga contrassegnata da Object/Library Modules. Controlliamo inoltre che siano inseriti i path corretti ai file del SDK, in tools->options->directories, in “Include files” deve esserci una voce tipo C:\DXSDK\INCLUDE e C:\DXSDK\LIB sotto “Library files”.
Proviamo nuovamente ad eseguire l’applicazione, che adesso dovrebbe partire senza problemi, mostrando una finestra riempita di blu al suo interno.
Trovate maggiori informazioni (in inglese, come il resto del MSDN), sull’esempio di inizializzazione che vi ho spiegato, al seguente indirizzo:
Setup dello schermo
Ora che la base del nostro videogioco è funzionante, possiamo finalmente
passare alla parte più pratica, dove vedremo come impostare lo schermo,
caricare una texture in memoria ed usare parti di essa per comporre una scena
nel BackBuffer.
Dobbiamo innanzi tutto, dire a D3D che vogliamo usare un certo formato per visualizzare la scena, vediamo il codice:
- void SetupScreen(float WindowWidth, float WindowHeight)
- {
- D3DXMATRIX matOrtho;
- D3DXMATRIX matIdentity;
- D3DXMatrixOrthoLH(&matOrtho, WindowWidth, WindowHeight, 0.0f, 1.0f);
- D3DXMatrixIdentity(&matIdentity);
- g_pd3dDevice->SetTransform(D3DTS_PROJECTION, &matOrtho);
- g_pd3dDevice->SetTransform(D3DTS_WORLD, &matIdentity);
- g_pd3dDevice->SetTransform(D3DTS_VIEW, &matIdentity);
- // Disabilitiamo lo z-buffer e le luci, non ci servono
- g_pd3dDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
- g_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);
- }
In questa funzione incontriamo la struttura D3DXMATRIX, che contiene i dati descrittivi della matrice 3D usata da D3D, per avere un’idea di quante cose possa contenere:
Le due matrici che abbiamo dichiarato (matOrtho e matIdentity), vengono quindi popolate dalle due funzioni:
D3DXMatrixOrthoLH(&matOrtho, WindowWidth, WindowHeight,
0.0f, 1.0f);
Costruisce una matrice di proiezione ortogonale left-handed, popolando la struttura
D3DXMATRIX appena vista. I parametri sono, per l’appunto un puntatore
ad una struttura D3DXMATRIX, larghezza e altezza del volume visibile (quella
su cui disegneremo), valore minimo e massimo sull’asse zeta del suddetto
volume. Per ulteriori informazioni fate riferimento a:
D3DXMatrixIdentity
Crea una identity matrix (matrice identità), ovvero una matrice in cui
gli elementi sulla diagonale hanno valore 1 e gli altri 0. Tale matrice ha la
caratteristica di non mutare la scena a cui viene applicata, ci tornerà
utile al momento di effettuare trasformazioni sui vertici, per informazioni
vedi:
Il codice seguente setta alcuni parametri fondamentali della visualizzazione,
rivediamolo:
- g_pd3dDevice->SetTransform;(D3DTS_PROJECTION, &matOrtho;);
- g_pd3dDevice->SetTransform;(D3DTS_WORLD, &matIdentity;);
- g_pd3dDevice->SetTransform;(D3DTS_VIEW, &matIdentity;);
Il metodo SetTransform, modifica il modo in cui D3D applicherà certe trasformazioni, nel nostro caso:
- D3DTS_PROJECTION
Setta la matrice che verrà utilizzata per la proiezione. - D3DTS_WORLD
Setta una o più (256 per l’utente, 256 riservate) world matrix (matrici “mondiali”). - D3DTS_VIEW
Setta la matrice di, consentitemi il termine “visione”.
D3DTRANSFORMSTATETYPE
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/enums/d3dtransformstatetype.asp
Il metodo SetRenderState, modifica i parametri con cui D3D eseguirà il rendering, nel nostro caso abbiamo cambiato lo stato di D3DRS_ZENABLE su D3DZB_FALSE per disattivare lo z-buffer, e D3DRS_LIGHTING su FALSE per disattivare l’illuminazione di D3D.
D3DRENDERSTATETYPE
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/enums/d3drenderstatetype.asp
Caricare una texture
Lo schermo è pronto, ma non abbiamo niente da disegnarci, vediamo quindi
di creare una funzione per poter caricare in modo pratico una texture in memoria:
- LPDIRECT3DTEXTURE9 LoadTexture(char *filename,D3DCOLOR colorkey = 0xFF000000)
- {
- LPDIRECT3DTEXTURE9 pd3dTexture;
- D3DXIMAGE_INFO SrcInfo; // Opzionale
- D3DXCreateTextureFromFileEx(g_pd3dDevice, filename, 0, 0, 0, 0,
- D3DFMT_A8R8G8B8, D3DPOOL_MANAGED, D3DX_DEFAULT, D3DX_DEFAULT,
- colorkey, &SrcInfo , NULL, &pd3dTexture);
- // Controlli, prossimamente…
- return pd3dTexture;
- }
Per creare una nuova texture da un file, come si vede, chiamiamo la funzione D3DXCreateTextureFromFileEx, passandogli I seguenti argomenti:
- g_pd3dDevice
Device D3D attivo
- filename
Puntatore al nome del file da cui caricare la texture.
- 0, 0
Larghezza ed altezza della texture, se si inserisce 0 o D3DX_DEFAULT, la dimensione viene presa direttamente dal file.
- 0
Numero di mip levels richiesti. Se come in questo caso inseriamo 0 o D3DX_DEFAULT, D3D crea una sequenza completa di mipmap. Il discorso è più semplice di quanto si pensi; per snellire il numero di calcoli necessari, vengono create diverse versioni più della stessa texture, riducendone la dimensione per potenza di 2 ad ogni passaggio, utilizzando la più piccola per la massima distanza dal punto di vista (telecamera), e viceversa quella originale quando l’oggetto con la texture è molto vicino. Se abbiamo quindi una texture originale grande 512x512 pixel, otterremo una mipmap chain contenente due versioni ridotte (256x256, 64x64), dato che la dimensione minima impostata è di 64x64 pixel. Per approfondire l’argomento:
Texture Filtering with Mipmaps
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/dx8_c/directx_cpp/Graphics/ProgrammersGuide/UsingDirect3D/Textures/ filtering/TextureFilteringWithMipmaps.asp
- 0
Imposta il modo il modo di utilizzo della texture, dato che l’argomento è abbastanza ampio e difficile, per il momento non andiamo oltre.
- D3DFMT_A8R8G8B8
Indica il formato della texture, è possibile attribuire D3DFMT_UNKNOWN per far si che D3D cerchi le informazioni dal file stesso, o indicare uno dei formati D3DFORMAT disponibili, nel nostro caso il formato scelto è un ARGB (RGB con canale Alpha per le trasparenze), a 8 bit per canale, 32 quindi in totale. Per conoscere gli altri formati:
D3DFORMAT
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/enums/d3dformat.asp
- D3DPOOL_MANAGED
Indica a D3D come gestire la texture in memoria, nel nostro caso verrà copiata nella memoria della scheda video (MSDN dice letteralmente “nella memoria accessibile alla scheda video”) quando ve ne sarà bisogno.
D3DPOOL Enumerated Type
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/enums/d3dpool.asp
- D3DX_DEFAULT
Una combinazione di filtri D3DX_FILTER, che verranno applicati all’immagine, inserire D3DX_DEFAULT equivale ad usare i filtri D3DX_FILTER_TRIANGLE e D3DX_FILTER_DITHER, che rispettivamente indicano a D3D che ogni pixel dell’immagine originale contribuisce alla creazione della texture finale (se si rimpicciolisce viene quindi applicata un’interpolazione tra tutti i pixel originali) e che deve applicare un algoritmo di dithering 4x4. Per approfondire l’argomento:
D3DX_FILTER
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3dx/constants/D3DX_FILTER.asp
- D3DX_DEFAULT
Una combinazione di filtri D3DX_FILTER da applicare al mipmap. In questo caso però, D3DX_DEFAULT corrisponde al filtro D3DX_FILTER_BOX, che nel caso di una riduzione per potenza di due della texture, come avviene nel mipmap, calcola il nuovo pixel attraverso la media di un box da 2x2 pixel.
- colorkey
Colore per la trasparenza nel formato ARGB a 32-bit, o 0 (zero) per disabilitarlo.
- &SrcInfo
Puntatore ad una struttura D3DXIMAGE_INFO, su cui verranno memorizzati i dati della texture o null se non interessano.
D3DXIMAGE_INFO
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/directx9_c/directx/ graphics/reference/d3dx/structures/d3dximage_info.asp
- NULL
Puntatore ad una struttura PALETTEENTRY, che rappresenta una palette a 256 colori, lasciamolo NULL.
- &pd3dTexture
Puntatore ad un interfaccia IDirect3DTexture9, in cui verrà memorizzato l’oggetto texture appena creato.
IDirect3DTexture9 Interface
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3d/interfaces/ idirect3dtexture9/_idirect3dtexture9.asp
D3DXCreateTextureFromFileEx Function
http://msdn.microsoft.com/library/default.asp?url= /library/en-us/directx9_c/directx/graphics/reference/d3dx/functions/texture/d3dxcreatetexturefromfileex.asp
Per compilare il codice appena visto è necessario includere D3DX9Math.h,
quindi aggiungiamo all’inizio del codice:
- #include <D3DX9Math.h>
Bisogna inoltre aggiungere la libreria d3dx9dt.lib al clinker, esattamente come abbiamo fatto all’inizio per d3d9.lib.
La funzione Blit
Il nostro “piccolo motore” è quasi pronto, manca la funzione
più importante, quella per spostare le texture sullo schermo (non è
proprio così, ma il risultato finale è questo), e la funzione
per gestire il rendering.
Iniziamo quindi con la funzione per emulare il blit di DirectDraw:
- HRESULT BltSpriteEx( RECT *pDestRect, LPDIRECT3DTEXTURE9 pSrcTexture,
- RECT *pSrcRect, DWORD mFlag, D3DCOLOR modulate = 0xFFFFFFFF,
- float rotation = 0, POINT *prcenter = NULL )
- {
- D3DXVECTOR2 scaling(1,1), rcenter(0,0), trans(0,0);
- // Traslazione (posizione sullo schermo)
- if (pDestRect) {
- trans.x = (float) pDestRect->left;
- trans.y = (float) pDestRect->top;
- }
- // Centro per la rotazione
- if (prcenter) {
- rcenter.x = (float) prcenter->x;
- rcenter.y = (float) prcenter->y;
- } else if (pSrcRect) {
- // Setto il centro della rotazione al centro dell'oggetto
- rcenter.x = (float) (pSrcRect->right - pSrcRect->left) / 2;
- rcenter.y = (float) (pSrcRect->bottom - pSrcRect->top) / 2;
- }
- // Scala - Se le area di sorgente/destinazione hanno dimensioni diverse,
- // provvedo a scalare l'imamgine
- if (pDestRect && pSrcRect) {
- scaling.x = ((float) (pDestRect->right - pDestRect->left)) /
- ((float) (pSrcRect->right - pSrcRect->left));
- scaling.y = ((float) (pDestRect->bottom - pDestRect->top)) /
- ((float) (pSrcRect->bottom - pSrcRect->top));
- }
- // Eseguo l'eventuale mirror
- if (pSrcRect&&mFlag) {
- if (mFlag&MIRRORLEFTRIGHT) {
- scaling.x = -scaling.x;
- trans.x += (float) (pDestRect->right - pDestRect->left);
- }
- if (mFlag&MIRRORUPDOWN) {
- scaling.y = -scaling.y;
- trans.y += (float) (pDestRect->bottom - pDestRect->top);
- }
- }
- return pd3dxSprite->Draw( pSrcTexture, pSrcRect, &scaling, &rcenter, rotation, &trans, modulate );
- }
La funzione inizia con la dichiarazione di tre variabili di tipo D3DXVECTOR2, struttura che contiene le due coordinate x/y in formato float:
- scaling(1,1)
Conterrà l’eventuale differenza di dimensione tra l’immagine sorgente e quella di destinazione. - rcenter(0,0)
Servirà per immagazzinare le coordinate del centro dell’immagine, da utilizzare per eventuali rotazioni. - trans(0,0)
Verrà utilizzato per la traslazione dell’immagine, ovvero per definire la sua posizione sullo schermo
Le operazioni che seguono sono relativamente semplici sino a quando non incontriamo mFlag, un attributo che si rende utile per indicare se l’immagine deve essere specchiata sul piano orizzonatale (MIRRORLEFTRIGHT) e/o su quello verticale (MIRRORUPDOWN). Non essendo parte delle DirectX, dobbiamo dichiarare noi stessi i valori possibili per mFlag, prima di poterli usare:
- #define MIRRORLEFTRIGHT 0x01
- #define MIRRORUPDOWN 0x02
Infine, arriviamo al punto chiave di questa funzione, il metodo Draw dell’interfaccia ID3DXSprite:
- HRESULT Draw(
- LPDIRECT3DTEXTURE9 pSrcTexture,
- CONST RECT *pSrcRect,
- CONST D3DXVECTOR2 *pScaling,
- CONST D3DXVECTOR2 *pRotationCenter,
- FLOAT Rotation,
- CONST D3DVECTOR2 *pTranslation,
- D3DCOLOR Color
- );
Abbiamo già esaminato in precedenza le strutture utilizzate da questo metodo, pertanto non mi dilungo ulteriormente e vi rimando per un’ulteriore approfondimento ad MSDN:
Prima di continuare una nota “ottimizzativa”, il metodo Draw deve essere richiamato tra i metodi Begin & End, sempre della struttura ID3DXSprite. Se non si procede in tal senso, vengono richiamati automaticamente ad ogni utilizzo del metodo Draw, con un conseguente spreco di risorse, la funzione Render() diverrà quindi:
- VOID Render()
- {
- if( NULL == g_pd3dDevice ) return;
- // Cancella il BackBuffer ed inposta il colore blue
- g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
- // Inizio della scena
- if( SUCCEEDED( g_pd3dDevice->BeginScene() ) )
- {
- // Disegnamo allegramente
- D3DXCreateSprite(g_pd3dDevice,&pd3dxSprite);
- SetRect(&rect1,1,1,256,256);
- SetRect(&rect2,1,1,256,256);
- pd3dxSprite->Begin();
- BltSpriteEx(&rect1, g_pTexture, &rect2, NULL);
- // Blt...
- // Blt...
- // Blt...
- pd3dxSprite->End();
- // Fine della scena
- g_pd3dDevice->EndScene();
- }
- // Flippa il contenuto del BackBuffer sulla superficie primaria (va sullo schermo…)
- g_pd3dDevice->Present( NULL, NULL, NULL, NULL );
- }
Gestiamo meglio il tempo
Come suddetto, il loop per smistare i messaggi di Windows, così com’è
non fa proprio al caso nostro, quindi commentiamolo ed aggiungiamo una nuova
chiamata:
- /*
- MSG msg;
- while( GetMessage( &msg, NULL, 0, 0 ) )
- {
- TranslateMessage( &msg );
- DispatchMessage( &msg );
- }
- */
- GameLoop();
Il problema fondamentale è che il “loop standard” è pensato per attendere smistando i messaggi, sino a quando non ne viene intercettato uno diretto all’applicazione stessa. Questo sistema funziona perfettamente per le applicazioni, che ragionano in termini di “eventi”, per un videogioco però, come già detto non può funzionare, perché basato non sugli eventi ma sul tempo, vediamo quindi la funzione GameLoop():
- void GameLoop()
- {
- MSG msg;
- BOOL fMessage;
- PeekMessage(&msg, NULL, 0U, 0U, PM_NOREMOVE);
- while(msg.message != WM_QUIT)
- {
- fMessage = PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE);
- if(fMessage)
- {
- // Processo i messaggi
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- else
- {
- // Non ci sono messaggi, via al rendering !
- Render();
- }
- }
- }
Come potete vedere, la differenza sostanziale è nell’impiego del tempo in cui il precedente loop rimaneva in attesa, per effettuare il nostro rendering. Questo è solo un piccolo passo nella gestione del tempo, più che sufficiente comunque per i nostri scopi.
Concludendo
Se non ho commesso troppi errori, questo breve tutorial, che più che
altro è un “diario di bordo”, scritto in contemporanea con
lo studio delle DirectX stesse, dovrebbe risultarvi d’aiuto per cominciare
a scrivere il vostro primo gioco 2D in 3D.
Vi invito a continuare in questa direzione visitando i link che troverete di
seguito, e consultando l’ottima MSDN di Microsoft, che al dire il vero,
purtroppo, difetta un po’ sull’argomento trattato da questo testo.
Buon divertimento !
Bibliografia & link utili
- DirectX Tutorial 11: 2D in 3D
- A simple blit function for Direct3D
- Rendering Full Screen Images from Textures
- First Look at DX8 Graphics
- 2D programming in a 3D world
- 2D rendering in DirectX 8
- Serie Introduzione a DirectX + 2D






