\\ Home : Articoli : Stampa
Inizializzare DirectX 10
Di RobyDx (del 08/02/2007 @ 21:48:41, in Direct3D10, linkato 4232 volte)

Iniziamo con il primo tutorial pratico di DirectX10. Quello che faremo è creare la classica helloworld, una finestra che si limiterà ad utilizzare DirectX per colorare lo schermo.

Consiglio di fare un pò di pratica con il C++ prima di iniziare. Dato che il mio sito è stato frequentato per anni da programmatori .Net cercherò di affrontare le questioni di codice C++ nel modo più semplice possibile, tuttavia classi, puntatori e tutto ciò che riguarda le basi del C++ dovranno essere date per scontate.

Se invece siete pronti iniziamo. Per prima cosa sarà necessario inserire correttamente le directory per le librerie ed i riferimenti alle DirectX. Se avete installato Visual Studio prima di installare la DirectX SDK questa parte non vi serve. Altrimenti recatevi nel menu Strumenti->Opzioni e nel menù selezionate progetti e Soluzioni->Directory di VC++. Qui dovrete impostare i seguenti path

C:\Program Files\Microsoft DirectX SDK (February 2007)\Include per i file di inclusione

C:\Program Files\Microsoft DirectX SDK (February 2007)\Lib\x86 per le librerie.

I path cambieranno in base al tipo di installazione che avete effettuato e alla versione dell'SDK installata. Nel mio caso è la SDK di Febraio 2007. Nota importante per chi vuole lavorare a 64bit. In questo caso non dovrete usare la directory x86 ma x64. Qui ci sono le librerie compilate per questi processori.

La prima cosa da fare è creare una finestra win32. Creiamo un nuovo progetto Win32 di tipo windows e che sia vuota (potete anche farvi creare una applicazione Win32 già pronta da Visual Studio). Aggiungete un file Cpp ed inserite questo codice.

#include "windows.h"

LRESULT WINAPI MsgProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
PAINTSTRUCT ps;
HDC hdc;

switch (msg)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
break;

case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, msg, wParam, lParam);
}

return 0;
}


INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE instance, LPSTR, INT )
{

WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, MsgProc, 0L, 0L,
hInst, LoadIcon(NULL, L"directX.ico"), LoadCursor(NULL,IDC_ARROW), NULL, NULL,
L"DirectX10", NULL };
RegisterClassEx( &wc );

HWND hWnd = CreateWindow(L"DirectX10", L"DirectX10",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
NULL, NULL, NULL, NULL );

ShowWindow( hWnd, SW_SHOWDEFAULT );

MSG msg={0};

while( WM_QUIT != msg.message )
{
if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}

UnregisterClass( L"DirectX10", wc.hInstance );
return 0;
}

Questa porzione di codice crea una applicazione Win32 che mostra una finestra a video che rimane attiva finchè non viene chiusa.

Ora facciamo conoscenza con il primo oggetto di DirectX 10, l'interfaccia ID3D10Device, che chiameremo semplicemente Device.

Il device è la classe più importante di DirectX10 perchè gestisce e crea tutte le risorse che andremo ad utilizzare e per questo deve essere la prima ad essere creata e l'ultima ad essere distrutta. Per poterlo utilizzare dovremo per prima cosa includere gli header e le librerie necessarie. Per includere librerie nel progetto ci sono vari sistemi, io preferisco usare i pragma comment.

#pragma comment (lib,"d3d10.lib")
#pragma comment (lib,"d3dx10.lib")

E qui gli header.

#include d3d10.h
#include d3dx10.h

Il device, a differenza di DirectX9, non è in grado da solo di generare immagini. Nelle precedenti versioni di DirectX, il device conteneva al suo interno uno spazio di memoria chiamato frontbuffer ma che in alcuni casi era chiamato anche swapchain. Una swapchain gestisce la presentazione della scena. Sarà questa infatti ad occuparsi di mandare ciò che abbiamo preparato sullo schermo. Se con DirectX9 creavamo swapchain per adoperare DirectX su più form o su più schermi, qui dobbiamo crearne uno anche per applicazioni semplici. Da un lato avete il vantaggio di poter gestire in modo identico sia una che 1000 interfacce, dall'altro avete una cosa in più da creare. Lo swapchain è contenuto nell'interfaccia IDXGISwapChain

ID3D10Device* device;
IDXGISwapChain* swapChain;

DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory( &sd, sizeof(sd) );
sd.BufferCount=1;
sd.BufferDesc.Width=width;
sd.BufferDesc.Height=height;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.BufferUsage= DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = handle;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = !fullscreen;
HRESULT hr= D3D10CreateDeviceAndSwapChain( NULL, D3D10_DRIVER_TYPE_HARDWARE, NULL,0,D3D10_SDK_VERSION, &sd,&swapChain ,&device)

Questa funzione crea contemporaneamente sia il device che lo swapchain (li vedete in coda all'istruzione).

Se hr risulta uguale ad S_OK allora la creazione è andata a buon fine. Ora illustrerò un pò in dettaglio questa parzione di codice.

Per prima cosa ho creato una variabile CHAIN_DESC per descrivere il tipo di applicazione che voglio inizializzare. Ho impostato il numero di backbuffer, la zona di memoria in cui la grafica viene depositata in attesa di essere mandata sullo schermo, la larghezza e l'altezza della mia applicazione (che dovrebbe coincidere con il contenuto della finestra su cui si renderizza), il formato di colore (in questo caso R8G8B8A8 indica un formato a 32bit RGB con trasparenza alpha). Altri parametri sono il refresh rate, la velocità di aggiornamento, l'uso che vogliamo fare del buffer (come render target). Il più importante è l'OutputWindow che deve contenere l'handle, unico per ogni finestra e restituito dalla funzione createWindow, per il form su cui vogliamo renderizzare. Infine abbiamo la qualità e la proprietà windowed che ci serve per stabilire se vogliamo una applicazione in finestra (ossia contenuta in un form) o in fullscreen, una modalità in cui Windows viene nascosto e l'intero schermo viene sostituito dalla nostra applicazione. Infine utilizziamo la funzione D3D10CreateDeviceAndSwapChain per creare device e swapchain. Molto utile è il flag D3D10_DRIVER_TYPE_HARDWARE che indica che vogliamo utilizzare la scheda video per il rendering. Usando come impostazione software l'applicazione girerà anche su schede video non compatibili con DirectX10, ma le prestazioni non saranno sufficienti per creare qualcosa di gestibile. Un'altra opzione è D3D10_DRIVER_TYPE_NULL, che ci permette di creare un device che non è in grado di fare rendering a video, utile per utilizzare le classi e le funzioni di DirectX per manipolare file ad esempio. Il primo NULL serve invece ad indicare che vogliamo utilizzare la scheda video di default. Più avanti mostrerò come poterla scegliere.

Con questo codice abbiamo creato il device, il controllore di DirectX10, e lo swap chain che si occupa di mandare a video le nostre immagini. Manca però una cosa importante, il backbuffer. Questa è una zona di memoria in cui DirectX10 lavora. Tutto ciò che andremo a visualizzare verrà elaborato qui dentro. Solo quando sarà pronto lo manderemo a video. La procedura sarà quindi quella di fare operazioni su questo buffer e, quando pronto, mandarlo a video.

ID3D10RenderTargetView* renderTargetView;

ID3D10Texture2D *pBackBuffer;
HRESULT hr= swapChain->GetBuffer( 0, __uuidof( ID3D10Texture2D ), (LPVOID*)&pBackBuffer ) ;
HRESULT hr = device->CreateRenderTargetView( pBackBuffer, NULL, &renderTargetView );
pBackBuffer->Release();

La procedura in questo caso consiste nel farsi dare il backbuffer dallo swapchain e da questo creare un render target. Questo è una zona di memoria collegata al backbuffer e, come dice il nome, è il posto in cui il device farà il rendering. Una volta creato il target possiamo rilasciare il backbuffer. Ora abbiamo i 3 elementi fondamentali per fare il rendering. Ci mandano ancora qualche settaggio e saremo pronti per mandare la prima immagine a video.

device->OMSetRenderTargets( 1, &renderTargetView, NULL);

Con questa istruzione diciamo al device quale target usare (in futuro vi mostrerò come fare il rendering su target diversi o su più target insieme).

Ultima impostazione è il viewport, la zona dello schermo su cui fare il rendering.

D3D10_VIEWPORT vp;
vp.Width = width;
vp.Height = height;
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = 0;
vp.TopLeftY = 0;
device->RSSetViewports( 1, &vp );

Finito, il device è creato. Non preoccupatevi se non è tutto chiaro. Nel prossimo tutorial approfondirò questo argomento.

Ora dobbiamo trovare un punto in cui l'applicazione passa continuamente per poter fare il rendering che, ricordo, viene eseguito centinaia di volte al secondo. Il posto migliore è nel ciclo di gestione dei messaggi di windows.

while( WM_QUIT != msg.message )
{
if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
Render();
}

Render sarà la funzione in cui usare DirectX10. Ora, come promesso, la prima applicazione: rendere il backbuffer di un unico colore e mandarlo a video.

float ClearColor[4] = { 0, 0, 1, 0};
device->ClearRenderTargetView(this->renderTargetView,ClearColor);

swapChain->Present(0,0);

Incredibilmente semplice e veloce. Si crea un array di float con i 4 colori (rosso, verde, blu ed alpha che è la trasparenza) e la si invia alla istruzione clear. L'istruzione Present manderà il backbuffer allo schermo. I valori per i colori vanno da 0 ad 1. Nell'esempio il colore sullo schermo sarà blu. Queste 3 istruzioni andranno inserite nell'istruzione Render in modo da essere eseguite a ripetizione. Con buona probabilità questa applicazione effettuerà almeno 4000 cicli al secondo, 4000 FPS. Un valore destinato a crollare con l'utilizzo delle prime funzioni ma che comunque è indice di una buona qualità.

Quando avrete terminato l'applicazione il codice uscirà dal ciclo while. Qui dovrete distruggere le risorse (in C++ ricordatevi sempre di distruggere le risorse che non usate od occuperete spazio prezioso).

device->ClearState();
renderTargetView->Release();
swapChain->Release();
device->Release();

La memoria è stata liberata, l'applicazione è terminata. Nel prossimo tutorial approfondirò quanto spiegato in questa prima lezione per poi passare ai primi utilizzi. Utilizzate l'help includo nella SDK, a differenza di quello .Net per DirectX9 questo è molto ricco e, almeno per quanto riguarda cambi e proprietà delle interfacce, è molto esaustivo.

Vi lascio all'agognato Demo sui cui studiare e smanettare per capire come funziona DirectX10.

Un piccolo trucco, se premete alt + Invio l'applicazione passerà automaticamente in fullscreen (cosa che richiedeva molto lavoro in DirectX9). Nel prossimo tutorial approndiremo l'inizializzazione del device.

Demo