Laboratorul 05

Pentru rezolvarea cerințelor din acest laborator, aveți nevoie de codul utilizat în rezolvarea cerințelor din cadrul laboratorului 3. În situatia în care nu ați rezolvat laboratorul 3, va trebui să îl realizați mai întâi pe el și ulterior să reveniți la cerințele celui curent.

Reamintire!!! Puteți prezenta rezolvările cerințelor de până la 2 laboratoare, în fiecare săptămână. De exemplu, puteți prezenta laboratorul curent și pe cel din săptămâna anterioară, în totalitate sau parțial, inclusiv punctajul pentru cerința bonus :) .

Pentru rezolvarea cerințelor din cadrul acestui laborator:

  1. Descărcați framwork-ul de laborator și copiați, din arhiva descărcată, directorul Lab5, în interiorul directorului gfx-framework-ppbg\src\lab din versiunea voastră de proiect.
  2. Adăugați în fișierul lab_list.h, linia #include “lab/lab5/lab5.h”.
  3. Folosiți din nou utilitarul CMake pentru a regenera proiectul. Pentru a vă reaminti procesul de realizare a setup-ului, puteți să reconsultați pagina dedicată acestui lucru.

Banda grafică programabilă

În laboratorul anterior, am analizat modalitatea prin care sunt stocate listele de vârfuri și de indici ce descriu un model 3D compus dintr-o rețea de triunghiuri. În API-ul grafic OpenGL, noi am utilizat 3 tipuri de obiecte diferite, un obiect ce păstrează buffer-ul de vârfuri (VBO), un obiect ce păstrează buffer-ul de indici (IBO) și un container ce le înglobează pe amândouă, VAO. Pentru desenare, informația listelor de vârfuri și indici trebuie să se regăsească în memoria RAM a procesorului grafic.

În laboratorul 3, am analizat modalitatea prin care coordonatele vârfurilor unui model 3D sunt transformate din spațiul inițial în care au fost definite, numit spațiul obiect, la poziția și forma finală în scenă, al cărei spațiu este cunoscut sub numele de spațiul lume. Din acest spațiu, coordonatele vârfurilor sunt transformate în spațiul de vizualizare și ulterior în spațiul de decupare. După împărțirea perspectivă, decuparea triunghiurilor la limitele volumului de decupare, eliminarea triunghiurilor pe baza opțiunii de afișare a fețelor și transformarea în poarta de afișare, triunghiurile sunt transmise în procesul de rasterizare. Pe baza acestui proces, triunghiurile sunt desenate în grila de pixeli. Toți acești pași se regăsesc în imaginea de mai jos, unde este prezentată o privire de ansamblu a benzii grafice.

Procesorul grafic, prin intermediul driver-ului video și a standardului API OpenGL, ne pune la dispoziție posibilitatea de a scrie programe de calculator ce sunt executate direct de către unitatea grafică de procesare. Pașii în care putem executa aceste programe sunt ficși în banda grafică. Există mai multe tipuri de astfel de programe, dar în acest laborator, vom analiza doar două. Toate tipurile de programe poartă denumirea în limba engleză de shader. De asemenea, pentru programarea fiecărui tip de program, se utilizează un limbaj special, particular pentru fiecare tip de program. Acest limbaj se numeste generic Limbaj de colorare OpenGL, sau în limba engleză, OpenGL Shading Language (GLSL).

Înainte de a merge mai departe, pentru a avea evidențiere și auto-completare pentru limbajul de programare GLSL, în mediul de dezvoltare Visual Studio, este recomandat să instalați o extensie specifică :) . Pentru versiuni mai vechi de Visual Studio 2022, puteți utiliza această extensie.

Programe de tip shader pentru prelucrarea vârfurilor

Pentru a prelucra informația fiecărui vârf în parte, se introduce un program de tip shader, denumit în limba engleză Vertex Shader. Pentru fiecare vârf din lista de vârfuri, se execută o singură instanță de program de tip vertex shader. Sintaxa lui este în felul următor:

#version 330
 
layout(location = 0) in vec3 v_position;
layout(location = 1) in vec3 v_normal;
layout(location = 2) in vec2 v_tex_coord;
layout(location = 3) in vec3 v_color;
 
// Uniform properties
uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;
 
void main()
{
    gl_Position = Projection * View * Model * vec4(v_position, 1.0);
}

Directiva de mai jos specifică versiunea limbajului ce se dorește să fie utilizată. Limbajul GLSL are mai multe versiuni posibile. Directiva este obligatorie și trebuie să fie prima directivă din codul sursă al programului.

#version 330

Următoarele atribute sunt date de intrare specifice unui singur vârf. După cum se poate observa, tipurile de date sunt similare bibliotecii glm. Biblioteca respectă standardul limbajului de programare GLSL.

layout(location = 0) in vec3 v_position;
layout(location = 1) in vec3 v_normal;
layout(location = 2) in vec2 v_tex_coord;
layout(location = 3) in vec3 v_color;

În situația framework-ului utilizat în cadrul acestui laborator, aceste date sunt aceleași cu cele ce se regăsesc în structura VertexFormat. Felul în care se specifică legătura dintre formatul informației unui vârf și formatul datelor de intrare de mai sus, din programul de tip vertex shader, va fi descris mai jos.

struct VertexFormat
{
    glm::vec3 position;
    glm::vec3 normal;
    glm::vec2 text_coord;
    glm::vec3 color;
};

Următoarele atribute sunt de asemenea date de intrare. Ele sunt comune tuturor instanțelor de programe de tip vertex shader executate pentru vârfurile din listă. De asemenea, aceste atribute sunt comune tuturor instanțelor executate, indiferent de tipul de shader. Acest tip de atribut poartă numele de uniform în limba engleză.

uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;

Toate programele de tip shader trebuie sa contina un void main() de unde se incepe executia codului.

void main()
{
    gl_Position = Projection * View * Model * vec4(v_position, 1.0);
}

Atributul gl_Position este implicit în programul de tip vertex shader și reprezintă coordonatele vârfurilor în spațiul de decupare. Conform lanțului de transformări, vizibil și în imaginea de mai sus, asupra coordonatelor inițiale ale vârfului ce se află în spațiul obiect, s-au aplicat pe rând transformările de modelare, de vizualizare și de proiecție.

Programe de tip shader pentru prelucrarea pixelilor

Pentru fiecare pixel ce a fost procesat de către rasterizator și pentru care trebuie să i se atribuie o culoare, rasterizatorul execută o instanță a programului pentru prelucrarea de pixeli. Acest tip de program poate avea mai multe date de ieșire, dar în acest laborator ne vom concentra exclusiv pe culoarea pe care trebuie să o atribuie pixelului prelucrat. Tipul de program utilizat pentru prelucrarea pixelilor poartă numele, în limba engleză, de fragment shader sau pixel shader.

Majoritatea API-urilor grafice adoptă termenul de fragment shader, deoarece aceste API-uri ne oferă posibilitatea de a executa mai multe instanțe de fragment shader per pixel, în situația în care dorim acest lucru. Această abordare poartă numele de eșantionare multiplă și mai multe detalii despre ea pot fi consultate în documentația oficială: https://www.khronos.org/opengl/wiki/Multisampling.

Sintaxa unui fragment shader este în felul următor:

#version 330
 
// Uniform properties
uniform mat4 Model;
uniform mat4 View;
uniform mat4 Projection;
 
layout(location = 0) out vec4 out_color;
 
void main()
{
    out_color = vec4(1, 0, 0, 0);
}

Similar ca în situația programului de tip vertex shader, este obligatoriu ca prima directivă să fie cea care specifică versiunea utilizată pentru limbajul de programare GLSL. De asemenea, toate atributele de intrare de tip uniform sunt disponibile în fragment shader. În plus, și acest tip de program are un void main() din care se începe execuția codului.

Atributul de mai jos este un atribut de ieșire pentru fragment shader. În situația acestui laborator, singurul atribut de ieșire dintr-un program de tip fragment shader este culoarea pixelului din grilă.

layout(location = 0) out vec4 out_color;

Programul de mai jos asociază tuturor pixelilor culoarea roșie.

void main()
{
    out_color = vec4(1, 0, 0, 0);
}

API-ul grafic OpenGL NU permite utilizarea în banda grafică a unui singur tip de program din cele două. Este necesar să se utilizeze ambele tipuri de programe.

Gestionarea programelor de tip shader în API-ul grafic OpenGL

În API-ul grafic OpenGL, toate programele de tip shader sunt compilate și legate între ele pentru a fi executate de către procesorul grafic în timpul execuției aplicației grafice.

Gestionarea unui obiect de tip shader

Pentru a gestiona un program de tip shader, în API-ul grafic OpenGL, trebuie să utilizăm un obiect de tip shader pentru fiecare din cele două tipuri de programe vertex shader și fragment shader, împreună cu un obiect suplimentar ce înglobează programele de tip shader utilizate.

Pentru crearea unui obiect de tip shader, se utilizează:

// GL_VERTEX_SHADER sau GL_FRAGMENT_SHADER, pe baza tipului dorit
unsigned int shader_id = glCreateShader(GL_VERTEX_SHADER);
 
const char *shader_source = ...;
 
glShaderSource(shader_id, 1, &shader_source, 0);
 
glCompileShader(shader_id);

Pentru legarea a două programe vertex shader și fragment shader, se utilizează:

unsigned int program_id = glCreateProgram();
 
glAttachShader(program_id, vertex_shader_id);
glAttachShader(program_id, fragment_shader_id);
 
glLinkProgram(program_id);

Pentru desenarea cu cele două programe de tip shader create mai sus, se utilizează:

glUseProgram(program_id);
 
glBindVertexArray(VAO_id);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);

Specificarea formatului de date din lista de vârfuri

API-ul grafic OpenGL utilizează memoria stocată în VBO. Pentru a specifica legătura dintre formatul datelor din lista de vârfuri și formatul datelor de intrare din programul de tip vertex shader, API-ul OpenGL pune la dispoziție directiva glVertexAttribPointer:

glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)(2 * sizeof(glm::vec3) + sizeof(glm::vec2)));

Primul parametru reprezintă locația atributului de intrare din programul de tip vertex shader:

layout(location = 3) in vec3 v_color;

Pentru exemplul de mai sus, specificarea formatului se realizează în felul următor:

glBindVertexArray(VAO);
 
// Set vertex position attribute
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), 0);
 
// Set vertex normal attribute
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)(sizeof(glm::vec3)));
 
// Set texture coordinate attribute
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)(2 * sizeof(glm::vec3)));
 
// Set vertex color attribute
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(VertexFormat), (void*)(2 * sizeof(glm::vec3) + sizeof(glm::vec2)));

Pentru mai multe detalii despre directiva glVertexAttribPointer, puteți consulta documentația oficială: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml.

Transmiterea datelor de tip uniform la shader

Pentru a transmite un atribut de tip uniform la un program shader, API-ul grafic OpenGL ne pune la dispoziție mai multe directive, specifice fiecărui tip de date pe care îl permite limbajul de programare GLSL. Doar pentru a enumera câteva, directivele sunt:

glUseProgram(program_id);
 
//void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value)
glm::mat4 matrix(1.0f);
glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(matrix));
 
// void glUniform1f(GLint location, GLfloat v0)
glUniform1f(location, 1.5f);
 
// void glUniform4f(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3)
glUniform4f(location, 1, 0.5f, 0.3f, 0);
 
//void glUniform3i(GLint location, GLint v0, GLint v1, GLint v2)
glUniform3i(location, 1, 2, 3);
 
//void glUniform3fv(GLint location, GLsizei count, const GLfloat *value)
glm::vec3 color = glm::vec3(1.0f, 0.5f, 0.8f);
glUniform3fv(location, 1, glm::value_ptr(color));

API-ul grafic OpenGL nu impune obligativitatea utilizării unei locații explicite pentru atributele de tip uniform, dar această locație există implicit. Pentru a obține locația implicită a unui atribut, indiferent de tipul lui, utilizăm:

int location = glGetUniformLocation(program_id, "uniform_variable_name_in_shader");

Pentru mai multe detalii despre directivele de transmitere a atributelor de tip uniform, pe baza tipului, puteți consulta documentația oficială: https://registry.khronos.org/OpenGL-Refpages/gl4/html/glUniform.xhtml .

Comunicarea între vertex shader și fragment shader

Pe lângă atributul implicit gl_Position, un program de tip vertex shader poate avea atribute de ieșire explicite, ce devin atribute de intrare pentru următorul tip de program din banda grafică, respectiv pentru laboratorul curent, pentru programul de tip fragment shader. Există două posibilități de declarare a acestor atribute:

  • Utilizarea aceluiași nume pentru atribut în codul sursă al ambelor programe de tip shader, cum se poate vedea în continuare:
    • Vertex shader:
      out vec3 attribute_name;
    • Fragment shader:
      in vec3 attribute_name;
  • Utilizarea aceleiași locații explicite pentru atribut, după cum urmează:
    • Vertex shader:
      layout(location = 3) out vec3 some_attribute;
    • Fragment shader:
      layout(location = 3) in vec3 the_same_attribute;

Valoarea unui atribut de intrare dintr-un program de tip fragment shader, se obține prin interpolare între valorile acelorași atribute de ieșire de la programele de tip vertex shader ce au prelucrat cele 3 vârfuri care au format triunghiul rasterizat.

Laborator

Codul sursă al programelor de tip shader

În cadrul laboratorului, codul sursă al unui program de tip shader este într-un fișier dedicat special pentru fiecare tip de program în directorul cu numele shaders. Acest director se regăsește în interiorul directorului specific fiecărui laborator. Pentru laboratorul curent, fișierele codului sursă al programelor de tip shader se regăsește în lab5/shaders. Ierarhia filtrelor din mediul de dezvoltare Visual Studio pentru laboratorul curent poate fi vizualizată în imaginea de mai jos:

Erori de compilare sau de legare a programelor de tip shader

Posibilele erori de compilare a codului sursă pentru programele de tip shader se regăsesc în consolă. De exemplu, pentru codul sursă al unui program de tip vertex shader, ce conține o eroare de compilare după cum urmează:

#version 330
 
uniform mat4 Model;
 
void main()
{
    vec4 pos = Model * ;
}

Eroarea de compilare este marcată în consolă în felul următor:

Textul erorii menționează la început, prin marcajul 0(7), că eroarea se găsește la linia 7 din codul sursă.

Textul posibilelor erori de legare dintre două programe de tip shader apar tot în consolă.

Încărcarea modelelor 3D din fișiere

Framework-ul de laborator permite încărcarea și desenarea modelelor 3D din fișiere ce au diferite formate. De exemplu, pentru a încărca modelul unei sfere, se poate folosi:

Mesh* mesh = new Mesh("sphere");
mesh->LoadMesh(PATH_JOIN(window->props.selfDir, RESOURCE_PATH::MODELS, "primitives"), "sphere.obj");
meshes[mesh->GetMeshID()] = mesh;

Modelul se regăsește deja în interiorul proiectului, în directorul assets/models/primitives. Puteți încerca să încarcați alte modele puse la dispoziție în cadrul framework-ului de laborator :) .

Majoritatea modelelor 3D încărcate din fișiere NU au asociată informația de culoare în vârfuri. Din acest motiv, aceste modele, inclusiv modelul de sferă ce este utilizat în laboratorul curent și este vizibil în imaginea de mai jos, au valoarea vec3(0, 0, 0) asociată pentru atributul de intrare specific informației vârfului de la locația 3, v_color.

Cerințe laborator

Inițial, execuția laboratorului 5, fără rezolvarea niciunei cerințe, va aduce după sine o eroare de execuție. Este normal, deoarece utilizăm un program de tip shader ce nu a fost compilat și legat cu succes :) . După rezolvarea cerinței 1, de mai jos, eroarea de execuție va dispărea.

  1. 0.05p - Completați metoda CreateShader() pentru a crea un program de tip shader.
  2. 0.05p - Completați metoda RenderMesh() pentru a transmite atributele de tip uniform la cele două programe de tip shader. Atributele transmise sunt matricile utilizate pentru transformarea de modelare, vizualizare și proiecție.
  3. 0.05p - Completați codul sursă al programelor de tip shader:
    1. Completați codul sursă al programului de tip vertex shader, din fișierul LabShader.VS.glsl, ce se regăsește în interiorul directorului shaders:
      • Se declară atributele de intrare pentru programul de tip vertex shader folosind layout location
        layout(location = 0) in vec3 v_position;
        // same for the rest of the attributes ( check Lab5.cpp CreateMesh() );
      • Se declară atributele de ieșire către programul de tip fragment shader
        out vec3 color;
        // same for other attributes
      • Se salvează valorile pentru atributele de ieșire, în main()
        color = v_color;
        // same for other attributes
      • Se calculează poziția în spațiul de decupare a vârfului primit folosind matricile Model, View, Projection
        gl_Position = Projection * View * Model * vec4(v_position, 1.0);
    2. Completați codul sursă al programului de tip fragment shader, din fișierul LabShader.FS.glsl ce se regăsește în interiorul directorului shaders:
      • Se primesc valorile atributelor trimise de la programul de tip vertex shader
        in vec3 color;
      • Se calculează atributul de ieșire al programului de tip fragment shader, ce reprezentă culoarea pixelului
        out_color = vec4(color, 1);
      • Până în acest punct, rezultatul pe care ar trebui să îl obțineti este următorul:
        • Sfera din colorată în culoarea neagră deoarece geometria ei a fost încărcată dintr-un fișier text ce nu conține informație de culoare asociată în vârfuri
  4. 0.05p - Trimiteți timpul aplicației (opținut cu metoda Engine::GetElapsedTime()), și creați o animație de pulsație a obiectelor prin modificarea scării lor după o funcție de timp (trigonometrică etc.)
  5. 0.05p - Pe baza timpului aplicației, trimis anterior, modificați culoarea (unul sau mai multe canale de culoare) după o funcție de timp (trigonometrică etc.)
  6. 0.05p - Pe baza timpului aplicației, creați o animație de oscilație de-a lungul axei OY globală, după o funcție de timp (trigonometrică etc.) pentru cubul din partea dreaptă
    • Pentru desenarea cubului din dreapta, utilizați programul de tip shader denumit “LastTask”.
    • Completați codul sursă al programelor de tip shader din fișierele LastTask.VS.glsl și LastTask.FS.glsl, conform cerinței 3.
    • Pe baza timpului aplicației, modificați coordonatele vârfurilor de-a lungul axei OY globală.

Bonus: Creați o animație de deformare la nivel de vârfuri, diferită pentru fiecare vârf, similar cu animația vizibilă mai jos :) . Creați fișierele pentru un al treilea program de tip shader și utilizați-l pentru desenarea geometriei sferei din stânga. Realizați animatia pe geometria sferei. Pentru a asocia culori vârfurilor, puteți utiliza informația de poziție în spațiul obiect pentru culoarea pixelilor.

ppbg/laboratoare/05.txt · Last modified: 2024/01/10 18:06 by andrei.lambru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0