Ćwiczenia 1

Pierwsze uruchomienie

Kolejne zadania będą dostarczane jako projekty w visual studio, które powinny być otwierane jako część jednego solution. Żeby uruchomić zadania należy pobrać i rozpakować solution znajdujące się pod adresem /grk/do_pobrania/grk.zip. Zawiera ono wszelkie potrzebne zależności. Następnie pobierz projekt dla określonego zadania (w tym wypadku zadania 1 znajdujące się /grk/do_pobrania/cw1.zip) i wkleić zawartość do folderu z Solution.

Struktura powinna wyglądać nastęująco:

Otwórz plik grk-cw.sln w visual studio, ustaw projekt z ćwiczeniami jaki projekt startowy. Poszczególne zadania są dostarczane jako osobne pliki z rozszerzeniem hpp, żeby uruchomić zadanie zmień ostatni include.

Opis projektu

W zadaniach korzystamy z kilku bibliotek, mianowicie:

  • GLEW OpenGL Extension Wrangler Library - biblioteka odpowiedzialna za ładowanie opengla, umożliwia zdeterminowanie jakie wersje opengla są obsługiwane na maszynie i jakie rozszerzenia są dostęne.
  • GLFW Graphics Library Framework - biblioteka umożliwiająca tworzenie okien i obsługę wejścia użytkownika. Pozwala na stworzenie więcej niż jednego okna i posiada obsługę nie tylko klawiatury i myszy, ale również padów i joysticków.
  • GLM OpenGL Mathematics - biblioteka matematyczna.

Omówienie pliku main.cpp

Rozpocznijmy od funkcji main.

#include "glew.h"
#include <GLFW/glfw3.h>
#include "glm.hpp"

#include "Shader_Loader.h"
#include "Render_Utils.h"
#include "ex_1_1.hpp"



int main(int argc, char ** argv)
{
    // inicjalizacja glfw
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // tworzenie okna za pomocą glfw
    GLFWwindow* window = glfwCreateWindow(500, 500, "FirstWindow", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    // ladowanie OpenGL za pomoca glew
    glewInit();
    glViewport(0, 0, 500, 500);
    
    init(window);

    // uruchomienie glownej petli
    renderLoop(window);

    shutdown(window);
    glfwTerminate();
    return 0;
}

W pierwszych trzech instrukcjach inicjalizujemy glfw za pomocą glfwInit(). Za pomocą glfwWindowHint ustawiamy wersję opengla na 3.3. Ta funkcja pozwala na ustawienie różnych opcji. Pełną listę można zobaczyć tutaj. Należy je wykonać przed utworzeniem okna.

Okno tworzymy funkcją glfwCreateWindow, jako argumenty przekazujemy rozmiar okna i jego tytuł. Czwarty argument służy do ustawienia, na którym monitorze ma się okno pokazać, jeżeli będzie uruchomione w pełnym ekranie. Natomiast ostatni do przekazania kontekstu okna, jeżeli ma je współdzielić z innym. Nie będziemy korzystać z tych opcji, więc ustawiamy je na null. Funkcja zwraca wskaźnik na okno, który będzie nam potrzebny, żeby cokolwiek z nim zrobić. Ustawiamy okno na current za pomocą funkcji glfwMakeContextCurrent. W ten sposób przypisujemy okno do danego wątku.

glewInit inicjalizuje opengla a glViewport przekazuje jaki jest rozmiar okna do opengla.

Następne 3 funkcje init, renderLoop i shutdown są naszymi funkcjami, które znajdują się w plikach z zadaniami (nazwa plik ex_X_Y.hpp to X to numer ćwiczeń a Y to numer zadania).

Omówienie pliku ex_1_1.hpp

W funkcjach init i shutdown będziemy umieszczać instrukcje, które mają być wykonane raz przy odpowiednio uruchomieniu i wyłączeniu aplikacji.

W funkcji init znajduje się jedna instrukcja, która ustawia, że framebuffer_size_callback zostanie wywołana przy zmianie rozmiaru okna. Ta z kolei informuje opengla o zmianie rozmiaru ekranu za pomocą glViewport

Natomiast renderLoop jest funkcją, która ma zawierać główną pętlę i wygląda następująco

void renderLoop(GLFWwindow* window) {
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);

        renderScene(window);
        glfwPollEvents();
    }
}

pobiera ona okno i wykonuje na nim instrukcje w pętli, dopóki nie dostanie informacji, że ma być ono zamknięte. W tej chwili w pętli znajdują się dwie instrukcje odpowiedzialne za przetworzenie wejścia (czyli obsługę klawiatury i myszy) oraz odświeżenie sceny oraz glfwPollEvents(), która sprawdza czy są jakieś zadania do wykonania (na przykład sprawdza, czy rozmiar okna został zmieniony).

void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

W processInput sprawdzamy tylko czy wciśnięto Esc, jeżeli tak, to ustawiamy, że aplikacja powinna być zamknięta

void renderScene(GLFWwindow* window)
{

    // ZADANIE: Przesledz kod i komentarze
    // ZADANIE: Zmien kolor tla sceny, przyjmujac zmiennoprzecinkowy standard RGBA
    glClearColor(0.0f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // Powinno byc wywolane po kazdej klatce
    glfwSwapBuffers(window);
}

Natomiast w render scene ustawiamy kolor za pomocą funkcji glClearColor i czyścimy bufory za pomocą glClear, openGL korzysta z informacji, która przekazaliśmy w glClearColor, żeby określić jakiego koloru ma być tło. By uniknąć migotania glfw wykorzystuje double buffering to znaczy przechowuje dwa bufory, jeden jest wyświetlany a na drugim dokonuje się operacji (określa się je front i back). Po zakończeniu rysowania zamienia się je miejscami (swap). Robimy to za pomocą funkcji glfwSwapBuffers.

Zadanie 1

Zmień kolor tła na dowolny inny.

Zadanie 1b*

Zmodyfikuj funkcję render scene tak, żeby kolor się zmieniał co klatkę. Możesz do tego skorzystać ze zmiennej globalnej lub czas działania, który możesz uzyskać funkcją glfwGetTime().

Ćwiczenie 1_2

Celem tego cwiczenia będzie narysowanie trójkąta. Zanim przejdziesz dalej, zamień linię #include "ex_1_1.hpp" na #include "ex_1_2.hpp". Przejdź do pliku ex_1_2.hpp

Opengl operuje w kostce od $[-1,1]\times[-1,1]\times[-1,1]$, którą następnie patrzy wzdłóż osi Z. (nazywamy tą przestrzeń clip space więcej o tym na wykładzie). Pozostałe osie rozciąga na ekranie. Dodatkowo poza współrzędnymi X, Y, Z potrzebna jest wspórzędna W, która musi być równa 1. Znaczy to, że każdy punkt w trójkącie musi mieć cztery współrzędne, dwie pierwsze z nich zakresu od -1 do 1, trzecia równa 0 a ostatnie zawsze równa 1. Kolejność punktów jest również istotna, dzięki niej opengl określa orientację ścian. Domyślnie punkty powinny być zorientowane w kierunku przeciwnym do ruchu wskazówek zegra.

Zadanie 2

Wymyśl 3 punkty dla trójkąta i umieść je w płaskiej tablicy 12 floatów wewnątrz funkcji init. Stworzoną tablicę należy załadować do pamięci karty graficznej przy inicjalizacji. Wykorzystaj do tego funkcję pomocniczą Core::initVAO, jako pierwszy argument podaj tablicę, jako drugi liczbę wierzchołków a jako trzeci liczbę punktów w wierzchołku. Funkcja zwraca zmienną typu GLuint, która jest identyfikatorem VAO w pamięci karty, przypisz go do zmiennej globalnej triangleVAO.

W tym zadaniu w funkcji init dochodzi kopilacja shaderów i połączeinu ich w shader program. Jest to program, który posłuży nam do wyświetlenia trójkątów. “Shader” to specjalny typ programu używanego w grafice komputerowej do renderowania obrazów. Działa na poziomie pikseli lub wierzchołków i jest używany do określenia, jak obiekty powinny być rysowane na ekranie, w tym ich koloru, jasności i innych efektów wizualnych (więcej o shaderach będzie na późniejszych zajęciach).

W renderScene wykorzystaj funkcję Core::drawVAO do narysowania trójkąta.

Ćwiczenie 1_3

Celem tego zadania będzie narysowanie czworokąta. Możemy to zrobić poprzez narysowanie dwóch trójkątów, ale jest nieefektywne, ponieważ powielimy dwa pounkty podwójnie (teraz nie jest to taka duża różnica, ale przy większej liczbie trójkątów jest bardziej istotne). Zamiast tego skorzystamy z indeksowania, czyli oprócz tablicy wierzchołków prześlemy też tablicę indeksów, które będą określać jakie punkty należy wykorzystać do rysowania wierzchołków.

Zadanie 3

Tym razem wymyśl 4 punkty i umieść je w analogicznej tablic 16 floatów. Oprócz tego stwórz tablicę typu unsigned int i umieść w niej indeksy dla wóch trójkątów, które będą tworzych nasz czworokąt. Przekarz obie tablice do GPU za pomocą funcji Core::initVAOIndexed. Następnie w funcji renderScene użyj funkcji Core::drawVAOIndexed analogicznie jak Core::drawVAO w poprzednim zadaniu.

Ćwiczenia 1_4, 1_5, 1_6

Celem tych ćwiczeń jest wykorzystanie macierzy transformacji do przesuwania i obracania czworokąta definicje macierzy transformacji znajdziesz pod linkiem. Wykorzystaj definicję czworokąta z poprzedniego zadania. Zwróć uwagę na kolejność mnożenia macierzy.

Uważaj, konstruktor mat4 czyta tablicę float kolumnami, czyli domyślnie będzie ona transponowana w stosunku do tego co jest na ekranie. Aby temu zapobiec używamy operacji transpozycji zanim użyjemy funkcji, żeby to zniwelować.

Zadanie 4

Wykonaj zadania opisane w plikach ex_1_4.hpp,ex_1_5.hpp,ex_1_6.hpp,

Biblioteka GLM

OpenGL Mathematics (GLM) jest biblioteką matematyczną. Posiada to samo nazewnictwo co język shaderów GLSL, dzięki temu zaznajomienie się z jednym pomaga w obsłudze drugiego. Zawiera nie tylko funkcje znane z GLSL, ale jest również rozszerzony o dodatkowe możliwości jak obsługę macierzy przekształceń, kwaternionów, liczb losowych czy szum. Dokumentacja znajduje się pod linkiem https://glm.g-truc.net/0.9.9/api/modules.html. Ma ona opcję szukania, która pozwala na znalezienie konkretnych funkcji (choć czasem lepiej szukać na tej stronie przez inne wyszukiwarki np google). Przykładowo zobaczmy opis tyczący się transformacji, dostępny pod https://glm.g-truc.net/0.9.9/api/a00779.html. Na pierwszy rzut oka funkcje te wyglądają dość enigmatycznie, weźmy funkcję

mat< 4, 4, T, Q > translate (vec< 3, T, Q > const &v)

służy do tworzenia macierzy translacji. Przyjmuje ona jeden argument typu vec< 3, T, Q >, który jest wektorem translacji i zwraca zmienną typu mat< 4, 4, T, Q >, która jest macierzą translacji. Są to typy szablonowe, które w C++ umożliwiają programowanie generyczne. Więcej informacji o szablonach znajdziesz (tutaj) . Parametry szablonu są zapisywane między nawiasami klamrowymi, mogą to byś typy zmiennych lub inne parametry. W przypadku typu vec szablon przyjmuje 3 argumenty: pierwszym z nich jest liczba komponentów wektora (w tym przypadku trzy), drugim jest typ danych komponentów a trzecim stopień precyzji, dwa ostatnie są “dowolne”. Natomiast mat szablon przyjmuje 4 argumenty: dwa pierwsze określają kształt macierzy, dwa kolejne jak poprzednio określają typ danych i stopień precyzji. Zauważmy też, że T i Q są takie same w typach zmiennych na wejściu i wyjściu funkcji.

“Template” (w języku C++ nazywane też “szablonem”) to mechanizm w programowaniu, który pozwala na tworzenie funkcji lub klas, które mogą pracować z różnymi typami danych bez konieczności ich wielokrotnego definiowania. Umożliwia to tworzenie bardziej elastycznego i wielokrotnego użytku kodu.

W praktyce glm posiada już zdefiniowane typy macierzowe i wektorowe w oparciu te szablony, dzięki nie musimy myśleć co się za nimi kryje będą nas interesować poniższe:

namespace glm{
...
//wektory
typedef vec< 2, float, defaultp > vec2
typedef vec< 3, float, defaultp > vec3
typedef vec< 4, float, defaultp > vec4
...
//macierze
typedef mat< 2, 2, float, defaultp > 	mat2
typedef mat< 3, 3, float, defaultp > 	mat3 
typedef mat< 4, 4, float, defaultp > 	mat4
...
}

Jak zastąpimy generyczne typy z opisu translate typami powyżej otrzymamy

glm::mat4 glm::translate (glm::vec3 const &v)

Podobnie z funkcjami rotate i scale, których opis znajdziemy pod linkiem. Funkcja rotate przyjmuje kąt i wektor, który jest osią obrotu. może ona być trudna do użycia w niektórych wypadkach, łatwiejsza w użyciu może być implementacja obrotów za pomocą kątów eulera np korzystając z funkcji glm::eulerAngleXYZ (link).

Zadanie 5

W pliku ex_1_7.hpp powtórz zadanie z ex_1_6.hpp ale zamiast samodzielnie pisać macierze wykorzystaj funkcje z glm

UWAGA macierze w glm są definiowane kolumnowo, to znaczy nie dodawaj operacji transponowania jak to miało miejsce w poprzednich zadaniach.

Obsługa wejścia przez GLFW

Za obsługę urządzenia wejścia, jak klawiatura myszka, pady czy joysticki, odpowiada biblioteka GLFW. Jest to opisane w dokumentacji pod linkiem https://www.glfw.org/docs/latest/group__input.html

Obsługa klawiatury

Klawiaturę obsługujemy w funkcji processInput:

void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){
        glfwSetWindowShouldClose(window, true);
	}

Aby sprawdzić czy przycisk został wciśnięty musimy odpytać funkcję glfwGetKey. Pierwszym argumentem jest okno, natomiast drugim kod przycisku, listę wszystkich kodów można znaleźć pod linkiem https://www.glfw.org/docs/latest/group__keys.html.

Zadanie 6

W pliku ex_1_7.hpp zaimplementuj przesuwanie kwadratu za pomocą przycisków strzałek. Zdefiniuj zmienną globalną glm::vec3 quadPos, ustaw go jako wektor translacji. Następnie w funkcji processInput dodaj zapytania o przyciski strzałek (kody znajdują się poniżej) i przesunięcia w górę, dół, prawo i lewo.

  • GLFW_KEY_UP
  • GLFW_KEY_DOWN
  • GLFW_KEY_RIGHT
  • GLFW_KEY_LEFT

Zadanie 6b*

Zrób dodatkowo obracanie czworościanu w prawo i w lewo wokół własnej osi za pomocą przycisków O i P.

Obsługa myszy

“Callback” to funkcja, która jest przekazywana jako argument do innej funkcji i jest wywoływana w odpowiedzi na pewne zdarzenie. W praktyce pozwala to na definiowanie, jak program powinien reagować na określone akcje, takie jak kliknięcie przycisku myszy czy naciśnięcie klawisza.

Mysz obsługujemy za pomocą funkcji zwrotnych (callbacków), wykorzystamy dwa

  • cursor position callback - wywoływana, gdy ruszamy myszką na ekranie funkcja, którą podpinamy, to cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
  • mouse button callback - wywoływana, gdy wciskamy przycisk. którą podpinamy, to mouse_button_callback(GLFWwindow* window, int button, int action, int mods)

Wykorzystanie callbacków gwarantuje nam, że operacje będą wykonywane tylko wtedy, gdy tego chcemy. Nie gwarantowałaby nam tego funkcja processInput.

Zadanie 7

Zakomentuj rozwiązanie zadania 6. Zmodyfikuj kod z w ex_1_7.hpp, żeby kwadrat znajdował się w miejscu z myszką. wewnątrz funkcji cursor_position_callback przypisz do współrzędnych x i y quadPos wartości zmodyfikowane xpos i ypos. Zmienne xPos i yPos są opisane w przestrzeni okna. Ich wartość są z przedziałów odpowiednio \([0,\text{width}]\) do \([0,\text{height}]\), gdzie height i width to rozmiar ekranu w pikselach, a punkt \((0,0)\) to lewy górny róg ekranu (co w szczególności oznacza, że oś \(Y\) jest skierowana w dół). Musimy je przekonwertować do współrzędnych z przestrzeni ekranu, które są z przedziału \([-1,1]\), a oś \(Y\) jest skierowana w górę. Możemy to wykonać za pomocą wzorów. $$x’=\frac{2x}{width}-1$$ $$y’=-\left(\frac{2y}{height}-1\right)$$

Rozmiar ekranu w pikselach to 500 na 500.

Zadanie 7b*

Zakomentuj rozwiązanie poprzedniego zadania. Na bazie jego dodaj funkcjonalność, polegającą na tym, że po wciśnięciu lewego przycisku myszy nowy czworokąt pojawi się w miejscu, w którym kliknęliśmy. Aby to zrobić uzupełnij funkcję mouse_button_callback, wykorzystaj std::vector<glm::vec3> quadsPositions umieszczaj w niej nowe pozycje kwadratów, następnie narysuj je w pętli wewnątrz renderScene. Pamiętaj, żeby w mouse_button_callback sprawdzić czy wyciśnięto lewy przycisk, sprawdź czy wartości button i action są odpowiednie.

Obsługa pada**

GLFW obsługuje również pady i joysticki, jest możliwość obsługi do 16 urządzeń tego typu. Opisane to zostało w pod linkiem https://www.glfw.org/docs/latest/input_guide.html#joystick.

Zadanie 8**

Uzależnij szybkość obrotów czworokąta od stopnia wciśnięcia lewego spustu (trigger) pada.

Dalsza lektura i zasoby:

  • Oficjalna dokumentacja GLM: link
  • Tutorial GLFW: link