Ćwiczenia 2

Ładowanie VAO i VBO

W trakcie tych zajęć przećwiczymy ładowanie danych do pamięci. Tym razem, zamiast korzystać z gotowej funkcji utworzymy je samodzielnie. W openglu wykorzystujemy do tego VBO (Vertex Buffer Object) i VAO (Vertex Array Object). Pierwsza jest buforem, który zawiera dane modeli. Natomiast drugi zawiera informacje jak dane bufory interpretować.

W zadaniu 2_1 będziemy przesyłać prostopadłościan. Tablica zawierająca jego definicję jest w pliku Box.cpp. Każdy wierzchołek składa się z ośmiu floatów, pierwsze cztery określają jego pozycję, a cztery kolejny określają jego kolor.

Inicjalizacje będziemy wykonywać wewnątrz funkcji init. Pierwszym krokiem jest wygenerowanie jednego VAO i jednego VBO. Wykorzystuje się do tego odpowiednio funkcje glGenVertexArrays(GLsizei n, GLuint *arrays) i glGenBuffers(GLsizei n, GLuint *buffers). Pierwszym argumentem jest liczba buforów czy array object, które tworzymy, drugim jest adres, w którym ma być bufor/array object umieszczony. W naszym przypadku pierwszym argumentem będzie 1, natomiast drugim będzie wskaźnik na zmienną VAO i VBO odpowiednio. Następnie należy aktywować lub ‘wiązać’ VAO za pomocą funkcji glBindVertexArray, co oznacza, że wszelkie operacje dotyczące VAO będą miały wpływ na ten konkretny obiekt. Do akrtywowanego VAOpodpinamy bufor VBO za pomocą glBindBuffer(GLenum target, GLuint buffer) nasz target to GL_ARRAY_BUFFER, czyli bufor, który oznacza atrybuty wierzchołków.

Kolejnym krokiem jest umieszczenie danych w buforze za pomocą funkcji glBufferData(GLenum target, GLsizeiptr size, const void * data, GLenum usage). Pierwszym argumentem jest ponownie GL_ARRAY_BUFFER, drugi to rozmiar tablicy w bajtach (w naszym przypadku, statycznej tablicy wystarczy użyć sizeof(box) do uzyskania wielkości w bajtach, w przypadku dynamicznej tablicy lub vektora należy pomnożyć liczbę elementów przez rozmiar typu), trzecim adres tablicy, a czwartym sposób używania tablicy, w naszym przypadku GL_STATIC_DRAW.

Pozostaje opisanie atrybutów wierzchołków, musimy opisać, gdzie się znajdują, jaką mają strukturę i jak ma się do nich odnieść shader. My mamy 2 atrybuty, jest to pozycja i kolor. Pierwszym krokiem jest aktywacja atrybutów za pomocą glEnableVertexAttribArray(GLuint index), przy czym po indeksie będą one odnajdywane przez shader. W naszym przypadku będą to odpowiednio 0 i 1. Następnie należy opisać jak GPU ma odczytywać atrybuty z bufora za pomocą funkcji glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * offset), która mówi GPU, jak interpretować dane w buforze. Pozwala to na precyzyjne określenie, jakie części danych są używane do różnych atrybutów wierzchołka. Jej argumenty to kolejno:

  • index - indeksy odpowiadające atrybutowi,
  • size - liczba elementów w atrybucie wierzchołka, może wynosić 1, 2, 3 lub 4,
  • type - typ danych jako enum, w naszym przypadku GL_FLOAT,
  • normalized - określa czy wartość ma być znormalizowana, u nas będzie to GL_FALSE,
  • stride - określa dystans pomiędzy atrybutami w kolejnych wierzchołkach
  • offset - wskaźnik na pierwszy atrybut w tablicy, licząc względem początku tablicy i typu (void *)

Struktura naszego prostopadłościanu ma na przemian pozycje i kolory, dlatego w obu przypadkach stride będzie wynosił ośmiokrotność rozmiaru floata, (możesz obliczyć jego rozmiar instrukcją sizeof(float)). Natomiast offest będzie wynosił zero i czterokrotność rozmiaru floata odpowiednio dla pierwszego i drugiego atrybutu.

Na koniec uwolnij VAO za pomocą instrukcji: glBindVertexArray(0);.

Pozostaje narysować prostopadłościan. W funkcji renderScene wywołaj glBindVertexArray z argumentem VAO. Następnie narysuj za pomocą glDrawArrays(GLenum mode, GLint first, GLsizei count). Pierwszym argumentem jest typ rysowanego obiektu, w naszym przypadku jest to GL_TRIANGLES, indeks pierwszego wierzchołka, czyli 0 i liczba wierzchołków czyli 36.

Zadanie 1

Twój główny cel w tym zadaniu to zrozumienie, jak ładować i rysować obiekty w OpenGL. Upewnij się, że krok po kroku zastosowałeś się do powyższych instrukcji i zrozumiałeś, jakie funkcje są używane i dlaczego. podążając za powyższymi instrukcjami zainicjalizuj box, następnie obróć go za pomocą funkcji glm::eulerAngleXYZ tak, żeby było widać trzy ściany prostopadłościany. Następnie wykorzystaj ponownie funkcję glm::eulerAngleXYZ, żeby dodatkowo obracał się on względem osi Y ze stałą szybkością.

Zadanie 2*

Wykonaj zadania z ex_2_1b.hpp.

Shadery

Shadery są programami uruchamianymi na karcie graficznej. W openglu wykorzystujemy język GLSL, który jest bardzo podobny do C++, posiada on liczne słowa kluczowe i funkcje matematyczne. Istnieją różne rodzaje shaderów, w tej sekcji skupimy się na dwóch z nich: shader wierzchołków i shader fragmentów. Pierwszy rodzaj wykonują operacje na wierzchołkach, przykładowo w tym zadaniu odpowiada za przemnożenie wierzchołków przez macierze obrotu. Natomiast drugi określa kolor konkretnego fragmentu/piksela. Shadery są łączone w pipeline. W kontekście OpenGL, pipeline to seria etapów przetwarzania grafiki, gdzie każdy etap wykonuje określone operacje na danych. Shadery są jednym z tych etapów, a dane przetworzone przez jeden shader są przekazywane do następnego shadera w pipeline. W zadaniu 2_1 wykorzystujemy następujące shadery

shader wierzchołków

#version 430 core

layout(location = 0) in vec4 vertexPosition;
layout(location = 1) in vec4 vertexColor;

uniform mat4 transformation;


void main()
{
	gl_Position = transformation * vertexPosition;
}

shader fragmentów

#version 430 core


out vec4 out_color;
void main()
{
	out_color = vec4(0.8,0.2,0.9,1.0);
}

Shader wierzchołków odbiera 2 typy danych. Pierwszym są dane z bufora w liniach.

layout(location = 0) in vec4 vertexPosition;
layout(location = 1) in vec4 vertexColor;

są one różne dla każdego wierzchołka. Zmienną, która ma odebrać te dane deklaruje się globalnie, poza funkcją main i poprzedza się słowem kluczowym in. Prefiks layout(location = ..) jest opcjonalny i służy określeniu indeksu atrybutu, jest to ta sama wartość, którą ustawiliśmy w glVertexAttribPointer. Można je usunąć, wtedy o indeksie będzie decydować kolejność. Drugim typem danych, które shader odbiera jest typ uniform, w przeciwieństwie do danych z bufora, są one takie same dla każdego wierzchołka lub ogólniej dla każdego wywołania shadera. W tym przypadku przesyłamy za jej pomocą macierz obrotu.

Shader wierzchołków sam również wysyła dane. Domyślnie musi wysłać wyjściową pozycję wierzchołka, robi to przez zapisanie w gl_Position wektora 4-wymiarowego w funkcji main. Poza tym może również przesłać inne informacje. Wykonuje się to przez deklaracje zmiennej globalnej, którą poprzedza się słowem kluczowym out. Następnie należy ją wypełnić np. w funkcji main. W tym przykładzie przesyłamy tak kolor wierzchołka dalej.

Shader fragmentów odbiera kolor z Shadera wierzchołków. Podobnie jak z obieraniem danych z buforu robimy to za pomocą słowa kluczowego in, w przypadku przesyłania zmiennej z jednego shadera do drugiego nazwy zmiennych muszą być takie same przy słowie kluczowym out i in. Odebranej zmiennej nie można modyfikować.

W najnowszej wersji opengla fragment shader nie ma domyślnego wyjścia na kolor, musimy sami je z definiować. Robimy to instrukcją out vec4 out_color następnie w funkcji main przypisujemy mu wartość koloru w formacie RGBA z wartościami od 0 do 1.

Zadanie 3

W tej chwili nasz prostopadłościan jest jednolitego koloru i nie możemy rozróżnić jego ścian. Przesłana przez nas wcześniej informacja o kolorze nie została wykorzystana. Bazując na powyższych informacjach prześlij wartość koloru zapisaną w vertexColor z shadera wierzchołków do shadera fragmentu i przypisz ją do wyjściowego koloru. Dodaj zmienną out vec4 color w shaderze wierzchołków, następnie w funkcji main przypisz do niej wartość koloru. W shaderze fragmentów odbierz ją za pomocą in i przypisz do wyjściowego koloru.

Zauważ, że kolor ścian nie jest jednolity, zamiast tego przechodzą gradientem od jednego koloru do drugiego. Dzieje się tak, ponieważ na etapie rasteryzacji kolor jest interpolowany. To znaczy: wartość jest uśredniana pomiędzy wierzchołkami trójkąta.

Zadanie 4

Sprawdź, jak będzie wyglądać prostopadłościan z wyłączoną interpolacją. Dodaj przed in i out color słowo kluczowe flat.

Zadanie 5

Prześlij czas od startu aplikacji do shadera fragmentów. Użyj funkcji glfwGetTime w pętli renderowania, by uzyskać czas. Utwórz zmienną uniform typu float we shaderze fragmentów. Następnie prześlij do niej wynik funkcji glfwGetTime. Aby przesłać czas do shadera, wykorzystaj funkcje glUniform1f. Pierwszym argumentem jest lokacja uniforma, drugim przypisywana wartość. Lokację uzyskamy za pomocą funckji glGetUniformLocation(progam,"time") pierwszym argumentem jest program, którego używamy a drugim nazwa zmiennej uniform. Podziel kolor przez czas, by uzyskać efekt, w którym prostopadłościan robi się czarny.

Zadanie 6*

Wykorzystaj przesłany czas, żeby sprawić, żeby prostopadłościan znikał przez mieszanie go z kolorem tła. Wykorzystaj do tego następujące funkcje GLSL: mix, sin, vec4. Opis ich działania możesz znaleźć w dokumentacji https://docs.gl/.

Zadanie 7

Prześlij pozycję lokalną i globalną pozycję wierzchołków do shadera fragmentów i wyświetl ją. W shaderze wierzchołków obok deklaracji out vec4 color; dodaj analogiczne o nazwie pos_local i pos_global. Do pos_local przypisz vertexPosition, a do pos_global przypisz transformation * vertexPosition. Podobnie dopisz odebranie ich w shaderze fragmentów. Użyj pos_local, następnie pos_global zamiast koloru. Dlaczego otrzymaliśmy taki efekt?

Zadanie 8*

Użyj jednej ze zmiennych z poprzedniego zadania do zrobienia pasków na przynajmniej jednej ze ścian sześcianu. Wykorzystaj czas, aby sprawić, żeby paski się przesuwały.

Słownik pojęć

  • GLSL (Graphics Library Shader Language): Specjalizowany język programowania używany do pisania shaderów w OpenGL. Bardzo podobny do C++ i dostosowany do operacji na grafice.
  • Shader: Program wykonywany na karcie graficznej, przetwarzający dane wejściowe do efektów graficznych lub postaci wizualnych.
  • Shader wierzchołków (Vertex Shader): Typ shadera, który przetwarza pojedyncze wierzchołki i może zmieniać ich atrybuty, takie jak pozycja, kolor itp.
  • Shader fragmentów (Fragment Shader): Typ shadera, który oblicza kolor i inne atrybuty każdego fragmentu (często nazywany pikselem).
  • Pipeline: Seria etapów przetwarzania grafiki w OpenGL, gdzie każdy etap wykonuje określone operacje na danych. Shadery są jednym z tych etapów.
  • Rasteryzacja: Proces konwertowania geometrii 3D na piksele na ekranie.
  • Interpolacja: W kontekście grafiki komputerowej, oznacza obliczenie wartości między dwoma punktami. W shaderach jest używany do obliczania wartości dla fragmentów między wierzchołkami.
  • uniform: Zmienna w shaderze, która ma stałą wartość dla wszystkich wierzchołków lub fragmentów podczas jednego wywołania shadera.
  • layout(location): Dyrektywa w GLSL określająca lokalizację atrybutu lub zmiennej uniform w shaderze.