This is the Polish translation of Advanced-OpenGL/Blending article of learnopengl.com tutorial series.

Blending (mieszanie) w OpenGL jest również powszechnie znany jako technika implementująca przezroczystość (ang. transparency) wewnątrz obiektów. Przezroczystość polega na tym, że obiekty (lub ich części) nie mają jednolitego koloru, ale mają kombinację własnego koloru z kolorem dowolnego innego obiektu za nim o innej intensywności. Kolorowe szklane okno jest przezroczystym obiektem; szkło ma swój własny kolor, ale powstały kolor zawiera również kolory wszystkich obiektów znajdujących się za szkłem. Jest to również operacja, z której wywodzi się nazwa blending, ponieważ mieszamy kilka kolorów (różnych obiektów) w jeden kolor. Przezroczystość pozwala nam zobaczyć co jest po drugiej stronie obiektu.

Obraz pełnego przezroczystego okna i częściowo przezroczystego okna

Przezroczyste obiekty mogą być całkowicie przezroczyste (wszystkie kolory przechodzą przez taki obiekt) lub częściowo przezroczyste (pozwalają przebarwiać kolory, ale także pokazują niektóre ze swoich własnych kolorów). Ilość przezroczystości obiektu określa jego kolor alfa. Wartość koloru alfa jest czwartą składową wektora koloru, który zapewne widziałeś już dość często. Do tego czasu, zawsze ustawialiśmy ten czwarty komponent na 1.0, co sprawia, że obiekt był nieprzezroczysty. Wartość alfa równa 0.0 spowodowałaby całkowitą przezroczystość obiektu. Wartość alfa 0.5 mówi nam, że kolor obiektu składa się w 50% z jego własnego koloru i 50% kolorów za tym obiektem.

Wszystkie dotychczasowe tekstury składały się z 3-składnikowych komponentów koloru: czerwonego, zielonego i niebieskiego, ale niektóre tekstury mają również wbudowany kanał alfa, który zawiera wartość alfa na każdy teksel. Ta wartość alfa dokładnie określa, które części tekstury mają przezroczystość i ile ona wynosi. Na przykład, następująca tekstura okna ma w swojej szklanej części wartość alfa 0.25 (normalnie byłaby całkowicie czerwona, ale ponieważ ma 75% przezroczystości, w dużym stopniu pokazuje ona tło witryny, dzięki czemu wydaje się dużo mniej czerwone). Dodatkowo ma też ustawioną wartość alfa 0.0 w rogach:

Tekstura okna z przezroczystością

Wkrótce dodamy do sceny tę teksturę okna, ale najpierw omówimy łatwiejszą technikę zaimplementowania przezroczystości dla tekstur, które są w pełni przezroczyste lub całkowicie nieprzezroczyste.

Odrzucanie fragmentów

Niektóre obrazy nie dbają o częściową przezroczystość - albo chcą pokazać coś albo nic w oparciu o wartość koloru tekstury. Pomyśl o trawie; Aby stworzyć coś w rodzaju trawy przy niewielkim wysiłku, zazwyczaj wklejasz teksturę trawy na kwadrat 2D i umieszczasz ją w scenie. Jednak trawa nie jest w kształcie kwadratu 2D, więc chcesz wyświetlić tylko niektóre elementy tekstury trawy i zignorować pozostałe.

Poniższa tekstura jest dokładnie taką teksturą, która jest albo całkowicie nieprzezroczysta (wartość alfa 1.0), albo jest w pełni przezroczysta (wartość alfa 0.0) i nic pomiędzy. Możesz zobaczyć, że wszędzie tam, gdzie nie ma trawy, obraz pokazuje kolor tła witryny zamiast własnego.

Tekstura trawy z przezroczystością

Zatem dodając roślinność, taką jak trawa, nie chcemy widzieć kwadratowego obrazu trawy, a jedynie pokazać samą trawę i nic poza nią. Chcemy odrzucić (ang. discard) fragmenty, które pokazują przezroczyste części tekstury, nie przechowując fragmentu w buforze kolorów. Zanim to zrobimy, musimy najpierw nauczyć się ładować przezroczystą teksturę.

Aby wczytać teksturę z wartościami alfa, nie musimy wiele zmieniać. stb_image automatycznie ładuje kanał alfa obrazu, jeśli jest dostępny, ale musimy powiedzieć OpenGL, że nasza tekstura używa teraz kanału alfa w procedurze generowania tekstury:

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);  

Upewnij się również, że pobierasz wszystkie 4 składniki koloru tekstury w Fragment Shader, a nie tylko komponenty RGB:

    void main()
    {
        // FragColor = vec4(vec3(texture(texture1, TexCoords)), 1.0);
        FragColor = texture(texture1, TexCoords);
    }

Teraz, gdy wiemy, jak załadować przezroczyste tekstury, nadszedł czas, aby poddać ją testowi, dodając kilka liści trawy w podstawowej scenie wprowadzonej w tutorialu Test głębokości.

Tworzymy mały wektor, w którym dodajemy kilka zmiennych glm::vec3 reprezentujących położenie liści trawy:

    vector<glm::vec3> vegetation;
    vegetation.push_back(glm::vec3(-1.5f,  0.0f, -0.48f));
    vegetation.push_back(glm::vec3( 1.5f,  0.0f,  0.51f));
    vegetation.push_back(glm::vec3( 0.0f,  0.0f,  0.7f));
    vegetation.push_back(glm::vec3(-0.3f,  0.0f, -2.3f));
    vegetation.push_back(glm::vec3( 0.5f,  0.0f, -0.6f));  

Każdy z obiektów trawy jest renderowany jako pojedynczy kwadrat z dołączoną do niego teksturą trawy. Nie jest to doskonałe odwzorowanie trawy w 3D, ale jest o wiele bardziej wydajne niż ładowanie złożonych modeli. Dzięki kilku sztuczkom, takim jak dodanie kilku dodatkowych obróconych kwadratów o tej samej pozycji, możesz uzyskać całkiem dobre wyniki.

Ponieważ tekstura trawy jest dodawana do kwadratu, musimy ponownie utworzyć VAO, wypełnić VBO i ustawić odpowiednie wskaźniki atrybutów wierzchołków. Następnie, po narysowaniu podłogi i dwóch sześcianów, narysujemy liście trawy:

    glBindVertexArray(vegetationVAO);
    glBindTexture(GL_TEXTURE_2D, grassTexture);  
    for(unsigned int i = 0; i < vegetation.size(); i++) 
    {
        model = glm::mat4();
        model = glm::translate(model, vegetation[i]);				
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }  

Po uruchomieniu aplikacji powinieneś zobaczyć prawdopodobnie coś takiego:

Brak odrzucenia przezroczystych części tekstury powoduje dziwne artefakty w OpenGL

Dzieje się tak, ponieważ domyślnie OpenGL nie wie, co zrobić z wartościami alfa, ani kiedy je odrzucić. Musimy to zrobić samodzielnie. Na szczęście jest to całkiem łatwe dzięki zastosowaniu shaderów. GLSL daje udostępnia nam polecenie discard, które (po wywołaniu) zapewnia, że ​​fragment nie będzie dalej przetwarzany, a tym samym nie trafi do bufora kolorów. Dzięki temu poleceniu możemy sprawdzić w Fragment Shaderze, czy dostaje wartość alfa poniżej określonego progu, jeśli tak, to niech odrzuci fragment, tak jakby nigdy nie był przetwarzany:

    #version 330 core
    out vec4 FragColor;

    in vec2 TexCoords;

    uniform sampler2D texture1;

    void main()
    {             
        vec4 texColor = texture(texture1, TexCoords);
        if(texColor.a < 0.1)
            discard;
        FragColor = texColor;
    }

Tutaj sprawdzamy, czy próbkowany kolor tekstury zawiera wartość alfa poniżej progu 0.1, jeśli tak, to odrzucamy ten fragment. Ten Fragment Shader zapewnia nam, że renderujemy tylko te fragmenty, które nie są (prawie) całkowicie przezroczyste. Teraz będzie wyglądać to tak, jak powinno:

Obraz liści trawy wyrenderowany z odrzucaniem fragmentów w OpenGL

Zwróć uwagę, że podczas próbkowania tekstur na ich krańcach, OpenGL interpoluje wartości graniczne z kolejną powtarzającą się wartością tekstury (ponieważ ustawiamy jej parametry zawijania na GL_REPEAT). Zazwyczaj jest to w porządku, ale ponieważ używamy przezroczystych wartości, górna część obrazu tekstury otrzymuje przezroczystą wartość interpolowaną z wartością jednolitego koloru dolnej ramki. Rezultatem jest wtedy lekko półprzezroczysta kolorowa ramka, którą możesz zobaczyć wokół twojego teksturowanego kwadratu. Aby temu zapobiec, ustaw metodę zawijania tekstur na GL_CLAMP_TO_EDGE, gdy używasz tekstur alfa:

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);	
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    

Możesz znaleźć kod źródłowy tutaj.

Blending

Odrzucanie fragmentów jest wspaniałe i w ogóle, ale nie daje nam elastyczności jeżeli chodzi o renderowanie półprzezroczystych obrazów; albo wyrenderujemy fragment, albo całkowicie go odrzucimy. Aby renderować obrazy o różnych poziomach przezroczystości, musimy włączyć blending. Podobnie jak większość funkcjonalności OpenGL, możemy włączyć blending ustawiając opcję GL_BLEND:

    glEnable(GL_BLEND);  

Po włączeniu blendingu musimy poinformować OpenGL o tym, jak powinien mieszać ze sobą kolory.

Mieszanie w OpenGL odbywa się za pomocą następującego równania:

\[\bar{C}_{result} = \bar{\color{green}C}_{source} * \color{green}F_{source} + \bar{\color{red}C}_{destination} * \color{red}F_{destination}\]
  • $\bar{\color{green}C}_{source}$: źródłowy (ang. source) wektor koloru. Jest to wektor koloru, który pochodzi z tekstury.
  • $\bar{\color{red}C}_{destination}$: docelowy (ang. destination) wektor koloru. Jest to wektor koloru, który jest aktualnie przechowywany w buforze kolorów.
  • $\color{green}F_{source}$: wartość współczynnika źródłowego. Ustawia wpływ wartości alfa na kolor źródłowy.
  • $\color{red}F_{destination}$: wartość współczynnika docelowego. Ustawia wpływ wartości alfa na kolor docelowy.

Po uruchomieniu Fragment Shader’a i zakończeniu wszystkich testów, to równanie blendowania zostaje uruchomione na kolorowym wyjściu Fragment Shader’a i kolorze, który aktualnie znajduje się w buforze kolorów (poprzedni kolor fragmentu przechowywany przed bieżącym fragmentem). Kolory źródłowy i docelowy zostaną automatycznie ustawione przez OpenGL, ale współczynnik źródłowy i docelowy można ustawić na wybraną przez nas wartość. Zacznijmy od prostego przykładu:

Dwa kwadraty, w których jeden ma wartość alfa niższą niż 1

Mamy dwa kwadraty, na których chcemy narysować półprzezroczysty zielony kwadrat na czerwonym kwadracie. Czerwony kwadrat będzie kolorem docelowym (a zatem powinien być pierwszy w buforze kolorów), a teraz narysujemy zielony kwadrat na czerwonym kwadracie.

Powstaje zatem pytanie: jak ustalamy wartości współczynników? Cóż, chcemy co najmniej pomnożyć zielony kwadrat z jego wartością alfa, więc chcemy ustawić $F_{src}$ na wartość alpha źródłowego wektora koloru, który wynosi 0.6. Wtedy sensowne jest, aby kwadrat docelowy miał wkład równy pozostałej wartości alfa. Jeśli zielony kwadrat wnosi 60% do ostatecznego koloru, chcemy, aby czerwony kwadrat wniósł 40% do ostatecznego koloru (1.0 - 0.6). Dlatego ustawiamy $F_{destination}$ równe jeden minus wartość alfa źródłowego wektora koloru. Równanie staje się zatem:

\[\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \\ \color{green}{1.0} \\ \color{blue}{0.0} \\ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \\ \color{green}{0.0} \\ \color{blue}{0.0} \\ \color{purple}{1.0} \end{pmatrix} * \color{red}{(1 - 0.6)}\]

Powoduje to, że połączone fragmenty kwadratów zawierają kolor, który wynosi 60% zieleni i 40% czerwieni, dając zabrudzony kolor:

Dwa kwadraty, w których jeden ma wartość alfa niższą niż 1

Otrzymany kolor jest następnie przechowywany w buforze kolorów, zastępując poprzedni kolor.

To wszystko jest świetne, ale jak właściwie powiedzieć OpenGL, aby używał takich współczynników? Cóż, tak się składa, że ​​istnieje funkcja o nazwie glBlendFunc.

Funkcja glBlendFunc(GLenum sfactor, GLenum dfactor) oczekuje dwóch parametrów ustawiających opcję dla źródła i współczynnika docelowego. OpenGL zdefiniował kilka opcji, dla których zestawimy najczęstsze kombinacje poniżej. Zwróć uwagę, że stały wektor koloru $\bar{\color{blue}C}_{constant}$ można ustawić osobno za pomocą funkcji glBlendColor.

Opcja Wartość
GL_ZERO Współczynnik jest równy $0$.
GL_ONE Współczynnik jest równy $1$.
GL_SRC_COLOR Współczynnik jest równy źródłowemu wektorowi koloru $\bar{\color{green}C}_{source}$.
GL_ONE_MINUS_SRC_COLOR Współczynnik jest równy $1 - \bar{\color{green}C}_{source}$.
GL_DST_COLOR Współczynnik jest równy docelowemu wektorowi koloru $\bar{\color{red}C}_{destination}$
GL_ONE_MINUS_DST_COLOR Współczynnik jest równy $1 - \bar{\color{red}C}_{destination}$.
GL_SRC_ALPHA Współczynnik jest równy $\bar{\color{green}C}_{source}$.
GL_ONE_MINUS_SRC_ALPHA Współczynnik jest równy $\bar{\color{green}C}_{source}$.
GL_DST_ALPHA Współczynnik jest równy $\bar{\color{red}C}_{destination}$.
GL_ONE_MINUS_DST_ALPHA Współczynnik jest równy t $\bar{\color{red}C}_{destination}$.
GL_CONSTANT_COLOR Współczynnik jest równy stałemu wektorowi koloru $\bar{\color{blue}C}_{constant}$.
GL_ONE_MINUS_CONSTANT_COLOR Współczynnik jest równy $\bar{\color{blue}C}_{constant}$.
GL_CONSTANT_ALPHA Współczynnik jest równy $\bar{\color{blue}C}_{constant}$.
GL_ONE_MINUS_CONSTANT_ALPHA Współczynnik jest równy $1 - alpha$ stałego wektora koloru $\bar{\color{blue}C}_{constant}$.

Aby uzyskać efekt mieszania, który uzyskaliśmy z dwóch kwadratów wcześniej, chcemy pobrać wartość $alpha$ z wektora koloru źródłowego dla współczynnika źródłowego i $1 - alpha$ dla współczynnika docelowego. Przekłada się to na glBlendFunc w następujący sposób:

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

Możliwe jest również ustawienie różnych opcji dla kanału RGB i alfa indywidualnie za pomocą glBlendFuncSeparate:

    glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

Ta funkcja ustawia komponenty RGB tak, jak wcześniej je ustawiliśmy, ale tylko pozwala na to, aby wynikowy komponent alfa podlegał wpływowi wartości alfa źródła.

OpenGL daje nam jeszcze większą elastyczność, umożliwiając nam zmianę operatora między źródłową i docelową częścią równania. Obecnie komponenty źródłowy i docelowy są dodawane razem, ale możemy je również odjąć, jeśli chcemy. glBlendEquation(GLenum mode) pozwala nam ustawić tę operację i ma 3 możliwe opcje:

  • GL_FUNC_ADD: domyślny, dodaje oba komponenty do siebie: $\bar{C}_{result} = \color{green}{Src} + \color{red}{Dst}$.
  • GL_FUNC_SUBTRACT: odejmuje oba komponenty od siebie: $\bar{C}_{result} = \color{green}{Src} - \color{red}{Dst}$.
  • GL_FUNC_REVERSE_SUBTRACT: odejmuje oba komponenty, ale w odwrotnej kolejności: $\bar{C}_{result} = \color{red}{Dst} - \color{green}{Src}$.

Zwykle możemy po prostu pominąć wywołanie glBlendEquation, ponieważ GL_FUNC_ADD jest preferowanym równaniem blendingu dla większości operacji, ale jeśli naprawdę starasz się jak najlepiej wyjść poza główny nurt, każde inne równanie może odpowiadać twoim potrzebom.

Renderowanie półprzezroczystych tekstur

Teraz, gdy wiemy, jak działa blending w OpenGL, nadszedł czas, aby przetestować naszą wiedzę, dodając kilka półprzezroczystych okien. Będziemy używać tej samej sceny, co na początku tego samouczka, ale zamiast renderować teksturę trawy, będziemy używać tekstury przezroczystego okna z początku tego samouczka.

Po pierwsze podczas inicjalizacji włączamy mieszanie i ustawiamy odpowiednią funkcję mieszania:

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

Ponieważ włączyliśmy mieszanie, nie ma potrzeby usuwania fragmentów, więc zresetujemy Fragment Shader do jego oryginalnej wersji:

    #version 330 core
    out vec4 FragColor;

    in vec2 TexCoords;

    uniform sampler2D texture1;

    void main()
    {             
        FragColor = texture(texture1, TexCoords);
    }  

Tym razem (kiedykolwiek OpenGL renderuje fragment) łączy kolor bieżącego fragmentu z kolorem fragmentu znajdującym się obecnie w buforze kolorów na podstawie jego wartości alfa. Ponieważ szklana część tekstury okna jest półprzezroczysta, powinniśmy móc zobaczyć resztę sceny, patrząc przez to okno.

Scena wykorzystująca blending w OpenGL, gdzie kolejność jest niepoprawna.

Jeśli jednak przyjrzysz się bliżej, możesz zauważyć, że coś jest nie tak. Przezroczyste części przedniego okna przesłaniają okna w tle. Dlaczego tak się dzieje?

Powodem tego jest to, że test głębokości działa nieco skomplikowanie w połączeniu z blendingiem. Podczas zapisywania w buforze głębi, test głębi nie dba o to, czy fragment ma przezroczystość, czy też nie, więc przezroczyste fragmenty są zapisywane w buforze głębi jak każda inna wartość. W rezultacie cały kwadrat okna jest sprawdzany pod kątem głębokości, niezależnie od przezroczystości. Mimo, że przezroczysta część powinna pokazywać okna za nią, test głębokości odrzuca je.

Dlatego nie możemy po prostu renderować okien, jak chcemy, i oczekiwać, że bufor głębi pomoże nam rozwiązać wszystkie nasze problemy; to także miejsce, w którym mieszanie staje się trochę nieprzyjemne. Aby upewnić się, że okna pokazują okna za nimi, musimy najpierw narysować okna w tle. Oznacza to, że musimy ręcznie sortować okna od najdalszego do najbliższego i odpowiednio je narysować.

Zwróć uwagę, że przy całkowicie przezroczystych obiektach, takich jak liście trawy, możemy po prostu odrzucić przezroczyste fragmenty, zamiast je mieszać, oszczędza nam to takich bólów głowy (brak problemów z głębokością).

Zachowaj kolejność

Aby mieszanie działało dla wielu obiektów, musimy najpierw narysować najdalszy obiekt, a najbliższy obiekt jako ostatni. Normalne obiekty nieblendowane mogą być nadal rysowane normalnie za pomocą bufora głębi, więc nie trzeba ich sortować. Musimy upewnić się, że są one rysowane najpierw przed rysowaniem (posortowanych) przezroczystych obiektów. Podczas rysowania sceny z nieprzejrzystymi i przezroczystymi obiektami ogólny zarys jest zwykle następujący:

  1. Narysuj najpierw wszystkie nieprzezroczyste obiekty.
  2. Posortuj wszystkie przezroczyste obiekty.
  3. Narysuj wszystkie przezroczyste obiekty w posortowanej kolejności.

Jednym ze sposobów sortowania przezroczystych obiektów jest uzyskanie odległości obiektu od pozycji widza. Można to osiągnąć, mierząc odległość między wektorem pozycji kamery a wektorem pozycji obiektu. Następnie przechowujemy tę odległość wraz z odpowiednim wektorem pozycji w strukturze danych zwanej mapą z biblioteki STL. Mapa automatycznie sortuje wartości na podstawie swoich kluczy, więc po dodaniu wszystkich pozycji z ich odległością jako kluczem będą automatycznie posortowane według wartości odległości:

    std::map<float, glm::vec3> sorted;
    for (unsigned int i = 0; i < windows.size(); i++)
    {
        float distance = glm::length(camera.Position - windows[i]);
        sorted[distance] = windows[i];
    }

Wynikiem jest posortowana mapa, która przechowuje każdą z pozycji okna na podstawie wartości klucza distance od najmniejszej do największej odległości.

Następnie, podczas renderowania, bierzemy każdą z wartości mapy w odwrotnej kolejności (od najdalszego do najbliższego), a następnie rysujemy odpowiednie okna w odpowiedniej kolejności:

    for(std::map<float,glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it) 
    {
        model = glm::mat4();
        model = glm::translate(model, it->second);				
        shader.setMat4("model", model);
        glDrawArrays(GL_TRIANGLES, 0, 6);
    }  

Używamy odwrotnego iteratora z mapy, aby iterować po każdym z elementów w odwrotnej kolejności, a następnie przesunąć każde okno na jego odpowiednią pozycję. To względnie proste podejście do sortowania przezroczystych obiektów rozwiązuje poprzedni problem. Teraz scena wygląda następująco:

Obraz sceny OpenGL z włączoną funkcją mieszania, obiekty są sortowane względem odległości

Możesz znaleźć kompletny kod źródłowy z sortowaniem tutaj.

Chociaż to podejście do sortowania obiektów według ich odległości działa dobrze w tym konkretnym scenariuszu, nie uwzględnia ono rotacji, skalowania ani żadnej innej transformacji, a dziwnie ukształtowane obiekty wymagają innej metryki niż po prostu wektor pozycji.

Sortowanie obiektów w scenie jest trudnym zadaniem, które zależy w dużym stopniu od rodzaju sceny, a tym bardziej od dodatkowej mocy obliczeniowej. Całkowite renderowanie sceny za pomocą nieprzezroczystych i przezroczystych obiektów nie jest wcale takie proste. Istnieją bardziej zaawansowane techniki, takie jak przezroczystość niezależna od kolejności (ang. order independent transparency), ale wykraczają one poza zakres tego samouczka. Na razie musisz żyć z normalnym blendingiem twoich obiektów, ale jeśli będziesz ostrożny i znasz ograniczenia, nadal możesz uzyskać dość przyzwoite efekty mieszania.