A 3.1-es verzió óta az OpenGL nem tartalmazza a forgatással, eltolással kapcsolatos rutinokat. A profik azt írják, hogy ez egy jó ötlet volt, mert nem zárja korlátok közé a programozót. Egy API-nak viszont az a feladata, hogy megkönnyítse egy programozó életét. Ha egy alapvető funkcionalitás hiányzik az API-ból, akkor programozók tömegét kényszerítjük arra, hogy ugyan azt a feladatot elkészítse.
Aki nem akar saját maga bajlódni a mátrixok számolásával, az használhatja a glm-et, vagy egy korábbi posztomban említett Nopper úr GLUS könyvtárát. Én viszont egy merészet gondoltam, és úgy döntöttem, megírom saját rutinjaimat. Azért nevezem merésznek az ötletet, mert nem sokat konyítok a matematikához. Igyekeztem faék egyszerűségű kódot írni, amitől lehet, hogy a kód használhatósága nehézkes lesz. A megértést viszont biztosan nem fogja gátolni.
Az első és legfontosabb megemlíteni, hogy az OpenGL oszlopfolytonosan várja a mátrixokat, ami a Wikipédiában fellelhető mátrixok transzponáltja. Érthetően fogalmazva:
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
Legyenen az indexek. Az általam használt kódban minden mátrix egy 16 elemű tömb lesz.
Egységmátrix
A legalapvetőbb mátrix a 3D grafikában. Az átló mentén 1-t tartalmaz. Erre azért van szükség, mert a mátrixok szorzása alkalmával nem fogja egyik mátrix a másikat kinullázni. Minden további mátrix ebből indul ki, külön nem fogom leírni.
Perspektíva
A perspektíva viszonylag egyszerű. A képernyő oldalaránya és a látószög beállítása mellett kiszámítja a mátrix értékeit. Két további paramétere a közeli és távoli vágási távolság.
void perspectiveMatrix(float *m, float fov, float aspect, float near, float far){
float range;
range = tan(fov * M_PI/360.0) * near;
memset(m, 0, sizeof(float) * 16);
m[0] = (2.0 * near) / ((range * aspect) - (-range * aspect));
m[5] = (2.0 * near) / (2.0 * range);
m[10] = -(far + near) / (far - near);
m[11] = -1.0;
m[14] = -(2.0 * far * near) / (far - near);
}
Kamera pozíció
Egy demóban, megkockáztatom ez az egyik legfontosabb mátrix. Segítségével megadhatjuk, hogy honnan nézzük a jelenetet. Ennek megfelelően bonyolult, mert több vektorműveletet kell végezni.
void lookAt(float *m, float *eye, float *target, float *up){
int i;
float n[3];
float u[3];
float v[3];
float d[3];
for(i = 0; i < 3; i++) n[i] = eye[i] - target[i];
crossproduct3v(up, n, u);
crossproduct3v(n, u, v);
normalize(u, 3);
d[0] = dotproduct(eye, u, 3) * -1.0;
normalize(v, 3);
d[1] = dotproduct(eye, v, 3) * -1.0;
normalize(n, 3);
d[2] = dotproduct(eye, n, 3) * -1.0;
m[0] = u[0];
m[4] = u[1];
m[8] = u[2];
m[12] = d[0];
m[1] = v[0];
m[5] = v[1];
m[9] = v[2];
m[13] = d[1];
m[2] = n[0];
m[6] = n[1];
m[10] = n[2];
m[14] = d[2];
m[3] = 0.0;
m[7] = 0.0;
m[11] = 0.0;
m[15] = 1.0;
}
Pozíció
A pozíció mátrix a legegyszerűbb. Gyakorlatilag a mátrix három elemét helyettesítjük a koordinátákkal.
void translate(float *m, float x, float y, float z){
int i;
/* Identity matrix */
for(i = 0; i < 16; i++){
if( (i % 5) == 0){
m[i] = 1.0;
}
else m[i] = 0.0;
}
m[12] = x;
m[13] = y;
m[14] = z;
}
Forgatás
A forgatás néz ki a legijesztőbben. A legtöbb példaprogram külön veszi a tengelyek mentén történő forgatásokat, majd mátrix szorzásokkal egy forgatási mátrixot képez. Szerencsére lehet találni olyan leírást, ami leírja, hogyan néz ki az a mátrix, amiben elvégezték a mátrix szorzást. A kód ilyesztő, de csak egyszer kell megírni.
void rotate(float *m, float rx, float ry, float rz){
int i;
/* Identity matrix */
for(i = 0; i < 16; i++){
if( (i % 5) == 0){
m[i] = 1.0;
}
else m[i] = 0.0;
}
/* General rotation */
m[0] = cosf(ry) * cosf(rz);
m[4] = -cosf(rx) * sinf(rz) + sinf(rx) * sinf(ry) * cosf(rz);
m[8] = sinf(rx) * sinf(rz) + cosf(rx) * sinf(ry) * cosf(rz);
m[12] = 0.0;
m[1] = cosf(ry) * sinf(rz);
m[5] = cosf(rx) * cosf(rz) + sinf(rx) * sinf(ry) * sinf(rz);
m[9] = -sinf(rx) * cosf(rz) + cosf(rx) * sinf(ry) * sinf(rz);
m[13] = 0.0;
m[2] = -sinf(ry);
m[6] = sinf(rx) * cosf(ry);
m[10] = cosf(rx) * cosf(ry);
m[14] = 0.0;
m[3] = 0.0;
m[7] = 0.0;
m[11] = 0.0;
m[15] = 1.0;
}
Egyéb mátrix műveletek
Most pedig jöjjenek az egyéb nyalánkságok. A kamera pozíciójánál a kódban ilyen függvények láthatóak, mint crossproduct meg dotproduct. Mik ezek? Az első a vektoriális szorzat, aminek érdekessége, hogy csak 3 dimenzióban értelmezik.
void crossproduct3v(float *a, float *b, float *result){
result[0] = a[1] * b[2] - a[2] * b[1];
result[1] = a[2] * b[0] - a[0] * b[2];
result[2] = a[0] * b[1] - a[1] * b[0];
}
A másik a skaláris szorzat, ami meglepő módon egy számot ad vissza. A hossz arányos a két vektor által bezárt szöggel, ezért a fények programozásánál is sokszor előkerül.
float dotproduct(float *a, float *b, int size){
Ha programunkban egy térhálós modellt elhelyezünk, forgatunk, és még a kamerát is mozgatjuk, több mátrixot kapunk, de hogyan készítsünk egy mátrixot? A válasz a mátrix szorzás. A mátrix szorzásnál a tagok sorrendje nem felcserélhető. Ezt mindenki ki is próbálhatja, hogy mi történik, ha a modellnél az eltolási mátrixot szorozza a forgatásival, vagy fordítva.
int i;
float result = 0.0;
for(i = 0; i < size; i++) result += a[i] * b[i];
return(result);
}
A szorzás sorrendje a következő: perspektíva * forgatás * eltolás * kamera. A szorzást a következő kód valósítja meg:
void matrixMultiply4x4(float *a, float *b, float *c){
int i,x,y;
for(i = 0; i < 16; i++){
x = i & 12;
y = i % 4;
c[i] = a[x + 0] * b[y + 0] +
a[x + 1] * b[y + 4] +
a[x + 2] * b[y + 8] +
a[x + 3] * b[y + 12];
}
}
Mondtam, hogy egyszerű lesz, mint a faék. Hozzáértők a kommentekben mindenféle optimalizációt küldhetnek.
Mátrix betöltése shaderbe
A shaderben a mátrix uniform mat4 deklarációval szerepel. Kódunkban a glGetUniformLocation(shader, változónév) segítségével megkapjuk az erőforrás leíróját (egy egész szám), majd ezt felhasználva a glUniformMatrix4fv(leíró, 1, GL_FALSE, matrixpointer) segítségével beállítjuk azt.
Források
http://en.wikipedia.org/wiki/Transformation_matrix
http://en.wikipedia.org/wiki/Rotation_matrix#General_rotations
http://www.songho.ca/opengl/gl_transform.html