Jestem w trakcie pracy nad t膮 notatk膮.

Rysujemy pierwszy tr贸jk膮t.

Merge Request

Ale najpierw zacznijmy od utworzenia MergeRequesta 馃檪
Task: https://gitlab.com/mateusz.salski/learnopengl/issues/1
Merge request: https://gitlab.com/mateusz.salski/learnopengl/merge_requests/1

I lokalnie zmieniamy brancha

$ git fetch
From gitlab.com:mateusz.salski/learnopengl
  * [new branch]      1-tutorial-2-hello-triangle-learn-and-make-notes -> origin/1-tutorial-2-hello-triangle-learn-and-make-notes

$ git checkout 1-tutorial-2-hello-triangle-learn-and-make-notes
 Branch '1-tutorial-2-hello-triangle-learn-and-make-notes' set up to track remote branch '1-tutorial-2-hello-triangle-learn-and-make-notes' from 'origin'.
 Switched to a new branch '1-tutorial-2-hello-triangle-learn-and-make-notes'

$ git branch
 * 1-tutorial-2-hello-triangle-learn-and-make-notes
   master

Programowanie tr贸jk膮ta na GPU

Nasz MainComponent dziedziczy po klasie juce::OpenGLAppComponent, kt贸ra dostarcza kilku metod wirtualnych. Dla nas najwa偶niejsze s膮 trzy:

  • void initialise()
  • void shutdown()
  • void render()

void initialise()

Dla mnie kluczowe okaza艂o si臋 zrozumienie, 偶e inicjalizacja OpenGLa sk艂ada si臋 z dw贸ch etap贸w.

  1. Przygotowania programu dla procesora GPU
  2. Opisania danych, kt贸re b臋dziemy do niego wysy艂a膰

Przygotowanie programu dla GPU

OpenGL zamienia dane wierzcho艂k贸w na grafik臋 3D (na ekranie 2D) w sze艣ciu krokach, tzw. shaderach, ale nie posiada domy艣lnych vertexShader-a i fragmentShadera. Dlatego musimy sami zdefiniowa膰 co on w tych dw贸ch krokach ma wykona膰.

呕eby to zrobi膰 musimy napisa膰 dwa proste programy w GLSL, zleci膰 GPU ich kompilacj臋, zlinkowa膰 je do jednego programu i posprz膮ta膰 troch臋.

GLuint buildProgram()
     {
         // ... place for shaders code (see below) ...
         
         GLuint vertexShaderId = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
         checkCompilationStatus(vertexShaderId);
         
         GLuint fragmentShaderId = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
         checkCompilationStatus(fragmentShaderId);
         
         GLuint shaderProgramId = createProgremObject(vertexShaderId, fragmentShaderId);
         checkProgramLinkingStatus(shaderProgramId);
         
         glDeleteShader(vertexShaderId);
         glDeleteShader(fragmentShaderId);
         
         return shaderProgramId;
     }

Kalkuj膮c z https://learnopengl.com/Getting-started/Hello-Triangle najprostszy vertexShader mo偶e wygl膮da膰 tak:

GLuint buildProgram()
     {
         const char* vertexShaderSource =
         "#version 330 core\n"
         "layout (location = 0) in vec3 aPos;\n"
         "void main()\n"
         "{\n"
         "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
         "}
GLuint buildProgram()
    {
        const char* vertexShaderSource =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"
        "void main()\n"
        "{\n"
        "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
        "}\0";
// ...
"; // ...

a fragmentSHader tak:

        const char* fragmentShaderSource =
         "#version 330 core\n"
         "out vec4 color;\n"
         "\n"
         "void main()\n"
         "{\n"
         "    color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
         "}
        const char* fragmentShaderSource =
        "#version 330 core\n"
        "out vec4 color;\n"
        "\n"
        "void main()\n"
        "{\n"
        "    color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "}\0";
// ...
"; // ...

Kompilacja shader贸w mo偶e wygl膮da膰 tak:

    GLuint compileShader(GLenum shaderType, const char* shaderSource)
     {
         GLuint shaderId = glCreateShader(shaderType);
         
         glShaderSource(shaderId, 1, &shaderSource, NULL);
         glCompileShader(shaderId);
         
         return shaderId;
     }

Warto sprawdzi膰 jej status. Poni偶sza metoda nie uchroni aplikacji przed crushem w wypadku b艂臋du w kodzie shader贸w, ale zaloguje na konsoli komunikaty b艂臋d贸w.

    void checkCompilationStatus(GLuint shaderId)
     {
         GLint success;
         GLchar infoLog[512];
         glGetShaderiv(shaderId, GL_COMPILE_STATUS, &success);
         
         if(!success)
         {
             glGetShaderInfoLog(shaderId, 512, NULL, infoLog);
             std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << "\n";
         }
     }

Teraz 艂膮czymy nasze shadery z programem:

    GLuint createProgremObject(GLuint vertexShaderId, GLuint fragmentShaderId)
     {
         GLuint shaderProgramId = glCreateProgram();
         
         glAttachShader(shaderProgramId, vertexShaderId);
         glAttachShader(shaderProgramId, fragmentShaderId);
         glLinkProgram(shaderProgramId);
         
         return shaderProgramId;
     }

Warto sprawdzi膰 r贸wnie偶 status linkowania programu:

    void checkProgramLinkingStatus(GLuint shaderProgramId)
     {
         GLint success;
         GLchar infoLog[512];
         glGetProgramiv(shaderProgramId, GL_LINK_STATUS, &success);
         if (!success) {
             glGetProgramInfoLog(shaderProgramId, 512, NULL, infoLog);
             std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
         }
     }

Opisanie danych wysy艂anych do GPU

Teraz musimy przygotowa膰 miejsce w pami臋ci GPU na dane wierzcho艂k贸w i opisa膰 dla OpenGL.

Zdefiniujemy dla tego zadania metod臋 describeData(), kt贸ra na pocz膮tku b臋dzie zawiera艂a tablic臋 z wsp贸艂rz臋dnymi wierzcho艂k贸w tr贸jk膮ta. Wy艣wietlana grafika jest statyczna wi臋c nie ma potrzeby definiowa膰 tych danych w szerszym zakresie.

void describeData()
{
 聽 聽 聽 聽 float vertices[ 3 * 3 ] = {
 聽 聽 聽 聽 聽 聽 -0.5f, -0.5f, 0.0f,
 聽 聽 聽 聽 聽 聽 0.5f, -0.5f, 0.0f,
 聽 聽 聽 聽 聽 聽 0.0f,聽 0.5f, 0.0f
 聽 聽 聽 聽 };

Prosimy OpenGL o rezerwacj臋 nazw / identyfikator贸w dla Vertex Array Object-u i Vertex Buffer Object-u.

Vertex Array Object przechowuje pod jednym identyfikatorem konfiguracj臋 danych opisuj膮cych wierzcho艂ki. Dzi臋ki czemu podczas renderowania nie trzeba wywo艂ywa膰 (pisa膰) ca艂ej konfiguracji, tylko robimy to raz, a podczas renderowania przepinamy si臋 tylko mi臋dzy odpowiednimi VAO.

 聽 聽 聽 聽 glGenVertexArrays(1, &VAO);
 聽 聽 聽 聽 glGenBuffers(1, &VBO);
 聽聽 聽 聽 聽

Informujemy OpenGL, 偶e teraz b臋dziemy pracowa膰 w kontek艣cie danego VAO. Wszystkie kolejne operacje b臋d膮 skojarzone z tym VAO.

 聽 聽 聽 聽 glBindVertexArray(VAO);
 聽 聽 聽 聽 {

W ramach powy偶szego VAO b臋dziemy teraz konfigurowa膰 Vertex Buffer Object.

 聽 聽 聽 聽 聽 聽 glBindBuffer(GL_ARRAY_BUFFER, VBO);
 聽 聽 聽 聽 聽 聽 {

W ramach naszego VBO tworzymy buffor o okre艣lonym rozmiarze i inicjalizujemy go danymi.

 聽 聽 聽 聽 聽 聽 聽 聽 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 聽聽 聽 聽 聽 聽 聽 聽 聽

Opisujemy dane w buforze. Kolejne parametry metody glVertexAttribPointer okre艣laj膮:

  1. Indeks atrybutu, kt贸ry modyfikujemy. My mamy tylko jeden atrybut b臋d膮cy wsp贸艂rz臋dnymi wierzcho艂ka.
  2. Ile zmiennych opisuje dany atrybut. My mamy trzy wsp贸艂rz臋dne x, y, z.
  3. Typ zmiennych opisuj膮cych atrybut. W naszym wypadku float.
  4. Czy normalizowa膰 warto艣ci. Nasze s膮 ju偶 znormalizowane, wi臋c GL_FALSE.
  5. Ilo艣膰 bajt贸w mi臋dzy pocz膮tkami danych kolejnych wierzcho艂k贸w. To b臋dzie mia艂o sens jak dodamy do atrybut贸w wierzcho艂ka kolor i/lub wsp贸艂rz臋dne tekstury.
  6. Ile bajt贸w pomin膮膰 przed pierwszym wierzcho艂kiem. U nas zero.
 聽 聽 聽 聽 聽 聽 聽 聽 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

“W艂膮czamy” atrybut o indeksie 0. Ten, kt贸ry przed chwil膮 opisali艣my.

 聽 聽 聽 聽 聽 聽 聽 聽 glEnableVertexAttribArray(0);

“Odpinamy” VBO, bo ju偶 wszystko skonfigurowali艣my dla niego.

 聽 聽 聽 聽 聽 聽 }
 聽 聽 聽 聽 聽 聽 glBindBuffer(GL_ARRAY_BUFFER, 0);

“Odpinamy” VAO.

 聽 聽 聽 聽 }
 聽 聽 聽 聽 glBindVertexArray(0);
 聽 聽 }

Render

Dzi臋ki VAO kod renderuj膮cy jest bardzo prosty.

Czy艣cimy ca艂y ekran.

    void render() override
     {
         OpenGLHelpers::clear (Colours::black);
         
         renderTriangle();
     }

Aktywujemy na GPU nasz shaderProgram i opis danych wierzcho艂k贸w zawarty w VAO. Rysujemy tr贸jk膮ty zaczynaj膮c od wierzcho艂ka o indeksie 0 i bierzemy pod uwag臋 trzy wierzcho艂ki. Czyli narysujemy tylko jeden tr贸jk膮t.

    void renderTriangle()
     {        
         glUseProgram(shaderProgram);
         glBindVertexArray(VAO);
         glDrawArrays(GL_TRIANGLES, 0, 3);
     }

Shutdown

Na koniec usuwamy VAO i VBO.

    void shutdown() override
     {
         deleteVertexArraysAndBuffers();
     }
    void deleteVertexArraysAndBuffers()
     {
         glDeleteVertexArrays(1, &VAO);
         glDeleteBuffers(1, &VBO);
     }

Source code

Source code for this part: https://gitlab.com/mateusz.salski/learnopengl/-/tags/HelloTriangle