Előadás Home Introduction Environment setup Best homeworks

Transzformáció

Ahhoz, hogy egy objektumot mozgassunk a képernyőn minden egyes iteráció (rendering loop) során megváltoztathatjuk a vertex adatokat és újra konfigurálhatjuk a buffereket. Azonban ez feleslegesen sok erőforrást igényelne, ezért sokkal jobb módszer, ha a vertexeket nem módosítjuk, csak transzformációs mátrixok segítségével kiszámítjuk az objektum új pozícióját és ez alapján rajzoljuk ki az objektumot.

Eddig a vektorokat arra használtuk, hogy pozíció, szín és textúra információkat tároljunk bennük és a shaderek között kommunikáljunk velük. Azonban egy $Nx1$ méretű vektort és egy $MxN$ méretű mátrixot össze tudunk szorozni, hiszen a vektor sorainak száma megegyezik a mátrix oszlopainak a számával. Ha a vektor tárolja a pozíció információt a mátrix pedig leírja az adott transzformációt, akkor a szorzás eredménye az új pozícióba transzformált vektor.

Egység (Identity) mátrix


Általánosságban a számítógépes grafikában így az OpenGL esetén is $4x4$-es transzformációs mátrixokat alkalmazunk. Egyrészt ez lehetővé teszi, hogy az eltolást is mátrix formában tárolhassuk (affin geometria), másrészt így reprezentálni tudjuk a perspektív projekciónál történő mélységgel való osztást (projektív geometria).

Homogén koordináták: Egy vektor w komponensét más néven homogén koordinátának is nevezzük. Ha egy homogén vektorból szeretnénk megkapni a 3D vektort, egyszerűen le kell osztanunk az x, y és z komponenseket a w koordinátával. Általában azonban ezzel nem kell foglalkoznunk, mert a w komponens 1.0. A homogén koordináták használatának számos előnye van: lehetővé teszi, hogy egy mátrix szorzás segítségével eltoljunk egy 3D vektort, továbbá a w komponens segítségével 3D vizualitást hozhatunk létre.
Ha a homogén koordináta 0, akkor a vektort irány vektornak nevezzük, amin nem lehet eltolás műveletet végrehajtani.

A legegyszerűbb transzformációs mátrix az egység mátrix. Az egység mátrix egy $NxN$ méretű mátrix, ahol a főátlón kívül minden érték nulla. És ahogy látszik az egység transzformációs mátrix nem változtat a vektoron:

\begin{equation*} \begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{bmatrix} 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} = \begin{bmatrix} \color{red}1 \cdot 1 \\ \color{green}1 \cdot 2 \\ \color{blue}1 \cdot 3 \\ \color{purple}1 \cdot 4 \end{bmatrix} = \begin{bmatrix} 1 \\ 2 \\ 3 \\ 4 \end{bmatrix} \end{equation*}


Note: Az egység mátrix a számítógépes grafika szempontjából általában egy kiindulási pont, hogy újabb transzformációs mátrixokat generáljunk.

Skálázás (Scaling)


Mivel 2D vagy 3D vektorokkal dolgozunk a skálázási tényezőt a vektor egyes komponenseire külön definiálhatjuk. Vegyük például a $\color{red}{\bar{v}} = (3,2)$ vektort. Az x komponense mentén csökkentsük a felére, míg az y koordináta mentén nyújtsuk a kétszeresére. Azaz a skálázási vektor a következő: $(0.5,2)$ A skálázás után az új vektor $\color{blue}{\bar{s}}=(1.5,4)$

title


Definiáljunk egy transzformációs mátrixot, ami megcsinálja a skálázást helyettünk. Az egység mátrix esetén láttuk, hogy a diagonális elemek mindig a megfelelő vektor komponenssel szorzódtak össze, így ha az 1-es értékeket megváltoztatjuk mondjuk 3-as értékre, akkor a vektor minden komponensét háromszorosára nyújtjuk. Ha a skálázási változókat a $(\color{red}{S_1}, \color{green}{S_2}, \color{blue}{S_3})$ számhármas jelöli, akkor a transzformációs mátrixot a következő képpen definiálhatjuk:

\begin{equation*} \begin{bmatrix} \color{red}{S_1} & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{S_2} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}{S_3} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{S_1} \cdot x \\ \color{green}{S_2} \cdot y \\ \color{blue}{S_3} \cdot z \\ 1 \end{pmatrix} \end{equation*}


Note: A negyedik skálázási tényező 1, mivel a w komponens skálázása nem definiált a 3D térben.

Eltolás (Translation)


Az eltolás nem más, mint az adott vektor egy új pozícióba transzformálása azáltal, hogy hozzá adjuk az eltolás vektort. Az eltolás vektor $(\color{red}{T_x},\color{green}{T_y},\color{blue}{T_z})$ komponenseit a transzformációs mátrix negyedik oszlopának a felső három értéke reprezentálja:

\begin{equation*} \begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}{T_x} \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}{T_y} \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}{T_z} \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x + \color{red}{T_x} \\ y + \color{green}{T_y} \\ z + \color{blue}{T_z} \\ 1 \end{pmatrix} \end{equation*}
Ez azért működik, mert mindegyik eltolás komponenst a vektor w komponensével szorozzuk össze, ami 1 és adjuk hozzá a vektor eredeti értékéhez. (Ez nem működne, ha a transzformációs mátrix $3x3$ méretű lenne.) Az eltolás mátrix segítségével mozgatni tudjuk az objektumokat mind a három $(x, y, z)$ irányba

Forgatás (Rotation)


A forgatást szögek segítségével írjuk le 2D és 3D-ban. Egy szöget megadhatunk fokban, vagy radiánban, ahol a teljes kör 360 fok vagy 2 $pi$ radián.

Fok konvertálása radiánba: A legtöbb matematikai és OpenGL függvény a szögek megadását radiánban várja. Radián és fok között a következő képpen tudunk váltani, ahol a PI értéke közelítőleg = 3.14159265359:
  • szög_fok = szög_radian * (180.0f / PI)
  • szög_radian = szög_fok * (PI / 180.0f)

A lenti ábra a $\color{green}{\bar{k}}$ vektor 72 fokos jobbra forgatását mutatja be 2D-ban, ahol az eredmény a $\color{red}{\bar{v}}$ vektor:

title

A forgatást műveletét 3D-ban egy szöggel és egy tengellyel írjuk le, ami körül a forgatás történik. Egy forgatást sin és cos függvények kömbinációjával írhatunk le, ahol a $\theta$ szimbólum jelöli a forgatási szöget.

Forgatás az X tengely körül:

\begin{equation*} \begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}{\cos \theta} & - \color{green}{\sin \theta} & \color{green}0 \\ \color{blue}0 & \color{blue}{\sin \theta} & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} x \\ \color{green}{\cos \theta} \cdot y - \color{green}{\sin \theta} \cdot z \\ \color{blue}{\sin \theta} \cdot y + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix} \end{equation*}

Forgatás az Y tengely körül:

\begin{equation*} \begin{bmatrix} \color{red}{\cos \theta} & \color{red}0 & \color{red}{\sin \theta} & \color{red}0 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}0 \\ - \color{blue}{\sin \theta} & \color{blue}0 & \color{blue}{\cos \theta} & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x + \color{red}{\sin \theta} \cdot z \\ y \\ - \color{blue}{\sin \theta} \cdot x + \color{blue}{\cos \theta} \cdot z \\ 1 \end{pmatrix} \end{equation*}

Forgatás az Z tengely körül:

\begin{equation*} \begin{bmatrix} \color{red}{\cos \theta} & - \color{red}{\sin \theta} & \color{red}0 & \color{red}0 \\ \color{green}{\sin \theta} & \color{green}{\cos \theta} & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \cdot \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} \color{red}{\cos \theta} \cdot x - \color{red}{\sin \theta} \cdot y \\ \color{green}{\sin \theta} \cdot x + \color{green}{\cos \theta} \cdot y \\ z \\ 1 \end{pmatrix} \end{equation*}


A fenti forgatási mátrixok segítségével forgatni tudjuk a pozíció vektort a megfelelő egység tengely körül. Ezen mátrixokat kombinálhatjuk is és egy egységes mátrixban írhatjuk le a forgatást az adott tengelyek körül:

\begin{equation*} \begin{bmatrix} \cos \theta + \color{red}{R_x}^2(1 - \cos \theta) & \color{red}{R_x}\color{green}{R_y}(1 - \cos \theta) - \color{blue}{R_z} \sin \theta & \color{red}{R_x}\color{blue}{R_z}(1 - \cos \theta) + \color{green}{R_y} \sin \theta & 0 \\ \color{green}{R_y}\color{red}{R_x} (1 - \cos \theta) + \color{blue}{R_z} \sin \theta & \cos \theta + \color{green}{R_y}^2(1 - \cos \theta) & \color{green}{R_y}\color{blue}{R_z}(1 - \cos \theta) - \color{red}{R_x} \sin \theta & 0 \\ \color{blue}{R_z}\color{red}{R_x}(1 - \cos \theta) - \color{green}{R_y} \sin \theta & \color{blue}{R_z}\color{green}{R_y}(1 - \cos \theta) + \color{red}{R_x} \sin \theta & \cos \theta + \color{blue}{R_z}^2(1 - \cos \theta) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation*}


Transzformációs mátrixok kombinálása


A fő érv a transzformációs mátrixok használata mellett, hogy számos transzformációs mátrixot tudunk egy mátrixban reprezentálni a mátrix szorzásnak köszönhetően. Legyen $(x,y,z)$ egy vektor, amit a kétszeresére szeretnénk skálázni, majd az $(1,2,3)$ vektorral eltolni. Ehhez szükséges egy eltolás és egy skálázó mátrixot definiálni, amik összeszorzásával megkapjuk az összetett transzformációs mátrixot:

\begin{equation*} Trans . Scale = \begin{bmatrix} \color{red}1 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}1 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}1 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} . \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}0 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}0 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}0 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} = \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} \end{equation*}


Vigyázzunk arra, hogy a mátrix szorzás nem kommutatív, tehát fontos a mátrixok sorrendje, mert ez meghatározza a transzformáció végeredményét. Amikor több mátrixot szorzunk egy vektorral, akkor először a leg jobboldali mátrix szorzása történik meg. Jó tanács, hogy első lépésben skálázzunk, majd forgassunk és végső lépésként végezzük el az eltolás műveletet. Ha ezt nem vesszük figyelembe, és pl. először eltoljuk az objektumot, akkor a skálázás művelet az eltolás vektorra is hatással lesz! Az eltolás és skálázás eredményeként, a vektort, első lépésben skáláztuk, majd eltoltuk az $(1,2,3)$ vektorral:

\begin{equation*} \begin{bmatrix} \color{red}2 & \color{red}0 & \color{red}0 & \color{red}1 \\ \color{green}0 & \color{green}2 & \color{green}0 & \color{green}2 \\ \color{blue}0 & \color{blue}0 & \color{blue}2 & \color{blue}3 \\ \color{purple}0 & \color{purple}0 & \color{purple}0 & \color{purple}1 \end{bmatrix} . \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} \color{red}2x + \color{red}1 \\ \color{green}2y + \color{green}2 \\ \color{blue}2z + \color{blue}3 \\ 1 \end{bmatrix} \end{equation*}


Transzformáció a gyakorlatban


Mivel az OpenGL nem rendelkezik beépített mátrix és vektor műveleteket kezelő programkönyvtárral, ezért vagy definiáljuk a saját műveleteinket, vagy használunk egy másik programkönyvtárat.

GLM (OpenGL Mathematics): Egy header-only programkönyvtár, ami azt jelenti, hogy egyszerűen csak a megfelelő header fájlt kell include-olni a projekthez és már lehet is használni a könyvtár műveleteit, azaz nem szükséges se fordítani, se linkelni.

A legtöbb GLM funkcionalitás eléréséhez, csak a következő 3 include-ra lesz szükségünk:

In [ ]:
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

Első példában toljuk el az $(1,0,0)$ vektort, az $(1,1,0)$ vektorral. A glm::vec4 struktúrát használjuk, ahol az utolsó koordináta a homogén koordináta, ami 1:

In [ ]:
glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f);
glm::mat4 trans = glm::mat4(1.0f); // identity matrix as a starting point
trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f));
vec = trans * vec;
std::cout << vec.x << vec.y << vec.z << std::endl;

Első lépésben létrehozzuk a vec nevű vektort, amit transzformálni szeretnénk. Majd egy trans nevű egységmátrixot definiálunk és utána a glm::translate függvény segítségével az egység mátrix és a glm::vec3(1.0f, 1.0f, 0.0f) eltolás vektor kombinálásával előállítjuk a végleges transzformációs mátrixot. Ahhoz, hogy eltoljuk az eredeti vektort jobb oldalról beszorozzuk a transzformációs mátrixal.

Most skálázznuk és forgassunk el az objektumot, az előző tutorialból:

In [ ]:
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
Első lépésben skálázzuk az objektumot $0.5$ értékkel, majd forgassuk el a $Z$ tengely mentén 90 fokkal. Mivel a GLM radiánban várja a szöget ezért a glm::radians függvény segítségével átalakítjuk a 90 fokot radiánná. Mivel a textúrázott téglalapunk a $XY$ 2D képernyő síkon van, ezért most a $Z$ tengely körül forgatunk. Tartsuk észben, hogy a tengely ami körül forgatunk egység vektor kell hogy legyen, ezért ha nem az $X$, $Y$ vagy $Z$ tengelyek körül forgatunk, akkor először normalizálni kell a forgatási tengelyt. Mivel most GLM függvényeken keresztül hozzuk létre a transzformációkat, ezért az már össze is szorozza nekünk a komponenseket, előállítva a végső mátrixot.

A következő lépésben át kell adnunk a transzformációs mátrixot a shader programoknak. A mátrixot a GLSL beépített mat4 típusú uniform változója fogja reprezentálni, amivel jobbról megszorozzuk a pozíció vektort:
In [ ]:
#version 400 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;
uniform mat4 transform;

void main() {
    gl_Position = transform * vec4(aPos, 1.0f);
    ourColor = aColor;
}
A shader programba a következő képpen küldhetjük a transzformációs mátrixot:
In [ ]:
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
Első lépésben lekérjük a uniform változó lokációját, majd a mátrixot a glUniformMatrix4fv függvény segítségével a shaderbe küldjük. A második argumentum határozza meg, hogy mennyi mátrixot szeretnénk küldeni, ami most 1. A harmadik paraméter arra kíváncsi, hogy szeretnénk e transzponánlni a mátrixot, amire néha szükség lehet a mátrix definiálásától függően. Az utolsó paraméter a tényleges mátrix adat, de mivel az OpenGL más reprezentációban tárolja azt mint a GLM ezért kell a konverzió.

A transzformáció eredményeképpen valami hasonlót kell látnunk:

title


Az objektumunk kétszer olyan kicsi és el van forgatva, tehát a transzformáció sikeres volt. Most toljuk el az objektumot az ablak jobb alsó sarkába és folyamatosan forgassuk a középpontja körül. Ehhez a rendering loop-ban folyamatosan frissítenünk kell a transzformációs mátrixot, amihez a GLFW time függvényét fogjuk használni:

In [ ]:
glm::mat4 trans = glm::mat4(1.0f);
trans = glm::translate(trans, glm::vec3(0.5f, -0.5f, 0.0f));
trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
Mivel most minden egyes iterációban egy kicsit el kell fordulnia az objektumnak, ezért a rendering loop-ban mindig újra fogjuk definiálni a transzformációs mátrixot a GLFW time függvényében. Első lépésben az objektum origója $(0,0,0)$ körül forgatunk, majd a megfelelő helyre toljuk az objektum elforgatott verzióját. Vegyük észre, hogy a kódban először az eltolást majd a forgatást írtuk, azonban a transzformációk sorrendjét mindig fordított sorrendben kell olvasni, azaz először a forgatás majd az eltolás fog érvényelüslni. A következő eredményt kell látnunk:


  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code

Ahhoz, hogy több háromszöget rajzoljuk ki, egyszerűen csak egy ciklusban többször meg kell hívnunk a renderelést:

In [ ]:
ourShader.use();
        
glBindVertexArray(VAO);
        
for(int i = 0; i < 2; ++i) {
    glm::mat4 trans = glm::mat4(1.0f);
    if(i == 0 ) { // egyik háromszög kicsinyítjük, forgatjuk és eltoljuk
        trans = glm::translate(trans, glm::vec3(0.5, -0.5, 0.0));
        trans = glm::rotate(trans, -(float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
        trans = glm::scale(trans, glm::vec3(0.3, 0.3, 0.3));
    }        
    else { // másik háromszöget csak forgatjuk
        trans = glm::rotate(trans, (float)glfwGetTime(), glm::vec3(0.0, 0.0, 1.0));
    }
                    
    ourShader.setMat4("trans", trans);
    
    glDrawArrays(GL_TRIANGLES, 0, 3);
}


  • A teljes forráskód: Code
  • Vertex shader: Code
  • Fragment shader: Code