OpenGL - kompletní příklad

V této kapitole se naučíte první krok z pipeline, to jest, jak připravit a nahrát vertex data (souřadnice, barvy atp.), tj. data pro attribute (in) a uniform proměnné. A také, jak použít zkompilovaný GLSL program, jehož výrobu jsem popsal v minulé kapitole.

Příklad - trojúhelník

Nejednodušší, co můžete v OpenGL vykreslit, snad kromě bodu, je trojúhelník. Na něm vám ukáži první kompletní OpenGL příklad.

Co budete potřebovat? GLSL pro vertex a fragment shader. Tentokrát ještě použiji tu starší verzi (která se stále používá pro WebGL). Znáte je z kapitoly o GLSL, ale pro zopakování je uvedu znovu.

Vertex shader (soubor default.vs):

attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

varying vec4 vColor;

void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
}

Fragment shader (soubor default.fs:)

varying vec4 vColor;

void main(void) {
        gl_FragColor = vColor;
}

Dále budete potřebovat soubor linmath.h. Zatím se žádné transformace konat nebudou, ale kvůli přípravě dalších tutoriálů, a taky kvůli ukázce použití uniform proměnných se už teď budou inicializovat jednotkové matice modelu, pohledu a perspektivy.

Vytvořil jsem soubor linmath-extended.h, který includuje linmath.h a přidává některé další funkce. Můžete ho tedy použít namísto linmath.h. Třeba tam najdete funkci na výpis matic typu 3x3 a 4x4 do terminálu. Ostatní funkce z linmath-extended.h budu používat až v dalších kapitolách, takže se k tomuto souboru ještě později vrátím.

Samozřejmě budete potřebovat i shader.h z minulé kapitoly.

Pro inicializaci okna a OpenGL funkcí budu používat glew a glsl. Příklad bude vycházet z tohoto příkladu, resp. z tohoto. Funkce pro zpracování událostí myši a klávesnice jsem přesunul do souboru events.h. Zatím jen používám události klávesnice pro ukončení programu při stisku klávesy ESCAPE.

/*------------------------------------------------*/
/* 04shaders/trojuhelnik/events.h                 */
#include <GLFW/glfw3.h>

static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
        if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
                glfwSetWindowShouldClose(window, GLFW_TRUE);
        }
}

void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{

}
/*------------------------------------------------*/

Novinkou je soubor vertex-data.h. Zde jsou definované pole, které budou předány do attribute proměnných vertex shaderu. Konkrétně se jedná o souřadníce vertexů trojůhelníka (třikrát vektor x,y,z) a barvy vrcholů (třikrát r,g,b,a).

#ifndef  VERTEX_DATA_H
#define VERTEXT_DATA_H
#include <GLFW/glfw3.h>

GLfloat vertices[] = {
    // Front face
    0.0, 1.0, 0.0,
    1.0, -1.0, -1.0,
    -1.0, -1.0, -1.0
};

GLfloat colors[] = {
    // Front face
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0
};
#endif

Trojúhelník je definovaný vlastně jako jedna strana jehlanu. Horní vrchol má z souřadnici 0, ale vrcholy dole mají souřadnici -1 (jsou tedy blíže k vám, než horní vrchol). Protože se ale bude trojúhelník zobrazovat bez jakékoliv perspektivy, tak sklon trojúhelníka nepoznáte.

Barvy jsem definoval s průhledností alfa = 1, což znamená neprůhledné. Pokud byste nastasvili alfa na 0 (totálně průhledné), trojůhelník zmizí. Ale jenom když je zapnutý BLENDing. BLEND (smýchání) se používá v OpenGL pro průhlednost. Defaultně je BLEND vypnuté. Jako téměř všechno v OpenGL. Kvůli výkonu, samozřejmě. Jak se BLENDing zapíná si nechám na jinou kapitolu, takže teď alfu ignorujte. Pokud ji změníte, nic se nestane ;-).

Soubor main.c

A teď už zbývá jen soubor main.c. Hlavním úkolem bude dostat vertices do attribute vec3 aVertexPosition a colors do attribute vec4 aVertexColor. A pak ještě dostat matice do uniform proměnných pro ně určených. (V tomto příkladu zůstanou matice jednotkové, nic se zatím nebude transformovat.)

Na začátku jsou nezbytné hlavičkové soubory, dopředné deklarace funkcí (definovaných za funkcí main()), a pár globálních proměnných. (Globální proměnné jsou v praxi fuj, ale tady mi zjednodušší výklad).

  1. /*------------------------------------------------*/
  2. /* 04shaders/trojuhelnik/main.c                   */
  3. #define _CRT_SECURE_NO_WARNINGS
  4. #define GLEW_STATIC
  5. #include <GL/glew.h>
  6. #include <stdio.h>
  7. #include <GLFW/glfw3.h>
  8. #include "linmath.h"
  9. #include "vertex-data.h"
  10. #include "shader.h"
  11. #include "events.h"
  12.  
  13. void initVariables(Shader *program);
  14. void initBuffers(Shader *program);
  15. void uninitBuffers();
  16. void ticking(Shader * program, GLFWwindow* window);
  17. void draw(Shader *program, GLFWwindow* window, GLuint VAO);
  18. void animate();
  19.  
  20. GLuint VAO, BO[2];
  21.         GLuint aVertexPositionLoc;
  22.         GLuint aVertexColorLoc;
  23.         GLuint uPMatrixLoc;
  24.         GLuint uMVMatrixLoc;
  25. } variables;
  26.  

Funkce initVariables() zjistí ID attribute a uniform proměnných a uloží je do struktury variables.

Funkce initBuffers() je ta ze všech nejzajímavější. Ta se postará o nahrání vertex dat do bufferu grafické karty a provázání těchto dat s (attribute a uniform) proměnnými z GLSL shaderů. Bude k tomu potřebovat globální proměnné VAO a BO, do kterých se uloží identifikátory Vertex Array Objektu a bufferů (viz dále).

Funkce uninitBuffers() uklidí nepotřebná data z grafické karty před ukončením programu.

Funkce ticking() v nekonečném cyklu spouští funkci draw(), která vykreslí vertex data, a funkci animate(), která se stará o animaci scény (například může při každém zavolání měnit úhel natočení jehlanu a tím animovat otáčení.) V tomto příkladě ještě nebude animate() dělat nic.

Následuje funkce main(). Její začátek už znáte z povídání o GLFW a GLEW.

  1. int main(void)
  2. {
  3.         GLFWwindow* window;
  4.         /* Initialize the library */
  5.         if (!glfwInit()) {
  6.                 return -1;
  7.         }
  8.         glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  9.         glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
  10.         //glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  11.  
  12.         /* Create a windowed mode window and its OpenGL context */
  13.         window = glfwCreateWindow(640, 480, "04 - trojuhelnik", NULL, NULL);
  14.         if (!window)
  15.         {
  16.                 printf("Nelze inicializovat kontext\n");
  17.                 glfwTerminate();
  18.                 return -1;
  19.         }
  20.         /* Make the window's context current */
  21.         glfwMakeContextCurrent(window);
  22.         printf("%s\n", glGetString(GL_VERSION));
  23.  
  24.         /* Load modern OpenGL functions */
  25.         glewExperimental = GL_TRUE;
  26.         glewInit();
  27.  
  28.         /* events */
  29.         glfwSetKeyCallback(window, key_callback);
  30.         glfwSetMouseButtonCallback(window, mouse_button_callback);
  31.  
  32.         //glfwSwapInterval(1);

Dalším krokem je vytvoření shader programu, získání odkazů na GLSL proměnné (initVariables()), inicializace bufferů (initBuffers()) a nastavení OpenGL kontextu.

Pomocí funkcí glEnable() a glDisable() se dají zapínat a vypínat různé funkce OpenGL kontextu. V příkladu níže zapínám GL_DEPTH_TEST. To způsobí, že se bude OpenGL zajímat o z-souřadnici a vykreslí ten fragment, který ji má menší (je k vám blíže). Pokud bych depth test nezapnul, vypsal by se ten fragment, který se v fragment shaderu vykresluje jako poslední.

Pravdou je, že v tomto příkladu se vykresluje jen jeden trojúhelník, takže se nic nepřekrývá a depth test je zbytečný. V příkladu s rotujícím jehlanem (jenž bude následovat) už je ale potřeba bude (zkuste depth test v příkladu s rotujícím jehlanem vypnout …).

  1.         Shader program = shader("default.vs", "default.fs");
  2.         if(!program.Program) {
  3.                 return -1;
  4.         }
  5.  
  6.         initVariables(&program);
  7.         initBuffers(&program);
  8.  
  9.         // setup GL state
  10.         glEnable(GL_DEPTH_TEST);
  11.  
  12.         ticking(&program, window);
  13.         uninitBuffers();
  14.  
  15.         glfwDestroyWindow(window);
  16.         glfwTerminate();
  17.         return 0;
  18. }
  19.  

Ve funkci ticking() je cyklus, který stále dokola animuje a vykresluje scénu. Protože se teď nic neanimuje, je vlastně skoro zbytečný. Ale v dalších příkladech se bude hodit.
Po ukončení cyklu (zavřením okna, nebo klávesou ESCAPE) se provede úklid a funkce main() se ukončí.

Funkce initVariables() je velmi jednoduchá:

  1. void initVariables(Shader *program)
  2. {
  3.         variables.aVertexPositionLoc = program->getAttribLocation(program, "aVertexPosition");
  4.         variables.aVertexColorLoc = program->getAttribLocation(program, "aVertexColor");
  5.         variables.uMVMatrixLoc = program->getUniformLocation(program, "uMVMatrix");
  6.         variables.uPMatrixLoc = program->getUniformLocation(program, "uPMatrix");
  7. }
  8.  

Funkce initBuffers() už tak intuitivní není. Nejdříve generuje tzv. Vertex Array Object voláním funkce glGenVertexArrays(). Ta očekává jako argumenty počet VAO, které chcete vygenerovat a odkaz na pole typu GLuint, kam uloží výsledné ID (identifikátory) vytvořených VAO.

Protože generuji jeden VAO, můžu si dovolit místo pole předat odkaz na proměnnou typu GLuint.

Obdobně se generují buffery. Buffery se používají pro nahrání vertex dat na grafickou kartu. VAO se používá jako jakási „paměťová schránka“, která uchovává informace o tom, jaké buffery mají být GLSL programům přistupné, nebo jaké mají být zapnuté vertex proměnné, viz dále.

Tedy generují – požadují se po OpenGL. Jestli se generují, nebo jsou už připravené a vy dostanete ID volných bufferů, to je otázka implementace OpenGL API a na výrobcích grafických karet.

  1. void initBuffers(Shader *program)
  2. {
  3.         //vytvoreni bufferu na graficke karte
  4.         glGenVertexArrays(1, &VAO);
  5.         glGenBuffers(2, BO);
  6.  
  7.         // nahrani dat do bufferu graficke karty
  8.         glBindBuffer(GL_ARRAY_BUFFER, BO[0]);
  9.         glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
  10.  
  11.         glBindBuffer(GL_ARRAY_BUFFER, BO[1]);
  12.         glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
  13.  
  14.         // propojeni bufferu s attribute promennymi a "zapnuti" promennych
  15.         glBindVertexArray(VAO);
  16.         {
  17.                 glBindBuffer(GL_ARRAY_BUFFER, BO[0]);
  18.                 glVertexAttribPointer(variables.aVertexPositionLoc, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
  19.                 glEnableVertexAttribArray(variables.aVertexPositionLoc);
  20.        
  21.                 glBindBuffer(GL_ARRAY_BUFFER, BO[1]);
  22.                 glEnableVertexAttribArray(variables.aVertexColorLoc);
  23.                 glVertexAttribPointer(variables.aVertexColorLoc, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
  24.         }
  25.         // uklid
  26.         glBindVertexArray(0); // Unbind VAO
  27.         glBindBuffer(GL_ARRAY_BUFFER, 0); // Unbind GL_ARRAY_BUFFER
  28. }
  29.  

Funkce glBufferData(), která se stará o nahrání vertex dat do bufferu, nemá kupodivu jako jeden ze svých argumentů ID bufferu, do kterého se mají data nahrát. Místo toho se nahrají do bufferu, jehož typ je dán prvním argumentem a který je zrovna připojený – funkcí glBindbuffer().
Je to kvůli efektivitě. Existuje více funkcí pracujícíh s bufferem a tímto způsobem se jen buffer jednou přibinduje a funkce s ním mohou pracovat (bindování bufferů je relativně pomalé).

Prvním argumentem glBindBuffer() i glBufferData() je typ bufferu, se kterým hodláte pracovat. Pro pole vertex dat se používá GL_ARRAY_BUFFER.

Typů bufferů existuje celá řada, viz třeba dokumentace k glBindBuffer(). Pro každý typ můžete vytvořit několik bufferů (v příkladu jsou vytvořené dva buffery pro GL_ARRAY_BUFFER), ale jen jeden z bufferů pro daný typ může být v jeden okamžik přibindovaný.

Poslední argument funkce glBindBuffer() OpenGL napovídá, do jakého druhu paměti má data nahrád. GL_STATIC_DRAW říká, že data nebudeme chtít často měnit, takže se mohou nahrát do pmalejší paměti. Další možností jsou GL_DYNAMIC_DRAW, nebo GL_STREAM_DRAW, které požádají OpenGL o nahrání dat do rychlejší paměti (pokud je nějaká k dispozici).

V tomto tutoriálu vám ukáži práci ještě s GL_ELEMENT_ARRAY_BUFFER a GL_TEXTURE_BUFFER.

Teď už jste přenesli data do bufferu grafické karty a je na čase propojit buffer s attribute proměnnými. To se děje mezi voláním glBindVertexArray(VAO); a glBindVertexArray(0);. VAO (Vertex Array Oject), které je zrovna přibindované, si bude pamatovat, jaké buffery jste připojili k jakým proměnným (volání funkce glVertexAttribPointer()) a jaké attributy jste zapnuli (volání glEnableVertexAttribArray()). Pokud byste attribut voláním glVertexAttribPointer() nezapnuli, pak by se během provádění pipeline do attribute proměnné nesypali žádná data.

Funkce glEnableVertexAttribArray() můžete volat kdekoliv po přibindování VAO. Funkce glVertexAttribPointer() pracuje s bufferem, takže se musí zavolat až po přibindování bufferu (a VAO).

Pozorného čtenáře teď napadlo, jestli by nebylo možné přesunout volání funkcí glBufferData() za druhé volání glBindBuffer() níže a uštřit si tak to první volání glBindBuffer() pro každý buffer. Ano, je to možné. Je to dokonce žádoucí, vzhledem k výkonu. Důvod, proč jsem to tak neudělal, je edukační — chtěl jsem ukázat, že volání glBufferData() není starost VAO, takže se glBufferData() může volat mimo.

Voláním glBindVertexArray(0); se pojistím, aby se mi do VAO už nic nezapsalo. Je to slušnost, která zabrání tomu, abyste omylem do VAO zapsali někde jinde, v nějaké jiné části kódu. (Stejně tak by se mělo volat (glBindBuffer(…,0);.)

Funkce glVertexAttribPointer()

Prvním argumentem této funkce je identifikátor GLSL proměnné (typu attribute, resp. in v nové verzi GLSL).

Druhým počet prvků, který se do proměnné bude ukládat. Proměnná pro souřadnice vrcholů aVertexAttrib je definovaná jako typ vec3, takže hodnota druhého argumentu je 3, pro barvy používám vec4 aVertexColor, takže je 4.

Definování attrib pointeru je nezávislé na GLSL programu, OpenGL „nevidí“ GLSL program, takže ani datové typy proměnných. Z podobného důvodu musíte volat glEnableVertexAttribArray(). Ten samý buffer můžete používat pro různé GLSL programy a každý z nich může a nemusí proměnné využívat.

Třetí argument je datový typ prvku pole. Možné hodnoty viz dokumentace k glVertexAttribPointer() (GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_DOUBLE …).

Pokud je čtvrtý argument GL_TRUE, pak se hodnoty normalizují, tj. převedou se do intevalu <-1.0, 1.0>. Tento argument bude vždy GL_FALSE, protože k normalizaci budu používat perspektivu.

A teď k vysvětlení posledních dvou argumentů.

V příkladu jsem definoval vertex data pomocí dvou polí (vertices a colors). Vytvořil jsem pro ně dva buffery a každé pole do jednoho nahrál. To není nejefektivnější cesta. Efektivnější by bylo definovat vertex data v jednom poli, kde by se střídali data pro vrcholy a barvy, nějak takto:

GLfloat vertexdata[] = {
    0.0, 1.0, 0.0, //vrchol
    1.0, 0.0, 0.0, 1.0, // barva
    1.0, -1.0, -1.0, //vrchold
    0.0, 1.0, 0.0, 1.0, //barva
    ...

V takovém případě potřebuji jen jeden GL_ARRAY_BUFFER a data nahraji na grafickou kartu jedním voláním glBufferData(). Volání funkce glVertexAttribPointer() pro vrcholy a barvy by pak vypadalo takto:

glVertexAttribPointer(variables.aVertexPositionLoc, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (GLvoid*)0);
...
glVertexAttribPointer(variables.aVertexColorLoc, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (GLvoid*) (3*sizeof(GLfloat)));

Pátý argument říká, o kolik je posunut začátek další „dávky“ dat od začátku té předchozí. Souřadníce druhého vrcholu jsou posunuty o 7*sizeof(GLfloat) od souřadnic prvního vrcholu. Pro barvy to platí obdobně.

Poslední argument určuje posun od začátku bufferu. První vrchol je hned na začátku pole, takže je posun roven 0. První barva je posunuta o (3*sizeof(GLfloat)). Posun se zadává jako ukazatel (pointer), proto to divné přetypování (GLvoid*).

Tím je funkce initBuffers() popsána.

Funkce initBuffers() je relativně krátká, ale skrývá se za ní mnoho informací o OpenGL rozhraní. Je důležité, abyste jí porozuměli jak to jen jde. Pokuste se alespoň trochu si zapamatovat, co se v ní dělá a proč.

Funkce glDeleteVertexArrays() a glDeleteBuffers() uvolňují paměť z grafické karty. Měli byste je volat vždy, když už dané buffery/VAO nebudete potřebovat. Ono těch bufferů není k dispozici zas tak moc!

  1. void uninitBuffers()
  2. {
  3.         glDeleteVertexArrays(1, &VAO);
  4.         glDeleteBuffers(2, BO);
  5. }
  6.  

Funkci tick() už jsem popisoval. Co dělá je zřejmé. Funkce glfwSwapBuffers() a glfwPollEvents() jsou pospsané v kapitole o GLFW.

  1. void ticking(Shader * program, GLFWwindow* window) {
  2.         /* Loop until the user closes the window */
  3.         while (!glfwWindowShouldClose(window))
  4.         {
  5.                 animate();
  6.                 draw(program, window, VAO);
  7.                 glfwSwapBuffers(window);
  8.                 glfwPollEvents();
  9.         }
  10. }
  11.  

Funkce draw()

Funkce draw() na začátku zjišťuje velikost okna a podle toho nastavuje glViewport(). Díky tomu bude vykreslený obraz vždy vyplňovat celé okno. Toto je jednoduché řešení, ale ne úplně nejefektivnější.

Efektivnější by byloa nevolat glViewport() při každém volání draw(), ale jen tehdy, když se změní velikost okna. K tomu stačí zaregistrovat callback funkci:

void resize_callback(GLFWwindow* window, int width, int height) {
        glViewport(0, 0, width, height);
}
glfwSetFramebufferSizeCallback(window, resize_callback);

Nicméně, v příkladech použivám tu méně efektivní verzi (bůh ví proč).

Funkce glClearColor() nastavuje barvu, kterou se maže tzv GL_COLOR_BUFFER. To je buffer, do kterého se vykresluje výsledná barva fragmentů.

Funkce glClear() smaže zadané buffery. V příkladu se maže bufer GL_COLOR_BUFFER (barvou nastavenou pomocí glClearColor()) a vynuluje se GL_DEPTH_BUFFER. Tento buffer se používá pro ukládání nejmenší z-ové souřadnice shaderu. Pokud je zapnut GL_DEPTH_TEST (viz výše), pak se z souřadnice shaderu porovná s tou z depth bufferu a pokud je větší (dále od vás), tak se nevykreslí (v případě průhlednosti (zapnutého BLENDování) se může smíchávat s existující barvou).

  1. void animate() {
  2. }
  3.  
  4. void draw(Shader *program, GLFWwindow* window, GLuint VAO)
  5. {
  6.         int width, height;
  7.         glfwGetFramebufferSize(window, &width, &height);
  8.         glViewport(0, 0, width, height);
  9.  
  10.         glClearColor(1.f, 1.f, 1.f, 1.f);
  11.         glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
  12.  
  13.         program->Use(program);
  14.  
  15.         mat4x4 uMVMatrix, uPMatrix;
  16.         mat4x4_identity(uMVMatrix);
  17.         mat4x4_identity(uPMatrix);
  18.  
  19.         glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *) uMVMatrix);
  20.         glUniformMatrix4fv(variables.uPMatrixLoc, 1, GL_FALSE, (const GLfloat *) uPMatrix);
  21.  
  22.         // Draw container
  23.         glBindVertexArray(VAO);
  24.         glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices)/sizeof(vertices[0])/3);
  25.         glBindVertexArray(0);
  26. }
  27.  
  28. /*------------------------------------------------*/

Dále funkce draw() pokračuje nastavením shader programu, definováním jednotkových matic pohledu a perspektivy, a nahráním matic do uniform proměnných pomocí glUniformMatrix4fv().

OpenGL má spoustu funkcí glUniform*, kde poslední část určuje, jaký typ dat se nahrává. V našem příkladě jde o matice 4x4, s prvky typu float. Písmeno v na konci značí, že funkce očekává data jako vektor.

Jiným příkladem by mohla být funkce glUniform4i(GLuint Loc, GLint x, GLint y, GLint z, GLint w). Kompletní výpis funkcí najdete např. v nápovědě k glUniform().

No a až na konci je požádáno OpenGL o vykreslení trojúhelníků pomocí glDrawArrays(). Nejdříve je ale připojen VAO.

VAO ve starší verzi OpenGL neexistoval, neexistuje tak ani v současné verzi WebGL. Bindování VAO v WebGL verzi nenajdete. Místo bindování VAO tam musíte volat to, co si VAO tady pamatuje.
Použití VAO znamená volání méně OpenGL funkcí. Je tedy efektivnější (a v novějších verzích OpenGL i povinné).

První argument funkce glDrawArrays() specifikuje, jaký typ primitiva chcete vykreslit. Například GL_POINTS, GL_LINES nebo GL_TRIANGLES.

Druhý argument určuje prvek index, kterým se má začít. (V našem případě 0 znamená „začni od prvního trojúhelníka“.)

Poslední argument určuje počet indexů (primitiv), které se mají vykreslit. V našem příkladě je to jeden trojúhelník. Nenechte se zmást tím, že toto číslo určuji na základě délky pole vertices. Ke stejnému číslu bych došel i tímto výpočtem: glDrawArrays(GL_TRIANGLES, 0, sizeof(colors)/sizeof(colors[0])/4);.

A to je vše! Už umíte programovat v OpenGL! To chce panáka vodky!

Příklad - transformace

Tento příklad vám ukáže jednoduché transformace. Jediný rozdíl oproti předchozímu přikladu je ve funkci draw(). Před voláním glUniformMatrix4v() (před řádkem 147), je toto:

  1.         ...
  2.         mat4x4_identity(uPMatrix);
  3.         /* 1) Scale */
  4.          mat4x4_scale_aniso(uMVMatrix, uMVMatrix, 0.5, 0.5, 0.5);
  5.         // mat4x4_print(uMVMatrix);
  6.         /* 2) Translate + Scale */
  7.         // mat4x4_translate(uMVMatrix, 1.0, 0., 0.);
  8.         // mat4x4_scale_aniso(uMVMatrix, uMVMatrix, 0.5, 0.5, 0.5);
  9.         /* 3) Scale + Translate */
  10.         // mat4x4_scale_aniso(uMVMatrix, uMVMatrix, 0.5, 0.5, 0.5);
  11.         // mat4x4_translate_in_place(uMVMatrix, 1.0, 0., 0.);
  12.  
  13.         glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *) uMVMatrix);
  14.         ...

Vidíte ukázku tří možných transformací, jejichž výsledek je na následujících obrázcích:

1) Scale 2) Translate + Scale 3) Scale + Translate

Příklad - jehlan

Poslední příklad této kapitoly bude rotující jehlan. V příkladu použiji OpenGL verze 3.3 a s shadery ve verzi GLSL 3.3.

Vertex shader vypadá takto:

#version 330 core
layout (location = 0) in vec3 aVertexPosition;
layout (location = 1) in vec4 aVertexColor;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

out vec4 vColor;

void main(void) {
        gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
        vColor = aVertexColor;
}

Fragment shader vypadá takto:

#version 330 core
in vec4 vColor;

out vec4 fragColor;

void main(void) {
        fragColor = vColor;
}

Jehlan se skládá ze čtyř bočních trojúhelníků a čtvercové podstavy. A čtvercová podstava se skládá z dvou trojúhelníků.

Takto nějak vypadají vertex data pro jehlan:

  1. #ifndef  VERTEX_DATA_H
  2. #define VERTEXT_DATA_H
  3. #include <GLFW/glfw3.h>
  4.  
  5. GLfloat vertices[] = {
  6.     // Back face
  7.     0.0, 1.0, 0.0,
  8.     -1.0, -1.0, 2.0,
  9.     1.0, -1.0, 2.0,
  10.     // Left face
  11.     0.0, 1.0, 0.0,
  12.     1.0, -1.0, 1.0,
  13.     1.0, -1.0, -1.0,
  14.     // Front face
  15.     0.0, 1.0, 0.0,
  16.     1.0, -1.0, -1.0,
  17.     -1.0, -1.0, -1.0,
  18.     // Right face
  19.     0.0, 1.0, 0.0,
  20.     -1.0, -1.0, -1.0,
  21.     -1.0, -1.0, 1.0,
  22.     // Bottom right
  23.     1.0, -1.0, 1.0,
  24.     -1.0, -1.0, 1.0,
  25.     1.0, -1.0, -1.0,
  26.     // Bottom left
  27.     -1.0, -1.0, -1.0,
  28.     1.0, -1.0, -1.0,
  29.     -1.0, -1.0, 1.0
  30. };
  31.  
  32. GLfloat colors[] = {
  33.     // Back face
  34.     0.5, 0.0, 0.5, 1.0,
  35.     0.5, 0.0, 0.5, 1.0,
  36.     0.5, 0.0, 0.5, 1.0,
  37.     // Left face
  38.     0.0, 0.5, 0.0, 1.0,
  39.     0.0, 0.5, 0.0, 1.0,
  40.     0.0, 0.5, 0.0, 1.0,
  41.     // Front face
  42.     0.0, 0.5, 0.5, 1.0,
  43.     0.0, 0.5, 0.5, 1.0,
  44.     0.0, 0.5, 0.5, 1.0,
  45.     // Right face
  46.     0.5, 0.5, 0.0, 1.0,
  47.     0.5, 0.5, 0.0, 1.0,
  48.     0.5, 0.5, 0.0, 1.0,
  49.     // Bottom right
  50.     0.5, 0.0, 0.0, 1.0,
  51.     1.0, 0.0, 0.0, 1.0,
  52.     1.0, 0.0, 0.0, 1.0,
  53.     // Bottom left
  54.     0.5, 0.0, 0.0, 1.0,
  55.     1.0, 0.0, 0.0, 1.0,
  56.     1.0, 0.0, 0.0, 1.0
  57. };
  58. #endif

A to jsou všechny změny potřebné pro vykreslení jehlanu. Ještě tedy nesmím zapomenout na požádání o správnou verzi OpenGL:

  1.     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
  2.     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
  3.     glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

Rotace

Má to ovšem jeden háček. Díváte se přímo na přední stranu jehlanu, tak, že zbylé strany nejsou vidět.

Proto k příkladu ještě přidám animaci rotace.

Vytvořím si k tomu novou globální proměnnou rPyramid, která bude určovat o kolik má být jehlan pootočen kolem osy y.

GLfloat rPyramid = 0.0;

Tato hodnota se bude měnit ve funkci animate():

void animate() {
        const GLfloat SPEED = (2*3.14159)/4;
        rPyramid = - SPEED * (GLfloat) glfwGetTime();
        //printf("%f\n", (float) glfwGetTime());
}

Funkce glfwGetTime()vrací počet sekund od okamžiku inicializace GLFW.
Díky zahrnutí tohoto času do výpočtu rPyramid se jehlan otáčí konstantní rychlostí, bez ohledu na to, jak rychle a jak často se stihne draw() funkce zavolat. Geniální!

Díky násobení hodnotou SPEED se otočí jehlan o 360° (2*π) 1x za 4 vteřiny. Znaménko udává směr otáčení.

A použije se k otočení jehlanu ve funkci draw().

  1.         // ...
  2.         mat4x4_identity(uPMatrix);
  3.  
  4.        
  5.         mat4x4_rotate_Y(uMVMatrix, uMVMatrix, rPyramid);
  6.        
  7.  
  8.         glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *) uMVMatrix);
  9.         // ...

Poslední, čím ještě pohled na jehlan vylepším, je perspektiva:

  1.         // ...
  2.         mat4x4_identity(uMVMatrix);
  3.         mat4x4_identity(uPMatrix);
  4.  
  5.         mat4x4_translate(uMVMatrix, 0.0, 0.0, -5.0);
  6.         mat4x4_rotate_Y(uMVMatrix, uMVMatrix, rPyramid);
  7.         mat4x4_perspective(uPMatrix, degToRad(45.0), ((GLfloat) width)/height, 1.0, 100.0);
  8.  
  9.         glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *) uMVMatrix);
  10.         glUniformMatrix4fv(variables.uPMatrixLoc, 1, GL_FALSE, (const GLfloat *) uPMatrix);
  11.         // ...

Závěr

V této kapitole jste se konečně naučili vykreslit 3D objekty. Víte, k čemu je Vertex Array Object (VAO), jak se nahrávají vertex data na garfickou kartu a jak se připojují k (attribute/in) proměnným ve vertex shaderu, jak nahrát data do uniform proměnných (ve vertex i fragment shaderu), nebo jak nastavit vlastnosti kontextu.

Pokud jste tuto kapitolu zvládli a dobře pochopilli, zbytek OpenGL už pro vás bude procházka růžovým sadem.

Abyste si ověřili své znalosti, zkuste si následující cvičení:

Vytvořte si v fragment shaderu uniform proměnnou typu int. Ve funkci draw() do této proměnné vložte nějaké číslo. Pokud tato proměnná bude 0, pak v frament shaderu uložte do výstupu červenou barvu. Pokud bude nenulová, uložte barvu, kterou dostáváte z vertex shaderu (jak to funguje teď).

Předejte si pozici z vertex shaderu do fragment shaderu. V fragment shaderu uložte jako výslednou barvu modrou, pokud je x-ová souřadnice záporná, jinak zelenou.

Použijte pro vrcholy i barvy pouze jeden buffer, jak je popisováno u glVertexAttribPointer().

Použijte pro vrcholy i barvy pouze jeden buffer, ale tak, že na začátku pole s vertex daty budou souřadnice vrcholů a až za nimi hodnoty barev.

Transformujte trojúhelník na základě nějaké události myši. Třeba při kliknutí přesuňte trojúhelník tak, aby byl jeho střed tam, kam uživatel kliknul.

Upravte animaci jehlanu tak, aby pulsoval (zvětšoval se a zmenšoval). Využijte k tomu funkci glfwGetTime() a funkci pro sínus.

Komentář Hlášení chyby
Created: 24.12.2016
Last updated: 24.11.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..