Autor: Georg 'Black' Wicherski
In dieser Lesson lernt ihr, wie ihr ein einfaches OpenGL Framwork unter Windows erstellt, d.h. eine .exe die schon eine zeichenfähige Umgebung erstellt, jedoch an sich nichts zeichnet.
Als erstes binden wir die windows.h, die gl/gl.h sowie die gl/glu.h ein. Die windows.h sollte schon hinreichend bekannt sein, die gl.h enthält die Definitionen zu den Standard OpenGL Funktionen und Datentypen, die glu.h ist die OpenGL Utilities Header. Die GLU enthalten einige zum Teil sehr nützliche Funktionen, die uns Arbeit abnehemen aber auch selber geschrieben werden konnten und ist daher nicht essentiell.
Dann teilen wir dem Linker mit, wo er die nötigen Implementationen der GL/GLU Funktionen findet. Wer wegen des Styles oder sonstige Einwände gegen die #pragma-direktive hat, kann diese *.lib-s auch über die Projektoptionen hinzufügen.
#include <windows.h> #include <gl/gl.h> #include <gl/glu.h> #pragma comment(lib, "opengl32.lib") #pragma comment(lib, "glu32.lib")
Jetzt kommen ein paar globale Variablen, die unsere Fenster- und OpenGL-Rendercontextdaten festhalten, sowie ein bool indem wir speichern, ob wir uns im Vollbildmodus befinden.
bool g_fRun = 1; bool g_fFullscreen = 0; HWND g_hWnd = 0; HINSTANCE g_hInstance = 0; HDC g_hDC = 0; HGLRC g_hRC = 0;
Als nächstes kommen einige Prototypen fur ein paar Funktionen. Init(…) wird beim Programmstart aufgerufen, erstellt ein WinAPI Fenster und macht die entsprechenden Einstellungen, UnInit(…) ist das entsprechende Gegenstück und entlädt OpenGL wieder. Resize(…) wird jedesmal aufgerufen, wenn sich die Größe des Fensters ändert, in dieser Funktion wird die Perspektive eingestellt. WindowProcedure(…) wird die WndProc für unser Fenster. In der Funktion Render() wird der aktuelle Frame mit OpenGL gezeichnet, diese Funktion wird in diesem Tutorial noch fast leer sein, füllt sich später aber, versprochen.
bool Init(unsigned int nResX, unsigned int nResY, bool fFullscreen); void UnInit(); void Render(); void Resize(unsigned int nNewResX, unsigned int nNewResY); LONG WINAPI WindowProcedure(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
Nun kommen wir auch schon zu unserer WinMain, erst wird OpenGL initialisiert, dann geht es in die MessageLoop und darauf wird auch schon wieder alles deinitialisiert und das Programm beendet.
In der MessageLoop geschieht folgendes: Erst wird geschaut, ob eine Message zum Verarbeiten vorliegt (PeekMessage(…)), wenn ja, wird diese verarbeitet und dan die WindowProcedure weitergeleitet, wenn nicht wird der aktuelle Frame gerendert und die GFX-Buffer werden ausgetauscht (s. u.).
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, char * szCommandLine, int nDisplayMode) { g_hInstance = hInstance; if(!Init(640, 480, true)) { MessageBox(0, "Initilisation failed!", "Lesson 1", MB_ICONEXCLAMATION); UnInit(); return 0; } { MSG msgMessage; while(g_fRun) { if(PeekMessage(&msgMessage, 0, 0, 0, PM_REMOVE)) { TranslateMessage(&msgMessage); DispatchMessage(&msgMessage); } else { Render(); SwapBuffers(g_hDC); } } } UnInit(); return 0; }
Jetzt kommen wir zu den mehr oder weniger spannenden Sachen. Die Init(…) Funktion ist in zwei Teile unterteilt: a) den WinAPI cpp, der ein Fenster erstellt und in den Vollbildschirm-Modus wechselt und b) der wesentlich kleinere und plattformunabhängige OpenGL Teil, der verschiedene Parameter setzt und Einstellungen vornimmt.
Zunächst erstellen wir unsere eigene WNDCLASS, wichtig hierbei zu beachten ist der Style CS_OWNDC, der für ein OpenGL kompatibles Fenster unabgänglich ist. Darauf wird das Fenster erstellt und angezeigt.
bool Init(unsigned int nResX, unsigned int nResY, bool fFullscreen) { g_fFullscreen = fFullscreen; { // windows specific stuff { WNDCLASS wcWindowClass; wcWindowClass.cbClsExtra = 0; wcWindowClass.cbWndExtra = 0; wcWindowClass.hbrBackground = GetSysColorBrush(COLOR_BTNFACE); wcWindowClass.hCursor = 0; wcWindowClass.hIcon = LoadIcon(0, IDI_WINLOGO); wcWindowClass.hInstance = g_hInstance; wcWindowClass.lpfnWndProc = WindowProcedure; wcWindowClass.lpszClassName = "pixel-house|lesson1|main.wnd"; wcWindowClass.lpszMenuName = 0; wcWindowClass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC; if(!RegisterClass(&wcWindowClass)) return false; } { if(!(g_hWnd = CreateWindowEx(0, "pixel-house|lesson1|main.wnd", "Lesson 1", WS_VISIBLE | WS_POPUP, 0, 0, nResX, nResY, 0, 0, g_hInstance, 0))) return false; ShowWindow(g_hWnd, SW_SHOW); // show our window SetFocus(g_hWnd); // on foreground ShowCursor(!fFullscreen); // hide cursor if in fullscreen }
Da wir ja die Option bieten im FullScreen Modus zu laufen, müssen wir natürlich auch in diesen wechseln. Dies geschieht, indem wir eine Struct des Typs DEVMODE erstellen und in ihr die von uns gewünschten Parameter speichern. Das Ganze geschieht jedoch nur, wenn wir auch explizit den Vollbildschirmmodus wünschen (fFullscreen == true).
Die Funktion ChangeDisplaySettings hat zwei Parameter: einen Pointer auf eine DEVMODE Struct, in der die gewünschten Parameter gespeichert sind und Flags die beim wechseln des Bildschirmzustandes zu beachten sind. Das Flag CDS_FULLSCREEN bedeutet, dass es sich nicht um eine permanente Einstellung sondern nur um einen vorübergehenden Zustand handelt, was uns ermöglicht, mit ChangeDisplaySettings(0, 0); zum vorherigen Zustand zurückzukehren. Das zweite Flag, CDS_RESET bedeutet, dass der Bildschirm reinitialisiert wird, auch wenn es sich bereits um die gewünschte Bildschirmeinstellung handelt.
if(fFullscreen) // if we're in fullscreen, we've to change screen settings { DEVMODE dmScreen; dmScreen.dmBitsPerPel = 32; // 32 bbp dmScreen.dmPelsWidth = nResX; dmScreen.dmPelsHeight = nResY; dmScreen.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT; if(ChangeDisplaySettings(&dmScreen, CDS_FULLSCREEN | CDS_RESET) != DISP_CHANGE_SUCCESSFUL) return false; }
Dies ist schon der letzte Windos-Spezifische cpp-Abschnitt.
Windows arbeitet mit sogenannten Pixel Formats, das sind integer die eine bestimmtes Verhalten für ein Device Context bezeichnen. Da wir natürlich nicht irgendeinen integer erraten wollen, benutzen wir die von Windows bereitgestellte Funktion ChoosePixelFormat. Sie bekommt zwei Parameter, einen Handle zum Device Context und einen Pointer zu einer PIXELFORMATDESCRIPTOR Struct. In diese Struct schreiben wir die Informationen die wichtig sind, damit unser OpenGL Programm ordnungsgemäß läuft.
Als erstes versuchen wir mittels WinAPI herauszubekommen, welches Device Context zu unserem Fenster gehört, dann lassen wir das entsprechende Pixel Format auswählen. Dannach setzen wir das Pixel Format für unseren DC, der Pointer auf pfdPixelFormat dient hier nur als zusätzliche Informationsquelle, der Parameter iPixelFormat ist der eigentlich wichtige. Danach versuchen wir ein Handle auf einen neu erstellten GL Render Context mit der Windowsspzifischen OpenGL Funktion (Prefix wgl) wglCreateContext zu bekommen. Dann sagen wir Windows, dass alle folgenden gl* Befehle sich auf unser DC und unseren RC beziehen.
{ int iPixelFormat; PIXELFORMATDESCRIPTOR pfdPixelFormat = { sizeof(PIXELFORMATDESCRIPTOR), //size of structure in bytes 1, //version number PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, //what it needs 2 support PFD_TYPE_RGBA, //we want RGBA! 32, //32 bpp 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //these are unimportant for now 16, //16bit z-buffer 0, 0, //no stencil buffer, no auxilary buffer PFD_MAIN_PLANE, //main drawing plane 0, 0, 0, 0 //uninteresting }; if(!(g_hDC = GetDC(g_hWnd))) return false; if(!(iPixelFormat = ChoosePixelFormat(g_hDC, &pfdPixelFormat))) return false; if(!SetPixelFormat(g_hDC, iPixelFormat, &pfdPixelFormat)) return false; if(!(g_hRC = wglCreateContext(g_hDC))) return false; if(!wglMakeCurrent(g_hDC, g_hRC)) return false; } }
Nun endlich zum ersten Stück des lang ersehnten OpenGL cpps. Dieser cpp setzt nur einige Standard-Parameter, die sich den Kommentaren hinter den Funktionsaufrufen entnehmen lassen.
{ // platform independent stuff glShadeModel(GL_SMOOTH); //activate smooth shading glClearColor(.0f, .0f, .6f, 1.0f); //background is dark blue with full alpha glEnable(GL_DEPTH_TEST); //enable depth testing glDepthFunc(GL_LEQUAL); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); } Resize(nResX, nResY); return true; }
Das war's auch schon an Initialisierung, nun ein bisschen cpp, der das ganze dann nach Beenden oder wenn etwas schief gelaufen ist aufräumt. Dieser cpp-Block ist relativ einfach: wenn wir uns im Vollbildmodus befinden, wird wieder in den Standard-Modus gewechselt (siehe Oben). Wenn bereits ein Handle zu einem RC vorhanden ist wird Windows Bescheid gesagt, dass wir nix mehr mit folgenden gl* Kommandos am Hut haben, und dass wir den RC nicht mehr brauchen. Das gleiche geschieht mit unserem DC. Falls wir ein Handle zum Hauoptfenster haben, wird versucht dieses zu zerstören.
void UnInit() { if(g_fFullscreen) { ChangeDisplaySettings(0, 0); ShowCursor(true); } if(g_hRC) { wglMakeCurrent(0, 0); wglDeleteContext(g_hRC); if(g_hDC) ReleaseDC(g_hWnd, g_hDC); } if(g_hWnd) DestroyWindow(g_hWnd); }
Jetzt kommen wir zu der schon oben angesprochenen Funktion Resize(…). Sie tut nicht das, was man dem Namen nach vermutet, nämlich die Fenstergröße zu ändern. Sie sollte in eben solchen Fällen aufgerufen werden, und naturlich bei der Initialisierung.
Zunächst wird OpenGL ein Rectangle mitgeteilt, in das gezeichnet werden soll. Das ist in unserem Fall das ganze Fenster. Dann wechseln wir den Matrix Mode (alle Berrechnungen beziehen sich auf die aktuelle Matrix) zu GL_PROJECTION, d.h. folgende Befehle werden auf die Projetion Matrix angewandt, oder in anderen Worten: Wir wollen die Kamera-Sicht / Perspektive ändern. Nun wird die Identity Matrix geladen, eine Matrix aus lauter Nullen, die Projection Matrix wird also auf 0 zurückgesetzt. Dann benutzen wir die für dieses Tutorial einzige glu* Funktion, welche auf einfache Weise eine perspektivische Kamera erstellt. Der erste Parameter beschreibt das FoV (Field of View), 80 ° entsprechen ungefähr dem menschlichem Sichtfeld. Der zweite Parameter beschreibt das Verhältnis zwischen Breite * Höhe, im Vollbildmodus typischerweise 4/3. Dann wechseln wir zu Modelview Matrix und laden erneut die Identity Matrix, also wird auch die Modelview Matrix auf 0 gesetzt.
Die Modelview Matrix hat nichts mit Model zu tun, sondern einfach mit allen Vertexes. Mit anderen Worten, alles was aus einem vorherigem Frame noch anders war, wird gelöscht.
void Resize(unsigned int nNewResX, unsigned int nNewResY) { glViewport(0, 0, nNewResX, nNewResY); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(80.0f, (float) nNewResX / (float) nNewResY, .1f, 1024.0f); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); }
Endlich die Render Funktion! Sie wird jeden Frame aufgerufen und soll(te) den aktuellen Frame mit gl* Commands auf den Bildschirm bringen. Sie ist noch ein bisschen leer, wird sich aber in den nächsten Tutorials sicher füllen.
Als erstes löschen wir den Color Buffer (= Bildschirm löschen → cls) und darauf den Depth Buffer. Würden wir den Depth Buffer nicht löschen, würden Triangles aus den vorherigen Frames möglicherweise Frames aus dem aktuellen Frame überlappen. Dann laden wir mal wieder die mittlerweile bekannte Identity Matrix.
void Render() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); }
Nun noch die gute alte WindowProcedure, hierzu muss ich hoffentlich nichts sagen…
LONG WINAPI WindowProcedure(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) { switch(nMsg) { case WM_KEYDOWN: if(wParam == VK_ESCAPE) g_fRun = false; return 0; default: return DefWindowProc(hWnd, nMsg, wParam, lParam); } }
Dieses Tutorial stammt aus der ehemaligen Sammlung des resourcecode.de und konnte dank der freundlichen Zustimmung des Autors in das thewall-Wiki übertragen werden.