Wizualny OpenGL (III)

W poprzedniej części warsztatu utworzyliśmy model terenu na podstawie mapy wysokości zapisanej jako zwykła bitmapa. Tym razem skupimy się na realizmie scenerii w najprostszym symulatorze lotu.

W poprzedniej części warsztatu utworzyliśmy model terenu na podstawie mapy wysokości zapisanej jako zwykła bitmapa. Tym razem skupimy się na realizmie scenerii w najprostszym symulatorze lotu.

Program symulujący sterowanie latającym pojazdem musi zawierać kilka niezbędnych elementów. Przede wszystkim powinien generować realistyczny obraz terenu widzianego z kokpitu samolotu. Gra wymaga także podstawowego wykrywania kolizji. W naszym prostym symulatorze lotu nie będziemy naśladować prawdziwego funkcjonowania samolotu, uwzględniając zasady aerodynamiki. Również udostępnione graczowi sterowanie przypominać będzie bardziej gry FPP niż prawdziwe oprzyrządowanie.

Do utworzenia własnego symulatora lotu w ramach naszego warsztatu niezbędne będzie środowisko programistyczne Microsoft Visual C++ 6 (lub nowsze) oraz przynajmniej podstawowa znajomość języka C++ i biblioteki OpenGL.

Generowanie i wyświetlanie terenu

W poprzedniej części warsztatu opisaliśmy prosty sposób generowania realistycznego modelu terenu na podstawie mapy wysokości, zapisanej jako zwykły plik graficzny w formacie BMP. Sposób ten wykorzystamy w "symulatorze lotu".

Aby nasz prosty symulator choć trochę przypominał prawdziwą grę, wprowadzimy pewne ograniczenia ruchu. W porządnym symulatorze zderzenie z powierzchnią terenu powinno kończyć rozgrywkę, jednak w naszym przypadku kamera zostanie po prostu ustawiona nieco ponad bieżącym fragmentem mapy. Zamiast efektownego wybuchu, "samolot" ślizgać się będzie po powierzchni terenu. Wygenerowany teren rozpięty jest - w poziomie - między punktem (0,0) a (ROZMIAR_X*skala_xy, ROZMIAR_Y*skala_xy), gdzie ROZMIAR_X i ROZMIAR_Y oznaczają rozmiary mapy wysokości, a skala_xy jest współczynnikiem skalowania, za pomocą którego z mapy wysokości tworzymy właściwy teren. Jeśli te współczynniki wydają się niejasne, radzimy zajrzeć do poprzedniej części warsztatu (znajdziesz ją na naszych krążkach). W praktyce należy zadbać, aby kamera nie tylko nie przekraczała granic mapy, ale nawet zanadto się do nich nie zbliżała, dlatego funkcja ustalająca położenie i kierunek patrzenia kamery, UstawKamere(), zostanie wzbogacona o następujące polecenia:

// sprawdź, czy kamera nie przesunęła się poza mapę

if(PozycjaX < skala_xy)

PozycjaX = skala_xy;

if(PozycjaX > skala_xy*(ROZMIAR_X-2))

PozycjaX = skala_xy*(ROZMIAR_X-2);

if(PozycjaY < skala_xy)

PozycjaY = skala_xy;

if(PozycjaY > skala_xy*(ROZMIAR_Y-2))

PozycjaY = skala_xy*(ROZMIAR_Y-2);

// sprawdź, czy kamera nie przesunęła się poniżej

// powierzchni terenu

int i = (int)(PozycjaX/skala_xy);

int j = (int)(PozycjaY/skala_xy);

if(PozycjaZ<teren[i][j]+k)

PozycjaZ = teren[i][j]+k);

Wektor normalny n obliczany jest jako iloczyn wektorowy wektorów v i w.

Wektor normalny n obliczany jest jako iloczyn wektorowy wektorów v i w.

Zwróćmy uwagę na sposób sprawdzania, czy kamera nie przesunęła się poniżej powierzchni terenu. W wyniku dzielenia PozycjaX/skala_xy i zaokrąglenia otrzymanej wartości w dół do najbliższej liczby całkowitej (które tutaj otrzymaliśmy przez zrzutowanie do typu int) otrzymujemy indeks i tablicy z mapą wysokości. W analogicznym działaniu uzyskujemy współczynnik j. Teraz wystarczy porównać wartość PozycjaZ oraz teren[i][j]. Tajemniczy współczynnik k oznacza niewielkie przesunięcie kamery powyżej powierzchni terenu. Współczynnik ten najlepiej dobrać doświadczalnie.

Zajmiemy się teraz implementacją bardziej efektownego cieniowania terenu. W poprzedniej części warsztatu użyty został bardzo prosty sposób określania koloru danego fragmentu terenu, w którym po prostu sprawdzana była wysokość punktu na mapie. Im wyżej, tym punkt jaśniejszy.

Takie cieniowanie można zaakceptować na etapie testowania mapy, ale nawet najprostszy symulator lotu powinien używać nieco bardziej zaawansowanych technik.

Zamiast określać wprost kolor kolejnego wierzchołka, podamy wartość wektora normalnego do powierzchni terenu w tym punkcie. Wektor normalny (lub inaczej: normalna) wskazuje kierunek prostopadły do powierzchni. OpenGL, na podstawie wektora normalnego i kierunku padania światła, oblicza właściwe oświetlenie danego punktu, oczywiście z uwzględnieniem tekstury, koloru podstawowego, przezroczystości itd.

Często oblicza się wyłącznie wektory normalne do wielokątów tworzących model (w naszym przypadku - trójkątów budujących powierzchnię terenu). Takie rozwiązanie daje jednak nie najlepszy efekt - każdy z wielokątów jest cieniowany równomiernie, a model przypomina bardziej figurę geometryczną niż np. powierzchnię lądu. Znacznie lepszy efekt uzyskamy, podając wektor normalny do powierzchni terenu dla każdego wierzchołka danego trójkąta. Wtedy każdy punkt danego trójkąta otrzyma kolor obliczony na podstawie kolorów wierzchołków, odległości od tych wierzchołków i danych o materiale (np. teksturze). Ten sposób cieniowania to tzw. vertex shading, cieniowanie wierzchołkowe - spotykane w większości gier 3D, obecnie bardzo powoli wypierane przez cieniowanie punktowe (cieniowanie Phonga, pixel shading), obsługiwane przez najnowsze karty graficzne. Wartość wektora normalnego dodamy do tworzonego terenu za pomocą funkcji

glNormal3fv(normalne[i][j]);

Tablica float normalne[ROZMIAR_X][ROZMIAR_Y][3] przechowuje wartość wektorów normalnych dla każdego punktu mapy terenu. Tablica jest wypełniana raz, podczas wczytywania mapy wysokości.

W funkcji rysującej powierzchnię terenu warto zwrócić uwagę na sposób podawania współrzędnych tekstury, oparty na współrzędnych [i,j] punktu na mapie wysokości. Dzielenie tych współrzędnych przez liczbę 20 zostało dobrane doświadczalnie - tak, aby teksturowana powierzchnia prezentowała się jak najlepiej. W poważniejszych aplikacjach wszystkie informacje o sposobie nakładania tekstury nie są, oczywiście, zapisywane w kodzie programu, lecz przechowywane wraz z danymi o scenie 3D (patrz listing obok).


Zobacz również