Előadás Home Introduction Environment setup Best homeworks

Kamera

Az OpenGL önmagában nem értelmezi a kamera fogalmát, ezért úgy szimuláljuk azt, hogy a scene objektumait az ellenkező irányba toljuk, ezzal azt szimulálva mintha a kamera mozogna.

Camera/View space

Kamera space esetén azokat a vertex koordinátákat értjük, amiket a kamera perspektívájából látunk: a view mátrix a az összes world koordinátát a view spacebe transzformálja, ami a kamera pozíciójától és orientációjától függ. A kamera definiálásához szükségünk van a world space-beli pozíciójára, a nézőirányára, egy vektorra, ami a kamera jobb tengelyét írja le és egy up vektorra. A pozíció a kamera koordinátarendszerének a közepe, míg a három másik vektor a kamera egymásra merőleges tengelyeit definiálja.

title

1. Camera pozíció

A kamera pozícióját egy 3D world space-beli vektorral írhatjuk le:
In [ ]:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
Note: A pozitív z tengely a képernyőn megy keresztül és befelé mutat, tehát ha azt szeretnénk, hogy a kamera hátrafelé mozogjon, akkor a z tengely mentén kell mozognunk.

2. Camera nézőirány

Tegyük fel, hogy most a kamera a scene origójába néz: (0,0,0), ez lesz a target. Ha a kamera pozíciójából kivonjuk a target vektort, akkor megkapjuk a nézőirányt. (Azt akartuk, hogy a kamera a pozitív z irányba nézzen, ha felcseréljük a kivonást, akkor a negatív z irányba nézne a kamera.)
In [ ]:
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);

3. Jobb (Right) tengely

A right tengely a kamera koordinátarendszerének a pozitív x tengelyét reprezentálja. Ahhoz, hogy megkapjuk a right vektort először definiáljuk a scene (world space) up vektorát. Majd a cross product művelettel az up és az irány vektor segítségével előállítjuk a right vektort, ami mind a két vektorra merőleges. (Ha megcseréljük a cross product sorrendjét akkor a negatív x irányba mutató vektort kapjuk meg.)
In [ ]:
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));

4. Up tengely

Most, hogy megvan a kamera irány és right vektora, a cross product művelettel előállíthatjuk a kamera up vektorát:
In [ ]:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
A kamera vektorok felhasználásával mostmár definiálni tudjuk a LookAt mátrixot:

Look At

A három merőleges kamera vektor és a kamera pozíció (eltolás) segítségével elő tudjuk állítani a LooAt mátrixot. Ezután, ha bármilyen vektort megszorzunk a LookAt mátrixal, akkor a view/kamera spacebe transzformáljuk azt.

\begin{equation*} LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation*}


R a right vektort, U az up vektort, D az irány vektort, míg P a kamera pozícóját reprezentálja. A pozíció vektor invertált, hiszen a scene koordinátákat az ellenkező irányba szeretnénk tolni, mint amerre mozogni szeretnénk.

A GLM a kamera pozíció, a target és a scene up vektora megadásával létrehozza a LookAt mátrixot:
In [ ]:
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

A kamera mozgatása

Első lépésben definiálnunk kell a kamera koordinátarendszerét:
In [ ]:
glm::vec3 cameraPos   = glm::vec3(0.0f, 0.0f,  3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp    = glm::vec3(0.0f, 1.0f,  0.0f);

A LookAt mátrixot a következő képpen számítjuk:

In [ ]:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
A kamera nézőiránya az adott pozíció (cameraPos) + a definiált kamera nézőirány (cameraFront). Ez lehetővé teszi azt, hogy ugyan mozgunk, de a kamera mindig a kijelölt nézőirányba nézzen.
In [ ]:
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
    ...
    float cameraSpeed = 0.05f; // adjust accordingly
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        cameraPos += cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        cameraPos -= cameraSpeed * cameraFront;
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
A WASD billentyűk lenyomására frissítjük a kamera pozícióját. Ahhoz, hogy előre vagy hátra mozogjunk hozzá kell adnunk vagy ki kell vonnunk az irányvektort a pozíció vektorból. Az oldal irányú mozgáshoz a cross product segítségével előállítjuk a right vektort, majd ezen irány mentén mozgunk. A cameraSpeed változó segítségével tudjuk skálázni a mozgás sebességének a mértékét.
Note: A számított right vektort normalizálnunk kell. Ha ezt nem tennénk, akkor a cross product esetleg különböző méretű vektorokkal térne vissza a cameraFront változótól függően. Ennek eredménye az lenne, hogy nem konstans sebességgel mozogna a kamera.

Mozgás sebessége

Mivel az egyes számítógépeknek különböző a számítási kapacitása (gyorsabb-lassabb CPU/GPU konfiguráció) ezért, ha konstansra állítjuk a cameraSpeed-et akkor a gyorsabb gépeken túl nagy mozgást, míg a lassabb gépeken túl lassú mozgást fogunk tapasztalni. (Ez azért van mert a gyorsabb gép gyorsabban képes hívni a renderelési ciklust, így többször hívódik meg a key_callback függvény.)
Ennek kiküszöbölésére eltároljuk hogy mennyi idő telt el az előző frame renderelése és az aktuális között, majd ezzel az értékkel skálázzuk a cameraSpeed változót. Így elérhetjük, hogy a hardware sebességétől függettlenül mindenki ugyanazt az élményt tapasztalja.

Ehhez vezessünk be két globális változót:

In [ ]:
float deltaTime = 0.0f;	// Time between current frame and last frame
float lastFrame = 0.0f; // Time of last frame

A render ciklus elején számítsuk ki az eltelt időt:

In [ ]:
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;

Majd skálázzuk a sebességet:

In [ ]:
static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) {
  float cameraSpeed = 2.5f * deltaTime;
  ...
}

Körbenézés a kamerával (look around)

Ahhoz, hogy körbe nézzünk a kamerával a sceneen a cameraFront változót kell változtatnunk, ezt pedig az egér segítségével fogjuk megtenni. Ehhez viszont szükség lesz egy kis trigonometriai ismeretre.

Euler szögek

3db Euler szög segítségével tetszőleges 3D rotációt le tudunk írni. A három Euler szög a pitch, a yaw és a roll:

title

A pitch definiálja, hogy mennyire nézünk fel vagy lefelé. A yaw határozza meg, hogy mennyire nézünk balra vagy jobbra. A roll pedig azt definiálja, hogy mennyire van "megdőlve" a kamera. Ezt a szöget főleg space-flight kamerák esetén használják. A három Euler szög segítségével tettszőleges 3D rotációs vektort tudunk definiálni.
Most mi a pitch és a yaw értékeket fogjuk használni a kameránk definiálásához. A két szög segítségével fogjuk definiálni az új nézőirány vektort:

title

Ha a háromszög átfogóját 1-nek definiáljuk, akkor a trigonometria szabályait felhasználva megkapjuk a két rövidebb oldalt: $\cos \ \color{red}x/\color{purple}h = \cos \ \color{red}x/\color{purple}1 = \cos\ \color{red}x$, a szemben lévő oldal pedig $\sin \ \color{green}y/\color{purple}h = \sin \ \color{green}y/\color{purple}1 = \sin\ \color{green}y$.

title

Mivel a pitch esetén az x tengely rögzítve van ezért a forgatás az y és a z koordinátákra lesz hatással:
In [ ]:
direction.y = sin(glm::radians(pitch)); // Note that we convert the angle to radians first 
direction.z = cos(glm::radians(pitch));

A yaw érték esetén a következő eredményt kapjuk:

title

Az x komponens a cos(yaw), míg a z komponens a sin(yaw) értéktől függ. Tehát a három koordináta a következő képpen függ az Euler szögektől:
In [ ]:
direction.x = cos(glm::radians(yaw));
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

Egér kezelés

A yaw és a pitch értékeket az egér mozgásánal az irányából fogjuk számolni. A horizontális mozgás a yaw míg a vertikális mozgás a pitch értékre lesz hatással. Ehhez el fogjuk tárolni az előző frame egér pozícióját és az aktuális pozíció felhasználásával kiszámoljuk a mozgás irányát és nagyságát.
Első lépésben beállítjuk az OpenGL-nek, hogy folyamtosan monitorozza az egér pozíciót, ne engedje, hogy kihúzzuk a definiált ablakból és rejtse el a szemünk elől (ez tökéletes egy FPS szerű kamera leírásához):
In [ ]:
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
A pitch és a yaw értékek számításához a GLFW egy mouse_callback segítségével fogja visszaadni az aktuális egérpozíciót:
In [ ]:
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
glfwSetCursorPosCallback(window, mouse_callback);
Az FPS kamera definiálásához a következő lépéseket kell implementálni:
  • Az egér elmozdulásának a kiszámítása az előző pozíció és a mostani alapján
  • A yaw és a pitch értékek frissítése az offsetek alapján
  • Megszorítás hozzáadása a pitch értékhez (ne tudjun túl le vagy fel nézni a user)
  • Az új nézőirány vektor kiszámítása
Az offset számításához eltároljuk az előző frame egérpozícióját, amit kezdetben a képernyő közepére inicializálunk:
In [ ]:
float lastX = 400, lastY = 300;

A mouse_callback-ben kiszámítjuk az offsetet, amit skálázunk egy érzékenységet definiáló változóval:

In [ ]:
float xoffset = xpos - lastX;
float yoffset = lastY - ypos; // reversed since y-coordinates range from bottom to top
lastX = xpos;
lastY = ypos;

float sensitivity = 0.05f;
xoffset *= sensitivity;
yoffset *= sensitivity;

Következő lépésben frissítjük a yaw és a pitch értékeket:

In [ ]:
yaw   += xoffset;
pitch += yoffset;
A pitch értéket határok közzé szorítjuk, hogy ne tudjon túl le és túl fel nézni a user, mert az nagyon természetellenes érzetet keltene:
In [ ]:
if(pitch > 89.0f)
  pitch =  89.0f;
if(pitch < -89.0f)
  pitch = -89.0f;
Az utolsó lépés az új nézőirány kiszámítása:
In [ ]:
glm::vec3 front;
front.x = cos(glm::radians(yaw));
front.y = sin(glm::radians(pitch));
front.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
cameraFront = glm::normalize(front);
Az első iterációban a kamera túl nagyot ugrana, ennek kiküszöbölésére bevezetünk egy változót, ami azt nézi, hogy ez e az első egér mozgatás. Ha igen akkor beállítjuk ezt a pozíciót, mintha ez lenne az előző frame egérpozíciója. Ezt csak egyszer csináljuk meg a program futása során.
In [ ]:
if(firstMouse) { // this bool variable is initially set to true 
    lastX = xpos;
    lastY = ypos;
    firstMouse = false;
}

A teljes egérkezelő kód:

In [ ]:
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    if(firstMouse) {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
  
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; 
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.05;
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw   += xoffset;
    pitch += yoffset;

    if(pitch > 89.0f)
        pitch = 89.0f;
    if(pitch < -89.0f)
        pitch = -89.0f;

    glm::vec3 front;
    front.x = cos(glm::radians(yaw));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

Zoom

A zoom funkcióhoz a Field of View (fov) paraméterét fogjuk a perspektív projekciónak változtatni. Ez definiálja azt, hogy mekkora részét látjuk az adott scene-nek. Ahogy a fov egyre kisebbé válik, a projektált tér is egyre kisebb lesz, ami azt az érzetet kelti, mintha ráközelítenénk (zoom) az adott targetre. Ehhez az egér scroll funkcióját fogjuk használni:
In [ ]:
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) {
  if(fov >= 1.0f && fov <= 45.0f)
      fov -= yoffset;
  if(fov <= 1.0f)
      fov = 1.0f;
  if(fov >= 45.0f)
      fov = 45.0f;
}
Az y offset érték reprezentálja, hogy mennyit mozogtunk vertikálisan. A scroll szintet 1 és 45 fok közzé definiáljuk. Az új fov értékkel frissítjük a perspektív projekciót:
In [ ]:
projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);


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

Kamera osztály

Ahhoz, hogy ne kelljen mindig megírni a kamera kezeléséhez szükséges kódot, létrehozunk egy kamera osztályt, ami segítségével könnyen tudjuk kezelni majd a kamera mozgásokat.
  • A teljes forráskód a kamera osztállyal: Code
  • Kamera osztály: Code