DirectX: Labirynt 3D od podstaw

15.03.2010 - Adam Błaszkiewicz
TrudnośćTrudność

Z ramki po lewej stronie można pobrać dotychczasowy projekt.

Wierzchołki i tekstury

Będziemy używać paru funkcji i typów zdefiniowanych w D3DX, na przykład macierzy i wczytywania tekstur z plików. Musimy więc dołączyć odpowiedni plik nagłówkowy:

1
#include <d3dx9.h>

Oprócz tego, musimy dołączyć do projektu plik d3dx9.lib. Robimy to tak samo, jak poprzednio. Dopisujemy nazwę nowego pliku, oddzielając ją spacją od poprzedniej:

   Pokaż/ukryj demonstrację
   Pokaż/ukryj demonstrację

Aby utworzyć nasz pierwszy trójkąt, będziemy potrzebowali bufora wierzchołków. Będziemy chcieli nałożyć na niego teksturę. Potrzebujemy do tego dwóch kolejnych interfejsów, do których wskaźniki będziemy pamiętać globalnie:

1
2
IDirect3DVertexBuffer9  *pVB = 0;
IDirect3DTexture9       *pTexture = 0;

Utwórzmy też w tym miejscu zmienną typu HRESULT, której będziemy często chwilowo używać do sprawdzania wyniku wywołań funkcji:

1
HRESULT hr;

Następnie utworzymy strukturę wierzchołka wraz z jej opisem dla Direct3D (FVF - Flexible Vertex Format). Wierzchołkami w postaci tej struktury będziemy wypełniać bufor wierzchołków.

1
2
3
4
5
6
7
8
9
struct Vertex
{
    FLOAT x, y, z;
    FLOAT u, v;
};
#define FVF_VERTEX ( D3DFVF_XYZ | D3DFVF_TEX1 )
 
// Zapiszmy sobie, ile wierzchołków będziemy renderować:
int num_v = 3;

Flaga D3DFVF_XYZ mówi Direct3D, że w strukturze wierzchołka znajdują się jego współrzędne x, y, z. D3DFVF_TEX1 mówi, że wierzchołek przechowuje współrzędne jednej tekstury. Pola w strukturze muszą być ułożone w odpowiedniej kolejności. Zmiana kolejności flag D3DFVF oczywiście nie przynosi żadnych efektów - jest to zwykła operacja "lub" na bitach. Kolejność danych w strukturze wierzchołka jest z góry ustalona.

Przejdźmy do funkcji obsługi komunikatów okna i usuńmy całkowicie obsługę zdarzenia WM_PAINT. Będziemy renderowali kolejne klatki animacji nieustannie, więc zdarzenie to nie będzie nam potrzebne.

W miejscu, w którym utworzyliśmy już urządzenie Direct3D (najlepiej jeszcze przed ShowWindow), możemy utworzyć nasz bufor wierzchołków. Robimy to za pomocą metody CreateVertexBuffer interfejsu D3DDevice. Przekazujemy jej między innymi zdefiniowany przez nas FVF, opisujący naszą strukturę wierzchołka. Gotowy bufor wypełniamy wierzchołkami, po uprzednim zablokowaniu go za pomocą metody Lock. Następnie, bufor należy odblokować poprzez Unlock.

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
hr = pD3DDevice->CreateVertexBuffer(
    num_v * sizeof( Vertex ),
    0,
    FVF_VERTEX,
    D3DPOOL_DEFAULT,
    &pVB,
    NULL );
if( FAILED( hr ) ) goto WinMain_end;
 
Vertex* pVertices;
hr = pVB->Lock( 0, 0, ( void** )&pVertices, 0 );
if( FAILED( hr ) ) goto WinMain_end;
 
pVertices[0].x = -1.0f;
pVertices[0].y = 0.0f;
pVertices[0].z = 0.0f;
pVertices[0].u = 0.0f;
pVertices[0].v = 0.0f;
 
pVertices[1].x = 1.0f;
pVertices[1].y = 0.0f;
pVertices[1].z = 0.0f;
pVertices[1].u = 1.0f;
pVertices[1].v = 0.0f;
 
pVertices[2].x = 0.0f;
pVertices[2].y = 1.73f;
pVertices[2].z = 0.0f;
pVertices[2].u = 0.5f;
pVertices[2].v = 1.0f;
 
pVB->Unlock();

Lock daje nam wskaźnik do miejsca w pamięci (tutaj zapisywany jest do pVertices), gdzie powinniśmy zapisać tablicę naszych wierzchołków.

Następnie ustawimy następujące parametry:

1
2
3
pD3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );
pD3DDevice->SetRenderState( D3DRS_LIGHTING, FALSE );
pD3DDevice->SetRenderState( D3DRS_ZENABLE, TRUE );

D3DRS_CULLMODE określa, z której strony renderowane trójkąty mają być widoczne. Ustawienie D3DCULL_NONE mówi, że mają być widoczne z obu stron. D3DRS_LIGNTING określa, czy używamy oświetlenia. D3DRS_ZENABLE mówi Direct3D, czy chcemy używać bufora głębi. Bufor ten jest używany do zasłaniania trójkątów znajdujących się dalej od kamery przez te, które znajdują się bliżej.

Stworzymy także teksturę z pliku "tekstura.bmp". Plik ten musimy umieścić w katalogu z naszą grą. Jego wymiary powinny być potęgą dwójki - np. 512x512.

1
2
D3DXCreateTextureFromFile(
    pD3DDevice, _T("tekstura.bmp"), &pTexture );

Następnie, zmodyfikujemy główną pętlę programu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MSG msg;
for(;;)
{
    if( !PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
    {
        Render();
        continue;
    }
        
    if(msg.message == WM_QUIT)
        break;
 
    TranslateMessage( &msg );
    DispatchMessage( &msg );
}

Zamieniliśmy tu funkcję GetMessage na PeekMessage. Ta pierwsza zatrzymuje wykonywanie programu do czasu, aż do programu przybędzie jakiś komunikat. Natomiast PeekMessage, nawet w przypadku braku komunikatów, nie wstrzymuje wykonywania. Zwraca prawdę, jeśli pobrano nowy komunikat. W powyższym przykładzie, jeśli program nie otrzymał żadnego komunikatu, wywoływana jest funkcja Render, którą za chwilę napiszemy. W przeciwnym wypadku komunikat jest przetwarzany.

Pamiętajmy także o zwolnieniu nowych interfejsów:

1
2
if( pTexture ) pTexture->Release();
if( pVB ) pVB->Release();

Powinniśmy to zrobić przed zwolnieniem D3DDevice i D3D.

Pozostało nam napisanie funkcji renderującej.

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
void Render()
{
    // Czyszczenie ekranu i bufora głębi:
    pD3DDevice->Clear(
        0,
        NULL,
        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
        D3DCOLOR_XRGB( 50, 40, 30 ),
        1.0f,
        0 );
 
    hr = pD3DDevice->BeginScene();
    if( FAILED( hr ) )
        return;
 
    static int rotation;
 
    // Macierz świata:
    D3DXMATRIXA16 matWorld;
    D3DXMatrixIdentity( &matWorld );
    D3DXMatrixRotationY( &matWorld, ++rotation / 200.0f );
    pD3DDevice->SetTransform( D3DTS_WORLD, &matWorld );
 
    // Macierz widoku:
    D3DXMATRIXA16 matView;
    D3DXVECTOR3 vEyePt( 0.0f, 1.0f,-5.0f );
    D3DXVECTOR3 vLookatPt( 0.0f, 0.0f, 0.0f );
    D3DXVECTOR3 vUpVec( 0.0f, 1.0f, 0.0f );
    D3DXMatrixLookAtLH( &matView, &vEyePt, &vLookatPt, &vUpVec );
    pD3DDevice->SetTransform( D3DTS_VIEW, &matView );
 
    // Macierz projekcji:
    D3DXMATRIXA16 matProj;
    D3DXMatrixPerspectiveFovLH(
        &matProj, D3DX_PI / 3.0f, 1.0f, 1.0f, 100.0f );
    pD3DDevice->SetTransform( D3DTS_PROJECTION, &matProj );
 
    // Wybór tekstury:
    pD3DDevice->SetTexture( 0, pTexture );
 
    // Wybór bufora wierzchołków:
    pD3DDevice->SetStreamSource( 0, pVB, 0, sizeof( Vertex ) );
 
    // Wybór używanej struktury wierzchołka:
    pD3DDevice->SetFVF( FVF_VERTEX );
 
    // Rysowanie trójkątów:
    pD3DDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, num_v/3 );
 
    pD3DDevice->EndScene();
    pD3DDevice->Present( NULL, NULL, NULL, NULL );
}

W Direct3D powinniśmy ustawić trzy główne macierze, tak, jak zrobiliśmy to powyżej. Macierz świata transformuje wszystkie wierzchołki na scenie. W naszym przypadku jest to macierz identycznościowa obrócona wokół osi Y o zmienny kąt. Kolejną macierzą jest macierz widoku, która określa pozycję i obrót kamery, czyli punktu, z którego widzimy scenę. Używamy tu funkcji obliczającej macierz na podstawie współrzędnych kamery, współrzędnych punktu, w którego kierunku kamera jest skierowana i wektora wskazującego kierunek, który jest dla kamery kierunkiem w górę. Ostatnia macierz to macierz projekcji, która przekształca trójwymiarowe współrzędne wierzchołka na jego dwuwymiarowe współrzędne na ekranie.

W prawdziwym programie wierzchołki są transformowane przez dużo innych macierzy (a właściwie macierze najpierw mnożone są przez siebie, a następnie grupy wierzchołków są przemnażane przez tylko jedną, połączoną macierz).

Po pomnożeniu wierzchołków przez macierz świata i macierz widoku, ich współrzędne są takie, jakby kamera była umieszczona w punkcie 0,0,0 i skierowana wzdłuż rosnącej osi Z. Wtedy wystarczy użyć macierzy projekcji, aby rzutować punkty na dwuwymiarowy ekran.

Aby wyrenderować trójkąt w powyższym kodzie, wybieramy odpowiednią teksturę, bufor wierzchołków i FVF, po czym wywołujemy DrawPrimitive. Funkcja ta jako argument dostaje między innymi liczbę trójkątów, które ma wyrenderować - w naszym przypadku num_v/3.

Spróbujmy uruchomić nasz program (pamiętając o umieszczeniu tekstury "tekstura.bmp" w katalogu z projektem) i pozmieniać niektóre wartości w kodzie, takie jak kąt widzenia kamery, który ustawiamy jako argument funkcji D3DXMatrixPerspectiveFovLH - D3DX_PI / 3.0f, czyli 60 stopni.

W drugiej części tego tutoriala zajmiemy się już właściwym labiryntem.

Dotychczasowy kod źródłowy można pobrać z ramki po lewej.

4.666665
Twoja ocena: Brak Ocena: 4.7 (6 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com