Programování 3d Enginů

                                    Část II



      Po  předlouhé  době,  když  už  všichni mysleli, že se seriálem o 3d
      enginech  je  nadobro konec, nečekaně přichází druhé pokračování. Na
      minulý  díl  jsem  obdržel  relativně dost reakcí - vesměs se seriál
      líbil,  ale všem chybělo právě jeho pokračování, kde bych vysvětlil,
      jak  vlastně  3d engine s maticemi naprogramovat. Rovněž se objevilo
      upozornění   na   chybu  -  minule  jsem  na  jednom  místě  popletl
      vektorový  a  skalární součin, ale doufám, že to nikoho nezmátlo. To
      je  na  úvod tak všechno, samozřejmě mi posílejte připomínky dál, ať
      se jimi můžu řídit v dalších pokračováních!


   Úvod

      Minule  jsme  si  tedy  probrali základy, ale k programování jsme se
      vůbec  nedostali.  Tentokrát  tomu  ale bude jinak! Budu používat C,
      takže  Pascalisté  si musí moje zápisy upravit do jim srozumitelného
      tvaru.  Stejně  tak ukázkový program. Spíš bych jim ale doporučoval,
      aby přešli na Cčko :-)


   Jak bude 3d engine vlastně fungovat?

      V enginu máme jeden nebo více objektů. Každý objekt se skládá z bodů
      (vertexů)  a  stěn,  kterými se ale zatím zabývat nebudu. Ke každému
      objektu také náleží jedna matice, která udává jeho polohu v prostoru
      světa  (worldspace).  Díky  tomu,  že body objektu jsou definovány v
      prostoru  objektu (objectspace), se nikdy nebudou měnit! To je jedna
      z  věcí, které číní problémy začátečníkům. Měnit se totiž bude pouze
      a  jenom  matice  -  pomocí změn matice se v 3d enginu dosáhne změny
      polohy  objektu.  Ale  přesto se jeho body měnit nebudou. Při každém
      zobrazení  objektu  je  nutné  transformovat jeho body z objectspace
      pomocí  té  jeho matice do worldspace - teprve body ve worldspace se
      už  budou  pokaždé lišit, pokud dojde ke změnám matice. Každý objekt
      bude tedy vypadat takto:

          typedef struct {
            int           vertexnum;
            VERTEX      * vertex;
            MATRIX        omatrix;
          } OBJECT;

      Kde  omatrix  je  matice  onoho  objektu, vertex je ukazatel na pole
      všech  jeho  bodů  a  vertexnum jejich počet. Body i jejich počet se
      nastaví  na  začátku  programu  a potom se až do skončení nebude nic
      měnit.  Jediné,  co  se z této struktury měnit bude, je matice. Nyní
      popíši typ MATRIX. K VERTEXu se dostanu o něco později.


   Jak ukládat matice v počítači?

      Existuje  několik  způsobů  -  já  budu  využívat ten, který používá
      OpenGL. Mějme matici A. V matematice se její prvky značí takto:

          +                       +
          | a11   a12   a13   a14 |
          | a21   a22   a23   a24 |
          | a31   a32   a33   a34 |
          | a41   a42   a43   a44 |
          +                       +

      Vzhledem  k  tomu,  že  v  3d  enginu  je  při použití matic potřeba
      pracovat  se  značnou  přesností,  bude  jeden prvek typu float nebo
      double.   (O   použití   čísel   s   pevnou   desetinou  čárkou  ani
      nepřemýšlejte,   protože  dávají  naprosto  nepoužitelné  výsledky!)
      Matice  má  čtyři sloupce a čtyři řádky, takže zápis typu v C vypadá
      takto:
          typedef float MATRIX[4][4];

      Definoval  jsem  tedy  typ  s názvem MATRIX, který je pole a čtyřech
      řádcích  a  čtyřech  sloupcích.  K  prvkům  se  přistupuje  obvyklým
      způsobem:
          cislo = matice[x][y]

      A v tom je zádrhel. Co vyjadřuje řádek a co sloupec? Názory na to se
      různí. V paměti má typ MATRIX takovouto strukturu:
          [0][0],[0][1],[0][2],[0][3],[1][0],[1][1],[1][2],....

      Autoři  Direct3D  se  tedy  rozhodli  definovat matici tak, že vztah
      Cčkového a matematického vyjádření vypadá takto:
          a11 = [0][0]     a12 = [0][1]     a13 = [0][2]     a14 = [0][3]
          a21 = [1][0]     a22 = [1][1]     a23 = [1][2]     a24 = [1][3]
          a31 = [2][0]     a32 = [2][1]     a33 = [2][2]     a34 = [2][3]
          a41 = [3][0]     a42 = [3][1]     a43 = [3][2]     a44 = [3][3]

      Zdá  se  to  logické,  avšak  existuje oprávněná námitka, že by bylo
      lepší, kdyby číslo v první hranaté závorce udávalo sloupec a číslo v
      druhé  řádek,  čili  aby  to  bylo [x][y] místo [y][x]. To byl názor
      autorů  OpenGL,  a  proto  se matice v Direct3D a OpenGL liší - mají
      prohozeny  řádky  a  sloupce.  Ale jinak jsou identické, takže pokud
      používáte  Direct3D,  mějte  to  při  čtení  tohoto  článku stále na
      paměti.

      Jak  jsem  již  řekl,  budu  používat matice po vzoru OpenGL, kde je
      vztah matematického a Cčkového vyjádření takovýto:

          a11 = [0][0]     a12 = [1][1]     a13 = [2][0]     a14 = [3][0]
          a21 = [0][1]     a22 = [1][2]     a23 = [2][1]     a24 = [3][1]
          a31 = [0][2]     a32 = [1][3]     a33 = [2][2]     a34 = [3][2]
          a41 = [0][3]     a42 = [1][4]     a43 = [2][3]     a44 = [3][3]

      Existují  i další způsoby uložení matice v počítači, které využívají
      toho,  že  vektor  v posledním řádku matice je vždy (0,0,0,1) a není
      ho  tedy  nutné  ukládát,  ale  pro  zachování  jednoduchosti se jim
      zde nebudu věnovat.


   Počáteční matice

      Často  se  lidé  ptají,  jaká matice se má objektu přiřadit při jeho
      vytvoření.  Má to být matice, která odpovídá poloze, ve které chceme
      objekt  mít. V praxi se nejčastěji matice načte společně s objektem,
      nebo  se  jednoduše přiřadí jednotková, takže objectspace je dočasně
      shodný s worldspace - pouze do první změny matice objektu.


   Násobení matic

      R = A*B
      V  minulém  díle bylo popsáno násobení matic. Matice o rozměrech 4x4
      se násobí takto:
          rji = aj1*b1i + aj2*b2i + aj3*b3i + aj4*b4i
      Kde i a j jsou čísla od jedné do čtyř.

      V Cčku bude vypadat násobení matic r=a*b takhle:
          int i,j;
          for (i=0; i<4; i++) {
            for (j=0; j<4; j++)
            r[i][j] = a[0][j] * b[i][0] + a[1][j] * b[i][1] +
                      a[2][j] * b[i][2] + a[3][j] * b[i][3];
          }

      Na  první  pohled se to může zdát časově dosti náročné, ale rychlost
      programu tím nijak neutrpí, protože se to neprovádí příliš často.


   Transformace bodu maticí

      V minulém díle jsem vysvětloval, jak transformovat bod z objectspace
      do   worldspace,  a  došel  jsem  k  bodu  (x,y,z,w),  kde  w=1  a k
      transformační matici:
          +                      +
          | vx0   vx1   vx2   p0 |
          | vy0   vy1   vy2   p1 |
          | vz0   vz1   vz2   p2 |
          |   0     0     0    1 |
          +                      +

      Transformace spočívá ve vynásobení bodu maticí:
          x'= x*vx0 + y*vx1 + z*vx2 + w*p0
          y'= x*vy0 + y*vy1 + z*vy2 + w*p1
          z'= x*vz0 + y*vz1 + z*vz3 + w*p2
          w'= x*0   + y*0   + z*0   + w*1

      Je vidět, že w' bude vždy jedna. Navíc ho k ničemu nepotřebujeme, stejně
      jako w. Proto postačí definovat bod o třech souřadnicích:

          typedef struct {
            float x,y,z;
          } VERTEX;

      A transformaci (násobení) stačí v Cčku napsat takto:
          tx = x*m[0][0] + y*m[1][0] + z*m[2][0] + m[3][0];
          ty = x*m[0][1] + y*m[1][1] + z*m[2][1] + m[3][1];
          tz = x*m[0][2] + y*m[1][2] + z*m[2][2] + m[3][2];


   Kamera

      Minule jsem sliboval, že vysvětlím, jak do 3d enginu přidat kameru.

      Připomenu,  jak  vypadal  náš  hypotetický  3d  engine  minule.  Byl
      definován  prostor  světa (worldspace). V něm byl umístěn jeden nebo
      více předmětů (objektů), z nichž ke každému příslušela matice, která
      udávala  jeho  polohu  ve worldspace. Vlastní body (vertexy) objektu
      pak  byly  definovány  nikoliv  ve  worldspace,  ale v objectspace -
      prostoru  předmětu, a matice sloužila právě k převedení bodů objektu
      z objectspace do worldspace.

      Nyní  přidáváme  kameru,  která  je (stejně jako objekt) umístěna ve
      worldspace.

      Čím je taková kamera určena? Nejdůležitější jsou tři věci:
        1) Bod, ve kterém je kamera umístěna (p0,p1,p2)
        2) Směr, kterým kamera míří (vz0, vz1, vz2)
        3) Směr, kterým míří bok (vx0, vx1, vx2)

      Možná  se  ptáte, proč kamera míří směrem z. Je to proto, že jsme si
      určili, že osa z je ta osa, která probíhá středem obrazovky dozadu -
      to  je  právě  ten  směr, kterým se na obrazovku díváte (víceméně) a
      kterým by se proto měla "dívat" i kamera.
           Kdo  četl  minulý  díl  seriálu  opravdu  podrobně,  tomu jistě
      neunikla  podobnost  s  objektem,  kde  je  nutné  znát úplně stejné
      informace.  Stejně  tak i nyní k nim přidám čtvrtou - minule to bylo
      vysvětleno podrobněji:
        4) Směr, kterým míří vršek kamery (vy0, vy1, vy2)

      A stejně jako minule se z těchto informací sestaví matice:
          +                      +
          | vx0   vx1   vx2   p0 |
          | vy0   vy1   vy2   p1 |
          | vz0   vz1   vz2   p2 |
          |   0     0     0    1 |
          +                      +

      Minule  se  matice  používala pro transformaci z prostoru objektu do
      prostoru  světa  (objectspace  ->  worldspace).  Matice kamery proto
      logicky   transformuje   z   prostoru   kamery   do  prostoru  světa
      (cameraspace -> worldspace).
           Když  chceme  zobrazit  to,  co  "vidí"  kamera, musíme nějakým
      způsobem  transformovat  předmět  z objectspace do cameraspace. Jak?
      Transformovat  z  objectspace  do  worldspace  i  z  cameraspace  do
      worldspace  již umíme. Kdybychom mohli transformovat z worldspace do
      cameraspace, problém by byl vyřešen.


   Inverzní matice

      O  inverzní matici jsem se minule rovněž zmínil a je to právě to, co
      nyní potřebujeme. Inverzní matice k matici A je taková matice B, pro
      kterou platí:
          A*B=J

      J je jednotková matice a i o té jsem se zmínil. Ale neřekl jsem, jak
      inverzní   matici  vypočítat.  Existuje  několik  způsobů,  z  nichž
      nejjednoduší  je Jordanova metoda. Ta však není pro implementaci v C
      zrovna  nejlepší.  Mnohem  výhodnější  je  počítání  inverzní matice
      pomocí  determinantu,  avšak vysvětlování by bylo zdlouhavé a snad i
      zbytečné.   Vše  to  lze  najít  v  mnoha  učebnicích  matematiky. V
      ukázkovém   programu  však  inverzní  matici  počítám  právě  pomocí
      determinantů,  a  proto  na to zde upozorňuji. Při použití Jordanovy
      metody by se samozřejmě došlo ke shodným výsledkům.


   objectspace -> worldspace

      Inverzní  maticí  k matici kamery (cameraspace->worldspace) je právě
      ta  matice,  která transformuje z worldspace do cameraspace. Když jí
      spočítáme, dokážeme transformovat objekt z objectspace do worldspace
      a  následně z worldspace do cameraspace.
           Zdá  se  tedy,  že  každý  bod z objektu je nutno transformovat
      nadvakrát.  To  ale  není nutné! Když vynásobíme spočítanou inverzní
      matici  (worldspace->cameraspace)  maticí objektu, vyjde nám matice,
      která  transformuje  z  objetcspace  rovnou  do  cameraspace! Pomocí
      pouhých devíti  násobení tak můžeme každý  bod objektu transformovat
      do  prostoru  kamery (cameraspace) a následně zobrazit. A tím je vše
      vyřešeno!


   Co jsme tím získali?

      Nyní  máme  možnost  pohybovat  všemi objekty nezávisle na sobě i na
      kameře a pohybovat kamerou nezávisle na objektech. Ať jsou objekty a
      kamera   vůči   sobě   v  jakékoliv  pozici,  vždycky  jsme  schopni
      transformovat  všechny objekty do cameraspace a správně je zobrazit.
      Známe již vše, co je potřeba k napsání jednoduchého (i složitějšího)
      3d enginu a můžeme přejít k ukázkovému programu.


   Ukázkový program

      Došli  jste  téměř na konec. Zbývá již jen pár informací o ukázkovém
      programu. Tímto tlačítkem uložíte na váš disk soubor engine02.rar, z
      kterého  po  rozRARování  dostanete  hlavní  program engine02.c, tři
      hlavičkové  soubory  (pro  32bit  WATCOM  C,  32bit  DJGPP  a  16bit
      Turbo/Borland  C)  a  dva  souborů s objekty. Jeden objekt je svět a
      druhý letadlo. Letadlem se může pohybovat klávesami Q,W,E,R,A,S,D,F.
      'A'  například  otočí  letadlo doleva - a to nezávisle na poloze, ve
      které  se  nachází  a  nezávisle  na kameře. 'R' zase pohne letadlem
      dopředu.  Numerickou  klávesnicí (popřípadě čísly) se ovládá kamera.
      Při  pohybu  kamery  zůstávají  vůči  sobě  svět i letadlo ve stejné
      poloze.  To  si  ale  už  můžete  vyzkoušet  sami,  stačí  prográmek
      zkompilovat.  Podrobnější  instrukce  jsou  uvnitř.  Ještě  důležitá
      poznámka:  pod  Turbo/Borland  C  nebyl  prográmek  testován,  takže
      funkčnost nezaručuji. Ale mělo by to být v pořádku.

   Popis zdrojáku

      Program obsahuje tyto funkce pro práci s maticemi:

         void initmatrix (MATRIX * m)
           - nastaví matici m na jednotkovou

         void invertmatrix (MATRIX * src, MATRIX * inv)
           - spočítá inverzní matici inv k matici src

         void matmult (MATRIX * a, MATRIX * b, MATRIX * r)
           - vynásobí matici a maticí b a výsledek uloží do r

      Následující  funkce  se  používají  pro změny matice objektu, což se
      projeví jeho pohybem:

         void rotatematrixx (MATRIX * m, float angle)
           - vynásobí matici m maticí rotace kolem osy x o úhel angle

         void rotatematrixy (MATRIX * m, float angle)
           - vynásobí matici m maticí rotace kolem osy y o úhel angle

         void rotatematrixz (MATRIX * m, float angle)
           - vynásobí matici m maticí rotace kolem osy z o úhel angle

         void translatematrix (MATRIX * m, float dx, float dy, float dz)
           - vynásobí matici m maticí posunu o (dx,dy,dz)

      Funkce pro nahrání objektu z disku:
         OBJECT * LoadVEC (char * filename)
           - nahraje objekt ze souboru filename. Má takovýto formát:
             první unsigned short je počet bodů, druhý je 0 a poté
             následují body - každý má 3 signed shorty. Je nutné
             prohození y a z souřadnic, někdy příště vysvětlím proč.
             (Je to tím, že data pocházejí z 3D Studia.)

      A nakonec kreslení objektu:

         void DrawOBJECT (OBJECT * obj, MATRIX * cmatrix)
           Funkce transformuje každý bod objektu obj z objectspace do
           cameraspace, potom provede projekci do 2d a bod zobrazí.
           Ve této funkci si povšimněte zejména počítání inverzní matice
           a jejího vynásobení maticí objektu:
              invertmatrix (cmatrix,&m);
              matmult (&m, &obj->omatrix, &m);

      V  programu  je  samozřejmě  i  funkce  main, ve které se načtou dva
      objekty,  inicializuje  se  matice  kamery (ihned se kamera natočí a
      oddálí) a druhý z objektů se posune po ose y o 64:

         OBJECT * plane = LoadVEC ("letadlo.vec");
         OBJECT * world = LoadVEC ("svet.vec");
         MATRIX cmatrix;

         initmatrix(&cmatrix);
         rotatematrixy (&cmatrix, -PI/6);
         rotatematrixx (&cmatrix, PI/5);
         translatematrix (&cmatrix, 0,0,-512);

         translatematrix (&plane->omatrix, 0,64,0);

      Následuje smyčka, která zobrazí scénu, čeká na stisk klávesy a podle
      toho  "hýbe" objekty nebo kamerou. Program je public domain. Dělejte
      si  s  ním  co  chcete. Byl psán dost narychlo, takže může obsahovat
      chyby.  Při  jeho  používání  je  třeba  mít na paměti, že například
      neodalokovává paměť, protože to prostě není potřeba, když program po
      stisku klávesy escape hned skončí.


   Závěr

      A tím tento druhý díl seriálu o 3d enginech končím. Příště snad bude
      i  zobrazování  stěn objektů, eliminace neviditelných stěn a možná i
      hierarchie   objektů.  Někdy  zřejmě  přijdou  na  řadu  i  užitečné
      informace  o  formátu  3DS, který je i dnes, v době 3DS Max, jakýmsi
      standardem.  Na  závěr  vám přeji (nejen) při programování 3d enginů
      hodně štěstí!

                                                           - Shakul -
                                                        shakul@email.cz


            výheň