Modele 3D, czyli żegnaj boxie.
Hej!
Muszę się z czegoś zwierzyć. Pisząc dwie poprzednie części tego tutoriala dostawałem białej gorączki. Denerwowałem się okrutnie, a męczyłem się przez dobre parę minut – nad czym? Nad wymyśleniem współrzędnych, a potem koordynatów tekstur boxa, którego sobie rysowaliśmy, a miał on ich jedynie 8 zestawów. Przeciętny model w przeciętnej grze ma ich między 150 a 15000! Również graficy zarabiają często więcej niż my, programiści. Dlatego dzisiaj poznamy nową, lepszą metodę definiowana wierzchołków naszych siatek – a mianowicie, wczytamy je z pliku.
Opowiedzmy więc trochę o tym, jakie pliki i jak będziemy wczytywać. Za czasów DirectX 2 mądrzy ludzie z Microsoftu wymyślili, iż D3DX będzie posiadał “własny” format siatek. Przez parę lat zyskał on wiele na elastyczności i zawartości, dzisiaj wspierając animację szkieletową, instancje shaderów, podział na submeshe, zestawy texcoordów, oraz wiele innych, ciekawych rzeczy. Jest to format .x, którego to będziemy używać w tej i następnych lekcjach.
Zobaczmy kod, który się pojawia:
1: ID3DXMesh* pMesh;
2: IDirect3DTexture9** pMeshTex = NULL;
3: DWORD numMaterials;
4:
5: void CreateMesh(char* path)
6: {
7: D3DVERTEXELEMENT9 VertexDecl[] =
8: {
9: {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
10: {0, 12, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
11: D3DDECL_END()
12: };
13:
14: LPD3DXBUFFER materialsBuffer;
15: D3DXLoadMeshFromX(path, D3DXMESH_MANAGED, pDev, NULL,
16: &materialsBuffer, NULL, &numMaterials, &pMesh);
17:
18: pMesh->CloneMesh(D3DXMESH_MANAGED, VertexDecl, pDev, &pMesh);
19:
20: D3DXMATERIAL* pMeshMaterials = (D3DXMATERIAL*)materialsBuffer->GetBufferPointer();
21: pMeshTex = new LPDIRECT3DTEXTURE9[numMaterials];
22:
23: for( DWORD i = 0; i < numMaterials; i++ )
24: {
25: if (FAILED(D3DXCreateTextureFromFile(pDev,
26: pMeshMaterials[i].pTextureFilename, &pMeshTex[i])))
27: pMeshTex[i] = NULL;
28: }
29: materialsBuffer->Release();
30: }
Widzimy deklaracje paru zmiennych, w tym naszej zmiennej reprezentującej siatkę – ID3DXMesh, tablicy wskaźników na tekstury, oraz ilość materiałów. Dalej, definiujemy funkcję wczytującą siatkę z pliku. Zacznijmy więc od góry:
1: D3DVERTEXELEMENT9 VertexDecl[] =
2: {
3: {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
4: {0, 12, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
5: D3DDECL_END()
6: };
Ten dziwny twór, to tak zwana Vertex Declaration. Jest to nowocześniejszy odpowiednik FVF, o którym wiemy już co nieco. Składnia deklarowania VD jest następująca:
1: { Strumien, Offset, Rozmiar, SposobDeklarowania, SemantykaUzycia, IndeksSemantyki },
“Strumien” to numer strumienia (Stream), którym przesyłane będą dane. Karty Shader Model 2 wspierają jedynie 1 strumień przekazywania wierzchołków (o indeksie 0), więc narazie możemy spokojnie wpisywać tam null. Drugi parametr to przesunięcie w bajtach względem początku wierzchołka, trzeci to sposób, w jaki ma zostać zadeklarowany dany parametr (typowo D3DDECLMETHOD_DEFAULT, jest to najczęściej używana flaga). “SemantykaUzycia” to poprostu odpowiednik HLSL-owej Semantyki. W przykładzie używamy Usages: Position oraz Texcoord. Vertex Declaration kończymy zaś makrem D3DDECL_END().
1: LPD3DXBUFFER materialsBuffer;
2: D3DXLoadMeshFromX(path, D3DXMESH_MANAGED, pDev, NULL,
3: &materialsBuffer, NULL, &numMaterials, &pMesh);
To wywołanie nie jest skomplikowane. Tworzy ono obiekt o interface ID3DXMesh z pliku .x. Pierwszym parametrem jest ścieżka do pliku, drugim – flagi typu D3DXMESH. W tym przypadku używamy flagi D3DXMESH_MANAGED, która oznacza, iż mamy utworzyć naszą siatkę w pamięci zarządzanej przez urządzenie Direct3D i my nie musimy się martwić zarządzaniem takim zasobem. Kolejnym parametrem jest obiekt urządzenia, a następnie taki twór jak Adjacency Buffer – co to i do czego służy przekonamy się być może w przyszłych lekcjach, narazie zostawiamy null. Zmienna materialsBuffer, którą tworzymy w kolejnym parametrze oznacza bufor materiałów siatki – materiały to takie twory, które określają teksturę i kolor powierzchni, do której są przypisane. Kolejny parametr (NULL) to tablica plików efektów – zazwyczaj jednak jest to rzadko używany parametr. My z resztą sami zarządzamy naszymi shaderami, więc z czystym sumieniem zostawiamy ten parametr pusty.
1: pMesh->CloneMesh(D3DXMESH_MANAGED, VertexDecl, pDev, &pMesh);
Zadeklarowaliśmy sobie piękne Vertex Declaration, całe nawet omówiliśmy, ale w żadnym wywołaniu jak dotąd nie “powiedzieliśmy” buforom wierzchołków obiektu ID3DXMesh, że mamy zamiar używać takiego-a-takiego sposobu ułożenia danych! Dlatego też musimy to zrobić. Służy temu metoda CloneMesh interfejsu ID3DXMesh. Tworzy ona nową siatkę, na bazie starej oraz podanego VD. Niestety, jeśli stworzymy nowe pola (takie, których jeszcze nie ma w starej siatce, a które chcielibyśmy mieć wypełnione), musimy wypełnić je sami. Jeśli zaś pola sobie odpowiadają, sprawa jest prosta i CloneMesh ładnie sobie z tym radzi.
1: D3DXMATERIAL* pMeshMaterials = (D3DXMATERIAL*)materialsBuffer->GetBufferPointer();
2: pMeshTex = new LPDIRECT3DTEXTURE9[numMaterials];
3:
4: for( DWORD i = 0; i < numMaterials; i++ )
5: {
6: if (FAILED(D3DXCreateTextureFromFile(pDev,
7: pMeshMaterials[i].pTextureFilename, &pMeshTex[i])))
8: pMeshTex[i] = NULL;
9: }
10: materialsBuffer->Release();
Dalej sprawa jest trywialna. Tworzymy tablicę materiałów, a następnie tablicę tekstur. Potem wypełniamy tablicę tekstur, w pętli. Tekstury wczytujemy znaną nam funkcją D3DXCreateTextureFromFile, nazwę tekstury pobieramy z materiału o odpowiednim indeksie. Na koniec “sprzątamy” po sobie.
Ostatnim krokiem jest wyrenderowanie naszego modelu 3D:
1: D3DXMATRIX matRot, matTrans, matWorld;
2: D3DXMatrixTranslation(&matTrans, 0,0,5);
3: D3DXMatrixRotationAxis(&matRot, &D3DXVECTOR3(1,1,0), angle);
4:
5: matWorld = matRot * matTrans;
6:
7: D3DXMATRIX mat_worldViewProj = matWorld * MatView * MatProj;
8: pEffect->SetMatrix("worldViewProj", &mat_worldViewProj);
9:
10: UINT passes;
11: D3DXHANDLE tech;
12: pEffect->FindNextValidTechnique(0, &tech);
13: pEffect->SetTechnique(tech);
14: pEffect->Begin(&passes,0);
15: for (UINT pass = 0; pass < passes; pass++)
16: {
17: pEffect->BeginPass(pass);
18:
19: for (int i = 0; i < numMaterials; i++)
20: {
21: pEffect->SetTexture("tex0", pMeshTex[i]);
22: pEffect->CommitChanges();
23: pMesh->DrawSubset(i);
24: }
25:
26: pEffect->EndPass();
27: }
28: pEffect->End();
Na początek obracamy nasz model wzdłuż osi (1,1,0), dzięki funkcji D3DXMatrixRotationAxis, następnie ustawiamy wszystko tak jak w poprzednich przykładach. Nowością natomiast jest sposób renderowania:
1: for (int i = 0; i < numMaterials; i++)
2: {
3: pEffect->SetTexture("tex0", pMeshTex[i]);
4: pEffect->CommitChanges();
5: pMesh->DrawSubset(i);
6: }
Na początku iterujemy się po wszystkich materiałach. Ustawiamy teksturę, wywołaniem SetTexture. Nowością natomiast jest metoda CommitChanges ID3DXEffect. Służy ona do wprowadzania w życie stanów zmienionych między wywołaniami pEffect->Begin() a pEffect->End. Metoda pMesh->DrawSubset(.) zaś odpowiada za narysowanie konkretnego subseta (submesha) siatki, Subset to taki fragment modelu, do którego przypisany jest jeden, konkretny materiał (tekstura), przy czym indeks materiału odpowiada odpowiedniemu indeksowi subsetu.
Dzisiejsza lekcja (wraz z kodami źródłowymi) była dosyć prosta i szybka, a jednak ważna – w końcu nasz przykładowy samolocik z Direct3D SDK wygląda znacznie lepiej niż prosty box!

shader.fx – nie chce się załadować… :( błąd wywala