This is the Polish translation of Lighting/Colors article of learnopengl.com tutorial series.

W poprzednich samouczkach krótko wspomnieliśmy, jak pracować z kolorami w OpenGL, ale do tej pory traktowaliśmy ten temat powierzchownie. Tutaj szczegółowo omówimy kolory i zaczniemy budować scenę dla kolejnych tutoriali związanych z oświetleniem.

W świecie rzeczywistym kolory mogą przyjmować praktycznie dowolną znaną wartość koloru, a każdy obiekt posiada własny kolor. W świecie cyfrowym musimy odwzorować (nieskończone) rzeczywiste kolory na (skończone) wartości cyfrowe, a zatem nie wszystkie rzeczywiste kolory mogą być reprezentowane cyfrowo. Możemy jednak reprezentować tak wiele kolorów, że prawdopodobnie i tak nie zauważysz różnicy. Kolory są reprezentowane cyfrowo za pomocą trzech składowych: czerwonej, zielonej i niebieskiej, które zwykle są określanego skrótem RGB (ang. red - green - blue). Używając różnych kombinacji tylko tych 3 wartości możemy przedstawić prawie każdy kolor. Na przykład, aby uzyskać kolor koralowy, definiujemy wektor koloru jako:

    glm::vec3 coral(1.0f, 0.5f, 0.31f);   

Kolory, które widzimy w rzeczywistości, nie są kolorami, które obiekty faktycznie mają, ale są kolorami odbitymi od obiektu; kolory, które nie zostały zabsorbowane (pochłonięte) przez obiekty, to kolory, które widzimy. Na przykład światło słońca jest postrzegane jako białe światło, które jest sumą wielu różnych kolorów (jak widać na obrazku). Gdybyśmy oświetlili białym światłem niebieską zabawkę, to pochłonęłaby ona wszystkie kolory tego białego światła z wyjątkiem niebieskiego. Ponieważ zabawka nie absorbuje niebieskiej składowej, jest odbijana, a to odbijane światło wpada do naszego oka, dzięki czemu zabawka ma niebieski kolor. Poniższy obrazek pokazuje to dla koralowej zabawki, która odzwierciedla kilka kolorów o różnej intensywności:

Widać, że białe światło słoneczne jest w rzeczywistości zbiorem wszystkich widocznych kolorów, a obiekt pochłania dużą część tych kolorów. Obiekt odbija tylko te kolory, które reprezentują kolor obiektu, a kombinacja tych dwóch zjawisk daje nam w tym przypadku kolor koralowy.

Te zasady odbijania kolorów stosuje się bezpośrednio w świecie grafiki komputerowej. Kiedy definiujemy źródło światła w OpenGL, chcemy nadać temu źródle światła kolor. W poprzednim akapicie mieliśmy biały kolor, więc źródle światła również dajemy biały kolor. Gdybyśmy następnie pomnożyli kolor źródła światła z wartością koloru obiektu, uzyskany kolor będzie odbijanym kolorem od obiektu (a tym samym jego postrzeganym kolorem). Wróćmy do naszej zabawki (tym razem z wartością koralową) i zobaczmy, jak obliczalibyśmy jej postrzegalny kolor w świecie grafiki komputerowej. Pobieramy uzyskany wektor koloru, wykonując mnożenie składników dla obu wektorów kolorów:

    glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
    glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
    glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);

Widzimy, że kolor zabawki absorbuje dużą część białego światła, ale odbija kilka wartości czerwonych, zielonych i niebieskich w oparciu o własną wartość koloru. Jest to przybliżenie tego, jak kolory działają w prawdziwym życiu. Możemy w ten sposób zdefiniować kolor obiektu jako ilość każdego składnika koloru źródła światła, która jest odbijana od obiektu. Co się stanie, jeśli użyjemy zielonego światła?

    glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
    glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
    glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);

Jak widać, zabawka nie ma czerwonego ani niebieskiego światła do wchłonięcia i/lub odbicia. Zabawka pochłania również połowę zielonej wartości światła, ale nadal odbija połowę zielonej wartości światła. Kolor zabawki, który postrzegamy, miałby kolor ciemnozielony. Widzimy, że jeśli używamy zielonego światła, to tylko zielone składniki mogą być odbijane i postrzegane; nie dostrzegalne są czerwone i niebieskie kolory. W rezultacie obiekt koralowy nagle staje się ciemnozielonym obiektem. Spróbujemy jeszcze jednego przykładu z ciemno-oliwkowo-zielonym światłem:

    glm::vec3 lightColor(0.33f, 0.42f, 0.18f);
    glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
    glm::vec3 result = lightColor * toyColor; // = (0.33f, 0.21f, 0.06f);

Jak widać, możemy uzyskać nieoczekiwane kolory obiektów przy użyciu różnych kolorów światła. Nietrudno puścić wodze kreatywność dzięki kolorom.

Ale już dość o kolorach, zacznijmy budować scenę, w której będziemy mogli eksperymentować.

Scena testowa oświetlenia

W nadchodzących tutorialach będziemy tworzyć ciekawe efekty wizualne, symulując oświetlenie w świecie rzeczywistym, wykorzystując w szerokim zakresie kolory. Odtąd będziemy używać źródeł światła, które chcemy wyświetlić jako obiekty wizualne w scenie i dodać co najmniej jeden obiekt do symulacji oświetlenia.

Pierwszą rzeczą, jakiej potrzebujemy, jest obiekt, który będzie oświetlany. W tym celu użyjemy niesławnej kostki kontenera z poprzednich tutoriali. Będziemy również potrzebować obiektu pokazującego, gdzie znajduje się źródło światła w scenie 3D. Dla uproszczenia będziemy reprezentować źródło światła również za pomocą kostki (mamy już dane wierzchołkowe).

Wypełnienie obiektu bufora wierzchołków (ang. vertex buffer object), ustawienie atrybutów wierzchołków i wszystkich innych dziwnych rzeczy powinno być teraz dla Ciebie łatwe, więc nie omówimy tych kroków. Jeśli nadal masz problemy z tymi ustawieniami, radzę przejrzeć poprzednie tutoriale i przed rozpoczęciem pracy, jeśli to możliwe, przejrzeć ćwiczenia.

Tak więc, pierwszą rzeczą, której potrzebujemy, jest Vertex Shader do narysowania kontenera. Pozycje wierzchołków kontenera pozostają takie same (chociaż tym razem nie będziemy potrzebować współrzędnych tekstury), więc kod nie powinien być niczym nowym. Będziemy używać uproszczonej wersji Vertex Shader’a z ostatnich samouczków:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    void main()
    {
        gl_Position = projection * view * model * vec4(aPos, 1.0);
    } 

Upewnij się, że zaktualizowałeś swoje dane wierzchołkowe i atrybuty wierzchołków, aby odpowiadały nowemu Vertex Shader’owi (jeśli chcesz, możesz rzeczywiście zachować dane tekstury i pozostawić aktywne atrybuty wierzchołków - po prostu nie będziemy ich teraz używać, ale nie jest złym pomysłem, by zacząć od początku).

Ponieważ zamierzamy również stworzyć kostkę pokazującą pozycję lampy, chcemy wygenerować nowy VAO specjalnie dla lampy. Moglibyśmy również reprezentować lampę używając tego samego VAO, a następnie po prostu dokonać pewnych transformacji na macierzy modelu, ale w nadchodzących tutorialach będziemy zmieniać dane wierzchołkowe i atrybuty wierzchołków obiektu kontenera dość często i nie chcemy, aby te zmiany dotyczyły również obiektu lampy (zależy nam tylko na pozycjach wierzchołków lampy), więc utworzymy nowe VAO:

    unsigned int lightVAO;
    glGenVertexArrays(1, &lightVAO);
    glBindVertexArray(lightVAO);
    // we only need to bind to the VBO, the container's VBO's data already contains the correct data.
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // set the vertex attributes (only position data for our lamp)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

Kod powinien być stosunkowo prosty. Teraz, gdy stworzyliśmy zarówno kontener, jak i obiekt lampy, pozostała jeszcze jedna rzecz do zdefiniowania i jest to Fragment Shader:

    #version 330 core
    out vec4 FragColor;

    uniform vec3 objectColor;
    uniform vec3 lightColor;

    void main()
    {
        FragColor = vec4(lightColor * objectColor, 1.0);
    }

Fragment Shader pobiera zarówno kolor obiektu, jak i kolor światła ze zmiennej typu uniform. Tutaj mnożymy kolor światła z kolorem obiektu (odbijanym), tak jak to omawialiśmy na początku tego samouczka. Ten Fragment Shader powinien być łatwy do zrozumienia. Ustawmy kolor obiektu na kolor koralowy z ostatniej sekcji i biały kolor światła:

    // nie zapomnij najpierw "użyć (ang. use)" odpowiedniego programu cieniującego (aby ustawić uniform)
    lightingShader.use();
    lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
    lightingShader.setVec3("lightColor",  1.0f, 1.0f, 1.0f);

Jedną rzeczą, na którą warto zwrócić uwagę, jest to, że gdy zaczniemy zmieniać Vertex i Fragment Shader, zmieni się także obiekt lampy i nie jest to tym, czego chcemy. Nie chcemy, aby kolor obiektu lampy był modyfikowany przez obliczenia oświetlenia, ale raczej aby lampa była odizolowana od reszty. Chcemy, aby lampa miała stały, jasny kolor, nienaruszony przez inne zmiany kolorów (dzięki temu będzie widać, że lampa jest źródłem światła).

Aby to osiągnąć, musimy stworzyć drugi zestaw shader’ów, których użyjemy do narysowania lampy, dzięki czemu będziemy ustrzeżemy się przed wszelkimi zmianami w shader’ach oświetlenia. Vertex Shader jest taki sam, jak bieżący Vertex Shader, więc możesz po prostu skopiować jego kod źródłowy do Vertex Shader’a lampy. Fragment Shader lampy zapewnia, że ​​kolor lampy pozostanie jasny, poprzez zdefiniowanie stałego białego koloru lampy:

    #version 330 core
    out vec4 FragColor;

    void main()
    {
        FragColor = vec4(1.0); // ustaw wszystkie 4 wartości wektora na 1.0
    }

Kiedy chcemy narysować nasze obiekty, chcemy narysować obiekt kontenera (lub możliwie wiele innych obiektów) za pomocą shadera oświetlenia, który właśnie zdefiniowaliśmy, a kiedy chcemy narysować lampę, używamy shader’a lampy. Podczas kolejnych tutoriali będziemy stopniowo aktualizować shader’y oświetlenia, aby powoli osiągać bardziej realistyczne wyniki.

Głównym celem obiektu lampy jest pokazanie, skąd pada światło. Zwykle określamy położenie źródła światła gdzieś w scenie, ale jest to po prostu pozycja, która nie ma żadnego wizualnego znaczenia. Aby wyświetlić lampę, rysujemy obiekt lampy w tym samym miejscu, co źródło światła. Osiąga się to przez narysowanie obiektu lampy za pomocą shader’a lampy, zapewniając, że obiekt lampy pozostaje biały, niezależnie od warunków oświetleniowych sceny.

Tak więc zadeklaruj globalną zmienną vec3, która reprezentuje położenie źródła światła we współrzędnych świata:

    glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

Następnie chcielibyśmy przesunąć obiekt lampy do pozycji źródła światła jeszcze przed jego narysowaniem, a także przeskalować go nieco, aby upewnić się, że lampa nie jest zbyt duża:

    model = glm::mat4();
    model = glm::translate(model, lightPos);
    model = glm::scale(model, glm::vec3(0.2f)); 

Wynikowy kod rysowania obiektu lampy powinien wyglądać mniej więcej tak:

    lampShader.use();
    // ustaw unfiromy macierzy modelu, widoku i projekcji
    ...
    // narysuj obiekt lampy
    glBindVertexArray(lightVAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);			

Wstrzykując wszystkie fragmenty kodu do odpowiednich miejsc da w rezultacie, czystą aplikację OpenGL odpowiednio skonfigurowaną do eksperymentowania z oświetleniem. Jeśli wszystko się skompiluje, powinno wyglądać tak:

Nie ma teraz za bardzo na co popatrzeć, ale obiecuję, że scena będzie bardziej interesujący w kolejnych tutorialach.

Jeśli masz problemy ze znalezieniem miejsc, w których wszystkie fragmenty kodu pasują do siebie jako całości, sprawdź kod źródłowy tutaj i starannie dostosuj swój kod/komentarze.

Teraz, gdy trochę pojęcia o kolorach i stworzyliśmy podstawową scenę dla kolejnych algorytmów oświetlenia, możemy przejść do kolejnego tutoriala, gdzie zaczynie się prawdziwa “magia”.