[OpenGL] Funkcje, kamera, wektory i kolejna biblioteka
Tytuł wydaje się długi i straszny. Postaram się przedstawić wszystko w miarę prosto i klarownie. Cel na dzisiaj to obsługa kamery za pomocą myszki i klawiatury. Będzie trochę działania na wektorach. Dołączam więc matematyczną bibliotekę GLM, która ułatwia operacje na nich oraz macierzach. Może to lekkie wybieganie w przyszłość, ale obsługę kamery opakuję w klasę. Pozwoli to na łatwe tworzenie oraz zarządzanie nimi w tym tworzenie kilku zdefiniowanych kamer i szybkie przełączanie się między nimi.
Słowem wstępu
Jak już wcześniej zostało powiedziane, wszystkie zmiany w kodzie są zapisywane i umieszczane w repozytorium na github.com. Będę dodawał nowe commity w miarę dodawania nowych funkcji, zmiennych i modyfikacji. Czyli ogólnie pojętej funkcjonalności.
OpenGL Mathematics (GLM)
Jest to bardzo przydatna biblioteka. Przynajmniej moim zdaniem :) Posiada ona wbudowane funkcje i klasy pozwalające manipulować zarówno macierzami jak i wektorami. Warto dodać, że jest to tak zwana biblioteka nagłówkowa. Oznacza to nie mniej, nie więcej, iż wystarczy dołączyć odpowiedni plik nagłówkowy i odpowiednia część biblioteki jest już gotowa do użycia. Pliki takich bibliotek zazwyczaj mają rozszerzenie .hpp, które jest swoistym połączeniem .h oraz .cpp. Instalacja sprowadza się do skopiowania z paczki folderu glm do naszego projektu, aby znajdował się on w includepath naszego projektu. Pamiętasz macro $(ProjectDir)include z wcześniejszego wpisu? U mnie odnosi się ono do folderu E:\code_remote\OGL\OpenGL\OpenGL\include i tam kopiuje bibliotekę.
Rzeczą ważną wspomnienia jest jeszcze sposób ładowania macierzy na stos macierzy w OpenGL. Do tej pory nie było to wymagane, ponieważ macierz projekcji była kasowana, tworzona i modyfikowana za pomocą wbudowanych funkcji.
glMatrixMode(GL_PROJECTION); // Aktualnie modyfikowana macierz, to macierz projekcji glLoadIdentity(); // Ustawia aktualną macierz na jednostkową glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); // Wprowadza modyfikacje
Teraz natomiast będziemy wykorzystywać macierzy z biblioteki GLM i wczytywać je na stos. Odbywa się poprzez przekazanie adresu zmiennej, którą chcemy wczytać.
glm::mat4 projekcja_ortho = glm::ortho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0); // Utworzenie zmiennej glMatrixMode(GL_PROJECTION); // Aktualnie modyfikowana macierz, to macierz projekcji glLoadMatrixf(&projekcja_ortho[0][0]); // Wczytanie macierzy na stot
Kamera
W poprzednim wpisie dotyczącym OpenGL, kamera, która została użyta wyświetlała obraz bez perspektywy. Jest to przydatny tryb kamery na przykład do wyświetlania interfejsu. Oczywiście jest tez funkcja tworząca kamerę z widokiem perspektywicznym. Biblioteka GLM obsługuje obydwie możliwości dzięki dwóm funkcjom glm::ortho(left, right, bottom, top, zNear, zFar) oraz glm::perspective(fov, aspect, zNear, zFar)
Pierwszą klasą, którą się zajmiemy będzie kamera bez perspektywy, ponieważ jest ona krótsza. Będzie ona zawierać zmienne potrzebne do inicjalizacji macierzy odpowiedniej projekcji, funkcje ustawienia zmiennych oraz załadowanie macierzy na stos, macierz projekcji oraz widoku.
#include <GL\freeglut.h> #include <glm\glm.hpp> #include <glm\gtc\matrix_transform.hpp> class Camera_o { protected: float left; float right; float top; float bottom; float znear; float zfar; glm::mat4 projection; glm::mat4 view; void Ortho(void); public: Camera_o(void); ~Camera_o(void); void Activate(void); glm::mat4 GetView(void); void SetOrtho(void); void SetOrtho(float, float, float, float, float, float); };
Funkcje te są dość proste, jednak Ortho i Activatemogą wymagać wyjaśnienia. Pierwsza z nich spełnia zadanie utworzenia macierzy projekcji ze zmiennych zawartych w klasie.
void Camera_o::Ortho(void){ projection = glm::ortho(left, right, bottom, top, znear, zfar); }
Druga natomiast zamieszcza wyżej utworzoną macierz na odpowiednim stosie w OpenGL.
void Camera_o::Activate(void){ Ortho(); glMatrixMode(GL_PROJECTION); glLoadMatrixf(&projection[0][0]); glMatrixMode(GL_MODELVIEW); }
Jak można zauważyć, wywoływana jest poprzednia funkcja w celu upewnienia się, że macierz jest zbudowana na podstawie uaktualnionych zmiennych.
void Camera_o::Ortho(void){ projection = glm::ortho(left, right, bottom, top, znear, zfar); }
Szczypta matematyki
Klasa odpowiadająca za kamerę z perspektywą będzie odrobinę bardziej skomplikowana ponieważ będzie przechowywała informacje o położeniu kamery, wektorach kierunkowych globalnych (skierowanych 3 strony "świata": w górę, do przodu i w prawo), wektorach kierunkowych lokalnych (skierowanych w 3 strony kamery: w górę, do przodu oraz w prawo), macierze projekcji oraz widoku, obroty kamery w okół osi, położenie oraz kierunek. Dodatkowo będą metody to wszystko ustawiające, wyliczające, a nawet wyświetlające zatem sam nagłówek klasy składa się z niecałych 80 linii.
#pragma once #include <GL\freeglut.h> #include <glm\glm.hpp> #include <glm\gtc\matrix_transform.hpp> #include <glm\gtx\quaternion.hpp> #include <glm\gtx\vector_angle.hpp> class Camera_p { protected: float fov; float aspect; float znear; float zfar; glm::mat4 projection; glm::mat4 view; glm::vec3 eye; glm::vec3 target; glm::vec3 direction; glm::vec3 up; glm::vec3 front; glm::vec3 right; glm::vec3 glob_front; glm::vec3 glob_right; glm::vec3 glob_up; glm::vec3 angles; void LookAt(void); void Perspective(void); void Calculate(void); void Rotations(void); public: Camera_p(void); ~Camera_p(void); void Activate(void); glm::mat4 GetView(void); void SetLookAt(void); void SetLookAt(glm::vec3); void SetLookAt(glm::vec3, glm::vec3); void SetLookAt(glm::vec3, glm::vec3, glm::vec3); void SetPerspective(void); void SetPerspective(float, float, float, float); void SetAspect(float); void SetPosition(glm::vec3); void SetDirection(glm::vec3); void SetTarget(glm::vec3); glm::vec3 GetPosition(void); glm::vec3 GetDirection(void); glm::vec3 GetTarget(void); glm::vec3 GetUp(void); glm::vec3 GetFront(void); glm::vec3 GetRight(void); void Rotate(glm::vec3); void Translate(glm::vec3); void Render(void); void RenderNormals(void); };
Eye, target i odpowiadają za położenie oraz skierowanie kamery w odpowiednie miejsce. Powiązane są z nimi angles oraz direction ponieważ determinują one obrót kamery wokół osi.
Moim zdaniem na szczególna uwagę zasługuje tylko kilka funkcji.
[img=cross] Idąc od góry pierwszą z nich jest Calculate(), ma ona za zadanie wyliczenie lokalnych wektorów kierunkowych na podstawie kierunku (zmienna direction) w który zwrócona jest kamera. Są one wynikami operacji
void Camera_p::Calculate(void){ right = glm::normalize(glm::cross(direction, glob_up)); up = glm::normalize(glm::cross(right, direction)); front = glm::normalize(glm::cross(glob_up, right)); target = eye + direction; }
Następną funkcją, do której warto zajrzeć jest Rotations() definiująca kąty między kierunkiem kamery a ujemną częścią osi z (czyli wektorem x=0 y=0 z=‑1). Jest to wektory wybrany arbitralnie, ponieważ jedynym wymogiem jest, aby był on stały przez cały czas wykonywania programu.
void Camera_p::Rotations(void){ angles.x = glm::angle(direction, glob_front); angles.y = glm::angle(direction, -glob_front); }
[img=angles] Ustawianie wektorów, czy zwracanie ich na zewnątrz klasy nie powinno nikomu sprawić problemów dlatego też do funkcji, która na początku nauki może zakręcić w głowie. Mianowicie chodzi o Rotate(glm::vec3). Najbardziej intuicyjną metodą na obrót wektora jest obrócenie go o jakiś przyrost kąta względem odpowiedniej osi, prawda? Przynajmniej mi przyszło coś takiego do głowy na samym początku :) Przekazywałem wtedy do funkcji o ile chciałem obrócić wektor direction na osi x (odpowiedzialnej za pochylenie do przody) lub osi y (odpowiedzialnej za obrót lewp/prawo) i modyfikowałem odpowiednio wektor. Tak, to jest rozwiązanie lecz prowadzi ono do problemu zwanego gimbal lock. Dla osób dobrze radzących sobie z angielskim polecam zapoznanie się z wikipedią lub ten film. W telegraficznym skrócie gimbal lock polega na takim doborze kątów, iż w pewnym momencie któraś z osi zostaje obracana przez inną. Można to zauważyć na filmie.
Jednym z rozwiązań tego problemu są Kwaterniony :) Technicznie jest to macierz. Natomiast ja to widzę jako obrót w dowolnym kierunku. Nie kierunek, ponieważ kierunek ma swój punkt zaczepienia jak każdy wektor, kwaternion tego nie posiada. Kwaternion można zbudować na podstawię kąta oraz osi wokół której ten obrót będzie. Dlatego potrzebna nam zmienna do przechowywania kątów obrotu. Obroty, a co za tym idzie i kierunek obliczane oddzielnie dla każdej osi globalnej na podstawię kąta. Następnie otrzymane dwa kwaterniony są mnożone przez siebie dając już prawie oczekiwany przez nas nowy kierunek kamery. Pozostaje tylko pomnożyć nasz kwaternion przez wektor determinujący "przód" i mamy nasz nowy kierunek kamery. Ostatnim krokiem jest aktualizacja pozostałych wektorów oraz macierzy. Jako ciekawostkę zostawiam pierwsze rozwiązanie.
void Camera_p::Rotate(glm::vec3 rotate){ angles -= rotate; glm::quat yaw = glm::angleAxis(angles.y, glob_up); glm::quat pitch = glm::angleAxis(angles.x, glob_right); glm::quat res = yaw * pitch; direction = res * glob_front; /* // uwaga na gimbal lock direction = glm::rotate(direction, -rotate.y, up); direction = glm::rotate(direction, -rotate.x, right); */ Calculate(); LookAt(); }
Mając w taki sposób opisane kamery możemy już znacznie prościej nimi manipulować czy rozszerzać ich funkcjonalność w nowych klasach, oraz chyba co najważniejsze uprościć kod funkcji wyświetlającej. Możecie zauważyć, że w lewym dolnym roku jest utworzony drugi widok, w którym wyświetlane są lokalne kierunki kamery, tak samo jak w głównym widoku.
Obracać widokiem możecie za pomocą lewego przycisku myszy, a poruszać się klawiszami w, s, a, ,d ,q ,e. Zachęcam do eksperymentowania z kodem :) Kod z dzisiejszego wpisu znajdziecie w tym commicie.
Do następnego razu, ciao :)