Lekcja 34. Height Map
Autor: Stanisław Michał 'black_maq' Warych
Oryginał: Height Mapping (Jeff Molofee & Ben Humphrey)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson34.zip

Witamy w kolejnym ekscytującym tutorialu! Twórcą kodu źródłowego do tego tutorialu jest Ben Humphrey i bazuje na GL Framework z lekcji 1. Lecz teraz powinieneś już być ekspertem OpenGL i przenoszenie kodu do twojego własnego powinno być błyskawiczne.

Ten tutorial nauczy Cię jak zrobić dobrze wyglądający teren z height mapy. Pewnie nie masz pojęcia czym jest height mapa. Spróbuję Ci to wytłumaczyć w prosty sposób. Height mapa jest po prostu... wyparciem, wzniesieniem z powierzchni. Niektórzy z Was pewnie teraz pytają sami siebie "o czym ten gość gada?!"... po polsku height mapa prezentuje wysokie i niskie punkty naszego widoku. Do Ciebie należy decyzja, który odcień prezentuje wysokie punkty, a który niskie. Warto także zapamiętać, że height mapy wcale nie muszą powstawać z obrazków, mogą być utworzone z różnych typów danych. Na przykład możesz użyć strumienia audio i za pomocą height mapy stworzyć jego wizualną prezentację. Wciąż jesteś skołowany? Czytaj dalej, wszystko nabierze sensu w trakcie czytania.

#include <windows.h>         // Nagłówek windows
#include <stdio.h>         // Nagłówek standardowych operacji wejścia/wyjścia ( NOWE )
#include <gl\gl.h>         // Nagłówek biblioteki OpenGL32
#include <gl\glu.h>         // Nagłówek biblioteki glu32
#include <gl\glaux.h>         // Nagłówek biblioteki GLaux
#pragma comment(lib, "OpenGL32.lib")         // Podlinkowanie biblioteki OpenGL32
#pragma comment(lib, "glu32.lib")         // Podlinkowanie biblioteki Glu32

Zaczniemy od zdefiniowania kilku ważnych zmiennych. MAP_SIZE jest rozmiarem naszej mapy. W tym tutorialu mapa ma wymiary 1024x1024. STEP_SIZE jest rozmiarem każdego kwadratu jakiego użyjemy do rysowania widoku. Zmniejszając tą wartość sprawiamy, że widok staje się bardziej wygładzony. Trzeba pamiętać, że im mniejszy rozmiar tym więcej operacji program będzie musiał wykonać (należy wybrać między jakością, a szybkością) szczególnie, gdy używamy dużych height map. HEIGHT_RATIO złuży do skalowania widoku przez oś Y, niskie wartości powodują, że wzniesienia są spłaszczone. Wyższe wartości robią wzniesienia bardziej wyraziste (wyostrzone).

Dalej w kodzie zobaczysz bRender. Jeżeli bRender jest ustawione na TRUE (standardowo), to będziemy rysować pełne wielokąty. Jeżeli będzie FALSE to będą rysowane tylko linie łączące.

#define        MAP_SIZE    1024         // Rozmiar naszej Height Mapy (w formacie .RAW)    ( NOWE )
#define        STEP_SIZE    16         // Wysokośc i szerokość każdego kwadratu    ( NOWE )
#define        HEIGHT_RATIO    1.5f         // Wartość skalowania przez oś Y    ( NOWE )
HDC        hDC=NULL;         // Kontekst urządzenia
HGLRC        hRC=NULL;         // Kontekst rysujący
HWND        hWnd=NULL;         // Uchwyt okna
HINSTANCE    hInstance;         // Instancja aplikacji
bool        keys[256];         // Tablica do obługi klawiatury
bool        active=TRUE;         // Aktywność okna ustawiona na TRUE
bool        fullscreen=TRUE;         // Tryb pełnoekranowy ustawiony na TRUE
bool        bRender = TRUE;         // Typ rysowania    ( NOWE )

Tutaj zrobimy tablicę bajtów (g_HeightMap[]) do przetrzymywania danych naszej height mapy. Plik .RAW przechowuje wartości od 0 do 255, możemy użyć tych wartości jako wysokości, gdzie 255 będzie najwyższym punktem, a 0 najniższym. Tworzymy także zmienną scaleValue do skalowania całej sceny. Daje to możliwości przybliżania i oddalania widoku.

BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];         // Przechowuje dane Height Mapy    ( NOWE )
float scaleValue = 0.15f;         // Skala widoku    ( NOWE )
LRESULT    CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);         // Deklaracja WndProc

Funkcja ReSizeGLScene() jest taka sama jak w lekcji pierwszej, tylko dalszy dystans został zmieniony z 100.0f do 500.0f.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)         // Zmienia wielkość i inicjalizuje okno OpenGL
{
... WYTNIJ ...
}

Następna część kodu wczytuje plik .RAW. Niezbyt skomplikowane ! Otwieramy plik w trybie read binary. Następnie upewniamy się, że plik istnieje i został otwarty. Jeżeli wystąpił jakikolwiek błąd to wyświetlamy stosowny komunikat.

        // Ładuje plik .RAW i przechowuje w pHeightMap
void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap)
{
    FILE *pFile = NULL;
        // Otwiera plik w trybie read binary
    pFile = fopen( strName, "rb" );
        // Check To See If We Found The File And Could Open It
    if ( pFile == NULL )    
    {
        // Wyświetla komunikat błędu i kończy funkcję
        MessageBox(NULL, "Nie odnalazłem Height Mapy!", "Error", MB_OK);
        return;
    }

Jeżeli nie było problemów z otwarciem pliku to idziemy dalej. Teraz wczytamy dane. Wykorzystamy fukcję fread(). pHeightMap jest przechowalnią dla naszych danych (jest wskaźnikiem na tablicę g_Heightmap). wczytywanie zaczynamy od pierwszego bitu, kończąc na maksymalnym - nSize (rozmiar to wysokość * szerokość). Na końcu pFile jest wskaźnikiem do struktury naszego pliku.

Po wczytywaniu sprawdzamy czy wystąpiły błędy. Wynik przechowujemy w zmiennej result i ją sprawdzamy, jeżeli błąd miał miejsce to informujemy o tym użytkownika. Ostatnią rzeczą jaką robimy to zamknięcie pliku za pomocą fclose(pFile).

        // Ładujemy cały plik .RAW do tablicy pHeightMap (od 1 do nSize = width*height)
    fread( pHeightMap, 1, nSize, pFile );
        // Po wczytaniu danych warto sprawdzić czy wszystko było OK
    int result = ferror( pFile );
        // Sprawdza czy wystąpił błąd
    if (result)
    {
        MessageBox(NULL, "Błąd w odczycie danych!", "Error", MB_OK);
    }
        // Zamyka plik
    fclose(pFile);
}

Kod inicjalizujący jest całkiem prosty. Ustawiamy kolor tła na czarny, włączamy depth testing, wygładzanie wielokątów (ang. polygon smoothing) itd. Po wykonaniu tego wszystkiego ładujemy nasz plik .RAW, aby to zrobić podajemy ścieżkę do naszego pliku ("Data/Terrain.raw"), jego rozmiary (MAP_SIZE * MAP_SIZE), i tablice danych (g_HeightMap) dp funkcji LoadRawFile(). Plik .RAW zostanie wczytany i jego dane będą zapisane w tablicy (g_HeightMap).

int InitGL(GLvoid)         // Ustawienia OpenGL
{
    glShadeModel(GL_SMOOTH);         // Włącza gładkie cieniowanie (ang. Smooth Shading)
    glClearColor(0.0f, 0.0f, 0.0f, 0.5f);         // Czarne tło
    glClearDepth(1.0f);         // Ustawienia bufora głebi(ang. Depth Buffer)
    glEnable(GL_DEPTH_TEST);         // Włącza testowanie głębi
    glDepthFunc(GL_LEQUAL);         // Typ testowania głębi
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);         // Ustawienia perspektywy
        // Wczytujemy height mapę z pliku .RAW u mieszczamy ją
        // w tablicy g_HeightMap. Podajemy takźe rozmiar naszego pliku (1024).
    LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap);         // ( NOWE )
    return TRUE;         // Inicjalizacja przebiegła bez problemów
}

Zawsze, gdy mamy do czynienia z tablicy chcemy mieć pewność, że nie wykroczymy poza nie. Aby mieć pewność, że to się nie zdaży używamy % (tłum. reszta z dzielenia). % powstrzyma wartości x, y przez przekraczaniem MAX_SIZE - 1.

Upewaniamy się, że nasze dane są w porządku, jeśli nie, zwracamy 0.

Inaczej zwracamy wartości x, y w naszej height mapie. Teraz powinieneś wiedzieć, że musimy pomnożyć y przez MAP_SIZE aby poruszać się wśród danych. Więcej o tym poniżej!

int Height(BYTE *pHeightMap, int X, int Y)         // Zwraca wysokość z indeksu height mapy
{
    int x = X % MAP_SIZE;         // Sprawdzenie wartości x
    int y = Y % MAP_SIZE;         // Sprawdzenie wartości y
    if(!pHeightMap) return 0;         // Sprawdzamy poprawnośc danych

Musimy traktować pojedynczą tablicę jak tablicę 2D. Możemy użyć równania indeks = (x + (y * arrayWidth) ). To zakłada, że reprezentujemy to jako: pHeightMap[x][y], inaczej to jest przeciwieńtwo, czyli: (y + (x * arrayWidth) ).

Teraz mamy poprawny indeks i zwrócimy wysokość (dane pod x, y w naszej tablicy).

    return pHeightMap[x + (y * MAP_SIZE)];         // Zwraca wysokość z naszej tablicy
}    

Ustalamy tutaj kolor punktu bazując na jego wysokości. Żeby zrobić to ciemniejsze zacząłem od -0.15f. Wartość otzrymujemy dzieląc wysokość przez 256.0f. Jeżeli nie ma danych to funckja się kończy bez ustalenia koloru. Jeśli wszystko jest ok to ustawiamy odcień niebieskiego używając glColor3f(0.0f, fColor, 0.0f), jeżeli interesują Cię inne kolory niż niebieski to pozmieniaj miejsce zmiennej fColor.

void SetVertexColor(BYTE *pHeightMap, int x, int y)         // Ustawia wartość koloru dla poszczególnych indeksów
{         // w zależności od wysokości
    if(!pHeightMap) return;         // Upewniamy się, że dane są w porządku
    float fColor = -0.15f + (Height(pHeightMap, x, y ) / 256.0f);
        // Przypisujemy odcień niebieskiego do aktualnego wierzchołka
    glColor3f(0.0f, 0.0f, fColor );
}    

To jest kod, który aktualnie rysuje nasz teren. Zmienne x, y będą użyte w pętli do przejścia przez dane mapy i będą użyte do rysowania kwadratów.

Jak zwykle sprawdzamy poprawność danych, jeżeli jest błąd do kończymy nie robiąc nic.

void RenderHeightMap(BYTE pHeightMap[])         // Rysuje Height Mapę jako kwadraty
{
    int X = 0, Y = 0;         // Tworzymy zmienne do poruszania się po tablicach
    int x, y, z;         // Tworzymy zmienne dla odczytywalności
    if(!pHeightMap) return;         // Upewniamy się, że dane są w OK    

Sprawdzamy zmienną bRender, jeżeli ma wartość TRUE to rysujemy całe wielokąty, jeżeli ma wartość FALSE to rysujemy linie.

    if(bRender)         // Co chcemy rysować ?
        glBegin( GL_QUADS );         // Wielokąty
    else
        glBegin( GL_LINES );         // Linie

Następnie będziemy rysować teren z height mapy. Aby to zrobić po prostu przejdziemy przez naszą tablicę danych, będziemy wyciągać wysokości aby nanieść nasze punkty. Gdybyśmy mogli zobaczyć to zdarzenie to najpierw zostałyby narysowane kolumny (Y), a następnie wiersze. Zauważ, że mamy STEP_SIZE. To określa jak jest sprecyzowana nasza height mapa. Im większa wartość tym teren jest bardziej "blokowy". Gdy jest mniejsza wartość to teren jest bardziej wygładzony. Jeśli ustawimy STEP_SIZE = 1 to będzie tworzył wierzchołek dla każdego piksela height mapy. Wybrałem 16 jako przyzwoity rozmiar. Każda za niska wartość będzie przykładem szaleństwa i wszystko będzie powolne. Oczywiśćie możesz zwiększyć wartość, gdy dodasz oświetlenie. Wtedy oświetlenie przysłoni klocowate kształty. Zamiast oświetlenia daliśmy kolor skojarzony z każdym wielokątem, aby uprościć lekcję. Im wyżej jest wielokąt tym jaśniejszy jest kolor.

    for ( X = 0; X < (MAP_SIZE-STEP_SIZE); X += STEP_SIZE )
        for ( Y = 0; Y < (MAP_SIZE-STEP_SIZE); Y += STEP_SIZE )
        {
        // Bierzemy wartości (X, Y, Z) dla dolnego lewego wierzchołka
            x = X;                            
            y = Height(pHeightMap, X, Y );    
            z = Y;
        // Ustalamy kolor dla aktualnego wierzchołka
            SetVertexColor(pHeightMap, x, z);
            glVertex3i(x, y, z);         // Rysujemy wierzchołek
        // Bierzemy wartości (X, Y, Z) dla górnego lewego wierzchołka
            x = X;                                        
            y = Height(pHeightMap, X, Y + STEP_SIZE );
            z = Y + STEP_SIZE ;
        // Ustalamy kolor dla aktualnego wierzchołka
            SetVertexColor(pHeightMap, x, z);
            glVertex3i(x, y, z);         // Rysujemy wierzchołek
        // Bierzemy wartośći (X, Y, Z) dla górnego prawego wierzchołka
            x = X + STEP_SIZE;
            y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );
            z = Y + STEP_SIZE ;
        // Ustalamy kolor dla aktualnego wierzchołka
            SetVertexColor(pHeightMap, x, z);
            glVertex3i(x, y, z);         // Rysujemty wierzchołek
        // Bierzemy wartośći (X, Y, Z) dla dolnego prawego wierzchołka
            x = X + STEP_SIZE;
            y = Height(pHeightMap, X + STEP_SIZE, Y );
            z = Y;
        // Ustalamy kolor dla aktualnego wierzchołka
            SetVertexColor(pHeightMap, x, z);
            glVertex3i(x, y, z);         // Rysujemy wierzchołek
        }
    glEnd();

Po skończonym rysowaniu ustawiamy kolor tła na jasny biały z ustawionym kanałem alpha na 1.0f. Jeżeli były jakiekolwiek inne obiekty na ekranie to nie chcemy ich mieć na niebiesko :D

    glColor4f(1.0f, 1.0f, 1.0f, 1.0f);         // Resetujemy kolor
}

Dla tych z Was, którzy nie używali gluLookAt(), co to robi ? pozycja kamery, widoku, i wektor widza. Tutaj ustawiamy kamerę w zaciemnionym obszarze, aby widok na nasz teren był wyraźny. W celu unikania używania tak wysokich numerów podzielimy wierzchołki terenu przez stałą scale jak poniżej za pomocą glScalef().

Wartościami gluLookAt() są w kolejności: pierwsze numery prezentują, gdzie kamera się znajduje. Więc pierwsze trzy wartości przenoszą naszą kamerę 212 jednostek na osi X, 60 jednostek na osi Y oraz 194 jednostki na osi Z ze środka. Następne trzy wartości prezentują, gdzie kamera ma się patrzeć. W tej lekcji, zauważysz podczas oglądania dema, że patrzymy lekko na lewo. Patrzymy także na dół, na nasz teren. 186 z 212 dają nam widok w lewo natomiast 55 jest mniejsze od 60 stąd mamy wrażenie patrzenia w dół pod lekkim kątem. Wartość 171 wyznacza jak daleko znajdujemy się od obiektu. Ostatnie trzy wartości mówią jakie kierunki nas interesują. W naszych górach interesuje nas oś Y i ustawiamy ją na 1, reszte ustawiamy na 0.

gluLookAt może wydawać się bardzo straszne, gdy używasz tego pierwszy raz. Pomimo przeczytania różnych wyjaśnień wciąż

możesz być zdezorientowany. Najelpszą moją radą jest to abyś po porstu eksperymentował z różnymi wartościami. Na

przykład możesz zmienić wysokość kamery na 120, wtedy zobaczysz teren bardziej od góry, ponieważ punkt na, który

patrzysz wciąż ma wartość 55.

Nie wiem czy to pomoże, ale podam jeden z moich życiowych przykładów :). Powiedzmy, że masz wysokość 6 stóp i trochę (tłum. 1 stopa angielska ~ 30,479 cm, czyli, że masz około 182,874 cm wzrostu + trochę) i załóżmy, że twoje oczy są na wysokości 6 stóp (twoje oczy reprezentują kamerę - 6 stóp reprezentuje 6 jednostek na osi Y). Teraz stoisz naprzeciwko muru, który ma 2 stopy wysokości (2 jednostki na osi Y), więc jak się patrzysz na mur to patrzysz się w DÓŁ i widzisz górę muru. Jeśli mur miałby 8 stóp wysokości to patrzyłbyś w górę i nie widziałbyś góry muru. Widok się zmienia w zależności czy patrzysz w górę czy w dół(czy jesteś wyższy czy niższy od obiektu, na który patrzysz). Mam nadzieję, że to zaczyna nabierać sensu dla Ciebie.

Po skończonym rysowaniu ustawiamy kolor tła na jasny biały z ustawionym kanałem alpha na 1.0f. Jeżeli były jakiekolwiek inne obiekty na ekranie to nie chcemy ich mieć na niebiesko :D

int DrawGLScene(GLvoid)         // To jest funckja rysująca
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // Czyści ekran i bufor głębi
    glLoadIdentity();         // Resetuje macierz
        //      Pozycja     Widok        Wektor
    gluLookAt(212, 60, 194, 186, 55, 171, 0, 1, 0);         // Ustala pozycję kamery i widok

Teraz skalujemy nasz teren, łatwiej się będzie oglądać i nie będzie za duży. Możemy zmienić wartość scaleValue za pomocą strzałek GóRA/Dół na klawiaturze. Zauważysz, że mnożymy Y scaleValue przez HEIGHT_RATIO. Dzięki temu teren wydaje się wyższy i jest bardziej wyrazisty.

    glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);

Jeśli przekazaliśmy dane g_HeightMap funkcji RenderHeightMap() to narysuje teren w kwadratach. Jeżeli zmiaerzasz wykorzystać tą funkcję to dobrym pomysłem będzie dodanie wartośći X, Y do rysowania albo po prostu użyć operacji na macierzach OpenGL (glTranslatef(), glRotate() itd.) aby umieścić teren dokładnie tam gdzie chcesz.

    RenderHeightMap(g_HeightMap);         // Rysuje Height Mapę
    return TRUE;         // Zwraca TRUE
}

Funkcja KillGLWindow() jest taka sama jak w lekcji 1.

GLvoid KillGLWindow(GLvoid)         // Prawidłowo zniszczy okno
{
}

Funkcja CreateGLWindow() jest taka sama jak w lekcji 1.

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
}

Jedyną zmianą w WndProc() jest dodanie WM_BUTTONDOWN. To sprawdza czy lewy przycisk myszy został naciśnięty. Jeśli był, to sposób rysowania został zmieniony z polygon mode na line mode.

LRESULT CALLBACK WndProc(    HWND    hWnd,         // Uchwyt okna
                UINT    uMsg,         // Komunikat dla tego okna
                WPARAM    wParam,         // Dodatkowe parametry wiadomości
                LPARAM    lParam)         // Dodatkowe parametry wiadomości
{
    switch (uMsg)         // Sprawdza komunikaty okna
    {
        case WM_ACTIVATE:         // Czeka na komunikat aktywności okna
        {
            if (!HIWORD(wParam))         // Sprawdza czy jest zminimalizowane
            {
                active=TRUE;         // Program jest aktywny
            }
            else
            {
                active=FALSE;         // Program nie jest aktywny
            }
            return 0;         // Powrót do pętli
        }
        case WM_SYSCOMMAND:         // Przechwytuje komendy systemowe
        {
            switch (wParam)         // Sprawdza wywołania systemowe
            {
                case SC_SCREENSAVE:         // Wygaszacz ekranu
                case SC_MONITORPOWER:         // Monitor chce włączyć tryb oszczędzania energii
                return 0;         // Powstrzymanie zdarzenia
            }
            break;         // Wyjście
        }
        case WM_CLOSE:         // Otrzymaliśmy komunikat o zakończeniu?
        {
            PostQuitMessage(0);         // Wysyłamy komunikat Quit
            return 0;         // Powrót
        }
        case WM_LBUTTONDOWN:         // Lewy przycisk myszy wciśnięty?
        {
            bRender = !bRender;         // Zmiana sposobu rysowania
            return 0;         // Powrót
        }
        case WM_KEYDOWN:         // Klawisz został wciśnięty?
        {
            keys[wParam] = TRUE;         // Jeśli tak to ustawiamy na TRUE
            return 0;         // Powrót
        }
        case WM_KEYUP:         // Klawisz został zwolniony?
        {
            keys[wParam] = FALSE;         // Jeśli tak to ustawiamy na FALSE
            return 0;         // Powrót
        }
        case WM_SIZE:         // Zmiana wielkości okna OpenGL
        {
            ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));         // LoWord=szerokość, HiWord=wysokość
            return 0;         // Powrót
        }
    }
        // Przekazuje niewychwycone komunikaty do DefWindowProc
    return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

Nie ma większych zmian w tej części kodu. Jedyną zmianą jest zmiana nazwy okna. Wszystko jest tak samo aż do momentu sprawdzania klawiszy.

int WINAPI WinMain(    HINSTANCE    hInstance,         // Instancja
            HINSTANCE    hPrevInstance,         // Poprzednia instancja
            LPSTR        lpCmdLine,         // Parametry linii poleceń
            int        nCmdShow)         // Stan wyświetlania okna
{
    MSG        msg;         // Struktura komunikatów okna
    BOOL    done=FALSE;         // Zmienna BOOL to zakończenia pętli
        // Pytamy użytkownika, który tryb woli
    if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
    {
        fullscreen=FALSE;         // Tryb okienkowy
    }
        // Create Our OpenGL Window
    if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial", 640, 480, 16, fullscreen))
    {
        return 0;         // Wychodzimy jeśli okno nie zostało utworzone
    }
    while(!done)         // Pętla działa dopóki done=FALSE
    {
        if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))         // Mamy komunikat?
        {
            if (msg.message==WM_QUIT)         // Otrzymaliśmy wiadomość o zakończeniu ?
            {
                done=TRUE;         // Jeśli tak to done=TRUE
            }
            else         // Jeśli nie to działamy z komunikatami okna
            {
                TranslateMessage(&msg);         // Przetwarza komunikat
                DispatchMessage(&msg);         // Wysyła komunikat
            }
        }
        else         // Jeśli nie ma wiadomości
        {
        // Rysujemy scenę. Sprawdzamy klawisz ESC i komunikaty z DrawGLScene()
            if ((active && !DrawGLScene()) || keys[VK_ESCAPE])         // Okno nie aktywne? Koniec pracy?
            {
                done=TRUE;         // Dotarł komunikat o zakończeniu działania
            }
            else if (active)         // Jeśli nie jeszcze nie kończymy pracy
            {
                SwapBuffers(hDC);         // Przełącza bufory (podwójne buforowanie)
            }
            if (keys[VK_F1])         // F1 nacisnięty?
            {
                keys[VK_F1]=FALSE;         // Jeśli tak ustawiamy zmienną na FALSE
                KillGLWindow();         // Niszczy aktualne okno
                fullscreen=!fullscreen;         // Przełącza z pełnego ekranu na tryb okienkowy
        // Odtwarza nasze okno OpenGL
                if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial", 640, 480, 16, fullscreen))
                {
                    return 0;         // Kończy jeżeli okno nie zostało utworzone
                }
            }

Kod poniżej pozwala kierować wartością scaleValue za pomocą strzałek. Naciskając strzałkę w górę - wartość rośnie, a strzałkę w dół - wartość maleje i widok się oddala.

            if (keys[VK_UP])         // Jeśli strzałka w GÓRĘ naciśnięta
                scaleValue += 0.001f;         // Zwiększa wartość
            if (keys[VK_DOWN])         // Jeśli strzałka w DÓŁ naciśnięta
                scaleValue -= 0.001f;         // Zmniejsza wartość
        }
    }
        // Wyłącz
    KillGLWindow();         // Niszczy okno
    return (msg.wParam);         // Kończy program
}

WSzystko na temat tworzenia pięknego widoku z height mapy. Mam nadzieję, że doceniacie pracę Ben-a. Jak zawsze, jeśli znaleźliście błędy w tekście lub kodzie to skontaktujcie się ze mną a ja postaram się to naprawić.

Więc rozumiesz jak kod działa, pobaw się nim trochę. Jedną z rzeczy, które możesz zrobić jest dodanie malutkiej piłki toczącej się wszerz powierzchni. Znasz wysokość każdej części terenu więc dodanie piłki nie powinno być problemem. Inną rzeczą jaką możesz zrobić jest zrobienie ręcznie height mapy. Zrób przewijany widok, dodaj kolory do reprezentowania ośnieżonych szczytów, wody itd. dodaj tekstury, użyj efektu plazmy do stworzenia stale zmiennego widoku. Możliwości są nieskończone :)

Mam nadzieję, że lekcja Ci się podobała, możesz odwiedzić stronę Ben'a http://www.GameTutorials.com