jak wspomniano w rozdziale Hello Triangle, shadery są małymi programami, które spoczywają na GPU. Programy te są uruchamiane dla każdej określonej sekcji potoku graficznego. W podstawowym sensie shadery to nic innego jak programy przekształcające wejścia na wyjścia. Shadery są również bardzo odizolowanymi programami, ponieważ nie mogą komunikować się ze sobą; jedyna komunikacja, jaką mają, to ich wejścia i wyjścia.
w poprzednim rozdziale krótko dotknęliśmy powierzchni shaderów i sposobu ich prawidłowego użytkowania. Teraz wyjaśnimy shadery, a w szczególności język cieniowania OpenGL, w bardziej ogólny sposób.
GLSL
shadery są pisane w języku C-podobnym do GLSL. GLSL jest dostosowany do użycia z grafiką i zawiera przydatne funkcje specjalnie ukierunkowane na manipulację wektorową i macierzową.
shadery zawsze zaczynają się od deklaracji wersji, po której następuje lista zmiennych wejściowych i wyjściowych, uniformów i ich głównej funkcji. Punkt wejścia każdego shadera znajduje się w jego głównej funkcji, gdzie przetwarzamy dowolne zmienne wejściowe i wypisujemy wyniki w jego zmiennych wyjściowych. Nie martw się, jeśli nie wiesz, co to są mundury, zajmiemy się nimi wkrótce.
shader ma zazwyczaj następującą strukturę:
#version version_numberin type in_variable_name;in type in_variable_name;out type out_variable_name; uniform type uniform_name; void main(){ // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed;}
gdy mówimy konkretnie o cieniowaniu wierzchołków, każda zmienna wejściowa jest również znana jako atrybut wierzchołka. Istnieje maksymalna liczba atrybutów wierzchołków, które możemy zadeklarować, ograniczona przez sprzęt. OpenGL gwarantuje, że zawsze dostępnych jest co najmniej 16 4-komponentowych atrybutów wierzchołków, ale niektóre urządzenia mogą pozwolić na więcej, które można pobrać, pytając GL_MAX_VERTEX_ATTRIBS:
int nrAttributes;glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
często zwraca minimum16
, które powinno być więcej niż wystarczające dla większości celów.
typy
GLSL ma, jak każdy inny język programowania, typy danych do określania, z jaką zmienną chcemy pracować. GLSL ma większość domyślnych podstawowych typów, które znamy z języków takich jak C: int
float
double
uint
I bool
. GLSL posiada również dwa typy kontenerów, których będziemy często używać, a mianowicie vectors
I matrices
. Omówimy macierze w późniejszym rozdziale.
Wektory
wektor w GLSL jest zbiornikiem składowym 1,2,3 lub 4 dla dowolnego z wymienionych typów podstawowych. Mogą one mieć następującą postać (n
reprezentuje liczbę komponentów):
-
vecn
: domyślny wektorn
pływa. -
bvecn
: wektorn
wartości logiczne. -
ivecn
: wektorn
liczby całkowite. -
uvecn
: wektorn
niepodpisanych liczb całkowitych. -
dvecn
: wektorn
podwójne elementy.
przez większość czasu będziemy używać podstawowego vecn
, ponieważ pływaki są wystarczające do większości naszych celów.
składniki wektora można uzyskać poprzezvec.x
, gdziex
jest pierwszym składnikiem wektora. Możesz użyć .x
.y
.z
I .w
, aby uzyskać dostęp do pierwszego, drugiego, trzeciego i czwartego komponentu odpowiednio. GLSL pozwala również na użycie rgba
dla kolorów lub stpq
dla współrzędnych tekstury, uzyskując dostęp do tych samych komponentów.
typ danych wektorowych pozwala na ciekawy i elastyczny wybór komponentów zwany swizzlingiem. Swizzling pozwala nam używać składni takiej jak ta:
vec2 someVec;vec4 differentVec = someVec.xyxx;vec3 anotherVec = differentVec.zyw;vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
możesz użyć dowolnej kombinacji do 4 liter, aby utworzyć nowy wektor (tego samego typu), O ile oryginalny wektor ma te komponenty; na przykład nie można uzyskać dostępu do.z
komponentuvec2
. Możemy również przekazać wektory jako argumenty do różnych wywołań konstruktorów wektorowych, zmniejszając liczbę wymaganych argumentów:
vec2 vect = vec2(0.5, 0.7);vec4 result = vec4(vect, 0.0, 0.0);vec4 otherResult = vec4(result.xyz, 1.0);
wektory są więc elastycznym typem danych, którego możemy użyć dla wszystkich rodzajów wejścia i wyjścia. W całej książce zobaczysz wiele przykładów tego, jak możemy kreatywnie zarządzać wektorami.
Ins and out
shadery są ładnymi małymi programami samymi w sobie, ale są częścią całości i dlatego chcemy mieć wejścia i wyjścia na poszczególnych shaderach, abyśmy mogli przenosić rzeczy. GLSL zdefiniował słowa kluczowein
Iout
specjalnie w tym celu. Każdy moduł cieniujący może określać wejścia i wyjścia za pomocą tych słów kluczowych i wszędzie tam, gdzie zmienna wyjściowa pasuje do zmiennej wejściowej następnego etapu modułu cieniującego, są one przekazywane dalej. Vertex i fragment shader różnią się jednak nieco.
moduł cieniujący wierzchołków powinien otrzymać jakąś formę danych wejściowych, w przeciwnym razie byłoby to dość nieskuteczne. Moduł cieniowania wierzchołków różni się tym, że otrzymuje dane wejściowe bezpośrednio z danych wierzchołków. Aby zdefiniować, w jaki sposób dane wierzchołków są zorganizowane, określamy zmienne wejściowe z metadanymi lokalizacji, dzięki czemu możemy skonfigurować atrybuty wierzchołków na procesorze. Widzieliśmy to w poprzednim rozdziale jako layout (location = 0)
. Moduł cieniowania wierzchołków wymaga zatem dodatkowej specyfikacji układu dla swoich wejść, abyśmy mogli powiązać go z danymi wierzchołków.
Możliwe jest również pominięcielayout (location = 0)
specyfikatora i zapytania o lokalizacje atrybutów w kodzie OpenGL poprzez glGetAttribLocation, ale wolałbym ustawić je w cieniu wierzchołków. Jest łatwiejszy do zrozumienia i oszczędza ci (i OpenGL) trochę pracy.
innym wyjątkiem jest to, że moduł cieniujący fragmentu wymaga zmiennej wyjściowej koloruvec4
, ponieważ moduły cieniujące fragmentu muszą wygenerować końcowy kolor wyjściowy. Jeśli nie określisz koloru wyjściowego w module cieniowania fragmentów, wynik bufora kolorów dla tych fragmentów będzie niezdefiniowany (co zwykle oznacza, że OpenGL renderuje je na czarno lub na biało).
więc jeśli chcemy wysyłać dane z jednego shadera do drugiego musielibyśmy zadeklarować wyjście w wysyłającym shaderze i podobne wejście w odbierającym shaderze. Gdy typy i nazwy są równe po obu stronach OpenGL połączy te zmienne ze sobą i wtedy możliwe jest przesyłanie danych między shaderami (odbywa się to podczas łączenia obiektu programu). Aby pokazać, jak to działa w praktyce, zmienimy shadery z poprzedniego rozdziału, aby moduł cieniujący wierzchołków decydował o kolorze modułu cieniującego fragmentów.
moduł cieniowania wierzchołków
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color}
moduł cieniowania fragmentów
#version 330 coreout vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main(){ FragColor = vertexColor;}
możesz zobaczyć, że zadeklarowaliśmy zmienną koloru wierzchołka jako wyjścievec4
, które ustawiliśmy w module cieniowania wierzchołków i deklarujemy podobne wejście koloru wierzchołka w module cieniowania fragmentów. Ponieważ oba mają ten sam typ i nazwę, kolor wierzchołka w module cieniującym fragmentu jest połączony z kolorem wierzchołka w module cieniującym wierzchołków. Ponieważ w module cieniowania wierzchołków ustawiliśmy kolor na ciemnoczerwony, powstałe fragmenty również powinny być ciemnoczerwone. Poniższy obraz przedstawia wyjście:
proszę bardzo! Właśnie udało nam się wysłać wartość z modułu cieniującego wierzchołków do modułu cieniującego fragmentów. Dodajmy trochę pikanterii i zobaczmy, czy możemy wysłać kolor z naszej aplikacji do modułu fragment shader!
Uniforms
Uniforms to kolejny sposób przekazywania danych z naszej aplikacji na CPU do shaderów na GPU. Uniformy różnią się jednak nieco od atrybutów wierzchołków. Po pierwsze, mundury są globalne. Globalne, co oznacza, że jednolita zmienna jest unikalna dla każdego obiektu programu cieniującego i może być dostępna z dowolnego modułu cieniującego na dowolnym etapie programu cieniującego. Po drugie, niezależnie od tego, na co ustawisz wartość uniform, uniformy zachowają swoje wartości, dopóki nie zostaną zresetowane lub zaktualizowane.
aby zadeklarować uniform w GLSL po prostu dodajemyuniform
słowo kluczowe do shadera z typem i nazwą. Od tego momentu możemy użyć nowo zadeklarowanego uniform w shaderze. Zobaczmy, czy tym razem możemy ustawić kolor trójkąta za pomocą munduru:
#version 330 coreout vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code.void main(){ FragColor = ourColor;}
zadeklarowaliśmy jednolity vec4
nasz kolor w cieniowaniu fragmentu i ustawiliśmy kolor wyjściowy fragmentu na zawartość tej jednolitej wartości. Ponieważ uniformy są zmiennymi globalnymi, możemy je zdefiniować w dowolnym etapie shadera, więc nie ma potrzeby ponownego przeglądania shadera wierzchołków, aby uzyskać coś do modułu cieniującego fragmentu. Nie używamy tego munduru w cieniowaniu wierzchołków, więc nie ma potrzeby definiowania go tam.
Jeśli zadeklarujesz uniform, który nie jest nigdzie używany w kodzie GLSL, kompilator po cichu usunie zmienną ze skompilowanej wersji, co jest przyczyną kilku frustrujących błędów; pamiętaj o tym!
mundur jest obecnie pusty; nie dodaliśmy jeszcze żadnych danych do munduru, więc spróbujmy. Najpierw musimy znaleźć indeks / lokalizację atrybutu uniform w naszym shaderze. Gdy już mamy indeks / lokalizację uniformu, możemy zaktualizować jego wartości. Zamiast przekazywać pojedynczy kolor do modułu cieniującego fragment, urozmaićmy to, stopniowo zmieniając kolor w czasie:
float timeValue = glfwGetTime();float greenValue = (sin(timeValue) / 2.0f) + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUseProgram(shaderProgram);glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
najpierw pobieramy czas działania w sekundach za pomocą glfwgettime(). Następnie zmieniamy kolor w zakresie0.0
1.0
za pomocą funkcji sin i zapisujemy wynik w greenValue.
następnie sprawdzamy lokalizację munduru ourColor za pomocą glGetUniformLocation. Dostarczamy program shader i nazwę uniform (z którego chcemy pobrać lokalizację) do funkcji zapytań. Jeśli glGetUniformLocation zwróci -1
, nie może znaleźć Lokalizacji. Na koniec możemy ustawić jednolitą wartość za pomocą funkcji glUniform4f. Zauważ, że znalezienie jednolitej lokalizacji nie wymaga użycia najpierw programu cieniującego, ale aktualizacja jednolitego wymaga użycia najpierw programu (przez wywołanie glUseProgram), ponieważ ustawia on jednolity na aktualnie aktywnym programie cieniującym.
ponieważ OpenGL jest w swojej podstawowej bibliotece C, nie ma natywnego wsparcia dla przeciążania funkcji, więc wszędzie tam, gdzie można wywołać funkcję z różnymi typami, OpenGL definiuje nowe funkcje dla każdego wymaganego typu; gluniform jest tego doskonałym przykładem. Funkcja wymaga specyficznego postfixu dla typu munduru, który chcesz ustawić. Kilka możliwych poprawek to:
-
f
: funkcja oczekujefloat
jako swojej wartości. -
i
: funkcja oczekujeint
jako swojej wartości. -
ui
: funkcja oczekujeunsigned int
jako swojej wartości. -
3f
: funkcja oczekuje 3float
s jako swojej wartości. -
fv
: funkcja oczekujefloat
wektora/tablicy jako swojej wartości.
gdy chcesz skonfigurować opcję OpenGL, po prostu wybierz przeciążoną funkcję, która odpowiada Twojemu typowi. W naszym przypadku chcemy ustawić 4 pływy munduru indywidualnie, więc przekazujemy nasze dane przez glUniform4f (zauważ, że mogliśmy również użyć wersjifv
).
teraz, gdy wiemy jak ustawić wartości zmiennych jednorodnych, możemy ich użyć do renderowania. Jeśli chcemy, aby kolor stopniowo się zmieniał, chcemy aktualizować ten mundur w każdej ramce, w przeciwnym razie Trójkąt zachowałby jeden jednolity kolor, jeśli ustawimy go tylko raz. Tak więc obliczamy wartość greenValue i aktualizujemy uniform przy każdej iteracji renderowania:
while(!glfwWindowShouldClose(window)){ // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents();}
kod jest stosunkowo prostą adaptacją poprzedniego kodu. Tym razem aktualizujemy jednolitą wartość każdej klatki przed narysowaniem trójkąta. Jeśli zaktualizujesz Mundur poprawnie, powinieneś zobaczyć, że kolor trójkąta stopniowo zmienia się z zielonego na czarny iz powrotem na zielony.
Sprawdź kod źródłowy tutaj, jeśli utknąłeś.
jak widzisz, uniformy są przydatnym narzędziem do ustawiania atrybutów, które mogą zmieniać każdą klatkę, lub do wymiany danych między aplikacją a shaderami, ale co jeśli chcemy ustawić kolor dla każdego wierzchołka? W takim przypadku musielibyśmy zadeklarować tyle mundurów, ile posiadamy wierzchołków. Lepszym rozwiązaniem byłoby włączenie większej ilości danych do atrybutów wierzchołków, co zrobimy teraz.
więcej atrybutów!
widzieliśmy w poprzednim rozdziale, Jak możemy wypełnić VBO, skonfigurować wskaźniki atrybutów wierzchołków i przechowywać to wszystko w VAO. Tym razem chcemy również dodać dane kolorów do danych wierzchołków. Dodamy dane kolorów jako 3 float
s do tablicy wierzchołków. Przypisujemy czerwony, zielony i niebieski kolor odpowiednio do każdego z rogów naszego trójkąta:
float vertices = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top };
ponieważ mamy teraz więcej danych do wysłania do modułu cieniującego wierzchołki, konieczne jest dostosowanie modułu cieniującego wierzchołki, aby również otrzymać naszą wartość koloru jako wejście atrybutu wierzchołka. Zauważ, że ustawiliśmy lokalizację atrybutu aColor na 1 ze specyfikatorem układu:
#version 330 corelayout (location = 0) in vec3 aPos; // the position variable has attribute position 0layout (location = 1) in vec3 aColor; // the color variable has attribute position 1 out vec3 ourColor; // output a color to the fragment shadervoid main(){ gl_Position = vec4(aPos, 1.0); ourColor = aColor; // set ourColor to the input color we got from the vertex data}
ponieważ nie używamy już jednolitego dla koloru fragmentu, ale teraz używamy zmiennej wyjściowej ourColor, będziemy musieli również zmienić moduł cieniujący fragmentu:
#version 330 coreout vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0);}
ponieważ dodaliśmy kolejny atrybut wierzchołka i zaktualizowaliśmy VBO pamięć musimy ponownie skonfigurować wskaźniki atrybutów wierzchołków. Zaktualizowane dane w pamięci VBO wyglądają teraz trochę tak:
znając aktualny układ możemy zaktualizować format wierzchołka za pomocą glvertexattribointer:
// position attributeglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// color attributeglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));glEnableVertexAttribArray(1);
kilka pierwszych argumentów glvertexattribointer jest stosunkowo proste. Tym razem konfigurujemy atrybut wierzchołek w miejscu atrybutu 1
. Wartości kolorów mają rozmiar 3
float
s i nie normalizujemy wartości.
ponieważ mamy teraz dwa atrybuty wierzchołków, musimy ponownie obliczyć wartość kroku. Aby uzyskać następną wartość atrybutu (np. następnyx
Składnik wektora położenia) w tablicy danych musimy przesunąć6
float
s W prawo, trzy dla wartości położenia i trzy dla wartości koloru. Daje nam to wartość kroku 6 razy większą niż float
w bajtach (=24
bajtów).
również tym razem musimy określić offset. Dla każdego wierzchołka atrybut wierzchołka pozycji jest pierwszy, więc deklarujemy przesunięcie o 0
. Atrybut kolor rozpoczyna się po danych pozycji, więc przesunięcie wynosi 3 * sizeof(float)
w bajtach (=12
bajty).
uruchomienie aplikacji powinno skutkować następującym obrazkiem:
Sprawdź kod źródłowy tutaj, jeśli utknąłeś.
obraz może nie być dokładnie tym, czego można się spodziewać, ponieważ dostarczyliśmy tylko 3 kolory, a nie ogromną paletę kolorów, którą teraz widzimy. Wszystko to jest wynikiem czegoś, co nazywa się interpolacją fragmentu w module cieniującym fragmentu. Podczas renderowania trójkąta etap rasteryzacji zwykle powoduje o wiele więcej fragmentów niż pierwotnie określone wierzchołki. Następnie rasteryzator określa położenie każdego z tych fragmentów w oparciu o to, gdzie znajdują się na kształcie trójkąta.
opierając się na tych pozycjach, interpoluje wszystkie zmienne wejściowe modułu cieniującego fragmentu. Powiedzmy na przykład, że mamy linię, w której górny punkt ma kolor zielony, a dolny niebieski. Jeśli moduł cieniujący fragmentu jest uruchamiany na fragmencie, który znajduje się wokół pozycji 70%
linii, jego wynikowy atrybut wejściowy koloru byłby wtedy liniową kombinacją zielonego i Niebieskiego; aby być bardziej precyzyjnym: 30%
niebieski i 70%
zielony.
tak właśnie było w trójkącie. Mamy 3 wierzchołki, a więc 3 kolory, a sądząc po pikselach trójkąta, prawdopodobnie zawiera około 50000 fragmentów, w których fragment shader interpolował Kolory między tymi pikselami. Jeśli dobrze przyjrzysz się kolorom, zobaczysz, że wszystko ma sens: czerwony do niebieskiego najpierw przechodzi w fioletowy, a następnie niebieski. Interpolacja fragmentu jest stosowana do wszystkich atrybutów wejściowych modułu cieniującego fragmentu.
nasza własna Klasa shaderów
pisanie, kompilowanie i zarządzanie shaderami może być dość uciążliwe. Na koniec na temat shaderów zamierzamy ułatwić sobie życie, budując klasę shaderów, która odczytuje shadery z dysku, kompiluje je i łączy, sprawdza błędy i jest łatwa w użyciu. To również daje wam trochę wyobrażenia, jak możemy zawrzeć część wiedzy, którą do tej pory zdobyliśmy, w użyteczne abstrakcyjne obiekty.
stworzymy klasę shader w całości w pliku nagłówkowym, głównie w celach edukacyjnych i przenośności. Zacznijmy od dodania wymaganych includes i zdefiniowania struktury klas:
#ifndef SHADER_H#define SHADER_H#include <glad/glad.h> // include glad to get all the required OpenGL headers #include <string>#include <fstream>#include <sstream>#include <iostream> class Shader{public: // the program ID unsigned int ID; // constructor reads and builds the shader Shader(const char* vertexPath, const char* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;}; #endif
użyliśmy kilku dyrektyw preprocesora u góry pliku nagłówkowego. Używanie tych małych linii kodu informuje kompilator, aby uwzględniał i kompilował ten plik nagłówkowy tylko wtedy, gdy nie został jeszcze dołączony, nawet jeśli wiele plików zawiera nagłówek modułu cieniującego. Zapobiega to konfliktom łączenia.
Klasa shader posiada ID programu shader. Jego konstruktor wymaga ścieżek pliku kodu źródłowego wierzchołka i modułu cieniującego fragmentu, które możemy przechowywać na dysku jako proste pliki tekstowe. Aby dodać trochę więcej dodajemy również kilka funkcji narzędziowych, aby ułatwić nasze życie trochę: użyj aktywuje program shaderów, a wszystko ustawione… funkcje odpytywają jednorodną lokalizację i ustawiają jej wartość.
odczyt z pliku
używamy strumieni plików c++, aby odczytać zawartość z pliku do kilku obiektówstring
:
Shader(const char* vertexPath, const char* fragmentPath){ // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream << vShaderFile.rdbuf(); fShaderStream << fShaderFile.rdbuf(); // close file handlers vShaderFile.close(); fShaderFile.close(); // convert stream into string vertexCode = vShaderStream.str(); fragmentCode = fShaderStream.str(); } catch(std::ifstream::failure e) { std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl; } const char* vShaderCode = vertexCode.c_str(); const char* fShaderCode = fragmentCode.c_str();
następnie musimy skompilować i połączyć shadery. Zauważ, że sprawdzamy również, czy kompilacja / łączenie nie powiodło się, a jeśli tak, wypisujemy błędy czasu kompilacji. Jest to bardzo przydatne podczas debugowania (będziesz potrzebował tych dzienników błędów):
// 2. compile shadersunsigned int vertex, fragment;int success;char infoLog; // vertex Shadervertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// print compile errors if anyglGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){ glGetShaderInfoLog(vertex, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}; // similiar for Fragment Shader // shader ProgramID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// print linking errors if anyglGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){ glGetProgramInfoLog(ID, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;} // delete the shaders as they're linked into our program now and no longer necessaryglDeleteShader(vertex);glDeleteShader(fragment);
funkcja użycia jest prosta:
void use() { glUseProgram(ID);}
podobnie dla dowolnej funkcji ustawiacza jednorodnego:
void setBool(const std::string &name, bool value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); }void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value); }void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value); }
i tam mamy to, ukończoną klasę shader. Korzystanie z klasy shaderów jest dość łatwe; tworzymy obiekt shadera raz i od tego momentu po prostu zaczynamy go używać:
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");while(...){ ourShader.use(); ourShader.setFloat("someUniform", 1.0f); DrawStuff();}
tutaj przechowujemy kod źródłowy wierzchołka i fragmentu shadera w dwóch plikach o nazwieshader.vs
Ishader.fs
. Możesz dowolnie nazywać swoje pliki shaderów; Osobiście uważam, że rozszerzenia I .fs
są dość intuicyjne.
Możesz znaleźć kod źródłowy tutaj używając naszej nowo utworzonej klasy shader. Pamiętaj, że możesz kliknąć ścieżki plików shaderów, aby znaleźć kod źródłowy shaderów.
ćwiczenia
- Dostosuj shader wierzchołków tak, aby trójkąt był odwrócony do góry nogami: rozwiązanie.
- Określ Przesunięcie poziome za pomocą munduru i przesuń trójkąt po prawej stronie ekranu w cieniowaniu wierzchołków, używając tej wartości przesunięcia: rozwiązanie.
- wyprowadza pozycję wierzchołka do modułu cieniującego fragmentu za pomocą słowa kluczowego
out
I ustawia kolor fragmentu równy tej pozycji wierzchołka (zobacz, jak interpolowane są wartości pozycji wierzchołka w całym trójkącie). Gdy już ci się to udało, spróbuj odpowiedzieć na następujące pytanie: dlaczego lewy dolny bok naszego trójkąta jest czarny?: rozwiązanie.