W tym artykule stworzymy rozbudowany szablon programu pracującego na OpenGl.
1. Przygotowanie podłoża dla programu
Nasz program pracujący na OpenGl nie będzie posiadał obiektu TForm oraz żadnych komponentów VCL.
Dzięki temu aplikacja będzie działać szybciej oraz będzie miała mniejszy rozmiar.
Na początku utwórz na dysku katalog dla naszego projektu np. Szablon. Następnie zrób w nim podkatalogi :
- data – będzie zawierał wszystkie tekstury, obiekty oraz muzykę
- dcu – w nim będą znajdować się skompilowane pliki źródeł.
- source – katalog dla kodu źródłowego (pliki pas, dpr). Utwórz w nim podkatalog library. W nim
znajdować się będą biblioteki opengl.
Teraz skopiuj gl.pas oraz glu.pas z archiwum do katalogu library.
W ten sposób przygotowaliśmy katalog dla naszego projektu. Nadszedł czas, aby uruchomić Delphi. W górnym menu wybierz File -> New -> Other … a następnie Console Application.
Musimy wyłączyć tryb konsoli, w tym celu usuwamy tekst {$APPTYPE CONSOLE}. W rezultacie powinniśmy zobaczyć taki oto kod :
1 2 3 4 5 6 7 8 |
program Project1; uses SysUtils; begin end. |
Klikamy teraz w górnym menu Project -> Options… -> Directories/Conditionals . Wpisujemy w opdowiednie pola :
Output directory : ../
Unit output directory : ../dcu/
Klikamy OK, następnie Save All. Wybieramy wcześniej utworzony katalog i w podkatalogu source zapisujemy nasz program.
2. WinApi – obsługa komunikatów, wywołanie okna
Skończyliśmy przygotowywać „podłoże” dla programu. Teraz zabierzemy się za coś bardziej skomplikowanego.
Utwórzmy trzy dodatkowe unity o nazwach : api_func.pas, render.pas, timer.pas i zapiszmy je w katalogu source.
Teraz w naszym projekcie musimy zadeklarować wszystkie pliki z których będziemy korzystać. Umieścimy dodatkowo wywołanie funkcji, która będzie obsługiwać cały program. Jej tłumaczeniem zajmiemy się trochę później.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
program nakiel_gl; uses windows, messages, sysutils, opengl, gl in 'librarygl.pas', glu in 'libraryglu.pas', api_func in 'api_func.pas', render in 'render.pas', timer in 'timer.pas'; begin WinMain(hInstance, hPrevInst, CmdLine, CmdShow); end. |
Otwórzmy przed chwilą utworzony plik api_func.pas. Wprowadź do niego kod :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
unit api_func; interface uses Windows, Messages, OpenGl, Gl, Glu; function WinMain(hInstance: HINST; hPrevInstance: HINST; lpCmdLine: PChar; nCmdShow: integer): integer; stdcall; var keys: array[0..255] of BOOL; // tablicy klawiatury h_RC: HGLRC; //kontekt rysowania OpenGl h_DC: HDC; //kontekst urządzenia implementation uses Render; |
Oprócz deklaracji plików w uses wprowadziliśmy trzy zmienne. Pierwsza zawiera tablicę klawiszy dwie następne są wymagane do wejścia w tryb OpenGl. Teraz dopiszmy procedurę, która będzie ustawiać początkowe parametry OpenGl.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
procedure GLInit(AWidth: GLsizei; AHeight: GLsizei); begin glEnable(GL_TEXTURE_2D); // Mapowanie tekstur 2D glClearColor(0, 0, 0, 0); // Kolor tla glClearDepth(1.0); // Czyszczenie bufora glEnable(GL_DEPTH_TEST); // Włącz testowanie głębokości glDepthFunc(GL_LEQUAL); // Rodzaj testowania głębokości : GL_LESS glMatrixMode(GL_PROJECTION); // ustawienie macierzy rzutowania glLoadIdentity(); // reset macierzy gluPerspective(45.0, AWidth / AHeight, 0.1, 500.0); // ustawianie perspektywy glMatrixMode(GL_MODELVIEW); // ustawienie widoku modelu glLoadIdentity(); // Reset macierzy end; |
Na początku uruchomiliśmy obsługę dwuwymiarowych tekstur. Następnie ustawiliśmy kolor tła na czarny.
W poszczególnych parametrach wpisujemy wartość od 0 do 1:
1 |
glClearColor(czerwony, zielony, niebieski, przezroczystość); |
Wyczyściliśmy bufor, włączyliśmy testowanie głębokości a jako jego rodzaj wybraliśmy GL_LEQUAL.
Oto wszystkie dostępne rodzaje :
– GL_NEVER – nigdy nie rysuje
– GL_ALWAYS – zawsze rysuje.
– GL_LESS – rysuje, jeżeli ( ref & mask) GL_LEQUAL – rysuje, jeżeli ( ref & mask) ? ( stencil & mask).
– GL_GREATER – rysuje, jeżeli ( ref & mask) > ( stencil & mask).
– GL_GEQUAL – rysuje, jeżeli ( ref & mask) ? ( stencil & mask).
– GL_EQUAL – rysuje, jeżeli ( ref & mask) = ( stencil & mask).
– GL_NOTEQUAL – rysuje, jeżeli ( ref & mask) 1 ( stencil & mask).
ref – Wartość odniesienia, którego zakres jest wyznaczany przez ilość bitów (2n – 1)
mask – Jest do maska, do której przyrównywane są piksele. Piksel jest rysowany tylko dla wartości 1 maski.
Poniższa procedura resetuje ustawienia OpenGl przystosowując go do nowego rozmiaru okna:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
procedure GLResizeScene(AWidth: GLsizei; AHeight: GLsizei); begin if (AHeight = 0) or (AWidth = 0) then exit; glViewport(0, 0, AWidth, AHeight); // Reset okna glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0, AWidth / AHeight, 0.1, 500.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); end; |
Zmienne AWidth i AHeight to szerokość i wysokość nowego okna. Resztę poleceń wytłumaczyliśmy przy opisywaniu poprzedniej procedury. Kolejną funkcją w api_func.pas będzie WndProc. Jest ona odpowiedzialna na obsługę komunikatów WinApi : informowanie o wciśniętych klawiszach, wywołuje tryb OpenGl, zamyka go przy wychodzeniu z programu, uruchamia przed chwilą stworzoną procedurę podczas zmiany rozmiaru okna. Szczegółowy opis poszczególnych poleceń znajduje się w komentarzach obok nich.
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
function WndProc(hWnd: HWND; message: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall; var Screen: TRECT; // prostokąt z rozmiarem ekranu (X,Y, X + Szerokość, Y + Wysokość) PixelFormat: GLuint; //format pikseli const pfd: PIXELFORMATDESCRIPTOR = ( nSize: sizeof(PIXELFORMATDESCRIPTOR); //wielkość nVersion: 1; // wersja dwFlags: PFD_DRAW_TO_WINDOW or PFD_SUPPORT_OPENGL or PFD_DOUBLEBUFFER; // udostępnienie podwójnego buforowania iPixelType: PFD_TYPE_RGBA; // typ koloru cColorBits: 32; // rozdzielczość koloru cRedBits: 0; // bity koloru (ignorowane) cRedShift: 0; cGreenBits: 0; cBlueBits: 0; cBlueShift: 0; cAlphaBits: 0; // wyłączenie bufora alfa cAlphaShift: 0; cAccumBits: 0; // wyłączenie akumulacji bufora cAccumRedBits: 0; // akumulowanie bitów (ignorowane) cAccumGreenBits: 0; cAccumBlueBits: 0; cAccumAlphaBits: 0; cDepthBits: 32; // wielkość bufora cStencilBits: 0; // bez buforu szablonu cAuxBuffers: 0; // bez buforu pomocniczego iLayerType: PFD_MAIN_PLANE; // główna powłoka bReserved: 0; dwLayerMask: 0; dwVisibleMask: 0; dwDamageMask: 0); // brak widoczności powłoki, zniszczenie maski begin Result := 0; case (message) of // Obsluga komunikatów WM_CREATE: // tworzenie okna dla OpenGl begin h_DC := GetDC(hWnd); PixelFormat := ChoosePixelFormat(h_DC, @pfd); //przypisanie formatu pikseli if (PixelFormat = 0) then begin MessageBox(0, 'Nie można znaleźć pasującego formatu piksela', 'Error', MB_OK or MB_ICONERROR); PostQuitMessage(0); // komunikat zamyka program exit; end; if (not SetPixelFormat(h_DC, PixelFormat, @pfd)) then // ustawianie formatu pikseli begin MessageBox(0, 'Nie można ustawić formatu pikseli', 'Error', MB_OK or MB_ICONERROR); PostQuitMessage(0); exit; end; h_RC := wglCreateContext(h_DC); //tworzenie kontekstu renderowania if (h_RC = 0) then begin MessageBox(0, 'Nie można utworzyć kontekstu renderowania', 'Error', MB_OK or MB_ICONERROR); PostQuitMessage(0); exit; end; if (not wglMakeCurrent(h_DC, h_RC)) then //aktywacja kontekstu renderowania begin MessageBox(0, 'Nie można uaktywnić kontekstu renderowania', 'Error', MB_OK or MB_ICONERROR); PostQuitMessage(0); exit; end; GetClientRect(hWnd, Screen); //pobieranie rozmiaru okna GLInit(Screen.right, Screen.bottom); // inicjacja OpenGl end; WM_DESTROY, WM_CLOSE: // zamykanie, niszczenie okna begin ChangeDisplaySettings(DEVMODE(nil^), 0); // przywracanie ustawień ekranu wglMakeCurrent(h_DC, 0); wglDeleteContext(h_RC); // usuwanie kontekstu renderowania ReleaseDC(hWnd, h_DC); PostQuitMessage(0); //komunikat wyjścia z programu end; WM_KEYDOWN: // reakcja na wciśnięcie klawisza begin keys[wParam] := TRUE; end; WM_KEYUP: // reakcja na puszczenie klawisza begin keys[wParam] := FALSE; end; WM_SIZE: // reakcja na zmianę rozmiaru okna begin GLReSizeScene(LOWORD(lParam), HIWORD(lParam)); // wywołanie procedury zmiany rozmiaru end; else begin // Przekazuje wszystkie nieprzetworzone wiadomości Result := DefWindowProc(hWnd, message, wParam, lParam); exit; end; end; end; |
Ostatnią funkcją w tym pliku jest WinMain. Spotkaliśmy się z nią na początku artykułu. Jest to „centrum zarządzania” naszego programu. W niej tworzymy okno, które następnie będzie wykorzystywane przez OpenGl, ustawiamy rozdzielczość ekranu.
Funkcja jest odpowiedzialna za wysyłanie i odbieranie komunikatów, które są interpretowane przez poprzednią funkcję WndProc.
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
function WinMain(hInstance: HINST; hPrevInstance: HINST; lpCmdLine: PChar; nCmdShow: integer): integer; stdcall; var msg: TMsg; // Komunikat wc: TWndClass; // Klasa okna h_Wnd: HWND; // Uchwyt dmScreenSettings: DEVMODE; kom1: shortstring; kom2: pchar; begin // zeruje zawartość ZeroMemory(@wc, sizeof(wc)); // styl okna wc.style := CS_HREDRAW or CS_VREDRAW or CS_OWNDC; wc.lpfnWndProc := @WndProc; wc.hInstance := hInstance; // kursor wc.hCursor := LoadCursor(0, IDC_ARROW); // nazwa klasy okna wc.lpszClassName := 'OpenGL WinClass'; if (RegisterClass(wc) = 0) then // próba rejestracji klasy begin // reakcja na nieudaną próbę rejestracji MessageBox(0, 'Wystąpil bląd podczas próby rejestracji klasy okna', 'Error', MB_OK or MB_ICONERROR); Result := 0; exit; end; // tworzneie okna h_Wnd := CreateWindow( 'OpenGL WinClass', // klasa 'basecode', // Tytul programu WS_POPUP or WS_CLIPCHILDREN or WS_CLIPSIBLINGS, 0, 0, // Pozycja okna na ekranie 800, 600, // Szerokosc i wysokosc okna 0, 0, hInstance, nil); if (h_Wnd = 0) then // jeżeli nie uchwyt będzie pusty .... begin MessageBox(0, 'Wystąpil bląd podczas próby tworzenia okna', 'Error', MB_OK or MB_ICONERROR); Result := 0; exit; end; // zerowanie struktury ZeroMemory(@dmScreenSettings, sizeof(DEVMODE)); dmScreenSettings.dmSize := sizeof(DEVMODE); dmScreenSettings.dmPelsWidth := 800; // Szerokosc dmScreenSettings.dmPelsHeight := 600; // Wysokosc dmScreenSettings.dmFields := DM_PELSWIDTH or DM_PELSHEIGHT; // rozdzielczość koloru ChangeDisplaySettings(dmScreenSettings, CDS_FULLSCREEN); // Przelącz na pełny ekran ShowWindow(h_Wnd, SW_SHOW); // wyświetl okno UpdateWindow(h_Wnd); // odśwież okno SetFocus(h_Wnd); // ustaw jako aktywne while (true) do // odbieranie i wysyłanie komunikatów begin while (PeekMessage(msg, 0, 0, 0, PM_NOREMOVE)) do begin if (GetMessage(msg, 0, 0, 0)) then begin TranslateMessage(msg); DispatchMessage(msg); end else begin Result := 1; exit; end; end; // Renderowanie sceny - procedura pochodzi z render.pas RenderScene; // reakcja na przycisk ESC - wyjście z programu if (keys[VK_ESCAPE]) then begin; SendMessage(h_Wnd, WM_CLOSE, 0, 0); end; end; end; |
3. Rendering sceny – wyświetlanie obiektów
Ufff … nareszcie koniec tego WinApi. Może to wyglądało dość skomplikowanie, ale w rzeczywistości nie dzieją się tam żadne cude. Ten kod niemalże w każdym projekcie wygląda identycznie, zmianie ulegają tylko pojedyncze parametry.
Przejdźmy teraz do rysowania i wyświetlania różnych elementów w OpenGl. Otwórz plik render.pas.
Jego górna część musi wyglądać następująco :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
unit render; interface // tradycyjne pliki uses Windows, OpenGl, Gl, Glu, Api_Func, Timer; //procedura renderująca scenę procedure RenderScene; //procedura zawiera elementy, które mają być wyświetlane procedure RenderFrame; var j, t, x, y, z: real; i: integer; implementation |
Poniższa procedura czyści ekran i wyświetla grafikę:
1 2 3 4 5 6 7 8 |
procedure RenderScene; begin glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT); // czyszczenie buforow koloru i glebokosci glLoadIdentity(); FrameMath; //procedura przeliczająca pozycję dla rysowanych elementów RenderFrame; //procedura rysująca elementy SwapBuffers(h_DC); end; |
Ostania procedura w tym pliku odpowiedzialna jest za rysowanie elementów sceny. Oto ona:
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 |
procedure RenderFrame; begin //Obrót glTranslatef(0.0, 0.0, -12.0); // uruchamiamy tryb rysowania trójkątami glbegin(GL_TRIANGLES); for i := -5 to 5 do begin j := j + 0.1; glColor3f(1 - x, 0.0, x); // kolor dla danego wierzchoła glVertex3f(x + i, sin(j), z); // pozycja wierzchołka j := j + 0.1; glColor3f(0.0, 0.0, 1.0); glVertex3f(-x + i, sin(j), z); j := j + 0.1; glColor3f(1.0, 0.0, 0.0); glVertex3f(i, sin(j) + 1, z); //-------------------------------- j := j + 0.1; glColor3f(1 - x, 0.0, x); glVertex3f(x + i, sin(j), z); j := j + 0.1; glColor3f(0.0, 0.0, 1.0); glVertex3f(-x + i, sin(j), z); j := j + 0.1; glColor3f(1.0, 0.0, 0.0); glVertex3f(i, sin(j) - 1, z); end; glEnd(); end; |
Nie będę opisywał szczegółowo, co dzieje się w pętli. Jak uruchomicie program to zobaczycie. Oto jakie parametry ustawiamy w GlColor3f :
1 |
glColor3f(czerowny, zielony, niebieski); |
Każdy kolor przyjmuje wartości od 0 do 1. Jeżeli chcemy dodatkowo dodać kanał Alfa, odpowiedzialny za przezroczystość musimy użyć polecenia:
1 |
glColor4f(czerowny, zielony, niebieski, alfa); |
glVertex3f ustawia współrzędne przesunięcia dla wierzchołka. np.
1 |
glVertex3f(0.0,-5.0,0.0); |
rysuje wierzchołek przesunięty w górę o -5.
Szczegółowo prymitywy (podstawowe elementy w OpenGL) omówimy w następnym artykule.
3. Timer – obliczenia wykonywane przed wyświetleniem każdej klatki
Otwórzmy plik timer.pas. W nim będziemy umieszczać wszystkie procedury oraz funkcje odpowiedzialne za wszelkiego rodzaju obliczenia. Oto cały kod w pliku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
unit timer; interface procedure FrameMath; implementation uses Render; procedure FrameMath; begin t := t + 0.1; x := sin(t); y := cos(t); z := 5 * sin(t); end; end. |
Na razie znajduje się w nim tylko jedna procedura obliczająca pozycję dla wierzchołków wyświetlanych w RenderFrame.
Na tym zakończymy ten artykuł. Zapewne zauważyłeś, że został nałożony duży nacisk na porządek w kodzie. Oddzielne są pliki dla grafiki, obliczeń oraz winapi. Na tym nie koniec. Wkrótce dołączymy pliki odpowiedzialne za obsługę klawiatury, myszki itd. Każde następne artykuły będą kontynuacją tego, opuścimy w nich wszelkie wiadomości opisane tutaj, a zajmiemy się wprowadzaniem nowych możliwości.