Rysowanie w prawdziwym 3D!

Hej!

Trójkąty które rysowaliśmy w poprzednich lekcjach są fajne, jednak nie aż tak, by nie czuć niedosytu – 3D to 3D, co nie? Dlatego dzisiaj zajmiemy się renderingiem prawdziwych bryłek w prawdziwym trójwymiarze. Poznamy przy tym parę nowych pojęć i porządny kawał świata grafiki 3D. Więc zapnimy pasy i do roboty!

W ostatniej lekcji tworzyliśmy trójkąt, który obracał się, przetestowaliśmy również zasadę działania cullingu. Tę lekcję więc zaczniemy od utworzenia siatki sześcianu – w taki sam sposób jak poprzednio trójkąta – oraz spróbujemy go wyrenderować.

   1: OurVertex verts[] =
   2: {
   3:     { -1.0f,-1.0f,-1.0f, 0xffff8888 }, 
   4:     { -1.0f, 1.0f,-1.0f, 0xff88ff88 }, 
   5:     {  1.0f,-1.0f,-1.0f, 0xff8888ff }, 
   6:     {  1.0f, 1.0f,-1.0f, 0xff8888ff }, 
   7:     {  1.0f,-1.0f, 1.0f, 0xff88ff88 }, 
   8:     {  1.0f, 1.0f, 1.0f, 0xffff8888 }, 
   9:     { -1.0f,-1.0f, 1.0f, 0xff88ff88 }, 
  10:     { -1.0f, 1.0f, 1.0f, 0xff8888ff }, 
  11:     { -1.0f,-1.0f,-1.0f, 0xff8888ff }, 
  12:     { -1.0f, 1.0f,-1.0f, 0xff88ff88 },
  13: };
  14:  
  15: pDev->CreateVertexBuffer(sizeof(verts), 0, OURVERT_FVF, D3DPOOL_DEFAULT, &pVB, 0);
  16:  
  17: void* data;
  18:  
  19: pVB->Lock(0, sizeof(verts), &data, 0);
  20:  
  21: memcpy(data, (void*)verts, sizeof(verts));
  22:  
  23: pVB->Unlock();
  24:  
  25: pDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);

Darowaliśmy sobie tworzenie górnej i dolnej ściany boxa, zostawiliśmy boczne – 8 wierzchołków. Do tego używamu cullingu counterclockwise. Narazie wszystko jest zrozumiałe, więc zobaczmy rendering:

   1: pEffect->Begin(&passes,0);
   2: for (UINT pass = 0; pass < passes; pass++)
   3: {
   4:     pEffect->BeginPass(pass);
   5:     
   6:     pDev->SetFVF(OURVERT_FVF);
   7:     pDev->SetStreamSource(0, pVB, 0, sizeof(OurVertex));
   8:     pDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 8);
   9:  
  10:     pEffect->EndPass();
  11: }
  12: pEffect->End();

Zmieniło się jedynie wywołanie metody DrawPrimitive. Ostatni parametr, jak wiemy, oznacza ilość trójkątów które rysujemy, drugi parametr to przesunięcie od początku VB, od którego zaczynamy rysować. Nas natomiast teraz szczególnie interesuje parametr pierwszy – D3DPT_TRIANGLESTRIP. Mówiliśmy już wcześniej, że jest to typ prymitywów jaki rysujemy, dziś zajmiemy się tym troszkę dokładniej.

image 

Obrazek ten prezentuje sposób działania flagi D3DPT_TRIANGLELIST. Oznacza ona, że każdy trójkąt opisują dokłanie 3 wierzchołki; na obrazku numer każdego wierzchołka jest oznaczony takim samym kolorem jak trójkąt. Widać wyraźnie, że pewne wierzchołki są wspólne dla nawet trzech trójkątów – to wielkie marnotractwo pamięci i czasu karty. Dlatego wymyślono coś takiego jak paski trójkątów – zasadza działania na obrazku poniżej:

image 

Pasek trójkątów powoduje, że każdy następny trójkąt dzieli dwa ostatnie wierzchołki z poprzednim. Dzięki temu, każdy kolejny trójkąt wymaga tylko jednego dodatkowego wierzchołka! W ciągu kolejnych lekcji natomiast poznamy inny, bardziej nowoczesny i używany w dzisiejszych czasach praktycznie zawsze sposób rysowania naszych prymitywów – indeksowane listy.

Tymczasem, obejrzmy efekt naszej dotychczasowej pracy:

image

Wygląda nieźle! Mamy bryłkę 3D, która się obraca. Ale spróbujmy wykonać eksperyment – narysujemy jeszcze jedną bryłkę, taką samą, zaraz za tą i zobaczymy co się stanie (a przy okazji poćwiczymy posługiwanie się macierzami).

   1: void RysujBrylke()
   2: {
   3:     UINT passes;
   4:     D3DXHANDLE tech;
   5:     pEffect->FindNextValidTechnique(0, &tech);
   6:     pEffect->SetTechnique(tech);
   7:     pEffect->Begin(&passes,0);
   8:     for (UINT pass = 0; pass < passes; pass++)
   9:     {
  10:         pEffect->BeginPass(pass);
  11:  
  12:         pDev->SetFVF(OURVERT_FVF);
  13:         pDev->SetStreamSource(0, pVB, 0, sizeof(OurVertex));
  14:         pDev->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 8);
  15:  
  16:         pEffect->EndPass();
  17:     }
  18:     pEffect->End();
  19: }
  20:  
  21: ///////////////////  Oraz w petli renderujacej: //////////////
  22:  
  23: pDev->Clear(0, 0, D3DCLEAR_TARGET, 0xff000000, 1, 0);
  24: pDev->BeginScene();
  25:  
  26: static float angle = 0.0f;
  27: angle += 0.01f;
  28:  
  29: // Obracamy pierwsza brylke...
  30: D3DXMATRIX matWorldFirst;
  31: D3DXMatrixRotationY(&matWorldFirst, angle);
  32:  
  33: D3DXMATRIX mat_worldViewProj = matWorldFirst * MatView * MatProj; 
  34: pEffect->SetMatrix("worldViewProj", &mat_worldViewProj);
  35:  
  36: // ... i ja rysujemy:
  37: RysujBrylke();
  38:  
  39: // Teraz obracamy i przesuwamy druga brylke:
  40: D3DXMATRIX rotation, translate, matWorldSec;
  41:  
  42: D3DXMatrixRotationZ(&rotation, angle);
  43: D3DXMatrixTranslation(&translate, 0.1f, 0.1f, 5);
  44: matWorldSec = rotation * translate;
  45:  
  46: mat_worldViewProj = matWorldSec * MatView * MatProj; 
  47: pEffect->SetMatrix("worldViewProj", &mat_worldViewProj);
  48:  
  49: // te tez rysujemy:
  50: RysujBrylke();
  51:  
  52: pDev->EndScene();
  53: pDev->Present(0, 0, 0, 0);

Utworzyliśmy sobie pomocniczą funkcję rysującą nam naszą bryłkę, pierwszą bryłkę zostawiliśmy bez zmian, drugą zaś obracamy wzdłuż osi Z, następnie przesuwamy kawałek w bok, kawałek w górę oraz 5 jednostek do tył. Przekształcenia składamy oczywiście w prawidłowej kolejności (SRT – Scale, Rotate, Translate). Zobaczmy co się brzydkiego stało:

image

Jak widzimy, coś jest nie tak – w końcu bryłka będąca z przodu powinna zasłaniać tę, będącą z tył! Tymczasem to ta tylna wybija nam się na przedni plan. Cóż na to zaradzić? Będziemy potrzebowali trochę wiedzy na temat działania naszej karty graficznej.

Mianowicie, prawda oczywista – karta jest głupia. Nie wie ona, co ma być z przodu a co z tył. Powiemy – nasza wina, mogliśmy rysować nasze boxy od końca, algorytmem malarza – czyli to co ma być na wierzchu, rysujemy ostatnie – jak malarz obraz (dociekliwy może spróbować zmienić kolejność renderowania bryłek – ciekawe zadanie domowe). Jednak ta technika się nie sprawdza dla bardziej skomplikowanych scen, a nawet mniej trywialnych modeli – dlatego mądre głowy wiele lat temu wymyśliły coś takiego, jak Z-Buffer (bufor głębi). Jest to taka tekstura, o rozmiarach bufora ramki, którą tworzymy sobie razem z urządzeniem Direct3D. Bufor Z w każdej klatce jest czyszczony na wartość 1 (maksymalna głębia), a następnie z wartością przechowywaną w nim porównywana jest głębia aktualnie rysowanego piksela. Jeśli jest mniejsza niż ta w buforze, jest w nim zapisywana. Również kolor takiego piksela trafia na bufor ramki. Jeśli zaś wartość z rysowanego piksela jest większa niż ta w buforze głębi, nie dzieje się nic – tym sposobem nie musimy stosować algorytmu malarza, życie wydaje się proste i wszyscy są szczęśliwi.

Obejrzmy więc kod, który pomoże nam załączyć bufor głębi:

   1: D3DPRESENT_PARAMETERS d3dpp;
   2: ZeroMemory(&d3dpp, sizeof(d3dpp));
   3: d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
   4: d3dpp.Windowed = true;
   5: d3dpp.BackBufferFormat = D3DFMT_X8R8G8B8;
   6: // Zalaczanie zbuffora:
   7: d3dpp.EnableAutoDepthStencil = true;
   8: d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
   9:  
  10: pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, 
  11:     D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &pDev);
  12:  
  13: pDev->SetRenderState(D3DRS_ZENABLE, true);

Oto zmiany, które musimy wprowadzić przy tworzeniu urządzenia Direct3D. Linijka

   1: d3dpp.EnableAutoDepthStencil = true;

oznacza, iż pozwalamy urządzeniu zająć się tworzeniem bufora głębi (automatycznie – ono robi to zaraz – EnableAutoDepth..). Możemy zrobić to ręcznie, co również przećwiczymy w innych lekcjach. Kolejna linijka pozwala nam wybrać format, w jakim przechowywane będą dane w buforze Z. Tutaj wybieramy format D3DFMT_D16 – 16 bitów na bufor Z. Są dostępne inne formaty, zawierające również w sobie miejsce na bufor matrycy – Stencil Buffer, o którym powiemy sobie przy innej okazji. Ostatnim krokiem na tym etapie jest powiadomienie urządzenia, że ma właściwie włączyć ten cały bufor głębi prostym wywołaniem SetRenderState.

Wspominałem również, że Z-buffer czyszczony jest na początku każdej klatki. Dzieje się to w znanej już nam metodzie urządzenia – Clear:

   1: pDev->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
   2:             0xff000000, 1, 0);

Flagi D3DCLEAR_TARGET nie muszę tłumaczyć. Flaga D3DCLEAR_ZBUFFER to właśnie ta flaga, która pozwala nam wyczyścić bufor głębi na wartość z przedostatniego parametru – 1 – a więc maksymalną głębię („nieskończenie” głęboko).

Tak więc, oto dzisiejsza lekcja. W miarę długa i dosyć trudna, ale ważna – mam nadzieję, że zrozumienie jej nie sprawi kłopotów. Dołączone oczywiście kody źródłowe oraz exe. W samych kodach zmienione zostało lekko przesunięcie drugiej bryłki i jej oś obrotu, celem pokazania prawidłowego działania całości.

image

Do pobrania: Kody źródłowe


Jedna odpowiedź to “Rysowanie w prawdziwym 3D!”

  1. Wyskakuje błąd, że niby plik konfiguracyjny jest niezgodny. Badając kod źródłowy nie znalazłem niczego podejrzanego…

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Log Out / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Log Out / Zmień )

Facebook photo

Komentujesz korzystając z konta Facebook. Log Out / Zmień )

Google+ photo

Komentujesz korzystając z konta Google+. Log Out / Zmień )

Connecting to %s

 
%d bloggers like this: