Wprowadzenie
Poniższy artykuł ma na celu przekonanie młodych programistów gier do DirectX oraz ułatwienie życia tym, którzy zastanawiają się nad przesiadką z OpenGL’a. Przedstawię tutaj parę podstawowych kwestii w programowaniu w DirectX 9 przy okazji pisania swojej własnej kasy do symulacji trybu bezpośredniego znanego z OpenGL.
Implementacja klasy do obsługi buforów wierzchołków
Wydaje mi się, że każdy początkujący programista ma przerażenie w oczach, gdy zobaczy operacje konieczne, aby w ogóle utworzyć bufor wierzchołków, a co dopiero coś narysować na ekranie. Te operacje mogą się początkowo wydawać trudne i trochę niepotrzebne.
Operacje konieczne do zainicjalizowania bufora wierzchołków:
1. Inicjalizacja DirectX 9
2. Utworzenie bufora wierzchołków (g_d3d9->CreateVertexBuffer(…))
3. Ustalenie formatu wierzchołków, które będą przechowywane w buforze
4. Zablokowanie bufora
5. Skopiowanie przygotowanych wierzchołków w odpowiednim formacie do bufora
6. Odblokowanie bufora
Teraz, aby jeszcze cos narysować za pomocą tego bufora konieczne są następujące operacje:
1. Ustawienie źródła skąd będą pobierane dane o wierzchołkach
2. Ustawienie formatu wierzchołków
3. Narysowanie wierzchołków (np. za pomocą g_d3d9->DrawPrimitive(…))
I na koniec po zakończeniu pracy z buforem zwolnienie bufora wierzchołków.
Ufff… aż tyle operacji do zapamiętania dla początkującego w DirectX może być zniechęcające. To nie to samo, co stare dobre glBegin i glEnd. Dlatego, aby uniknąć ewentualnych błędów przy wykonywaniu tych samych czynności zamkniemy je w klasę.
Przykładowa implementacja (musimy wykorzystać szablony, aby nasza klasa była jak najbardziej elastyczna):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
// niezbędne nagłówki #include <windows.h> #include <d3dx9.h> // klasa do obsługi "statycznego" bufora wierzchołków template<class T> class DXVertexBuffer { public: // domyslny konstruktor DXVertexBuffer() {} // utworzymy również konstruktor mogący służyć utworzenie bufora DXVertexBuffer(LPDIRECT3DDEVICE9 device, T* data, int count, UINT format) { // sprawdzenie czy przypadkiem bufor już nie jest utworzony if(vb) Release(); uFormat = format; device->CreateVertexBuffer( count * sizeof(T), 0, format, D3DPOOL_DEFAULT, &vb, NULL); VOID* pVertices; vb->Lock( 0, 0, (void**)&pVertices, 0 ) ; memcpy( pVertices, data, sizeof(T) * count); vb->Unlock(); } // domyślny destruktor, jeśli przypadkiem zapomnieliśmy zwolnić // bufora to zwolni go za nas ~DXVertexBuffer() { if(vb) Release(); } // tworzy bufor void Create(LPDIRECT3DDEVICE9 device, T* data, int count, UINT format) { // sprawdzenie czy przypadkiem bufor już nie jest utworzony // jeśli tak to robimy to jeszcze raz(zabezpieczenie…) if(vb) Release(); uFormat = format; device->CreateVertexBuffer( count * sizeof(T), 0, format, D3DPOOL_DEFAULT, &vb, NULL); VOID* pVertices; vb->Lock( 0, 0, (void**)&pVertices, 0 ) ; memcpy( pVertices, data, sizeof(T) * count); vb->Unlock(); } // renderuje bufor void Render(LPDIRECT3DDEVICE9 device, D3DPRIMITIVETYPE type, int count) { if(!vb) return; device->SetStreamSource( 0, vb, 0, sizeof(T) ); device ->SetFVF( uFormat ); device ->DrawPrimitive( type, 0, count); } // funkcja dostępu do bufora wierzchołków, daje możliwość dynamicznej // zmiany wierzchołków w buforze LPDIRECT3DVERTEXBUFFER9 GetVertexBuffer() { return vb; } // zwalnia bufor wierzchołków void Release() { if(vb) vb->Release(); } private: LPDIRECT3DVERTEXBUFFER9 vb; // bufor wierzcholków UINT uFormat; // format wierzcholków }; |
Kod wydaje mi się, że jest dość przejrzysty. Jeśli czegoś nie rozumiesz polecam zerknąć do odpowiedniej książki o DirectX lub do SDK. Wadą powyższego kodu jest to, że prawie w każdej funkcji konieczne jest przekazywanie wskaźnika do urządzenia DirectX. Można to rozwiązać poprzez utworzenie globalnego obiektu DirectX.
Teraz czas na przedstawienie wykorzystanie tej klasy w praktyce(na podstawie kodu z SDK):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// obiekt urządzenia DirectX 9 LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; // struktura przykładowych wierzchołków struct CUSTOMVERTEX { FLOAT x, y, z, rhw; DWORD color; }; // nasz format wierzcholkow #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE) DXVertexBuffer< CUSTOMVERTEX > g_VertexBuffer; // domyslny konstruktor // inicjalizacja bufora gdzieś w jakiejś funkcji.... CUSTOMVERTEX g_Vertices[] = // przykładowe wierzchołki { { 150.0f, 50.0f, 500.5f, 1.0f, 0x00ff0000, }, { 250.0f, 250.0f, 500.5f, 1.0f, 0x0000ff00, }, { 50.0f, 250.0f, 500.5f, 1.0f, 0x0000ffff, }, }; g_VertexBuffer->Create(pd3dDevice, g_Vertices, 3, D3DFVF_CUSTOMVERTEX); // i teraz użycie w funkcji renderujacej.... g_VertexBuffer->Render(g_pd3dDevice, D3DPT_TRIANGLELIST, 1); // i na koniec zwolnienie bufora g_VertexBuffer->Release(); |
Jak widać użycie naszej klasy znacznie uprościło nam sprawę 😉
Implementacja klasy symulującej glBegin i glEnd
Naszym celem będzie stworzenie odpowiedników tych funkcji w DirectX 9. Na początek przypomnienie dla tych, którzy nie pamiętają jak te funkcje działają:
glBegin(typ_wielokatow);
typ_wielokatow może być np.: GL_TRIANGLES, GL_LINES, GL_QUADS, GL_TRIANGLESTRIP… itp. Typ wielokątów tak naprawdę określa, w jaki sposób maja być interpretowane wierzchołki znajdujące się w bloku glBegin – glEnd.
glEnd() – koniec podawania wierzchołków
Pomiędzy tymi funkcjami umieszcza się instrukcje, np. glVertex2f, glColor3f… itp. Określające poszczególne parametry wierzchołków.
Dla tych, którzy nie rozumieją, co oznaczają poszczególne operacje odsyłam do artykułów wyjaśniających podstawy operacji graficznych w OpenGL’u lub jeśli Cię to API nie interesuje możesz je po prostu zignorować (nie powinno ci to przeszkodzić w zrozumienie pozostałej części artykułu).
Zacznijmy od przedstawienie naszej implementacji w DirectX:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// klasa do obsługi „dynamicznego” bufora wierzchołków template<class T> class DXDirectMode { public: // konstruktor i destruktor DXDirectMode() : vertex_count(0) {} ~DXDirectMode() {} // inicjalizacja “dynamicznego” bufora wierzchołków void Init(LPDIRECT3DDEVICE9 _device, UINT format, int max_vertex = 32) { device = _device; max_count = max_vertex; uFormat = format; data = new T[max_vertex]; device ->CreateVertexBuffer(max_count * sizeof(T), 0, uFormat, D3DPOOL_DEFAULT, &vb, NULL ); } // zwalnia dane bufora wierzchołków void Release() { if(data) delete []data; vb->Release(); } // początek określania listy wierzchołków void Begin(D3DPRIMITIVETYPE _type) { type = _type; } // wstawienie pojedynczego wierzchołka void Vertex(const T& vertex) { data[vertex_count] = vertex; ++vertex_count; } // koniec listy wierzchołków, rysowanie, trzeba niestety podać // ilość wielokątów do wyswietlenia(rzecz która można poprawić) void End(int primitive_count) { // kopiujemy wierzcholki do bufora VOID* pVertices; vb->Lock( 0, 0, (void**)&pVertices, D3DLOCK_DISCARD) ; memcpy( pVertices, data, sizeof(T) * vertex_count); vb->Unlock(); device ->SetStreamSource(0, vb, 0, sizeof(T)); device ->SetFVF(uFormat); device ->DrawPrimitive(type, 0, primitive_count); // zerujemy ilosc wierzchokow do narysowania vertex_count = 0; } private: int max_count; // maksymalna liczba wierzcholkow int vertex_count; // obecna liczba wierzchołków T* data; // dane wierzcholkow LPDIRECT3DDEVICE9 device; // urządzenie D3D, można usunąć singletona LPDIRECT3DVERTEXBUFFER9 vb; // bufor wierzchołków D3DPRIMITIVETYPE type; // typ wyświetlania wierzchołków UINT uFormat; // format wierzchołków }; |
Powyższa klasa jest dość prosta i z pewnością ma wiele niedoskonałości. Chociaż osobiście nie widzę zbyt wielu możliwości na jej rozbudowę. Jej implementacja stawia jednak pewne wymaganie odnośnie struktury przechowującej wierzchołki:
1. Klasa reprezentująca wierzchołek musi mieć publiczny konstruktor domyślny
2. Dla wygody przydałby się również konstruktor do inicjalizacji wszystkich składowych klasy jakimiś wartościami
Dlaczego, jest tak a nie inaczej postaram się przedstawić na poniższym przykładzie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// to ten sam przykład co powyżej tylko z małymi zmianami LPDIRECT3DDEVICE8 g_pd3dDevice = NULL; // struktura przykładowych wierzchołków struct CUSTOMVERTEX { FLOAT x, y, z, rhw; DWORD color; // konstruktor domyślny CUSTOMVERTEX() {} // konstrukor do inicjalizacji wartosciami wierzchołka CUSTOMVERTEX(FLOAT _x, FLOAT _y, FLOAT _z, FLOAT _rhw, DWORD color) : x(_x), y(_y), z(_z), rhw(_rhw), color(_color) {} }; // nasz format wierzcholkow #define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZRHW|D3DFVF_DIFFUSE) DXDirectMode<CUSTOMVERTEX> g_DirectMode; // podczas inicjalizacji programu, korzystamy z domyślnej maksymalnej // ilości wierzchołków bloku begin - end g_DirectMode.Init(g_pd3dDevice, D3DFVF_CUSTOMVERTEX); // w tym miejscu rysujemy nasza scenę // do wykorzystanie tej klasy polecam napisać makro #define VERTEX(x, y, z, rhw, color) g_DirectMode->Vertex(CUSTOMVERTEX(x, y, z, rhw, color)); // i teraz podajemy wierzchołki g_DirectMode.Begin(); VERTEX(150.0f, 50.0f, 500.5f, 1.0f, 0x00ff0000); VERTEX(250.0f, 250.0f, 500.5f, 1.0f, 0x0000ff00); VERTEX( 50.0f, 250.0f, 500.5f, 1.0f, 0x0000ffff); // podajemy ilos wielokątów do narysowania g_DirectMode.End(1); // i na koniec g_DirectMode.Release(); |
Powyższy przykład spowoduje narysowanie na ekranie takiego samego trójkącika jak ten narysowany powyżej z „zwykłego” bufora wierzchołków.
Od Autora
Mam nadzieję, że powyższy artykuł okaże się dla kogoś przydatny. Wszelkie pytania i uwagi proszę kierować na adres: gosuwachu@o2.pl
Autor: Wachu