This is the Polish translation of Advanced-OpenGL/Framebuffers article of learnopengl.com tutorial series.
Do tej pory używaliśmy kilku typów buforów ekranowych: bufora kolorów do zapisywania wartości kolorów, bufora głębi do zapisu informacji o głębokości i wreszcie bufora szablonu, który pozwalał nam odrzucić pewne fragmenty na podstawie pewnych warunków. Kombinacja tych buforów nazywa się
Operacje renderowania, które do tej pory wykonywaliśmy, zostały wykonywane na buforach renderowania (ang. render buffers) dołączonych do
Zastosowanie framebufferów może nie mieć natychmiastowego sensu, ale renderowanie sceny do innego framebuffera pozwala nam tworzyć na przykład lustra w scenie lub robić fajne efekty post-processingu. Najpierw omówimy, jak działają bufory ramki, a następnie wykorzystamy je, implementując różne efekty post-processingu.
Tworzenie bufora ramki
Podobnie jak każdy inny obiekt w OpenGL, możemy stworzyć obiekt bufora ramki (w skrócie FBO) za pomocą funkcji o nazwie
unsigned int fbo;
glGenFramebuffers(1, &fbo);
Ten schemat tworzenia i używania obiektów jest czymś, co widzieliśmy dziesiątki razy, więc ich funkcje są podobne do wszystkich innych obiektów, które widzieliśmy; najpierw tworzymy obiekt framebuffer, ustawiamy go jako aktywny framebuffer, wykonujemy niektóre operacje i usuwamy framebuffer. Aby powiązać framebuffer z kontekstem (ustawić go jako domyślny framebuffer) używamy
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
Przez powiązanie framebuffera z opcją GL_FRAMEBUFFER wszystkie następne operacje odczytu (ang. read) i zapisu (ang. write) będą miały wpływ na aktualnie powiązany framebuffer. Możliwe jest również powiązanie bufora ramki tylko do odczytu lub zapisu, przez powiązanie z opcjami GL_READ_FRAMEBUFFER lub GL_DRAW_FRAMEBUFFER. Bufor ramki powiązany z GL_READ_FRAMEBUFFER jest następnie używany do wszystkich operacji odczytu, takich jak
Niestety nie możemy użyć naszego framebuffera, ponieważ nie jest on
- Musimy dołączyć co najmniej jeden bufor (koloru, głębokości lub bufor szablonu).
- Powinien być przynajmniej jeden załącznik koloru (ang. color attachment).
- Wszystkie załączniki powinny być również kompletne (muszą mieć zarezerwowaną pamięć).
- Każdy bufor powinien mieć taką samą liczbę próbek.
Nie martw się, jeśli nie wiesz, co to są próbki, omówimy je w późniejszym samouczku.
Patrząc na warunki powinno być jasne, że musimy stworzyć pewien rodzaj załącznika dla bufora ramki i dołączyć go do tego bufora ramki. Po spełnieniu wszystkich wymagań możemy sprawdzić, czy faktycznie udało nam się stworzyć kompletny bufor ramki, wywołując
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
// wykonaj taniec zwycięstwa
Wszystkie kolejne operacje renderowania będą teraz renderowane do załączników aktualnie powiązanego bufora ramki. Ponieważ nasz framebuffer nie jest domyślnym buforem ramki, polecenia renderowania nie będą miały wpływu na wizualne wyjście twojego okna. Z tego powodu proces renderowania do innego bufora ramki jest nazywany 0
:
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Kiedy skończyliśmy ze wszystkimi operacjami na buforze ramki, nie zapomnijmy usunąć tego obiektu:
glDeleteFramebuffers(1, &fbo);
Teraz przed sprawdzeniem kompletności musimy dołączyć jeden lub więcej załączników do bufora ramki.
Załączniki tekstur
Po dołączeniu tekstury do bufora ramki wszystkie polecenia renderowania będą zapisywać dane do tej tekstury tak, jakby był to normalny bufor koloru, głębi lub szablonu. Zaletą korzystania z tekstur jest to, że wynik wszystkich operacji renderowania będzie przechowywany jako obraz tekstury, który możemy następnie łatwo wykorzystać w naszych shaderach.
Tworzenie tekstury dla bufora ramki jest w prawie takie samo jak tworzenie normalnej tekstury:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Główną różnicą jest to, że ustawiamy wymiary równe rozmiarowi ekranu (chociaż nie jest to wymagane) i przekazujemy NULL
jako parametr data
tekstury. W przypadku tej tekstury przydzielamy tylko pamięć, a nie faktycznie ją wypełniamy. Wypełnienie tekstury nastąpi zaraz po renderowaniu do bufora ramki. Zwróć też uwagę, że nie dbamy też o żadne metody zawijania tekstury ani mipmapping, ponieważ w większości przypadków nie będziemy ich potrzebować.
Jeśli chcesz renderować cały ekran do tekstury o mniejszym lub większym rozmiarze, musisz ponownie wywołać
Teraz, gdy stworzyliśmy teksturę, ostatnią rzeczą, którą musimy zrobić, jest podpięcie jej do bufora ramki:
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
Funkcja
target
: typ trybu bufora ramki (do zapisu, odczytu lub do odczytu i zapisu).attachment
: typ załącznika, który chcemy dołączyć. W tej chwili dołączamy załącznik koloru. Zauważ, że0
na końcu sugeruje, że możemy dołączyć więcej niż jeden załącznik koloru. Omówimy to w późniejszym samouczku.textarget
: typ tekstury, którą chcesz dołączyć.texture
: tekstura do dołączenia do framebuffera.level
: poziom mipmapy. W tym przypadku ustawiamy wartość0
.
Oprócz załączników koloru możemy również dołączyć teksturę głębi i szablonu do obiektu bufora ramki. Aby dołączyć załącznik głębokości, określamy typ załącznika jako GL_DEPTH_ATTACHMENT. Zwróć uwagę, że
Możliwe jest również dołączenie zarówno bufora głębi, jak i bufora szablonu jako pojedynczej tekstury. Każda 32-bitowa wartość tekstury obejmuje 24 bity informacji o głębokości i 8 bitów informacji o szablonie. Aby dołączyć bufor głębi i szablonów jako jedną teksturę, używamy typu załącznika GL_DEPTH_STENCIL_ATTACHMENT i konfigurujemy formaty tekstur tak, aby zawierał połączone wartości głębi i szablonu. Przykład dołączenia bufora głębi i szablonu jako jednej tekstury do bufora ramki pokazano poniżej:
glTexImage2D(
GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0,
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
Załączniki obiektu Renderbuffer
Obiekty renderbuffer zostały wprowadzone do OpenGL po teksturach jako możliwy typ załączników bufora ramki. Tekstury były jedynymi załącznikami używanymi w starych czasach OpenGL. Podobnie jak obraz tekstury, obiekt renderbuffer jest rzeczywistym buforem, np. tablicą bajtów, liczb całkowitych, pikseli lub czegokolwiek innego. Obiekt renderbuffer ma tę dodatkową zaletę, że przechowuje dane w natywnym formacie OpenGL, dzięki czemu jest zoptymalizowany pod kątem renderowania poza ekranowym do bufora ramki.
Obiekty Renderbuffer przechowują wszystkie dane renderowania bezpośrednio w swoich buforach bez żadnych konwersji do formatów specyficznych dla tekstury, dzięki czemu dane są szybciej zapisywane. Jednak obiekty renderbuffer są obiektami tylko do zapisu, więc nie można ich odczytać (jak to można robić w przypadku tekstur). Można z nich odczytać dane za pomocą
Ponieważ dane są już w oryginalnym formacie, to renderbuffery są szybkie przy zapisywaniu danych lub po prostu kopiowaniu danych do innych buforów. Operacje, takie jak przełączanie buforów, są dość szybkie podczas korzystania z obiektów renderbuffer. Funkcja
Tworzenie obiektu renderbuffer wygląda podobnie do kodu tworzenia bufora ramki:
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
Chcemy powiązać obiekt renderbuffer, aby wszystkie kolejne operacje renderbuffer’a miały wpływ na bieżący rbo:
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
Ponieważ obiekty renderbuffer są generalnie tylko do zapisu, często są używane jako załączniki głębi i szablonu, ponieważ zwykle nie musimy odczytywać wartości z buforów głębi i szablonu, ale wciąż dbają one o test głębi i szablonu. Potrzebujemy wartości głębokości i szablonu do testów, ale nie musimy próbkować tych wartości, więc obiekt renderbuffer idealnie pasuje do tego zadania. Gdy nie pobieramy próbek z tych buforów, zazwyczaj preferowany jest obiekt renderbuffer, ponieważ jest on bardziej zoptymalizowany.
Tworzenie obiektu renderbuffer z głębią i szablonem odbywa się poprzez wywołanie funkcji
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
Tworzenie obiektu renderbuffer jest podobne do tworzenia obiektów tekstur, z tą różnicą, że obiekt ten został specjalnie zaprojektowany do użycia jako obraz zamiast bufora ogólnego przeznaczenia, taki jak tekstura. Tutaj wybraliśmy GL_DEPTH24_STENCIL8 jako wewnętrzny format (ang. internal format), który zawiera bufor głębokości i szablonu z odpowiednio 24 i 8 bitami.
Ostatnia rzecz do zrobienia to dołączenie obiektu renderbuffer:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
Obiekty renderbuffer mogą zapewnić pewne optymalizacje w buforze ramki, ale ważne jest, aby zdać sobie sprawę, kiedy używać obiektów renderbuffer i kiedy używać tekstur. Ogólna zasada jest taka, że jeśli nigdy nie będziesz potrzebować próbkować/pobierać danych z określonego bufora, dobrze jest użyć obiektu renderbuffer dla tego konkretnego bufora. Jeśli potrzebujesz pobierać dane, takie jak kolory lub wartości głębokości, powinieneś zamiast tego użyć załącznika tekstury. Jednak pod względem wydajności nie ma to ogromnego wpływu.
Rendering do tekstury
Teraz, gdy wiemy, jak działają framebuffery, nadszedł czas, aby je dobrze wykorzystać. Zamienimy naszą scenę w kolorową teksturę dołączoną do obiektu bufora ramki, który stworzyliśmy, a następnie narysujemy tę teksturę na kwadracie obejmującym cały ekran. Wizualne wyjście jest dokładnie takie samo jak bez bufora ramki, ale tym razem wszystko jest narysowane na pojedynczym kwadracie. Dlaczego to jest przydatne? W następnej sekcji zobaczymy, dlaczego.
Pierwszą rzeczą do zrobienia jest stworzenie rzeczywistego obiektu bufora ramki i powiązanie go z kontekstem, to wszystko jest stosunkowo proste:
unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
Następnie tworzymy obraz tekstury, który załączamy jako załącznik koloru do bufora ramki. Ustawiamy wymiary tekstury równe szerokości i wysokości okna i nie inicjalizujemy danych:
// wygeneruj teksturę
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
// dołącz ją do aktualnego obiektu framebuffer
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);
Chcemy również upewnić się, że OpenGL jest w stanie wykonać test głębi (i opcjonalnie test szablonu, jeśli tego potrzebujesz), więc musimy upewnić się, że dodajemy również głębię (i szablon) do bufora ramki. Ponieważ próbkujemy tylko bufora kolorów, a innych buforów nie, to możemy w tym celu utworzyć obiekt renderbuffer. Pamiętaj, że to dobry wybór, gdy nie chcesz pobierać danych z określonych buforów.
Tworzenie obiektu renderbuffer nie jest zbyt trudne. Jedyne, o czym musimy pamiętać to to, że tworzymy go jako obiekt renderbuffer z głębią i szablonem. Ustawiliśmy jego wewnętrzny format na GL_DEPTH24_STENCIL8, który jest wystarczająco dokładny dla naszych celów.
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
Po przydzieleniu wystarczającej ilości pamięci dla obiektu renderbuffer możemy odpiąć renderbuffer od kontekstu.
Następnie, jako ostatni krok przed końcowym wypełnieniem bufora ramki, dołączamy obiekt renderbuffer do załączników głębokości i szablonu bufora ramki:
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
Następnie jako końcowy krok. chcemy sprawdzić, czy framebuffer jest rzeczywiście kompletny, jeśli nie, napiszemy komunikat o błędzie.
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Następnie pamiętaj, aby odłączyć bufor ramki, aby upewnić się, że przypadkowo nie renderujemy do niewłaściwego bufora ramki.
Teraz, gdy framebuffer jest kompletny, wszystko, co musimy zrobić, aby renderować do naszego framebuffera zamiast do domyślnego framebuffera, to po prostu powiązanie naszego framebuffera z kontekstem. Wszystkie następne polecenia renderowania będą miały wpływ na aktualnie powiązany bufor ramki. Wszystkie operacje na wartościach głębi i szablonów będą również odczytywane z aktualnie powiązanych załączników głębokości i szablonu bufora ramki, jeśli są one dostępne. Jeśli na przykład pominąłeś bufor głębi, wszystkie operacje testowania głębi przestaną działać, ponieważ w aktualnie powiązanym buforze ramki nie ma bufora głębi.
Aby narysować scenę do pojedynczej tekstury, musimy wykonać następujące kroki:
- Wyrenderuj scenę jak zwykle z powiązanym nowym framebufferem jako aktywnym framebufferem.
- Powiąż domyślny bufor ramki.
- Narysuj kwadrat, który obejmuje cały ekran z teksturą z nowego załącznika koloru nowego bufora ramki.
Narysujemy tę samą scenę, której użyliśmy w tutorialu test głębokości, ale tym razem z starą znajomą teksturą kontenera.
Aby narysować kwadrat pełno-ekranowy, stworzymy nowy zestaw prostych shaderów. Nie uwzględnimy żadnych fantazyjnych przekształceń macierzy, ponieważ będziemy dostarczać współrzędne wierzchołków jako znormalizowane współrzędne urządzenia, abyśmy mogli bezpośrednio określić je jako dane wyjściowe Vertex Shader. VS wygląda następująco:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
TexCoords = aTexCoords;
}
Nic nadzwyczajnego. Fragment Shader będzie jeszcze prostszy, ponieważ jedyną rzeczą, którą musimy zrobić, to spróbkować teksturę:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D screenTexture;
void main()
{
FragColor = texture(screenTexture, TexCoords);
}
Teraz to do Ciebie należy stworzenie i skonfigurowanie VAO dla pełno-ekranowego kwadratu. Iteracja renderowania za pomocą tekstury bufora ramki wygląda następująco:
// pierwsze przejście
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // nie używamy teraz bufora szablonu
glEnable(GL_DEPTH_TEST);
DrawScene();
// drugie przejście
glBindFramebuffer(GL_FRAMEBUFFER, 0); // powrót do domyślnego FBO
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader.use();
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
Jest kilka rzeczy do zauważenia. Po pierwsze, ponieważ każdy bufor ramek, z którego korzystamy ma własny zestaw buforów, chcemy wyczyścić każdy z tych buforów za pomocą odpowiednich masek bitowych, wywołując
Jest kilka rzeczy, które mogą pójść nie tak, więc jeśli nic się nie pojawia, spróbuj zdebugować kod, gdzie to tylko możliwe, i ponownie przeczytaj odpowiednie sekcje samouczka. Jeśli wszystko przebiegło pomyślnie, uzyskasz efekt podobny do tego:
Lewa strona pokazuje efekt wizualny dokładnie taki, jaki widzieliśmy w samouczku test głębokości, ale tym razem wyrenderowany na prostym kwadracie. Jeśli renderujemy scenę z widoczną siatką (ang. wireframe mode), staje się oczywiste, że narysowaliśmy tylko jeden kwadrat w domyślnym buforze ramki.
Możesz znaleźć kod źródłowy aplikacji tutaj.
Więc jaki był z tego pożytek? Cóż, ponieważ teraz możemy swobodnie uzyskać dostęp do każdego z pikseli wyrenderowanej sceny jako pojedynczego obrazu tekstury, możemy stworzyć interesujące efekty w Fragment Shaderze. Zbiór tych wszystkich interesujących efektów nazywa się
Post-processing
Teraz, gdy cała scena jest renderowana do pojedynczej tekstury, możemy stworzyć interesujące efekty, po prostu manipulując danymi tekstury. W tej sekcji pokażemy niektóre z bardziej popularnych efektów post-processingu i tego, jak możesz tworzyć własne efekty z odrobiną kreatywności.
Zacznijmy od jednego z najprostszych efektów post-processingu.
Inwersja
Mamy dostęp do każdego z wyjściowych kolorów renderowania, więc nie jest tak trudno odwrócić te kolorów w Fragment Shaderze. Pobieramy kolor tekstury ekranowej i odwracamy go przez odjęcie tego koloru od 1.0
:
void main()
{
FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}
Inwersja jest stosunkowo prostym efektem post-processingu, ale jest dosyć ciekawy:
Cała scena ma teraz wszystkie odwrócone kolory za pomocą pojedynczej linii kodu w Fragment Shaderze. Całkiem fajnie, nie?
Skala szarości
Kolejnym interesującym efektem jest usunięcie wszystkich kolorów ze sceny, z wyjątkiem kolorów białego, szarego i czarnego, tworząc obraz w skali szarości. Prostym sposobem na zrobienie tego jest po prostu pobranie wszystkich składników koloru i uśrednienie ich wartości:
void main()
{
FragColor = texture(screenTexture, TexCoords);
float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
FragColor = vec4(average, average, average, 1.0);
}
To już daje całkiem dobre wyniki, ale ludzkie oko jest bardziej wrażliwe na zielone kolory, a najmniej na niebieskie, więc aby uzyskać jak najbardziej dokładne wyniki, musimy użyć średniej ważonej kanałów:
void main()
{
FragColor = texture(screenTexture, TexCoords);
float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
FragColor = vec4(average, average, average, 1.0);
}
Prawdopodobnie od razu nie zauważysz różnicy, ale przy bardziej skomplikowanych scenach, ważona skali szarości będzie bardziej realistyczna.
Efekty jądra (ang. kernel effects)
Kolejną zaletą post-processingu jest to, że możemy próbkować wartości kolorów z innych części tekstury. Możemy na przykład zdefiniować mały obszar wokół aktualnego teksela i próbkować wiele wartości tekstur wokół aktualnej wartości tekstury. Możemy wtedy tworzyć ciekawe efekty, łącząc je na różne sposoby.
Jądro (lub macierz splotu) to mała macierzopodobna tablica wartości, gdzie środkowa wartość to wartość bieżącego piksela, który mnoży otaczające go wartości pikseli przez swoją wartość jądra i sumuje je razem w celu utworzenia pojedynczej wartości. Zasadniczo dodajemy małe przesunięcie do współrzędnych tekstury w otoczeniu bieżącego piksela i łączymy wyniki w oparciu o jądro. Przykład jądra podano poniżej:
\[\begin{bmatrix}2 & 2 & 2 \\ 2 & -15 & 2 \\ 2 & 2 & 2 \end{bmatrix}\]To jądro pobiera 8 otaczających wartości piksela i mnoży je przez 2
, a bieżący piksel przez -15
. To przykładowe jądro zasadniczo mnoży otaczające piksele przez wagę określoną w jądrze i równoważy wynik przez pomnożenie bieżącego piksela przez dużą ujemną wagę.
Większość jąder, które znajdziesz w Internecie, sumuje się do 1
, jeśli dodasz wszystkie wagi do siebie. Jeśli nie sumują się do 1
, oznacza to, że wynikowy kolor tekstury staje się jaśniejszy lub ciemniejszy niż oryginalna wartość tekstury.
Jądra są niezwykle przydatnym narzędziem post-processingu, ponieważ są dość łatwe w użyciu, eksperymentowaniu i wiele ich przykładów można znaleźć w Internecie. Musimy nieco zmodyfikować Fragment Shader, aby faktycznie obsługiwać jądra. Zakładamy, że każde jądro, z którego będziemy korzystać, jest jądrem 3x3 (większość jąder ma taki rozmiar):
const float offset = 1.0 / 300.0;
void main()
{
vec2 offsets[9] = vec2[](
vec2(-offset, offset), // lewy górny
vec2( 0.0f, offset), // górny środek
vec2( offset, offset), // prawy górny
vec2(-offset, 0.0f), // lewy po środku
vec2( 0.0f, 0.0f), // środkowy
vec2( offset, 0.0f), // prawy po środku
vec2(-offset, -offset), // lewy dolny
vec2( 0.0f, -offset), // dolny środkowy
vec2( offset, -offset) // prawy dolny
);
float kernel[9] = float[](
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
);
vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
}
vec3 col = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];
FragColor = vec4(col, 1.0);
}
W Fragment Shader najpierw tworzymy tablicę 9 przesunięć vec2
dla każdej otaczającej współrzędnej tekstury. Przesunięcie to po prostu stała wartość, którą można dostosować do własnych upodobań. Następnie definiujemy jądro, które w tym przypadku jest jądrem
Efekt wyostrzenia wygląda następująco:
Może to stworzyć interesujące efekty, gdzie gracz znajduje się w jakiejś narkotycznej przygodzie.
Rozmycie
Jądro tworzące efekt
Ponieważ wszystkie wartości sumują się do 16, po prostu łączenie próbkowanych kolorów wygeneruje bardzo jasny kolor, dlatego musimy podzielić każdą wartość jądra przez 16
. Powstała tablica jądra staje się:
float kernel[9] = float[](
1.0 / 16, 2.0 / 16, 1.0 / 16,
2.0 / 16, 4.0 / 16, 2.0 / 16,
1.0 / 16, 2.0 / 16, 1.0 / 16
);
Zmieniając tablicę kernel na typ
Taki efekt rozmycia tworzy interesujące możliwości. Możemy zmieniać wielkość rozmycia w czasie, aby na przykład stworzyć efekt upijania się lub zwiększyć rozmycie, gdy główna postać nie nosi okularów. Rozmycie daje nam również przydatne narzędzie do wygładzania wartości kolorów, które wykorzystamy w późniejszych samouczkach.
Widać, że gdy mamy już tak małą implementację jądra, łatwo jest stworzyć fajne efekty post-processingu. Pokażmy ostatni popularny efekt, aby zakończyć ten artykuł.
Wykrywanie krawędzi
Poniżej znajduje się jądro
To jądro podświetla wszystkie krawędzie i przyciemnia resztę, co jest bardzo przydatne, gdy zależy nam tylko na krawędziach w obrazie.
Prawdopodobnie nie jest zaskoczeniem, że takie jądra są używane jako narzędzia/filtry do obróbki obrazów w narzędziach takich jak Photoshop. Ze względu na zdolność karty graficznej do przetwarzania fragmentów o ekstremalnie równoległych możliwościach, możemy z łatwością manipulować obrazami w ujęciu pikselowym w czasie rzeczywistym. Narzędzia do edycji obrazu mają tendencję do częstszego korzystania z kart graficznych do przetwarzania obrazów.