Poligon rajzolás
Grafikus pipeline
Vertex input
Ha egy háromszöget szeretnénk rajzolni, akkor 3db 3D vertex-et kell definiálnunk, melyek normalized device koordinátákkal adottak:
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
Mivel mi most egy 2D háromszöget szeretnénk rajzolni, ezért a vertex-ek z koordinátáit 0.0 értékre állítjuk.
Ahogy minden OpenGL objektum esetén a VBO buffer objektumhoz is tartozik egy egyedi id, ami az adott buffer objektumot hivatkozza. A glGenBuffers függvény segítségével generálhatunk egy buffert egy egyedi id-val.
unsigned int VBO;
glGenBuffers(1, &VBO);
A létrehozott VBO buffer objektumot a glBindBuffer függvénnyel bind-olhatjuk, mint egy GL_ARRAY_BUFFER target.
glBindBuffer(GL_ARRAY_BUFFER, VBO);
A glBufferData függvénnyel másolhatjuk a vertex adatot a buffer memóriájába:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
A negyedik paraméter határozza meg, hogy a GPU hogyan kezelje az adatot, mely a következő három módon történhet:
- GL_STATIC_DRAW: az adat nem vagy csak nagyon ritkán változik.
- GL_DYNAMIC_DRAW: az adat sokat változik.
- GL_STREAM_DRAW: az adat folyamatosan változik.
Vertex shader
Ha renderelni szeretnénk valamit az OpenGL-el, akkor legalább egy vertex és egy fragment shader-t létre kell hoznunk. Most egy hármoszög rajzolásához szükséges egyszerű példát nézünk meg, majd a későbbi tutorialok során egyre jobban belemegyünk a részletekbe.
Első lépésben a GLSL nyelv segítségével megírunk egy egyszerű vertex shader-t, majd lefordítjuk (compile), hogy használni tudjuk az applikációban. A vertex shader kód:
#version 400 core
layout (location = 0) in vec3 _pos;
void main() {
gl_Position = vec4(_pos.x, _pos.y, _pos.z, 1.0);
}
A shader fordítása (compile)
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
A GL_VERTEX_SHADER enum segítségével beállítjuk, hogy a glCreateShader egy vertex shader típusú objektumot hozzon létre. A következő lépésben csatoljuk a shader forrás kódját a shader objektumot és lefordítjuk:
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if(!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
Fragment shader
#version 400 core
out vec4 FragColor;
void main() {
FragColor = vec4(0.0f, 0.0f, 1.0f, 1.0f);
}
A fragment shader fordítási folyamata hasonló a vertex shader-éhez, de a típust most GL_FRAGMENT_SHADER-re állítjuk:
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
Most, hogy mind a vertex, mind a fragment shader-t lefordítottuk, a keletkezett shader objektumot össze kell linkelni egy shader program-má, melyet a rendereléshez használhatunk.
Shader program
Egy program objektumot a következő képpen hozunk létre:
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
A következő lépésben a glAttachShader függvénnyel hozzácsatoljuk shader objektumokat a program objektumhoz, majd a glLinkProgram-mal össze linkeljük őket:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
Majd megvizsgáljuk, hogy sikeres volt e a linkelés:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
Ha renderelni szeretnénk valamit, akkor a glUseProgram segítségével aktiváljuk a megfelelő program objektumot (azaz a megfelelő shader-eket):
glUseProgram(shaderProgram);
Miután egy programhoz linkeltük a shader objektumokat, utána töröljük őket, mert már nincs rájuk szükségünk:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
Vertex attribútumok összekötése
A vertex shader-ben számos bemeneti attribútumot definiálhatunk, ami nagy rugalmasságot biztosít, viszont manuálisan kell megmondanunk, hogy az input adat mely része mely attribútumhoz tartozzon.
Jelenlegi példánkban (háromszög) a vertex buffer adat a következő képpen néz ki:
- A háromszög csúcsának a pozíció adatai float típusúak, azaz 32 bit-en (4 byte) tárolódnak.
- Minden csúcs 3 float típusú attribútumot tárol
- Egy tömbben (array) tároljuk ezeket a csúcs pontokat, ezért a memóriában egymás mellett helyezkednek el.
A fenti tudás ismeretében a glVertexAttribPointer függvényen keresztül definiálhatjuk az OpenGL-nek, hogy hogyan értelmezze az adatot:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
A glVertexAttribPointer függvény paraméterei:
- Az első paraméter határozza meg, hogy melyik vertex attribútot konfiguráljuk be. A vertex shader-ben a bemeneti (in) _pos attribútumot a layout (location = 0) értékre állítottuk, tehát az első paraméter ezért 0.
- A második paraméter az adott attribútum mérete. (vec3)
- A harmadik paraméter a típust határozza meg. (A vec3 float típusokat tartalmaz.)
- A következő paraméterrel lehet állítani, hogy szeretnénk e normalizálni az adatot.
- Az ötödik paraméter megadja az eltolást (stride) az egymást követő háromszög csúcsok között. (Egy vec3 3 db float értéket tartalmaz.)
- Az utolsó paraméter határozza meg, hogy a _pos data hova kerül a bufferbe (offset).
Azt, hogy a GPU memóriából az adat, hogyan kerül a megfelelő vertex shader attribútumba a VBO szabályozza. De mivel több VBO is lehet ezért a megfelelőt VBO-t hozzá kell adni (bind) a glVertexAttribPointer-hez.
A következő lépésben a glEnableVertexAttribArray függvénnyel engedélyeznünk kell a vertex attribútumot a lokációjának átadásával:
// 0. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. use our shader program when we want to render an object
glUseProgram(shaderProgram);
// 3. now draw the object
someOpenGLFunctionThatDrawsOurTriangle();
Vertex array objektum
A VAO a következő dolgokat tárolja:
- A glVertexAttribPointer-en keresztül tárolja a vertex attribútum konfigurációkat.
- Meghívhatjuk rajta keresztül a glEnableVertexAttribArray és a glDisableVertexAttribArray függvényeket.
A VAO létrehozása nagyon hasonló a VBO létrehozásához:
unsigned int VAO;
glGenVertexArrays(1, &VAO);
A glBindVertexArray függvénnyel aktiváljuk (bind) a VAO-t, majd be kell töltenünk és konfigurálnunk a VBO-t és az attribútum pointer-t majd deaktiváljuk (unbind) a VBO-t és a VAO-t. Amikor rajzolni szeretnénk csak újra betöltjük (bind) a megfelelő VAO-t:
// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// the call to glVertexAttribPointer registered VBO as the vertex attribute's
// bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
// You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens.
// VAOs requires a call to glBindVertexArray.
glBindVertexArray(0);
// ..:: Drawing code (in render loop) :: ..
// 4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
A VAO tárolja a vertex attribútumok konfigurációját és hogy melyik VBO-t kell használni. Ha több objektumot szeretnénk rajzolni, először lőtrehozzuk az összes VAO-t és a megfelelő VBO-kat, majd mindig azt a VAO-t aktiváljuk amelyiket használni szeretnénk.
A glDrawArrays függvény a megfelelő primitíveket fogja rajzolni az aktív shader felhasználásával:
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
A glDrawArrays első paraméterében meghatározzuk a primitív típusát amit rajzolni szeretnénk, majd a második paraméter meghatározza a vertex array kezdő indexét, végül az utolsó paraméter, hogy hány vertex-et szeretnénk rajzolni.
Element buffer objektum
Az element buffer objektum (EBO) szerepét egy példán keresztül mutatjuk be, amiben egy négyszöget fogunk rajzolni. Egy négyszöget kirajzolhatunk két háromszög rajzolásával:
float vertices[] = {
// first triangle
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, 0.5f, 0.0f, // top left
// second triangle
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};
unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
A következő lépésben létrehozzuk az EBO-t:
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
Viszont most a glDrawArrays parancs helyett a glDrawElements parancsot kell használnunk, ami az index buffert használja a rajzoláshoz. A rajzolás előtt a megfelelő EBO-t aktiválnunk kell:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
Ha sok mindent kell rajzolnunk, akkor ez is nehézges lehet, ezért a VAO az EBO konfigurációkat is tárolja. Így a rajzolásnál a megfelelő VAO-t kell betölteni, ami automatikusan be fogja tölteni a megfelelő EBO-t is.
A végső inicializáló és rajzoló kód a következő képpen néz ki:
// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
A rajzolás eredménye, továbbá ha wireframe módban rajzolunk látható a két háromszög, ami a négyszöget definiálja:
Ha wireframe módban szeretnék rajzolni, akkor a következő paranccsal engedélyezhetjük és, amíg vissza nem váltunk, az összes rajzolás ebbe a módba fog történni.
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)