2010. február 10., szerda

Tao Start 04 - Transzformációk

Végre elérkeztünk egy nagyon lényeges részhez. Nyugi! Nem kell megijedni, nem olyan felfoghatatlan, de kell hozzá ez a kis matek. Megpróbálom érthetően leírni úgy, hogy aki tud osztani, szorozni, összeadni, kivonni az is megértse. Először átvesszük a szükséges matematikát, aztán meg azt, hogyan kapcsolódik ez az egész az OpenGL-hez. Ami fontos, hogy ne próbáljuk meg görcsösen megérteni a matematikai részt, ha nem megy elsőre, sebaj, olvassátok tovább, mivel ennyire mélyen nem lesz szükség a matekos ismeretekre, egyelőre, de később már nem úszható meg. Kezdetnek egy furfangos kérdés: mi az a mátrix?

Mi sem egyszerűbb, ez gyakorlatilag egy 2 dimenziós tömb, ami N sorból és M oszlopból áll. Nekünk, ennek egy speciális esetére lesz szükségünk, a négyzetes mátrixra, amely ugyanannyi oszlopból áll, mint amennyi sorból (N*N-es). Eddig gondolom minden stimmel. Most jön a "neheze". A mátrixokkal lehet műveleteket végezni. Ezek például a következők:
  1. Skalárszorzás
  2. Összeadás
  3. Szorzás
Persze ennél több művelet is elvégezhető, de ez nekünk egyenlőre bőven elég. Nézzük a skalárszorzást: ez azt jelenti, hogy egy N*M-es mátrix minden elemét megszorozzuk egy skalárral (számmal). Így egyértelmű:


Itt egy konkrét számmal szoroztuk be (vagyis szám és mátrix között volt értelmezve a művelet), ez gondolom világos. Az összeadás már mátrixok között van értelmezve. Persze ez már N*M-esre nem megy, csak N*N-re. Ilyenkor a megfelelő pozícióban lévő elemeket összeadjuk:


A szorzással már problémák is vannak. Legyen az egyik mátrix "A" a másik "B". Ekkor az C=AB mátrix egy eleme úgy jön ki, hogy az "A" mátrix megfelelő sorában végighaladva az i. elemet összeszorzom a "B" mátrix megfelelő oszlopában az i. elemmel (i megy 1-től N-ig persze) és az így kijött szorzatokat összeadjuk. Ezt papíron úgy érdemes kiszámolni, hogy az "A" mátrixot leírod, majd a "B" mátrixot fölé írod úgy, hogy a "B" mátrix bal alsó sarka hozzáérjen az "A" mátrix jobb felső sarkához. Ok, jöjjön inkább egy ábra:


Itt a fehér mátrix az eredmény. Nem is részletezném, szerintem a rajzon nagyon jól látszik a dolog. Ha kicsit gondolkodtok rájöhettek, hogy ez a mátrixszorzás művelet nem csak N*N-es mátrixok közt van értelmezve, hanem N*M es és M*N-es között is. Tehát egy M*N-es mátrixot összeszorozhatok egy N*M-essel, de már például egy N*M-eset nem szorozhatok N*M-essel. Ebből az a tanulság, hogy ez a mátrixszorzás dolog fordítva nem ugyanazt az eredményt adja sőt, NEM NÉGYZETES MÁTRIX ESETÉN FORDÍTVA NINCS IS ÉRTELMEZVE! De vissza a négyzetes mátrixokhoz. Mint általában a szorzásnál kell lennie egy egységelemnek, aminek olyan tulajdonságának kell, lennie, hogy ezzel beszorozva egy tetszőleges számot a szám nem változik. Ez a számoknál ugye az EGY (A*1=A). Gondolom világos. Nos mátrixszorzásnál is van ilyen, általában E-vel jelöljük és őt egységmátrixnak nevezzük. Ez úgy néz ki, hogy minden eleme 0 kivéve a főátló, ami csupa 1-es. A főátló az [1,1] elemen kezdődik és az [N,N]-ediken végződik. Próbáljátok összeszorozni egy mátrixszal és meglátjátok, hogy igazam van. Lehet még mátrixnak inverzét is számolni, ami egy olyan mátrix amivel az eredetit megszorozva egységmátrixot kapunk, de ezt most nem írom le, mert itt a transzformációknál egyenlőre nem kell.
Akkor, hogy is jön ez az egész mátrixosdi az OpenGL-hez? A válasz nagyon egyszerű: az OpenGL a transzformációkat mátrixok szorzása formájában hajtja végre. Nézzünk konkrét példákat. A legegyszerűbb az eltolás. Itt ugye annyi kell, hogy az P1(x, y, z) koordinátánkhoz hozzáadunk valamennyit így az új pontunk: P2(x+dx, y+dy, z+dz). Gondolom ez érthető (aki nem érti rajzoljon 2D-s koordináta-rendszert, vegyen fel rajta egy pontot ez a P1 és x irányba adjon hozzá 2-t y-ban 1-et, az így keletkezett pont a P2, ekkor a dx=2 és dy=1 ez azt jelzi, hogy x-en és y-on mennyit toltuk). Mielőtt felírnám ezt mátrix alakban el kell mondanom még egy kis "nehezítést", méghozzá a homogén alakot. Ugyanis az OpenGL a mátrixszorzásokat homogén koordinátákkal hajtja végre. Hogy ez miért kell azt most nem írom le, majd, ha saját transzformációs mátrix megadását tárgyalom megtudhatjátok. A lényeg, hogy x, y, z koordináták miatt 3x3-as mátrixokat kellene használni, ehelyett 4x4-eseket használunk, aminek az utolsó eleme mindig 1.


Itt szépen látszik, hogy P1-et egy 4x1-es mátrixként tekintjük, a transzformációs mátrix meg nyilván 4x4-es az eredmény szintén 4x1-es mátrix lesz, ha M1-et szorozzuk P1-el és nem fordítva! Ha elvégzitek a szorzást, akkor a bal felső (zöld betűs) mátrixnak kell kijönnie. Remélem érthető. Egyenlőre több transzformációs mátrixot nem írok fel, ha valakit érdekel írjon! Egyébként az OpenGL-ben alapból benne vannak ezek a mátrixok, szóval nem kell fejből nyomni őket. Akkor talán térjünk is rá a lényegi részre, az OpenGL-re. Most az eddig megírt kódunkon fogunk gyakorolni, ahogy az előző lecke végén abbahagytuk

Gyakorlat

Az előző leckében egy színes négyzetet rajzoltunk ki, most ezt a négyzetet forgassuk el a saját tengelye körül. Nyilván a Paint() metódust fogjuk buherálni. Először is figyeljük meg, hogy a rajzolás glPushMatrix és glPopmatrix közé lett írva. Ez nem véletlen. Ha kivesszük, semmi nem változik, viszont a transzformáció szempontjából nagyon lényeges lesz. Egyelőre e két utasítás közé dolgozzunk. A többit majd később megértjük. Szóval forgassuk meg a négyzetünket a saját tengelye körül! A forgatás a glRotatef függvénnyel történik. Ez annyit tesz, hogy az aktuális mátrixot megszorozza a forgatás mátrixával. Négy paramétere van, az első, hogy hány fokkal forgasson (fokban kell megadni: 0°-360°) aztán pedig 3 érték jön, melyek [0..1] tartományban változhatnak, ezek azt adják meg sorban, hogy mennyire vegyen részt a forgatásban az x, y és z tengely. Az utolsó 3 paraméternél célszerű csak 0-t vagy 1-et írni. Írjuk be a glRotatef(45,0,1,0);-t a glBegin és a glPushMatrix közé. Ha most futtatjuk a programot látszik, hogy tényleg történt valami, valóban elforgatta. Most animáljuk a dolgot, úgy szebb. Mi kell ehhez? Hozzunk létre egy float alfa = 0 változót. Most adjunk a Form-hoz egy Timer-t és a Tick eseményébe írjuk be a következő kódot:

       private void timer_Tick(object sender, System.EventArgs e)
       {
           if (alfa < 360)
           {
               alfa = alfa + 1;
           }
           else
           {
               alfa = 0;
           }
           glControl.Refresh();
       }

Ezután a glRotatef-ünket ne 45-tel hívjuk meg, hanem alfa-val. Most toljuk el a forgó négyzetünket x tengely mentén hárommal. Ezt úgy tehetjük meg, hogy a glRotatef elé bírunk egy glTranslatef(3,0,0)-át, ugyanis ez az eltolás mátrixával szorozza meg az aktuális mátrixot, ami jelen esetben leegyszerűsítve a forgatási mátrixunk, tehát a forgatott négyzetünket eltolja. Ennek 3 paramétere van, sorban: mennyivel tolja el x-en, y-on és z-n (a már említett dx, dy és dz). Mi van, ha fordítva hajtjuk végre a két műveletet? Próbáljuk is ki! Először glRotatef aztán glTranslatef. Azt vesszük észre, hogy a négyzetünk most nagy ívben kering ahelyett, hogy a saját tengelye körül forogna. Mért van ez? A mátrixműveleteket a következő sorrendben kell végrehajtani, ha az M1-eset elsőnek az M2-eset 2.-nak az Mn-eset n-ediknek szeretnénk végrehajtani: M eredmény = Mn*Mn-1*...*M2*M1 tehát visszafele kell gondolkodni. Ugyanis, ha jobban megnézzük a rajzolás (glBegin és glEnd közti rész) a transzformációink után van megadva. Akkor hogy is van ez? Bizony-bizony fordítva hajtódik végre. Először a rajzoláskor megadott minden pontra külön elvégzi a transzformációkat az OGL. Méghozzá úgy, mint a fenti mátrixszorzásnál a P2-t számoltuk a P1-ből, csak itt az M mátrix már a transzformációink szorzatából áll (Forgatás*Eltolás) tehát a P2-t így kapjuk: P2=Forgatás*Eltolás*P1, ha kering a négyzet és P2=Eltolás*Forgatás*P1, ha forog a tengelye körül és közben ki van tolva. Ha most ezt nem értjük, akkor mindenképp próbáljunk még néhány transzformációt berakni és bátran kísérletezzünk tovább. Figyeljétek meg milyen sorrendben hajtódnak végre a transzformációk! Bonyolítsunk kicsit! Tegyünk be még egy négyzetet, ami ugyanazt fogja csinálni, mint az előző, csak x-en (-3)-al toljuk el, tehát ellentétes irányban lesz. Ha az ember logikusan belegondol elég annyi, hogy kimásoljuk a forgatós-eltolós résztől a glEnd-ig az egészet, újra beillesztjük és átírjuk a glTranslatef-nél a 3-at (-3)-ra:

private void gl_Paint(object sender, PaintEventArgs e)
{
  Gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);

  // Ide jöhetnek a rajzoló utasítások
  Gl.glPushMatrix();
  {
      Gl.glTranslatef(-3f, 0f, -5f);
      Gl.glRotatef(alfa, 0f, 1f, 0f);

      // A második négyzet
      Gl.glBegin(Gl.GL_QUADS);
      {
          Gl.glColor3f(1, 0, 0);
          Gl.glVertex3f(-0.5f, +0.5f, 0f);
          Gl.glColor3f(0, 1, 0);
          Gl.glVertex3f(+0.5f, +0.5f, 0f);
          Gl.glColor3f(0, 0, 1);
          Gl.glVertex3f(+0.5f, -0.5f, 0f);
          Gl.glColor3f(1, 1, 1);
          Gl.glVertex3f(-0.5f, -0.5f, 0f);
      }
      Gl.glEnd();

      Gl.glTranslatef(3f, 0f, 0f);
      Gl.glRotatef(alfa, 0f, 1f, 0f);

      // Az első négyzet
      Gl.glBegin(Gl.GL_QUADS);
      {
          Gl.glColor3f(1, 0, 0);
          Gl.glVertex3f(-0.5f, +0.5f, 0f);
          Gl.glColor3f(0, 1, 0);
          Gl.glVertex3f(+0.5f, +0.5f, 0f);
          Gl.glColor3f(0, 0, 1);
          Gl.glVertex3f(+0.5f, -0.5f, 0f);
          Gl.glColor3f(1, 1, 1);
          Gl.glVertex3f(-0.5f, -0.5f, 0f);
      }
      Gl.glEnd();
  }
  Gl.glPopMatrix();

  Gl.glFlush();
}

Erre azt kéne tapasztalnunk, hogy a két négyzet forog a saját y tengelye körül és a képernyő két ellentétes oldalán vannak. De nagyon nem ez történik. Bár az egyik valóban egy helyen van és forog a tengelye körül, a másik össze - vissza szálldos. Miért van ez? Az első négyzet (ami a kód végén van) kirajzolása után végrehajtódik a tengely körüli forgás, majd az eltolás, ez eddig rendben. Aztán az origóba (a koordináta rendszer közepébe) rajzolunk még egy ábrát, amit szeretnénk hasonlóan forgatni, meg eltolni. Ez meg is történik, viszont ez a forgatás meg eltolás vonatkozik az elsőnek kirajzolt négyzetre is, tehát azon is végrehajtja ugyanezeket, ami ezért abszolút máshová kerül. Ebből le lehet vonni a következtetést, hogy az OpenGL a transzformációk végrehajtásakor az aktuálisan megrajzolt összes ponton végrehajtja a megadott transzformációt. Mi a megoldás? A glPushMatrix és glPopMatrix pár alkalmazása. Ezek rendre elmentik, visszatöltik az aktuális transzformációs mátrixot. Ezeket programozás technikai sorrendben kell megadni, tehát először glPushMatrix, majd glPopMatrix. A glPushMatrix és glPopMatrix az érvényes mátrix módnak megfelelő mátrixokkal dolgoznak.

Mátrix verem, dobozolás felsőfokon
Kezdetben minden veremben egy mátrix van, mégpedig az egységmátrix. A kezdő mátrix mód a MODELVIEW mód. A mátrix veremműveletekkel lehetővé válik az egyes mátrixok elmentése, hogy később, ha szükségünk van rájuk, akkor újra használni tudjuk. A mátrix veremműveleteket többnyire akkor használjuk, amikor olyan objektumot akarunk pl. animálni, mely több egyszerűbb objektumból épül fel, és az egyes részeket eltérő módon akarjuk mozgatni/transzformálni. Ez az elmentés dolog egy verembe történik, ami úgy működik, mint egy doboz, amibe rakhatunk könyveket (csak egymás tetejére). Mindig azt a könyvet tudjuk kivenni amit legutoljára raktunk be. Itt is azt a mátrixot tölti vissza, amit legutoljára betettünk. Nyilván, ha többet akarunk kivenni, mint amennyit betettünk (több glPopMatrix van, mint glPushMatrix) akkor lefagy a rendszer. A legcélszerűbb, hogy mindent, amin külön transzformációt akarunk végrehajtani egy glPushMatrix-glPopMatrix párba tesszük. Most fejlesszük tovább az animációnkat úgy, hogy az eltolt saját tengelyük körül forgó négyzeteink keringjenek az origó körül. Ekkor az eddigi transzformációnkat mind befoglaljuk egy glPushMatrix - glPopMatrix párba és itt hajtunk végre egy forgatást y tengely körül alfa szöggel:

private void gl_Paint(object sender, PaintEventArgs e)
{
   Gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
   Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);

   // Ide jöhetnek a rajzoló utasítások
   Gl.glPushMatrix();
   {
       // Közös transzformáció
       Gl.glTranslatef(0f, 0f, -5f);
       Gl.glRotatef(alfa, 0f, 1f, 0f);

       Gl.glPushMatrix();
       {
           // A 2. négyzet transzformációja
           Gl.glTranslatef(-3f, 0f, 0f);
           Gl.glRotatef(alfa, 0f, 1f, 0f);

           // A 2. négyzet kirajzolása
           Gl.glBegin(Gl.GL_QUADS);
           {
               Gl.glColor3f(1, 0, 0);
               Gl.glVertex3f(-0.5f, +0.5f, 0f);
               Gl.glColor3f(0, 1, 0);
               Gl.glVertex3f(+0.5f, +0.5f, 0f);
               Gl.glColor3f(0, 0, 1);
               Gl.glVertex3f(+0.5f, -0.5f, 0f);
               Gl.glColor3f(1, 1, 1);
               Gl.glVertex3f(-0.5f, -0.5f, 0f);
           }
           Gl.glEnd();
       }
       Gl.glPopMatrix();

       Gl.glPushMatrix();
       {
           // Az 1. négyzet transzformációja
           Gl.glTranslatef(3, 0, 0);
           Gl.glRotatef(alfa, 0, 1, 0);

           // Az 1. négyzet kirajzolása
           Gl.glBegin(Gl.GL_QUADS);
           {
               Gl.glColor3f(1, 0, 0);
               Gl.glVertex3f(-0.5f, +0.5f, 0f);
               Gl.glColor3f(0, 1, 0);
               Gl.glVertex3f(+0.5f, +0.5f, 0f);
               Gl.glColor3f(0, 0, 1);
               Gl.glVertex3f(+0.5f, -0.5f, 0f);
               Gl.glColor3f(1, 1, 1);
               Gl.glVertex3f(-0.5f, -0.5f, 0f);
               Gl.glEnd();
           }
       }
       Gl.glPopMatrix();
   }
   Gl.glPopMatrix();

   Gl.glFlush();
}

Itt már szépen keringenek, és forognak, ahogy kell, persze még nem takaráshelyesen. Azt majd a következő leckében, amikor is már egy kockát fogunk forgatni. Az nagyságrendekkel rövidebb és világosabb lesz, mint ez a lecke.

A lecke eredménye, lehet gyönyörködni

Ha most elsőre nem volt minden világos az csakis a szerző hibája, de előbb-utóbb biztosan le fog esni. A példa teljes forrása itt tölthető le:


A fenti tutorial PowR által írt a http://free-pascal.extra.hu/ oldalon megtalálható OGL tutorial alapján készült el. Köszönet érte PowR-nek, valamint Kuba Attila - OGL Programozása jegyzetének!

0 megjegyzés :

Megjegyzés küldése