Lekcja 1. Ustawianie okna pod OpenGL
Autor: Herion
Oryginał: Setting Up An OpenGL Window (Jeff 'NeHe' Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson01.zip

Witam na moich kursach OpenGL. Jestem przeciętnym gościem z pasją OpenGL. Pierwszy raz usłyszałem o OpenGL kiedy 3Dfx wydało swoje pierwsze akceleratory OpenGL dla Voodoo1. Poczułem, że OpenGL jest czymś, czego muszę się nauczyć. Niestety, nie mogłem znaleźć żadnych informacji w książkach i necie. Spędziłem wiele godzin aby sprawić żeby kod zaczął działać. Mailowałem i Ircowałem szukając pomocy. Ludzie znający OpenGL uważali się za elitę i nie chcieli dzielić się informacjami. To było bardzo frustrujące.

Stworzyłem tą stronę dla ludzi zainteresowanych OpenGL'em, aby mogli tu przyjść i zaczerpnąć pomocy, jeśli będą jej potrzebować. W każdym swoim tutorialu staram się wyjaśniać wszystko tak dokładnie jak to tylko możliwe. Staram się aby mój kod był prosty (żadnego MFC!) Kompletny nowicjusz w Visual C++ i OpenGL będzie w stanie zrozumieć ten kod. Jest to strona jakich wiele. Jeśli jesteś hardcorowym programistą OpenGL, to może ona być dla ciebie za prosta.

Ten tutorial został napisany na nowo w styczniu 2000. Nauczy cię otwierać puste okno GL w pełnym ekranie lub okienku. Kod jest bardzo elsatyczny, więc bedziesz mógł go używać w swoich projektach. Wszystkie moje kursy są bazowane na tym właśnie kodzie!. Stworzyłem kod elastyczny i potężny zarazem. Wszystkie błędy zostały wykryte. Nie ma wycieków pamięci, kod jest łatwy w modyfikacji. Podziękowania dla Fredryka Echolsa za modyfikacje.

Zacznę ten kurs od wskoczenia w kod. Pierwszą rzeczą do zrobienia jest projekt w Visual C++. Jeśli nie umiesz go utworzyć, to nie powinieneś się uczyć OpenGL (czyt. Wynocha). Kilka wersji Visual C++ wymaga zmiany "bool" na BOOL, true na TRUE i false na FALSE. Kod był kompilowany na VC 4, VC 5 i VC 6 bez problemu.

Kiedy stworzysz nowy projekt Win32 (NIE konsola) w VC++, zalinkuj biblioteki OpenGL. W VC przejdź do Project->Settings->LINK. Pod "objekt/Library Modules" na początku linii (przed kernel32.lib) dopisz: opengl32.lib, glu32.lib i galux.lib. Kiedy to zrobisz, kliknij OK i jesteś gotowy do pisania :)

UWAGA #1: Kilka kompilatorów nie ma zdefiniowanego CDS_FULLSCREEN, jeśli otrzymasz error o tym, dodaj gdzieś na początku programu: #define CDS_FULLSCREEN 4.

UWAGA #2: Kilka kompilatorów nie posiada biblioteki GLAUX. Dograj ją sobie z działu download.

Pierwsze 4 linie kodu to dołączanie bibliotek, których używamy. Spójrz:

#include <windows.h>         // nagłówek dla windowsa
#include <gl\gl.h>         // nagłówek dla opengl32
#include <gl\glu.h>         // nagłowek dla glu32
#include <gl\glaux.h>         // nagłówek dla glaux

Następnie musisz stworzyć wszystkie zmienne które zamierzasz użyć w programie. Program będzie tworzyć czyste okno OpenGL, więcc wiele tych zmiennych nie będzie ;) Te zmienne są bardzo ważne, ponieważ każdy program OpenGL ich używa.

Najpierw ustawiamy Kontekst Renderowania. Każdy program OpenGL jest połączony z kontekstem renderowania. To jest takie coś, co łączy polecenia OpenGL z kontekstem urządzenia. Kontekst renderowania jest zdefniowany jako hRC ((handle) Rendering Context). Aby rysować po oknie twój program potrzebuje kontekstu urządzenia (druga linia). Ten kontekst zdefiniowany jest jako hDC (Device Context). hDC łączy okno z GDI (interfejs graficzny). hRC łączy OpenGL z hDC.

Trzecia linia to zmienna hWnd - uchwyt okna podpięty pod nasze okno. Ostatnia tworzy instancję naszego programu.

HGLRC hRC=NULL;         // kontekst renderownia
HDC hDC=NULL;         // kontekst graficzny
HWND hWnd=NULL;         // uchwyt okna
HINSTANCE hInstance;         // instancja aplikacji

Dalej. Pierwsza linia to tablica która będzie prechowywać stan wciśniętych klawiszy. Jest wiele sposobów na pobranie stanu klawiatury, ale ten sposób wybrałem ja. Jest dobry, bo może przechowywać więcej klawiszy niż jeden.

Zmienna active będzie używana do sprawdzania czy program nie jest zminimalizowany.

Zmienna fullscreen mówi sama za siebie. Jeśli będzie true, ekran będzie w trybie pełnoekranowym. Jeśli zaś false, będzie on w trybie okienkowym. Ważne jest aby ta zmienna była widziana przez wszystkie funkcje w programie.

bool keys[256];         // Stan klawiszy
bool active=TRUE;         // flaga zminimalizowania, domyślnie na true
bool fullscreen=TRUE;         // tryb pełnoekranowy. domyślnie na true

Teraz musimy zadeklarować WndProc(). Powodem jest to, że CreateGLWindow() ma odwołanie do WndProc() a, WndProc przychodzi po kodzie CreateWindow(). Robimy to ze względu na język C.

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

Zadaniem następnej sekcji kodu jest zmiana rozmiaru sceny OpenGL, wtedy gdy okno zmieni rozmiar (w trybie okienkowym). Kiedy rozciągniesz okno, będzie ustawiać perspektywę do rozmiarów okna.

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height==0)         // zapobiegnij dzieleniu przez zero...
{
height=1;         // ...ustawiając liczbę 1
}
glViewport(0, 0, width, height);         // zresetuj pole widzenia

Następne linie ustawiają ekran dla widoku perspektywicznego. Znaczy to, że to co jest dalej, jest mniejsze. Stworzy to realistyczniejszą scenę. Perspektywa obliczana jest z 45 stopniowym widokiem bazowanym na rozmiarze okna. 0.1f i 100.0f są to początkowe i końcowe punkty zasięgu widzenia.

glMatrixMode(GL_PROJECTION) oznacza, że następne linie będą oddziaływać na macierz projekcji. Macierz projekcji jest potrzebna by dodać perspektywę do naszej sceny. glLoadIdentity() resetuje macierz do pierwotnego stanu. Po glMatrixMode(GL_MODELVIEW) oznacza że następne transformacje będą odziaływać na macierz modeli. Ta macierz przechowuje informacje o objekcie. Na koniec resetujemy macierz modeli. Nie martw się, jeśli nie rozmiesz tego. Wyjaśnię to w następnych kursach. Wystarczy tylko że wiesz, że to jest potrzebne do perspektywy na scenie.

glMatrixMode(GL_PROJECTION);         // wybierz macierz projekcji
glLoadIdentity();         // zresetuj ją
        // oblicz perspektywę dla okna
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);
glMatrixMode(GL_MODELVIEW);         // wybierz macier modeli
glLoadIdentity();         // zresetuj ją
}

W następnej sekcji kodu ustawimy OpenGL. Ustawimy kolor czyszczenia ekranu, włączymy bufor głębokości i gładkie cieniowanie modeli, itp. Ta rutyna nie będzie włączana, póki okno OpenGL się nie stworzy.

int InitGL(GLvoid)
{

Następna linia właczna gładkie cieniowanie. Gładkie cieniowanie ładnie miesza kolory na wielkącie i wygładza światło. Wyjaśnię to w innym kursie.

glShadeModel(GL_SMOOTH);

Następne linie ustawiają kolor czyszczacy ekran. Jeśli nie wiesz jak działają kolory, szybko to wyjaśnię. Wartości kolorów są od 0.0 do 1.0. 0.0 oznacza całkowitą ciemność, a 1.0 najjaśniejszy. Pierwszy parametr po glClearColor to intensywność czerwonego, drugi zielonego, trzeci niebieskiego. Ostatni parametr to wartość alpha. Kiedy chodzi o czyszczenie ekranu nie martw się o 4 parametr.

Stworzysz różne kolory poprzez mixowanie trzech podstawowych kolorów (czerwony, zielony, niebiski). Mam nadzieję, że tego sie nauczyłeś w szkole. Więc glClearColor(0.0, 0.0, 1.0, 0.0) oznacza kolor jasny niebiski.

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

Następne trzy linie robią bufor głebi. Myśl o buforze głębi jak warstwach na ekranie. Ten bufor ustala jak głęboko jest objekt, czyli co ma być rysowane jako pierwsze. Bufor Głębi to bardzo ważna część OpenGL.

glClearDepth(1.0f);         // ustawienie bufora głebi
glEnable(GL_DEPTH_TEST);         // włączenie testowania głębi
glDepthFunc(GL_LEQUAL);         // ustawienie typu testowania

Teraz powiemy OpenGL, że chcemy najlepszą perspektywę. Obniży to szybkość działania programu, ale będzie ładniej.

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

Nareszcie zwracamy TRUE. Przy inicjalizacji sprawdzamy co zwróciła funkcja. Jeśli zwróci false, oznaczać to będzie, że wystąpił błąd.

return TRUE;
}

Ta sekcja służy do rysowania. Wszystko co zostanie narysowane i wyświetlone jest właśnie tutaj. Każdy kurs doda kod właśnie tu. Jeśli już coś umiesz w OpenGL, możesz spróbować stworzyć kształty przed glLoadIdentity i przed retrun true. Jeśli jesteś nowicjuszem, poczekaj na mój następnu kurs. Na razie tylko czyścimi ekran na ustawiony wcześniej kolor. return TRUE mówi programowi, że nie wystąpiły problemy. Jeśli chcesz zatrzymać program, daj return FALSE gdzieś przed return TRUE, lub powiedz programowi że wystąpił problem ;)

int DrawGLScene(GLvoid)         // funkcja rysująca
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);         // czyść ekran
glLoadIdentity();         // zresetuj macierz modeli
return TRUE;         // wszystko jest ok
}

Następna część kodu jest uruchamiana przed zakończeniem programu. Zadaniem KillGLWindow() jest zwolnienie pamięci z Kontekstu renderowania, kontekstu urządzeniowego i uchwyt okna. Dodałem dużo sprawdzania błędów. Jeśli program nie będzie mógł zwolnić czegoś, zostanie pokazane okienko z błędem.

GLvoid KillGLWindow(GLvoid)         // zamknij okno
{

Pierwszą rzeczą do zrobienia w tej funkcji jest sprawdzenie czy program jest pełnym ekranie. Jeśli tak, to trzeba przełączyć spowrotem. Powininiśmy zniszczyć okno przed wyłączeniem pełnego ekranu, ale niektóre karty tego nie obsługją.

if (fullscreen)
{

Użyjemy ChangeDisplaySettings(NULL, 0) do przywrócenia orginalnej rozdzielczości. Parametry oznaczają, że wartości są podane w rejestrze Windowsa (rozdzielczość, głębia, częstotliwośc).

ChangeDisplaySettings(NULL,0);         // przywróc rozdzielczość
ShowCursor(TRUE);         // pokaż kursor
}

Następny kod sprawdza czy mamy kontekst rendowania (hRC). Jeśli nie, program pominie ten krok.

if (hRC)         // mamy kontekst renderu?
{

Jeśli mamy kontekst renderowania, kod sprawdzi czy jesteśmy w stanie zwolnić go (nie myl hRC z hDC). Zwróć uwagę na sposób sprawdzania błędów. Normalnie każę programowi spróbować zwolnić to (z wglMakeCurrent(NULL, NULL), następnie sprawdzam czy się udało.

if (!wglMakeCurrent(NULL,NULL))         // Czy jesteśmy w stanie zwolnić go
{

Jeśli nie uda się zwolnić kontekstów, wyskoczy MessageBox() z wiadmością o błędzie. NULL mówi, że okienko nie ma okna pierwotnego. Następny parametr to treść wiadomości. Kolejny, "BŁĄD ZAMYKANIA" to tytuł wiadomości. Ostatnie MB_OK oznacza że ma być jeden guzik - OK. MB_ICONINFORMATION oznacza, że okienko ma mieć ikonkę "i" w chmurce. MB_ICONERROR oznacza czerwone kółko z białym krzyżykiem. Jeśli chcesz się dowiedzieć więcej o okienkach, ucz się WinAPI.

MessageBox(NULL,"Nie można zwolnić hDC lub hRC!","BŁĄD ZAMYKANIA",MB_OK | MB_ICONINFORMATION);
}

Teraz spróbujemy usunąć (nie mylić z "zwolnić"!) kontekst renderowania. Jeśli się nie uda, pokaże się okienko z errorem.

if (!wglDeleteContext(hRC))         // Czy możemy usunąć kontekst renderu?
{

Jeśli się nie uda pokaże się okienko z wiadomością o błędzie. W każdym wypadku ustawimy hRC na NULL.

MessageBox(NULL,"Nie można usunąć kontekstu renderowania","BŁĄD ZAMYKANIA",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL;
}

Teraz sprawdzimy czy program ma kontekst urządzenia. Jeśli ma, to go zwolnimy. Jeśli się nie uda zwolnić to sami wiecie - okienko z wiadomością i ustawiamy na NULL.<rb>

if (hDC && !ReleaseDC(hWnd,hDC))         // Czy możemy zwolnić hDC
{
MessageBox(NULL,"Nie można zwolnić kontekstu urządzenia (DC)","BŁĄD ZAMYKANIA",MB_OK | MB_ICONINFORMATION);
hDC=NULL;         // Ustawiamy DC na NULL
}

Teraz sprawdzimy czy jest stworzony uchwyt okna. Jeśli jest, to go zniszczymy używając funkcji DestroyWindow(hWnd). Jeśli się nie uda, pokaże się okienko z errorem i ustawimy hWnd na NULL.

if (hWnd && !DestroyWindow(hWnd))         // Czy możemy zwolnić uchwyt okna?
{
MessageBox(NULL,"Nie można zwolnić hWnd","BŁĄD ZAMYKANIA",MB_OK | MB_ICONINFORMATION);
hWnd=NULL;         // Ustaw hWnd na Null
}

Ustatnią rzeczą do zwolnienia jest klasa okna. Pozwoli nam to poprawnie zamknąć okno.

if (!UnregisterClass("OpenGL",hInstance))         // Czy możemy wyrejstrować klasę okna?
{
MessageBox(NULL,"Nie można wyrejstrować klasy okna","BŁĄD ZAMYKANIA",MB_OK | MB_ICONINFORMATION);
hInstance=NULL;         // Ustawiamy instancję na NULL
}
}

Nastepna funkcja stworzy okno OpenGL. Spędziłem wiele czasu rozmyślając, czy stworzyć tylko tryb pełnoekranowy który nie wymaga wiele kodu, czy przyjazne okienko, które wymaga dość sporo kodu. Zadecydowałem, że okienko będzie najlepszym wyborem. Wiele osób pytało mnie o to jak zmienić tytuł okienka, jak zmienić format piksela w oknie. Ten kod to robi.

Jak widzisz procedura zwraca bool (true lub false). Pobiera 5 parametrów: tytuł okna, jego szerokość, jego wysokość, głębię kolorów (16/24/32) i tryb pełnoekranowy. Funckja zwróci true jeśli wszystko pójdzie sprawnie.

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

Teraz zapytamy Windowsa o format piksela takiego jaki my chcemy. Tryb który zwróci Windows będzie przechowywany w zmiennej PixelFormat.

GLuint PixelFormat;         // Będzie przechowywać znaleziony format piksela

Dalej. wc będzie przetrzymywać naszą Klasę okna (nie mylić z pojęciem klasy z C++). Ta klasa przechowuje informacje o naszym oknie, poprzez zmiane jej niektórych pól, możemy zmienić wygląd okna ;) Każde okno musi mieć klasę okna, więc przed stworzeniem okna MUSISZ zarejestrować klase okna.

WNDCLASS wc;         // Struktura klasy okna

dwExStyle i dwStyle przechowywać będą rozszerzony i normalny styl okna. Rozszerzony pozwala nam na takie bajery jak ta wąska ramka wokół okna, guziki, i takie tam. Normalny w sam raz pasuje do trybu pełno ekranowego ;)

DWORD dwExStyle;         // Rozszerzony styl okna
DWORD dwStyle;         // Normalny styl okna

Następne 5 lini kodu pobiera górny-lewy i dolny-prawy róg prostokąta. Użyjemy tych wartości do określenia obszaru rysowania na oknie i dopasowania do rozdzielczości. Jeśli stworzymy okno 640 x 480, ramka zabierze trochę rozdzielczości.

RECT WindowRect;         // Tu będą rozmiary okna ;)
WindowRect.left=(long)0;         // Początek szerokości (od lewej) ma 0
WindowRect.right=(long)width;         // Szerokośc bierzemy z parametru naszej funkcji
WindowRect.top=(long)0;         // Wysokość też zaczynamy od 0 (od góry)
WindowRect.bottom=(long)height;         // Ustawiamy wysokość z parametru naszej funkcji

Ta linia kodu pobierze wartość z parametru funkcji i da ją do naszej zmiennej globalnej.

fullscreen=fullscreenflag;         // Ustawiamy globalną zmienną

W nastepnej porcji kodu pobierzemy instancję okna, a następnie zadeklarujemy klasę okna.

Styl CS_HREDRAW i CS_VREDRAW sprawia, że okno będzie odmalowywane przy zmianie rozmiarów. CS_OWNDC stworzy prywatny kontekst urządzenia dla okna. Kontkekst ten nie jest wspólny dla wszysytkich aplikacji. WndProc jest funkcją która wyczekuje na komunikaty od naszego okna (np. wciśnięcie klawiszy jest komunikatem). Nie używamy żadnych dodatkowych informacji okna, więc dajemy 0. Nastepnie ustawiamy instancję, później ikone na NULL, ponieważ nie chcemy ikonki. Kursor myszy będzie zwyczajną strzałką. Tło nie ma znaczenia bo ekran czyścimy przecież w OpenGL. Nie tworzymy też menu, więc dajmy NULL. Nazwa klasy jest obojętna. Ja dałem "OpenGL" aby było prościej :P

hInstance = GetModuleHandle(NULL);         // Pobieramy instancję dla okna
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;         // Ustawiamy odmalowywanie
wc.lpfnWndProc = (WNDPROC) WndProc;         // WndProc będzie obsługiwać komunikaty
wc.cbClsExtra = 0;         // nie ważne
wc.cbWndExtra = 0;         // nie ważne
wc.hInstance = hInstance;         // Ustawiamy instancję
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);         // Ikona domyślna
wc.hCursor = LoadCursor(NULL, IDC_ARROW);         // Kursor - strzałka
wc.hbrBackground = NULL;         // Tło nie jest ważne w OpenGL
wc.lpszMenuName = NULL;         // Nie chcemy menu
wc.lpszClassName = "OpenGL";         // Nazwa klasy okna

Teraz możemy spokojnie zarejstrować powyższą klasę okna. Jeśli się nie uda, pokaże się okienko z błędem, a funkcja zwróci false.

if (!RegisterClass(&wc))         // Spróbuj zarejstrować klasę okna
{
MessageBox(NULL,"Nie udało się zarejstrować klasy okna","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zakończ i zwróć fałsz.
}

Teraz sprawdzimy czy program chce być w trybie pełnoekranowym. Jeśli tak to spróbujemy go ustawić.

if (fullscreen)         // Czy ma być pełny ekran ?
{

Teraz ta część kodu z którą wiele osób ma kłopoty. Zmiana trybu na pełny ekran. Jest parę ważnych reguł, których trzeba przestrzegać przy zmianie na pełny ekran. Należy sprawdzić czy wymagana rozdzielczość jest dostępna w pełnym ekranie (np. 800x600) no i najważniejsze: Należy ustawić tryb pełno ekranowy PRZED stworzeniem okna. W tym kodzie nie musisz obawiać się o rozdzielczość, kod jest kontrolowany.

DEVMODE dmScreenSettings;         // Tryb karty graficznej
memset(&dmScreenSettings,0,sizeof(dmScreenSettings));         // Wyczyść pamięć
dmScreenSettings.dmSize = sizeof(dmScreenSettings);         // Ustaw rozmiar tej struktury
dmScreenSettings.dmPelsWidth = width;         // Wybież żądaną szerokość
dmScreenSettings.dmPelsHeight = height;         // Wybierz żądaną wysokość
dmScreenSettings.dmBitsPerPel = bits;         // Wybierz głębie kolorów
dmScreenSettings.dmFields=DM_BITSPERPEL|DM_PELSWIDTH|DM_PELSHEIGHT;

Teraz ważny moment. Ustawiamy ustalony tryb video: Wysokość, Szerokość i głębie kolorów. Wszystkie te informacje są w strukturze dmScreenSettings. ChangeDisplaySettings próbuje zmienić tryb do tego który my chcemy. Używam CDS_FULLSCREEN ponieważ pozwala on nam usunąć pasek start na spodzie ekranu no i nie pozmieniają się rozmiary okienek na ekranie po zakończeniu programu (jeśli nie wiesz o co chodzi daj tam 0, włącz program, i wyłącz).

        // Spróbuj ustawić pełny ekran. CDS_FULLSCREEN usuwa pasek start.
if(ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=DISP_CHANGE_SUCCESSFUL)
{

Jeśli tryb nie jest obsługiwany przez twoją kartę graficzną, pokaże się okienko z dwiema opcjami: Uruchomić w trybie okna, lub zakończyć program.

        // Jeśli się nie uda, przejdź do okna lub zakończ program
if(MessageBox(NULL,"Tryb graficzny nie jest obsługiwany przez twoją kartę graf. Czy użyć zamiast niego okna?","NeHe PL",MB_YESNO|MB_ICONEXCLAMATION)==IDYES)
{

Jeśli użytkownik zdecyduje się na tryb okienkowy, ustawimy fullscreen na false i program będzie się kontynuował.

fullscreen=FALSE;         // Tryb okienkowy. fullscreen na false.
}
else
{

Jeśli jednak człowiek zdecydował się na zakończenie, pokaże się okienko o zakończeniu i funkcja zwróci false mówiąc tym samym, że okno nie zostało utworzone. Co za tym idzie program się zamknie.

        // Uwaga o zamykaniu
MessageBox(NULL,"Program teraz się zamknie.","BŁĄD",MB_OK|MB_ICONSTOP);
return FALSE;         // Zakończ i zwróć false
}
}
}

Ponieważ wszystko mogło pójść dobrze, sprawdzamy jeszcze raz stan fullscreen.

if(fullscreen)         // Wciąż jesteśmy w trybie pełno ekranowym
{

Jeśli jesteśmy w tym trybie, ustawimy styl rozszerzony na WS_EX_APPWINDOW, który ustawi okno do pasku zadań, sprawiając, że okno będzie niewidoczne. Stylu okna damy na WS_POPUP, czyli puste okno, bez guzików, bez ramek. W sam raz dla pełnego ekranu.<rb>

Na koniec sprawimy że kursor myszy zniknie. Jeśli program nie jest interaktywny, miło jest usunąć kursor myszy.

dwExStyle=WS_EX_APPWINDOW;         // Rozszerzony styl okna
dwStyle=WS_POPUP;         // Styl okna
ShowCursor(FALSE);         // Ukryj kursor myszy
}
else
{

Jeśli używamy okna zamiast pełnego ekranu, dodamy WS_EX_WINDOWEDGE do naszego rozszerzonego stylu okna. Da to bardziej profesjonalny wygląd okna. Dla stylu normalnego damy WS_OVERLAPPEDWINDOW zamiast WS_POPUP. Stworzy to okno z paskiem tytułu, guzikami systemowymi (minializacja/maxymalizacja), ramką i ikonką.

dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;         // Rozszerzony styl okna
dwStyle=WS_OVERLAPPEDWINDOW;         // Styl okna
}

Linia poniżej dopasuje okno do naszego stylu. Dopasowanie stworzy okno w rozdzielczości takiej jaką dokładnie chcemy. Normalnie ramki powiększają okno, ale używając AdjustWindowRectEx przykryją one kawałeczek sceny OpenGL. W trybie pełno ekranowym ta funkcja nic nie robi.

AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);

Teraz stworzymy nasze okno i sprawdzimy czy stworzyło się dobrze. Użyjemy CreateWindowEx(). Używamy rozszerzonego stylu okna. Nazwa klasy musi być ta sama co w strukturze ("OpenGL"). Tytuł okna, Styl okna, Pozycja rogu okna (0,0 jest najbezpieczniejsza), szerokość i wysokość okna. Nie chcemy okna potomnego, nie chcemy też menu, więc dajemy te parametry na NULL. Podajemy instancje, i NULL na ostatni parametr.

Zauważ, że użyliśmy styli WS_CLIPSIBLINGS i WS_CLIPCHILDREN razem ze stylami które wcześniej użyliśmy. WS_CLIPSIBLINGS i WS_CLIPCHILDREN są wymagane do poprawnego działania OpenGL.

if( !(hWnd=CreateWindowEx( dwExStyle,         // Rozszerzony styl dla okna
"OpenGL",         // Nazwa klasy
title,         // Tytuł okna
WS_CLIPSIBLINGS |         // Wymagane style okna
WS_CLIPCHILDREN |         // Wymagane style okna
dwStyle,         // Wybrane style okna
0, 0,         // Pozycja okna
WindowRect.right-WindowRect.left,         // Szerokość
WindowRect.bottom-WindowRect.top,         // Wysokość
NULL,         // Nie używamy okna potomnego
NULL,         // Żadnego menu
hInstance,         // Instancja
NULL)))         // Nie dawaj nic do WM_CREATE

Teraz sprawdzimy czy okno się stworzyło poprawnie. Jeśli się stworzyło, hWnd będzie przetrzymywać uchwyt okna. Jeśli nie, pokaże się komunikat o błędzie i program się zakończy.

{
KillGLWindow();         // Zresetuj tryb ekranu
MessageBox(NULL,"Nie można stworzyć okna.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwróć false
}

Następujący kod opisuje format piksela. Wybierzemy format który jest obsługiwany przez OpenGL, podwójne buforowanie, razem z RGBA( czerwony, zielony, niebieski i alpha). Spróbujemy znaleźć taki format piksela który pasuje do wybranej głębi kolorów (16, 24, 32). Na koniec utworzymy 16 bitowy bufor Z. Pozostałe parametry nie są używane lub nie są ważne.

static PIXELFORMATDESCRIPTOR pfd =         // pfd mówi oknu co chcemy
{
sizeof(PIXELFORMATDESCRIPTOR),         // Rozmiar opisu piksela
1,         // Numer wersji
PFD_DRAW_TO_WINDOW |         // Format musi obsługiwać okno
PFD_SUPPORT_OPENGL |         // Format musi obsługiwać OpenGL
PFD_DOUBLEBUFFER,         // Musi obsługiwać Podwójne buforowanie
PFD_TYPE_RGBA,         // i format RGBA
bits,         // Wybieramy głębie kolorów
0, 0, 0, 0, 0, 0,         // ignorujemy
0,         // Bez bufora alpha
0,         // Bit ignorujemy
0,         // ignorujemy
0, 0, 0, 0,         // ignorujemy
16,         // 16 bitowy bufor Z
0,         // ignorujemy
0,         // ignorujemy
PFD_MAIN_PLANE,         // Główna warstwa rysowania
0,         // zarezerwowane
0, 0, 0         // ignorujemy maski warstw
};

Jeśli nie wystąpiiły błędy podczas tworzenia okna, spróbujemy pobrać kontekst urządzenia. Jeśli się nie uda, pokaże się tradycyjnie okienko z errorem ;) i program się zakończy.

if (!(hDC=GetDC(hWnd)))         // Mamy kontekst urządzenia?
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Nie można stworzyć kontekstu urządzenia.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

Jeśli udało się pobrać kontekst urządzenia dla naszego okna, spróbujemy znaleźć format piksela zgodny z tym który opisaliśmy wcześniej. Jeśli Windows nie znajdzie tego formatu, pokaże się sami wiecie co i program się zakończy.

if (!(PixelFormat=ChoosePixelFormat(hDC,&pfd)))         // Czy windows znajdzie taki format pixela?
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Nie można znaleźć żądanego formatu piksela.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

Jeśli się znajdzie taki format, spróbujemy go ustawić. Jeśli się nie da, pokaże się wiadomość o błędzie i program się zakończy.

if (!SetPixelFormat(hDC,PixelFormat,&pfd))         // Czy możemy ustawić taki format
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Nie można ustawić żądanego formatu piksela.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

Jeśli format ustawi się pomyślnie, spróbujemy pobrać kontekst renderowania. Jeśli się nie uda, zostanie wyświetlona wiadomość o błędzie.

if (!(hRC=wglCreateContext(hDC)))         // Czy możemy pobrać hRC
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Nie można stworzyć kontekstu renderowania.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

Jeśli nie wystąpią błędy i stworzyliśmy oba konteksty, pozostało uaktywnić kontekst Renderowania. Jeśli się nie uda pokaże się stosowna wiadomość.

if (!wglMakeCurrent(hDC,hRC))         // Czy możemy aktywować kontekst renderowania?
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Nie można aktywować kontekstu renderowania.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

Jeśli wszystko poszło sprawnie i okno OpenGL zostało stworzone, możemy je pokazać i ustawić na przód dając większy priorytet. Następnie wywołamy ReSizeGLScene() podając szerokość i wysokość ekranu aby ustawić perspektywę.

ShowWindow(hWnd,SW_SHOW);         // Pokazujemy okno
SetForegroundWindow(hWnd);         // Ustawiamy wyższy priorytet
SetFocus(hWnd);         // Działanie klawiatury skierowujemy na okno
ReSizeGLScene(width, height);         // Ustawiamy perspektywę

Na koniec wskakujemy w InitGL() gdzie możemy ustawić światła, tekstury, i wszystko z tym związane. Możesz zrobić własną kontrolę błędów w InitGL() i zwrócić true lub false. Na przykład podczas ładowania tekstur nie zostanie odnaleziona któraś, to pokaże się error ;)

if (!InitGL())         // Czy GL zanicjował się ?
{
KillGLWindow();         // Resetujemy ekran
MessageBox(NULL,"Inicjacja niepomyślna.","BŁĄD",MB_OK|MB_ICONEXCLAMATION);
return FALSE;         // zwracamy false
}

No i pozostaje koniec funkcji. Jeśli wcześniej nie wystąpiły błędy, zwróci ona true do WinMain(), kontynuując program.

return TRUE;
}

Teraz stworzymy funkcję obsługującą komunikaty okna zarejestrowaną w klasie okna.

LRESULT CALLBACK WndProc(
HWND hWnd,         // uchwyt do okna
UINT uMsg,         // Wiadomość dla okna
WPARAM wParam,         // Dodatkowe informajce wiadomości
LPARAM lParam)         // Dodatkowe informajce wiadomości
{

Teraz sprawdzimy jakiego typu jest wiadomość otrzymywana (uMsg).

switch (uMsg)         // Sprawdź komunikaty okna
{

jeśli tym komunikatem okaże się WM_ACTIVATE, sprawdzimy czy okno jest wciąż aktywne. Jeśli zostało zminimalizowane, ustawimy active na false. Jeśli jest aktywne,damy active na true.

case WM_ACTIVATE:         // Czy to wiadomość aktywowania?
{
if (!HIWORD(wParam))         // Czy program jest aktywowany
{
active=TRUE;         // Program jest aktywny
}
else
{
active=FALSE;         // Program nie jest aktywny
}
return 0;         // Powróć do pętli wiadomości
}

Jeśli tą wiadomością jest WM_SYSCOMMAND (komenda systemowa), sprawdzimy wParam. Jeśli jest nim SC_SCREENSAVE lub SC_MONITORPOWER zwócimy zero zapobiegając pojawieniu się zgaszacza ekranu.

case WM_SYSCOMMAND:         // Czy to komenda systemowa?
{
switch (wParam)         // Sprawdzimy typ
{
case SC_SCREENSAVE:         // Zgaszacz ekranu chce się włączyć
case SC_MONITORPOWER:         // Monitor chce się wyłączyć
return 0;         // Anulujemy wygaszacze itp.
}
break;         // koniec
}

Jeśli wiadomość to WM_CLOSE, okno zostanie zamknięte. Wyślemy wiadomość zamykania, pętla główna zotanie przerwana. Zmienną done ustawimy na true, więc pętla się przerwie i program się zamknie.

case WM_CLOSE:         // Czy to rozkaz zamknięcia?
{
PostQuitMessage(0);         // Wyślij wiadomość zamknięcia
return 0;         // skocz dalej
}

Jeśli klawisz został wciśnięty, możemy określić jaki on jest za pomocą wParam. Wtedy zapisujemy w odpowiednim polu tablicy keys[] true. Później możemy odczytać z tej tablicy jakie klawisze są wciśnięte ;) pozwala nam to sprawdzić wszystkie klawisze za jednym zamachem.

case WM_KEYDOWN:         // Czy klawisz został wciśnięty
{
keys[wParam]=TRUE;         // Odpowiednie pole zostaje ustawione
return 0;         // skocz dalej
}

Jeśli jakiś klawisz został odciśnięty, znajdujemy go za pomocą wParam. Następnie ustawiamy odpowiednie pole w tablicy keys[] na false. Każdy klawisz na klawiaturze reprezentowany jest przez liczbę od 0 - 255. Kiedy wciśniesz klawisz o numerze 40, pole tablicy keys[40] ustawione zostanie na true. Jeśli go odciśniesz, pole zostanie ustawione na false.

case WM_KEYUP:         // Czy klawisz został wciśnięty
{
keys[wParam]=FALSE;         // Odpowiednie pole zostaje ustawione na false
return 0;         // skocz dalej
}

Jeśli komendą jest zmiana rozmiaru okna, to jego nowa szerokość jest zapisana w lParam, w jego dwóch częściach: LOWORD i HIWORD. Po zmianie rozmiaru okna musimy przestawić perspektywe do nowych rozmiarów korzystając z naszej funkcji ReSizeGLScene(). Scena OpenGL będzie na nowo dopasowana do nowych wymiarów okna.

case WM_SIZE:         // Czy okno się zmieniło ?
{
ReSizeGLScene(LOWORD(lParam),HIWORD(lParam));         // Zmieniamy scene OpenGL
return 0;         // skocz dalej
}
}

To już wszystkie wiadomości na których nam zależy. Reszta będzie obsługiwana przez system Microsoft Windows.

        // Reszta wiadomości idzie do windy
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

Teraz czas na najważnieszą część programu: Funkcja WinMain().

int WINAPI WinMain(
HINSTANCE hInstance,         // Instancja
HINSTANCE hPrevInstance,         // Poprzednia instancja
LPSTR lpCmdLine,         // Parametry z linii komend
int nCmdShow)         // Stan okna
{

Ustawimy dwie zmienne. msg będziemy używać do sprawdzania czy są jakieś komunikaty okna. done użyjemy do sprawdzania czy program jeszcze ma działać. Jeśli done ustawimy na true program zostanie zakończony.

MSG msg;         // Struktura przechowująca komunikaty okna
BOOL done=FALSE;         // Stan działania programu

Ta część kodu jest opcjonalna. Pokazuje ona okienko pytające, czy chcemy aby program uruchomił się w pełnym ekranie, czy w trybie okienkowym.

        // Zapytaj o tryb ekranu
if (MessageBox(NULL,"Czy chcesz być w pełnym ekranie?", "Start?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE;         // tryb okienkowym
}

Teraz wywołamy naszą funkcję tworzącą okno OpenGL. Podamy tytuł, szerokość i wysokość, głebie koloru i true lub false (pełny ekran). Jestem bardzo szczęśliwy, że ten kod jest taki prosty.

        // Stwórz okno OpenGL
if (!CreateGLWindow("Tutoriale NeHe Po Polsku",640,480,16,fullscreen))
{
return 0;         // zakończ program
}

Teraz zaczniemy pętle główną programu. Będzie ona tak długo lecieć, póki done będzie ustawione na false.

while (!done)
{

Pierwszą rzeczą w takiej pętli jest sprawdzenie czy jakieś komunikaty okna czekają na obsługę. Sprawdzimy to przy pomocy PeekMessage().

if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))         // czy są jakieś wiadomości ?
{

W tej częsci kodu sprawdzimy czy komunikatem nie jest WM_QUIT spowodowany przez PostQuitMessage(0). Jeśli nim jest, ustawimy done na true i będzie koniec programu.

if(msg.message==WM_QUIT)         // czy otrzymano wiadomość zamknięcia ?
{
done=TRUE;         // skoro tak, to done=TRUE
}
else         // nie otrzymano wiadomości zamknięcia ?
{

Jeśli komunikat jest inny, to obsłuży go funkcja WndProc() lub Windows.

TranslateMessage(&msg);         // wytłumacz wiadomość
DispatchMessage(&msg);         // wyślij ją
}
}
else         // nie ma żadnych komunikatów
{

Jeśl nie ma żadnych komunikatów, możemy śmiało rysować naszą scene OpenGL. Pierwsza linia kodu sprawdzi czy program jest aktywny. Jeśli wciśnięto ESC, ustawimy done na true, przerywając tym samym pętle.

        // Rysuj scenę OpenGL
if(active)         // program jest aktywny ?
{
if(keys[VK_ESCAPE])         // czy wciśnięty jest ESC ?
{
done=TRUE;         // przerwanie warunku pętli
}
else         // nie ma czasu na zamknięcie, rysujemy scene
{

Jeśli program jest aktywny i Esc nie jest wciśnięty, możemy renderować scene i zamienić bufory (poprzez użycie podwójnego buforowania, aby uzyskać płynną animację). Dzięki podwójnemu buforowaniu wszystko jest rysowane na niewidzialnym ekranie (drugim buforze), a na koniec przerzucone na ten prawdziwy ;)

DrawGLScene();         // Rysuj scenę
SwapBuffers(hDC);         // Zamień bufory (ekrany)
}
}

Następna porcja kodu jest nowa. Pozwala ona przełączyć za pomocą klawisza F1 ekran z trybu pełnoekranowego na okienkowy i na odwrót :)

if(keys[VK_F1])         // czy F1 jest wciśnięte
{
keys[VK_F1]=FALSE;         // ustaw go na false, bo został użyty
KillGLWindow();         // Zamknij okno
fullscreen=!fullscreen;         // Zamień pełny ekran)
        // Stwórz nowe okno
if(!CreateGLWindow("Tutoriale NeHe Po Polsku",640,480,16,fullscreen))
{
return 0;         // Wystąpił błąd
}
}
}
}

Jeśli zmienna done nie jest ustawiona na false, program się zakończy. Zamkniemy okno OpenGL poprawnie.

KillGLWindow();         // Zamknij OpenGL
return (msg.wParam);         // Koniec programu
}

W tym kursie wyjaśniłem to bardzo dokładnie, każdy krok został opisany: włączanie trybu pełnoekranowego, tworzenie okna, uruchamianie OpenGL i zamykanie okna. Spędziłem 2 tygodnie pisząc ten kod, ciągle poprawiałem drobne błędy. Dziś możesz go ściągnąć i wykorzystać w swoich projektach. Jeśli gdzieś popełniłem błąd, powiadom mnie.