OpenGL - kamera
Tato kapitola by měla být o tom, jak pohybovat kamerou. Teorii jsem již probral v kapitole o perspektivě (kamera). Praxe je velmi jednoduchá – použijí se jen nějaké funkce z linmath.h, transformují se matice a je hotovo.
Abyste to neměli tak jednoduché, najdete tu ještě jedno téma – jak optimalizovat množství vrcholů potřebných pro vykreslení trojúhelníků sdílejících své vrcholy.
Element array
Podívejte se na následující obrázky. Zobrazují jehlan z předchozí kapitoly, ale navíc také souřadnicové osy. Každá osa je zobrazena pomocí dvou dlouhatánských obdélníků, které jsou navzájem kolmé a tvoří kříž.
Modrá osa je osa z, zelená je x a červená y.
Na prvním obrázku můžete vidět, jak se tloušťka osy z, díky perpsektivě, mění.
Trojúhelník není jediné primitivum, které OpenGL umí vykreslit. Zvládne např. i bod, nebo čáru. Čára má ale tu nevýhodu, že na ni nelze aplikovat perspektivu.
Na druhém obrázku je detail vnitřku jehlanu, kde se osy protínají. Je na něm lépe vidět, že je každá osa tvořena dvěma kolmo se protínajícímí obdélníky.
Každý obdélník je tvořen dvěma trojúhelníky, tj. šesti vrcholy. Trojúhelníky ale některé vrcholy sdílejí, takže by mělo stačit definovat jen čtyři. A pak určit, ze kterých tří vrcholů z těchto čtyř je tvořen jeden trojúhelník a pak ten další.
Pro vykreslení os potřebuji pole vrcholů pro všechny osy (3 osy * 2 obdélníky * 4 vrcholy = 24 vrochlů). A pak, pomocí indexů, určím, kterými vrcholy jsou trojúhelníky tvořeny. Nakonec nesmím zapomenout na barvy vrcholů (taky jich bude 24).
Soubor vertex-data.h jsem rozšířil o proměnné lines, lineIndices a lineColors:
#define VERTEXT_DATA_H
#include <GLFW/glfw3.h>
GLfloat vertices[] = {
//...
};
GLfloat colors[] = {
//...
};
GLfloat lines[] = {
-0.09, 100.0, 0.0,
0.09, 100.0, 0.0,
0.09,-100.0, 0.0,
-0.09,-100.0, 0.0,
-100.0, .09, 0.0,
100.0, .09, 0.0,
100.0, -.09, 0.0,
-100.0, -.09, 0.0,
.0, -.09, 100.0,
.0, .09, -100.0,
.0, -.09, -100.0,
.0, .09, 100.0,
-0.00, 100.0, -0.09,
0.00, 100.0, 0.09,
0.00,-100.0, 0.09,
-0.00,-100.0, -0.09,
-100.0, .0, 0.09,
100.0, .0, 0.09,
100.0, .0, -0.09,
-100.0, .0, -0.09,
-.09, .0, -100.0,
.09, .0, -100.0,
-.09, .0, 100.0,
.09, .0, 100.0
};
GLuint lineIndices[] = {
0, 1, 2, 2, 3, 0,
4, 5, 6, 6, 7, 4,
8, 9, 10, 8, 11, 9,
12, 13, 14, 14, 15, 12,
16, 17, 18, 18, 19, 17,
20, 21, 22, 22, 21, 23
};
GLfloat lineColors[] = {
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0,
0.0, 0.0, 1.0, 1.0
};
#endif
Tyto data se zase nahrají do bufferů a nastaví se VAO, tak, jak už to znáte.
Jen indexy se nahrají do pole typu GL_ELEMENT_ARRAY_BUFFER
místo
GL_ARRAY_BUFFER
.
Celý trik pak spočívá pouze v tom, že se místo funkce
glDrawArrays()
zavolá glDrawElements()
:
První argument určuje typ primitiva, který se má vykreslit. Druhý argument délku GL_ELEMENT_ARRAY_BUFFERU, resp. počet indexů. Délka může být jiná, například proto, protože indexy nezčínají na začátku bufferu. Kde začínají (jaký je posun od začátku), to určuje poslední argument.
Změny v kódu
Kromě změn ve vertex-data.h popsaných výše budou všechny změny jen v souboru main.c.
Nejdříve je potřeba změnit globální proměnné pro VAO a buffery:
První VAO budu používat pro vykreslení jehlanu jako v minulé kapitole, druhý VAO pro vykreslení os. Budu také potřebovat další 3 buffery.
Protože budu používat stejný program pro vykreslení os jako pro
vykreslení jehlanu, nemusím měnit nic ve vertex ani fragment shaderu,
ani např. ve funkci initVariables()
.
Zato musím změnit funkci initBuffers()
–
nahrát na grafickou kartu nové vrcholy, indexy a barvy.
{
glGenVertexArrays(2, VAO);
glGenBuffers(5, BO);
glBindVertexArray(VAO[0]);
{
glBindBuffer(GL_ARRAY_BUFFER, BO[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, BO[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
}
glBindVertexArray(VAO[1]);
{
glBindBuffer(GL_ARRAY_BUFFER, BO[2]);
glBufferData(GL_ARRAY_BUFFER, sizeof(lines), lines, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, BO[3]);
glBufferData(GL_ARRAY_BUFFER, sizeof(lineColors), lineColors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, BO[4]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(lineIndices), lineIndices, GL_STATIC_DRAW);
}
glBindVertexArray(0); // Unbind VAO
}
Všiměte si, že indexy se nepředávají do žádné proměnné ve vertex shaderu, takže
není potřeba volat glVertexAttribPointer()
ani
glEneableVertexAttribArray()
.
Funkce uninitBuffers()
se také malinko změnila. Jen maže více VAO
a BO:
Poslední změna je ve funkci draw()
, kde jsem dal před vykreslování
jehlanu vykreslování os:
// Vykresli osy
glBindVertexArray(VAO[1]);
glDrawElements(GL_TRIANGLES, sizeof(lineIndices) / sizeof(lineIndices[0]), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// Vykresli jehlan
glBindVertexArray(VAO[0]);
glDrawArrays(GL_TRIANGLES, 0, sizeof(vertices) / sizeof(vertices[0]) / 3);
glBindVertexArray(0);
To je ke kreslení os a optimalizaci pomocí glDrawArrays()
vše.
Kamera
Teď zpět k příkladu s kamerou.
Z kódu příkladu s jehlanem z minulé kaptioly jsem odstranil
rPyramdid a vše, co s otáčením souviselo. Naopak jsem přidal kód do
funkce draw()
, který se stará o transformaci světa (celý svět se
transformuje tak, aby to vypadalo, jako že se pousová místo a směr kamery).
vec3 targetPosition = { 0.0, 0.0, -5.0 };
mat4x4_perspective(uPMatrix, degToRad(45.0), ((GLfloat)width) / height, 1.0, 100.0);
mat4x4_translate(uMVMatrix, targetPosition[0], targetPosition[1], targetPosition[2]);
mat4x4_translate(uViewMatrix, cameraPos.x, cameraPos.y, cameraPos.z);
mat4x4_invert(uViewMatrix, uViewMatrix);
mat4x4_mul(uMVMatrix, uViewMatrix, uMVMatrix);
Kód nejdříve přesouvá jehlan na targetPosition. To je úvodní pozice jehlanu. Následně se nastaví view matrix podle pozice struktury cameraPos a tato pozice se invertuje (protože posun kamery vpřed se simuluje posunem celého světa vzad atd.). Nakonec se view matrix aplikuje se na uMVmatrix.
To je k popisu kamery vlastně vše. Už jen zbývá zodpovědět otázku, kde se vezme struktura cameraPos a jak se mění její složky x,y a z.
Tato struktura je definovaná v souboru events.h, kde je i definovaná
funkce pro obsluhu události klávesnice key_callback()
, jež
hodnoty členů struktury cameraPos mění.
float x;
float y;
float z;
} cameraPos = { 0.0, 0.0, 5.0 };
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);
return;
}
if(action != GLFW_PRESS) {
return;
}
GLfloat delta = 0.5;
if(key == GLFW_KEY_LEFT) cameraPos.x -= delta;
else if(key == GLFW_KEY_RIGHT) cameraPos.x += delta;
else if(key == GLFW_KEY_DOWN) cameraPos.z -= delta;
else if(key == GLFW_KEY_UP) cameraPos.z += delta;
else if(key == GLFW_KEY_S) cameraPos.y += delta;
else if(key == GLFW_KEY_X) cameraPos.y -= delta;
}
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
}
Z kódu je jasné, že k změně pozice kamery můžete používat šipky na klávesnici a znaky S a X.
Look At
Poslední problém řešený touto kapitolou je použití funkce
lookAt()
.
Zdrojový kód se bude oproti příkladu výše lišit jen ve funkci
draw()
a to na místě, kde se nastavují matice transformace.
mat4x4 uMVMatrix, uPMatrix, uViewMatrix;
mat4x4_perspective(uPMatrix, degToRad(45.0), ((GLfloat)width) / height, 1.0, 100.0);
vec3 targetPosition = { 0.0, 0.0, -5.0 };
mat4x4_translate(uMVMatrix, targetPosition[0], targetPosition[1], targetPosition[2]);
vec3 upVector = { 0.0, 1.0, 0.0 };
if (cameraPos.x == 0.0 && cameraPos.z == -5.0) {
upVector[1] = .0; upVector[2] = 1.0;
}
mat4x4_translate(uViewMatrix, cameraPos.x, cameraPos.y, cameraPos.z);
mat4x4_look_at(uViewMatrix, (float *)&cameraPos, targetPosition, upVector);
mat4x4_mul(uMVMatrix, uViewMatrix, uMVMatrix);
glUniformMatrix4fv(variables.uMVMatrixLoc, 1, GL_FALSE, (const GLfloat *)uMVMatrix);
glUniformMatrix4fv(variables.uPMatrixLoc, 1, GL_FALSE, (const GLfloat *)uPMatrix);
// ...
Funkce mat4x4_look_at()
inicializuje matici pohledu
uViewMatrix. Potřebuje k tomu znát pozici kamery
(cameraPos), pozici, kam se má dívat (targetPosition) a
vektor, kteý směřuje nahoru (upVector).
Jak bylo popsáno v kapitole o kameře, pro orientaci v prostoru jsou potřeba 3 kolmé vektory. Třetí se dá spočítat na základě prvních dvou (vektoru upVector a vektoru mezi cameraPos a targetPosition).
To ale funguje jen v případě, že tyto dva vektory nejsou rovnoběžné. Vektor
upVector směřuje podél osy y. Proto je ve funkci
draw()
podmínka, která ve chvíli, kdy by směr od
cameraPos k targetPosition také směřoval podél osy
y, upVektror nasměruje podél osy z. Hrubé, ale
funguje to.
Není stále vyřešen jeden problém, a to, když se přesune cameraPos do
místa targetPosition. V takovou chvíli se stane vektor určující
směr mezi těmito body nulový a funkce mat4x4_look_at()
přestane
fungovat.
Zmíněné řešení zmíněných problémů není jediné možné a správné. Mělo by vám dát jen představu o tom, jak funkce „look at“ funguje. V tomto tutoriálu vás ještě čeká jedna kapitola, kde uvidíte, jak vytvořit kameru alá FPS. (Kapitola o pohybu.)
Závěr
V této kapitole jste se naučili, jak použít element array pro optimalizaci objemu dat potřebného pro popis objektu. Je to velmi často používaná technika, takže ji určitě neopomíjejte.
Také jste mohli vidět, jak použít klávesnici pro „pohyb v prostoru“, a v neposlední řadě jak použít funkci „look at“ pro pohled určitým zadaným směrem.