Artikkelit

Artikkelit olivat Suomipelit.comissa sivuston sulkeuduttua. Näin ollen niihin saattaa sisältyä rikkinäisiä linkkejä tai epämääräisiä viitteitä asioihin, joita ei enää ole olemassa, ja joskus jokin saattaa näkyä väärin.

Palaa artikkelilistaan

DirectX 8 - Ensimmäinen kosketus

Kaikki vähänkin tietokoneita käyttäneet ovat varmasti tutustuneet Microsoftin DirectX ohjelmointirajapintaan vähintään pelien pelaamisen yhteydessä. Tämä artikkeli käy ensin lyhyesti läpi mistä DirectX:ssä on oikein kysymys, ja aloittaa DirectX -ohjelmointi artikkelisarjan Direct3D:n käynnistämisellä. Myöhemmät artikkelit kertovat kuinka muita DirectX:n sisältämiä osia käytetään. Lukijalta vaaditaan c/c++ perusosaaminen. Artikkelisarjan tarkoituksena on antaa lukijalleen DirectX:n käytön perusosaaminen

20.4.2002 julkaistun artikkelin on kirjoittanut Jarkko Parviainen.

  1. Direct3D käyttökuntoon
  2. Laitteen luominen
  3. Laitteen vapauttaminen
  4. Piirtäminen ruudulle
  5. Ensimmäinen kolmio ruudulle
  6. Verteksit, verteksipuskurit ja muut uudet tuttavat
  7. Direct3D:n matriisit
  8. World Matrix - Maailmamatriisi
  9. View Matrix - Näkymämatriisi
  10. Projection Matrix - Projektiomatriisi
  11. Miten tästä eteenpäin?
  12. Loppusanat

Kaikki vähänkin tietokoneita käyttäneet ovat varmasti tutustuneet Microsoftin DirectX ohjelmointirajapintaan vähintään pelien pelaamisen yhteydessä. Tämä artikkeli käy ensin lyhyesti läpi mistä DirectX:ssä on oikein kysymys, ja aloittaa DirectX -ohjelmointi artikkelisarjan Direct3D:n käynnistämisellä. Myöhemmät artikkelit kertovat kuinka muita DirectX:n sisältämiä osia käytetään. Lukijalta vaaditaan c/c++ perusosaaminen. Artikkelisarjan tarkoituksena on antaa lukijalleen DirectX:n käytön perusosaaminen, eikä se käsittele sivuviittauksia kummemmin eri alueiden optimointimenetelmiä. Näihin lukijat saavat tutustua omalla ajallaan, kun tuntevat olevansa valmiita siihen.
Koska kyseessä on opetustarkoitukseen tehty artikkeli, ei kaikki sen sisältämä koodi ole suoraan oikeaan ohjelmaan soveltuvaa ja muutenkin pääasiana on selkeys, ei tehokkuus. Artikkeli on pyritty jakamaan sopivan kokoisiin osioihin ja jokainen läpikäyty asia on pyritty selvittämään mahdollisimman selkokielisesti ilman aiheelle ominaisia monimutkaisia vieraskielestä johdettuja termejä. Artikkelin mukana tulee lähdekoodit esimerkkiohjelmaan, jota analysoiden asiat käydään lävitse. Lähdekoodi esimerkit on testattu Microsoft Visual C++ 6.0 ohjelmointiympäristössä.

Esimerkkien kääntymisen onnistuminen vaatii oikein asennettujen DirectX8.0 ohjelmointikirjastojen olemassaoloa, sekä d3d8.lib ja d3dx8.lib tiedostojen linkittämistä projektiin.

Koko artikkelin lähdekoodeineen voit imuroida tästä: http://www.suomipelit.com/artikkelit/art... .

Microsoftin DirectX on käytännössä ensimmäinen varteen otettava yritys helpottaa multimediaohjelmoijien, pääasiassa peliohjelmoijien, arkea Windows-ympäristössä. Kyseessä on ohjelmointirajapinta, jonka avulla useiden eri laitteiden käyttö on yksinkertaistettu yhden yhteisen liittymän alle. DirectX helpottaa äänikorttien, 2 ja 3 -ulotteisen grafiikan, näppäimistön, hiiren ja muiden peliohjainten käyttöä Microsoft Windows-ympäristöissä huomattavasti antaen samalla tuen usealle eri laitetyypille. Rajapinta on myös oikein käytettynä erittäin tehokas ja nopea. Ennen DirectX:n tulemista oli itse ohjelmien tekijöiden harteilla tukea eri laitteita, mikä rajoitti asiakaskuntaa sekä mahdollisten erikoistekniikoiden käyttöä.

DirectX:n rinnalla on myös kilpailevia ohjelmointirajapintoja, mutta nämä rajapinnat keskittyvät yleensä vain yhteen osa-alueeseen. Yleisemmin käytössä olevat näistä ovat OpenGL (2 ja 3-ulotteinen grafiikka), ja OpenAL (2 ja 3 -ulotteinen ääni), jotka ovat käyttöjärjestelmäriippumattomia, eli niitä voi käyttää (pienin muutoksin) Linuxista Windowsiin ilman ongelmia. Peliohjainten sekä multimedian (videokuva, musiikkitiedostot) käyttöjärjestelmäriippumatonta ohjelmointirajapintaa ei vielä ole vastaan tullut. Koska nämä rajapinnat ovat kuitenkin toisistaan erillisiä, ei niiden käyttö ole aina ruusuilla tanssimista vaikka varsinkin OpenAL on tehty erittäin läheisesti OpenGL:n kanssa käytettäväksi.

Myös DirectX:n käytössä on omat ongelmansa. Versiosta toiseen siirryttäessä rajapinta voi muuttua merkittävästi (esimerkkinä DirectX7.0 = > DirectX 8.0), ja ohjelmakoodi täytyy lähes aina uusia jotta uudet ominaisuudet saadaan käyttöön. Useimmille ohjelmoijille on myös Microsoftin käyttämä COM (Component Object Model) vähintäänkin järkyttävä kokemus vanhojen C-ohjelmointi päivien jälkeen. Kyseessä on kuitenkin asioita usein helpottava tapa hallita tiettyjä DirectX:n ohjelmakoodin osasia. Käytännössä ennen jokaisen COM-objektin käyttöä niiden rajapintaa \'pyydetään\' Windowsilta, Windows lisää COM-objektin viittausten määrää, ja useampi ohjelma voi käyttää siten samaa objektia yhtäaikaisesti. Kun ohjelma lopetetaan se vapauttaa pyytämänsä rajapinnat, jolloin niiden viittausten lukumäärää vähennetään. Kun viittauksia ei enää ole, COM-objektille varattu muisti vapautetaan. COM:ia ei kuitenkaan tarvitse käyttää kuin DirectX:n osioita luodessa, ellei halua itse tehdä omia COM-objektejaan (jossa yleensä onkin jo vaiva suurempi kuin saavutettu hyöty).

Direct3D käyttökuntoon #

Ennen DirectX:n versiota 8.0 tarkoitti Direct3D:n käyttökuntoon saaminen paria sivua koodia ja valmisteluja. Ja kun siihen lisäsi pelkän kolmion piirtämisen olikin koodia jo sen verran, että siihen määrään olisi mahtunut useammankin aloittelijan ensimmäinen peliprojekti. Tätä helpottamaan Microsoft teki D3DX-apukirjaston, joka nykyään käsittää muitakin helpotuksia. DirectX8.0:ssa Direct3D:n saa kuitenkin käyttöön helposti ja nopeasti myös ilman D3DX-kirjastoa, ja se prosessi neuvotaan seuraavaksi.

Laitteen luominen #

Ennen kuin varsinaiseen Direct3D asiaan päästään käsiksi, täytyy luoda ikkuna johon kaikki piirtäminen tapahtuu. Ikkunan luominen on kuvailtu tarkemmin edellisessä artikkelissa, joten siihen ei tässä tutustuta koodia tarkemmin. Sivulla oleva koodi pohjautuu edellisessä artikkelissa esiteltyyn ikkunointikoodiin.

Direct3D käyttää 3D-näytönohjainta, eli laitetta, kahden osoittimen kautta. LPDIRECT3D8 luo osoittimen Direct3D-rajapintaan, ja LPDIRECT3DDEVICE8 itse laitteeseen. Laitteen osoitin hankitaan Direct3D-rajapinnan kautta. Myöhemmin kaikki piirtäminen ruudulle tapahtuu laite-osoittimen avulla, eikä osoitinta Direct3D-rajapintaan juurikaan tarvita. Vaikka tämä voi kuulostaa monimutkaiselta, niin ei kuitenkaan ole. Seuraava koodi selventää mistä tässä kaikessa on oikein kyse.

// 
// Globaaleja muuttujia. 
// 
LPDIRECT3D8       g_pD3D;     // Globaali osoitin Direct3D-objektiin
LPDIRECT3DDEVICE8 g_pD3DLaite; // Globaali osoitin D3D-laitteeseen. 


//-------------------------------------------- 
// Nimi  : LuoD3D()  
// Kuvaus: Luo liittymän Direct3D-laitteeseen.  
//         Olettaa että g_hWnd sisältää 
//         kahvan ohjelman ikkunaan. 
//--------------------------------------------  
HRESULT LuoD3D( void )    
{ 
    HRESULT hr = S_OK; // palautusarvot 

    //  
    // Nollataan Direct3D:n käyttämät osoittimet. 
    //  
    g_pD3D      = NULL; 
    g_pD3DLaite = NULL; 


    // 
    // Luodaan Direct3D-objekti, joka toimii liittymänä
    // Direct3D-rajapintaan. 
    // 
    g_pD3D = Direct3DCreate8( D3D_SDK_VERSION );

    // 
    // Jos liittymää ei saatu on g_pD3D-osoitin nolla. 
    // 
    if( g_pD3D == NULL ) 
        return S_FAIL; 

    //
    // Hankitaan Direct3D-rajapinnalta tiedot nykyisestä 
    // näyttötilasta ja luodaan niiden avulla Direct3D- 
    // laite. 
    // 
    D3DDISPLAYMODE D3DDM;
    g_pD3D->GetAdapterDisplayMode( D3DADAPTER_DEFAULT, &D3DDM ); 
     
     
    //
    // Asetetaan laitteelle tiedot siitä kuinka kuva 
    // tulee piirtää ruudulle. 
    //
    D3DPRESENT_PARAMETERS D3DPP; 
    ZeroMemory( &D3DPP, sizeof( D3DPP ) ); // muistin nollaus 
     
    D3DPP.Windowed         = TRUE;
    D3DPP.SwapEffect       = D3DSWAPEFFECT_DISCARD; 
    D3DPP.BackBufferFormat = D3DDM.Format; 
    D3DPP.EnableAutoDepthStencil = TRUE;
    D3DPP.AutoDepthStencilFormat = D3DFMT_D16;
    
   
    // 
    // Luodaan Direct3D-laite yllä annettujen tietojen 
    // mukaan.
    //
    hr = g_pD3D->CreateDevice( D3DADAPTER_DEFAULT,  
                               D3DDEVTYPE_HAL, 
                               g_hWnd,
                               D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                               &D3DPP, 
                               &g_pD3DLaite ); 


    //
    // Palautetaan CreateDevice()-funktion palautusarvo. 
    // Se kertoo suoraan onnistuiko laitteen hankkiminen. 
    // 
    return hr; 
} 


Yllä olevassa koodissa hankitaan ensin osoitin Direct3D-rajapintaan Direct3DCreate8()-funktiolla, joka ottaa parametrinaan käytössä olevan SDK:n (Software Development Kit, Ohjelmiston Kehitys Pakkaus) version. Tämän jälkeen rajapinnan funktiolla GetAdapterDisplayMode() otetaan oletusnäytönohjaimelta (D3DADAPTER_DEFAULT) tiedot käytössä olevasta näyttötilasta Direct3D-näyttötilamuuttujaan D3DDM. Kun tiedot on hankittu, niitä käytetään D3DPP-muuttujassa (nimi tulee sanoista Direct3D Present Parameters, Direct3D Esitys Parametrit) kertomaan Direct3D-rajapinnalle millaisessa näyttötilassa Direct3D-laite tulee toimimaan. Ohjelmat voivat asettaa nämä tiedot myös itse, mutta koska esimerkki toimii ikkunoituna ruudulla on tiedot siis hankittava erikseen.

Ennen CreateDevice()-funktiota on asetettava tiedot kuinka luotavan Direct3D-laitteen tulee esittää piirretty kuva ruudulla. Se tehdään D3DPP-muuttujalla. Alla olevassa taulukossa on D3DPP:n kaikki jäsenmuuttujat sekä niihin sopivat arvot.

[DEFTABLE]
[D]BackBufferWidth[/D]
[E]Taustapuskurin leveys. Käytetään kaksoispuskuroidussa (engl. doublebuffer) piirrossa.

Kaksoispuskurointi tarkoittaa piirtotapaa, jossa on käytössä kaksi kuvapintaa. Kun näytönohjain piirtää toista kuvapinnoista näytölle edustapuskuriin (engl. frontbufferiin), näytönohjaimen muistissa olevaan taustapuskuriin (engl. backbuffer) piirretään seuraavaa kuvaa. Kun edustapuskuri on piirretty, vaihdetaan se samantien näytönohjaimelle piirrettäväksi taustapuskuriksi ja laitetaan taustalla piirretty kuva piirtymään näytölle.
[/E]

[D]BackBufferHeight[/D]
[E]Taustapuskurin korkeus.[/E]

[D]BackBufferFormat[/D]
[E]D3DFORMAT-tietue, joka kertoo millaisen taustapuskurin ohjelmoija tahtoo laitteelle luoda.[/E]

[D]BackBufferCount[/D]
[E]Taustapuskurien lukumäärä. Voi olla joko 0, 1, 2 tai 3. 0 käsitellään kuten 1, koska Direct3D asettaa taustapuskurien vähittäismääräksi 1. Kaksoispuskurointi saavutetaan asettamalla tämä parametri 1:ksi.[/E]

[D]MultiSampleType[/D]
[E]MultiSampleType -muuttujaa käytetään reunapehmennyksen (engl. anti-aliasing) aikaansaamiseen ja määrittää tavan, jolla yksittäisestä pikselistä otetaan useita näytteitä. Tämä on edistynyt aihe, jota ei tässä käsitellä tätä tarkemmin. [/E]

[D]SwapEffect[/D]
[E]Tämä muuttuja määrittää miten edustapuskuria käsitellään edustapuskurin ja taustapuskurin vaihdossa. Useimmiten käytetään D3DSWAPEFFECT_DISCARD-arvoa joka kertoo että edustapuskurin sisältö voidaan ruudulla näyttämisen jälkeen unohtaa.
hDeviceWindow Kahva ikkunaan johon laite tulee piirtämään. Kun tämä parametri on NULL, piirrettäväksi ikkunaksi saadaan ikkuna joka on aktiivisena CreateDevice()-funktiota kutsuttaessa.[/E]

[D]Windowed[/D]
[E]Muuttuja joka kertoo piirtääkö laite koko ruudulle vai ikkunaan. Arvoiksi käyvät TRUE tai FALSE.[/E]

[D]EnableAutoDepthStencil[/D]
[E]Jos tämän muuttujan arvo on TRUE, Direct3D hallitsee ohjelman syvyyspuskureita. Kun laite resetoidaan, Direct3D palauttaa syvyyspuskurit automaattisesti.[/E]

[D]AutoDepthStencilFormat[/D]
[E]D3DFORMAT-muuttuja joka määrittää millaisen syvyyspuskurin AutoDepthStencil tulee luomaan. Jätetään huomiotta jos EnableAutoDepthStencil on FALSE. [/E]

[D]Flags[/D]
[E]Lippumuuttuja. Voi olla joko nolla tai D3DPRESENTFLAG_LOCKABLE_BACKBUFFER. Tarkoittaa käytännössä sitä että ohjelma voi lukita taustapuskurin ja erikseen lukea/kirjoittaa siihen tietoa.[/E]

[D]FullScreen_RefreshRateInHz[/D]
[E]Kokoruututilan virkistystaajuus Hertzeinä. Ikkunoidussa tilassa tämän arvon tulee olla 0. Kokoruututilassa arvo voi olla D3DPRESENT_RATE_DEFAULT (oletusvirkistystaajuus), D3DPRESENT_RATE_UNLIMITED (niin nopea piirto kuin näytönohjain vain pystyy piirtämään) tai jokin numeroarvo.[/E]

[D]FullScreen_PresentationInterval[/D]
[E]Ruudunpäivityksen maksimitiheys. Määrittää millä nopeudella taustapuskurit esitetään ruudulla. Arvoksi kelpaa jokin seuraavista:

D3DPRESENT_INTERVAL_DEFAULT (0) - Pakollinen arvo jos laite toimii ikkunoidussa tilassa. Koko ruudun tilassa otetaan käyttöön oletusarvo.

D3DPRESENT_INTERVAL_IMMEDIATE - Taustapuskuri vaihtuu heti edustapuskuriksi kun näytönohjain on sen piirtänyt. Edellisen edustapuskurin piirto voi olla kesken, mikä aiheuttaa kuvan repeytymistä (engl. tearing). Näytölle tulevassa kuvassa voi siis ruudun ohi kiitävässä autossa yläpuoli korista olla hieman jäljessä korin alaosaa ja renkaita.

D3DPRESENT_INTERVAL_ONE ... D3DPRESENT_INTERVAL_FOUR - Taustapuskuri vaihtuu edustapuskuriksi vasta kun näytöltä on saatu Vertical Sync -signaali tarpeeksi monta kertaa. Piirtäessä siis odotetaan että edellinen kuva on piirtynyt kokonaisuudessaan ruudulle ennen seuraavan piirtämisen aloittamista. [/E]
[/DEFTABLE]

Kun D3DPP:ssä on sopivat arvot kutsutaan CreateDevice()-funktiota joka luo laitteen. CreateDevice() ottaa laitteen tunnisteen (nyt oletuslaite), laitteen tyypin (HAL tarkoittaa 3D-kiihdytettyä laitetta), kahvan ikkunaan johon laite tulee piirtämään, verteksitiedon käsittelytavan, esitystiedot (aiemmin kuvailtu Present Parameters) sekä osoittimen jonka kautta laitetta tullaan käyttämään.

Laitteen vapauttaminen #

DirectX:ssä laitteiden vapauttaminen on ohjelmakoodin puolesta helpoin osuus. Ainoa asia joka tulee muistaa on vapauttamisjärjestys, joka on käänteinen luomisjärjestykseen verrattuna.

//-------------------------------------------- 
// Nimi  : VapautaD3D()  
// Kuvaus: Vapauttaa liittymän Direct3D-  
//         laitteeseen. 
//--------------------------------------------  
void VapautaD3D( void )    
{  

    //  
    // Tarkistetaan ennen vapauttamista onko osoitin  
    // laitteeseen edes olemassa. Jos on, tehdään vapautus.  
    //  
    if( g_pD3DLaite != NULL )
    { 
        g_pD3DLaite->Release();  
        g_pD3DLaite = NULL;
    }   
      
    // 
    // Laitteen vapauttamisen jälkeen vapautetaan  
    // Direct3D-rajapinta. 
    // 
    if( g_pD3D != NULL )
    { 
        g_pD3D->Release();  
        g_pD3D = NULL;
    }   
} 

Piirtäminen ruudulle #

Direct3D:ssä kaikki piirtäminen tapahtuu Direct3D-laitteen avulla BeginScene(), EndScene() -funktioparin välissä (scene = näkymä). Jos BeginScene()-funktio antaa palautusarvona virheen, ei ruudulle voi piirtää. Alla oleva koodi siis tarkistaa ennen piirtoa onnistuiko BeginScene():n kutsuminen ja aloittaa piirron vain jos se on mahdollista.

//-------------------------------------------- 
// Nimi  : Piirto()  
// Kuvaus: Kaikki ohjelman piirtäminen  
//         tehdään tässä funktiossa.   
//--------------------------------------------  
HRESULT Piirto( void )    
{ 
    //  
    // Jos Direct3D-laitetta ei ole luotu, ei 
    // voida mitään piirtääkään. 
    //   
    if( g_pD3DLaite == NULL )   
        return S_FAIL; 

    HRESULT hr = S_OK; // palautusarvot 

 

    //    
    // Tyhjennetään ikkuna mustaksi.  
    //    
    g_pD3DLaite->Clear( 0,    
                        NULL,    
                        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,    
                        D3DCOLOR_XRGB( 0, 0, 0 ),    
                        1.0f,    
                        0 );    
         

    //  
    // Aloitetaan piirto BeginScene():llä. Tarkistetaan 
    // ennen varsinaista piirtoa virheet hr-muuttujalla.  
    //   
    hr = g_pD3DLaite->BeginScene(); 

    if( hr == S_OK ) 
    {   
             
        //   
        // Lopetetaan piirto EndScene()-funktiolla. 
        //   
        g_pD3DLaite->EndScene();  

        //   
        // Esitetään piirretty kuva ruudulla.   
        //  
        g_pD3DLaite->Present( NULL, NULL, NULL, NULL );
   
     } 


      
    //
    // Palautetaan BeginScene()-funktion palautusarvo. 
    // Se kertoo suoraan onnistuiko piirtäminen vai ei. 
    // 
    return hr; 
} 


Kuten näkyy, piirtäminen ei kovin kummoisia järjestelyjä näin ensialkuun tarvitse. Käytännössä yllä oleva koodi vain tyhjentää ikkunan mustaksi Direct3D-laitteen Clear() -funktiolla, joka ottaa seuraavat parametrit

[DEFTABLE]
[D]Count[/D]
[E]Tyhjennettävien D3DRECT-tietueiden määrä. Jos pRects on NULL, tulee tämän parametrin olla 0.
pRects Osoitin D3DRECT-tietuetaulukkoon jotka määrittävät eri alueita ikkunan piirtoalueesta. Jos tämä parametri on NULL, tyhjennetään koko ikkunan alue.[/E]

[D]Flags[/D]
[E]Lippumuuttujat. Parametri määrittää mitkä Direct3D pinnat tulee tyhjentää. Tähän on 3 muuttujaa joita voi yhdistellä vapaasti:

D3DCLEAR_STENCIL - Stencilpuskuri. Tämä on lisäpuskuri, jonka voi ajatella paperiseksi kaavaimeksi väri- ja syvyyspuskurien päälle. Stencilpuskurin käyttö on edistynyt aihe, ja siihen tutustutaan tarkemmin myöhemmissä artikkeleissa. Käytännössä tällä kuitenkin määrätään alueita ruudusta joille ei voida piirtää.

D3DCLEAR_TARGET - Väripuskuri, eli pinta jolle piirretään (voi olla joko ikkunan piirtoalue tai tekstuuri). Color-parametri määrittää värin jolla pinta tyhjennetään.

D3DCLEAR_ZBUFFER - Syvyyspuskuri. Jos tämä on määriteltynä, syvyyspuskuri tyhjennetään arvoon joka määrätään Z-parametrissa (kuvailtu alempana). Syvyyspuskuria käytetään poistamaan piiloon jäävät pikselit piirrettävässä 3-ulotteisessa kuvassa.

Koska flags-parametri on yllä D3DCLEAR_TARGET, tyhjennetään vain väripuskuri.[/E]

[D]Color[/D]
[E]Määrittää värin millä väripuskuri tyhennetään.
Z Määrittää arvon jolla syvyyspuskuri tyhjennetään.[/E]

[D]Stencil[/D]
[E]Määrittää arvon jolla stencilpuskuri tyhjennetään.[/E]
[/DEFTABLE]

Kun käännät tähän asti saadun koodin, saat ruudulle osittain mustan ikkunan. Tulos tuntunee nähtyyn vaivaan nähden vähäiseltä. Kyseessä on kuitenkin ensimmäinen tuntuma koneesi sisältämään 3D-näytönohjaimeen, ja myöhemmin opit käyttämään sitä luomaan mitä ihmeellisimpiä maailmoja tietokoneesi ruudulle.

Ensimmäinen kolmio ruudulle #

Kolmio, ja kolmioista koostuvia muita monikulmioitaKolmio, ja kolmioista koostuvia muita monikulmioita

Tietokoneille luodut 3-ulotteiset maailmat koostuvat pääasiassa polygoneista, eli monikulmioista. Yleisin 3-ulotteisessa grafiikassa käytetty polygonimuoto on kolmio. Kolmiolla voidaan luoda kaikki muut monikulmiot, ja sillä on helppo koostaa monimutkaisiakin 3-ulotteisia malleja eri esineistä. Oheiset kuvat selventävät asiaa.

Kuten huomaat, käytännössä kaikki esineet voidaan koostaa kolmioista. Mitä enemmän kolmioita koostamisessa käytetään, sitä luonnollisemmalta ja hienommalta mallinnettava esine näyttää. Mutta mitä enemmän kolmioita piirrettävä esine sisältää, sitä hitaammin se piirtyy. Jokainen kolmio on nimittäin muutettava kolmiuloitteisesta koordinaatistostaan ruudulle, 2-ulotteiseen koordinaatistoon. Tämän hoitaa Direct3D:ssä erilaiset matriisit, joihin ei vielä tässä vaiheessa syvällisemmin puututa. Matriiseilla voi kuitenkin helposti pyöritellä, siirrellä sekä muuttaa kolmiulotteisten mallien kokoa ruudulla. Tarkempi kuvaus matriiseista tehdään niille omistetussa osiossaan hieman myöhemmin.

Verteksit, verteksipuskurit ja muut uudet tuttavat #

Beethovenin pään kolmiomalliBeethovenin pään kolmiomalli

Jokainen kolmio itsessään koostuu kolmesta verteksistä (engl. sanasta Vertex). Kyseinen termi tarkoittaa käytännössä koordinaattipistettä joka kuvaa pisteen paikan kolmiulotteisessa koordinaatistossa, mutta voi sisältää myös muutakin tietoa tämän tiedon lisäksi. 2-ulotteisella ruudulla verteksin vastine on pikseli (engl. sanasta Pixel), eli yksi kuvapiste. Direct3D:ssä verteksien eri ominaisuuksia kuvaamaan käytetään FVF-lippumuuttujia (Flexible Vertex Format, Joustava Verteksi Muoto), ja näitä tarvitaan kertomaan 3D-näytönohjaimelle miten sille lähetetty verteksitieto tulee tulkita.
Direct3D säilöö verteksitiedon VerteksiPuskureihin (VertexBuffer). Tämä siksi, että näin 3D-näytönohjain saa helpoiten aseteltua verteksitiedon parhaaseen mahdolliseen paikkaan tietokoneesi muistissa ja saa siten lisää nopeutta itse 3-ulotteisen kuvan piirtämiseen. Seuraava koodi esittelee yhden verteksi-tyypin luomisen, verteksipuskurin luomisen sekä kolmion piirtämisen ruudulle.

   
//-------------------------------------------- 
// Nimi  : LuoVerteksipuskuri() 
// Kuvaus: Luo verteksipuskurin, joka sisältää
//         tiedon piirrettävästä geometriasta. 
//-------------------------------------------- 
HRESULT LuoVerteksipuskuri( void void ) 
{

       HRESULT hr = S_OK;
    //
    // Luodaan kolmio kolmesta verteksistä.
    //
    VERTEKSIMME verteksit[] = { 
        { -1.0f,-1.0f, 0.0f, D3DCOLOR_XRGB( 0, 0, 255 ), }, //x,y,z, sininen
        {  1.0f,-1.0f, 0.0f, D3DCOLOR_XRGB( 0, 255, 0 ), }, //x,y,z, vihreä
        {  0.0f, 1.0f, 0.0f, D3DCOLOR_XRGB( 255, 0, 0 ), }, //x,y,z, punainen
    };
    //
    // Verteksipuskurin luominen. Verteksipuskurit
    // luodaan Direct3D-laitteen avulla.
    //
        g_pVerteksiPuskuri = NULL;
        hr = g_pD3DLaite->CreateVertexBuffer( 3*sizeof(VERTEKSIMME),
             0, FVF_VERTEKSIMME,
             D3DPOOL_DEFAULT,
             &g_pVerteksiPuskuri );
    //
    // Tarkistus virheiden varalta.
    //
       if( FAILED( hr ) ){

       //
       // Verteksipuskurin luominen epäonnistui. Palauta saatu virhearvo.
       //
              return hr;
       }
    //
    // Jotta verteksipuskuriin voi kirjoittaa mitään
    // tietoa vertekseistä, se täytyy ensin lukita. 
    // Tällöin 3D-laite tietää, ettei siihen saa koskea
    // ja että verteksipuskurille suoritetaan toimenpiteitä. 
    //
        VOID* pVerteksit = 0;
        hr = g_pVerteksiPuskuri->Lock( 0,
                   sizeof( verteksit ),
                   ( BYTE** )&pVerteksit,
                   0 );
    //
    // Tarkistus virheiden varalta. 
    //
       if( FAILED( hr ) ){

       //
       // Verteksipuskuria ei voitu lukita. Tuhotaan se ja palautetaan
       // saatu virhe.
       //
              VapautaVerteksiPuskuri();
              return hr;
       }
    //
    // Kopioidaan tiedot vertekseistä verteksipuskuriin.
    //
       memcpy( pVerteksit, verteksit, sizeof( verteksit ) );

    //
    // Lopuksi pitää vielä vapauttaa verteksipuskuri 
    // lukosta. Jos tätä ei tee, koneesi todennäköisesti
    // hyytyy tai kaatuu täysin tähän vaiheeseen. 
    //
       g_pVerteksiPuskuri->Unlock();
    //
    // Palautetaan S_OK onnistumisen merkiksi.
    //
       return S_OK; 
}


Seuraavat pari riviä vapauttavat luodun verteksipuskurin. Tämäkin tulee muistaa tehdä, jotta ohjelmasi ei kaatuisi sitä sammutettaessa.

//-------------------------------------------- 
// Nimi  : VapautaVerteksipuskuri() 
// Kuvaus: Vapauttaa liittymän verteksi-
//         puskuriin. 
//-------------------------------------------- 
void VapautaVerteksipuskuri( void void ) 
{

       HRESULT hr = S_OK;
    //
    // Tarkistetaan ensin onko verteksipuskuri luotu, ja
    // sen jälkeen vapautetaan se.
    //
    if( g_pVerteksiPuskuri != NULL ){
        g_pVerteksiPuskuri->Release();
        g_pVerteksiPuskuri = NULL;
    }
}


Nyt pääsemme itse kolmion piirtämiseen. Kuten edellisellä sivulla mainittiin, kaiken 3-ulotteisen piirron tulee tapahtua BeginScene() ja EndScene() -funktioparin välissä. Seuraava koodi esittelee koko piirtämisfunktion.

//-------------------------------------------- 
// Nimi  : Piirto() 
// Kuvaus: Kaikki ohjelman piirtäminen
//         tehdään tässä funktiossa. 
//-------------------------------------------- 
HRESULT Piirto( void ) 
{

       HRESULT hr = S_OK;
    //
    // Jos Direct3D-laitetta ei ole luotu, ei
    // voida mitään piirtääkään.
    //
      if( g_pD3DLaite == NULL ) 
        return FALSE;

    HRESULT hr = S_OK;

    //
    // Tyhjennetään ruutu mustaksi.
    //
    g_pD3DLaite->Clear( 0, 
                        NULL, 
                        D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 
                        D3DCOLOR_XRGB( 0, 0, 0 ), 
                        1.0f, 
                        0 ); 


    //
    // Aloitetaan piirto BeginScene():llä. Tarkistetaan 
    // ennen varsinaista piirtoa virheet hr-muuttujalla.
    //
    hr = g_pD3DLaite->BeginScene(); 
    if( hr == S_OK ){

        //
        // Tässä kohtaa ruudulle piirretään kolmioita.
        //

        //
        // Asetetaan matriisit.
        //
        AsetaMatriisit();

        //
        // Asetetaan verteksipuskurimme tietovirraksi (stream), josta
        // D3D-laite hankkii tiedot piirrettävien kolmioiden käyttämistä
        // koordinaattipisteistä.
        //
               g_pD3DLaite->SetStreamSource( 0,
                            g_pVerteksiPuskuri,
                            sizeof( VERTEKSIMME ) );
        //
        // Asetetaan FVF-lippumuuttujamme kertomaan D3D-laitteelle
        // kuinka verteksit tulee piirtää.
        //
        g_pD3DLaite->SetVertexShader( FVF_VERTEKSIMME );

        //
        // Piirretään yksi kolmio ruudulle.
        //
        g_pD3DLaite->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 1 ); 

        //
        // Lopetetaan piirto EndScene()-funktiolla. 
        //
        g_pD3DLaite->EndScene(); 

        //
        // Esitetään piirretty kuva ruudulla. 
        //
        g_pD3DLaite->Present( NULL, NULL, NULL, NULL ); 
    }

    //
    // Palautetaan BeginScene()-funktion palautusarvo. 
    // Se kertoo suoraan onnistuiko piirtäminen vai ei.
    //
    return hr;

}


SetStreamSource()-funktio asettaa laitteen käyttämän tietovirran valmiiksi. Ensimmäisenä parametrina on asetettava tietovirtalähde (0 ... MaxTietoVirtoja - 1). Toinen parametri on verteksipuskurin osoite ja kolmas kertoo yhden verteksimuuttujan koon. Tämä on tarpeen, koska verteksejä voi määrittää sisältämään erilaisia määriä tietoja. SetVertexShader()-funktio puolestaan asettaa laitteen tarvitseman tiedon siitä miten verteksit tulee käsitellä. Koska emme käytä ohjelmoitavia Vertex Shadereita (Verteksi Sävyttäjä - käytännössä näytönohjaimelle ohjelmoitava pieni ohjelma joka käsittelee verteksitiedon ennen piirtämistä), asetamme vain tiedon verteksimuuttujamme sisällöstä. Lopuksi kolmio piirretään DrawPrimitive()-funktiolla ruudulle. Ensimmäisenä parametrina on piirrettävän primitiivin tyyppi, joka määrää kuinka Direct3D-laite käsittelee asetetusta tietovirtalähteesta luettavaa tietoa. Asetamme nyt sen arvoksi D3DPT_TRIANGLELIST koska piirrämme yhden ainoan kolmion. Muita mahdollisia arvoja:

D3DPT_POINTLIST, eli lista pisteitä
D3DPT_LINELIST, eli lista viivoja (2 pistettä / viiva)
D3DPT_LINESTRIP, eli viivanauha ( ensimmäisen viivan määrää 2 pistettä, jonka jälkeen seuraava piste määrää sillä hetkellä piirrettävän viivan päätepisteen)
D3DPT_TRIANGLELIST, eli lista kolmioita ( 3 pistettä / kolmio )
D3DPT_TRIANGLESTRIP, eli kolmionauha ( 3 ensimmäistä pistettä määrittävät ensimmäisen kolmion, jonka jälkeiset pisteet määrittävät aina seuraavan kolmion)
D3DPT_TRIANGLEFAN, eli kolmioviuhka ( 3 ensimmäistä pistettä määrittävät ensimmäisen kolmion, jonka jälkeiset pisteet määrittävät aina seuraavan kolmion)

Yllä olevista primitiivityypeistä STRIP ja FAN -tyypit ovat nopeimpia, koska niitä käytettäessä yhden primitiivin määrittämiseen vaaditaan keskimäärin yksi verteksi. DrawPrimitive()-funktion toinen parametri asettaa verteksin josta tietovirtalähteitä aletaan lukemaan. Viimeinen parametri kertoo piirrettävien primitiivien lukumäärän. Tämän asettamisessa on syytä olla tarkkana, koska tilanteessa jossa primitiivien lukumääräksi on asetettu liian suuri luku Direct3D-laite lukee muistia verteksipuskurin ulkopuolelta. Tällöin ohjelma todennäköisesti kaatuu.

Ohjelma ei ole vielä valmis. Jos käännät ohjelman, ilmoittaa kääntäjä AsetaMatriisit()-funktion puuttuvan. Kyseinen funktio käsitellään seuraavalla sivulla, kun tutustumme matriiseihin joilla 3D-malleja muokkaillaan. Asia on syytä opiskella hyvin, jotta tulevaisuudessa säästyy isolta kasalta harmaita hiuksia.

Aivan aluksi täytyy sanoa, että matriisit eivät ole yksinkertainen ja helppo asia minkä voi käsitellä täydellisesti ohjelmointiartikkelissa. Oletankin että ainakin perusteet matriisien käytöstä on hallussa. Jos matriisit ovat aivan täysin tuntematon käsite, seuraavista linkeistä on hyötyä:

Mathematics of 3D-graphics
(englanniksi, http://pages.infinit.net/jstlouis/3dbhol... )

Matrix and quaternion FAQ
(englanniksi, http://skal.planet-d.net/demo/matrixfaq.... )

Lisäksi kannattaa kysellä vaikka matematiikan opettajalta, jos englanti on heikoissa kantimissa. Suomenkielisiä materiaaleja internetistä ei tunnu löytyvän

Direct3D käyttää homogeenisia 4x4 matriiseja. Kyseiset matriisit määräävät koordinaatiston. Esimerkiksi matriisissa 1

X-akseli = (M11, M12, M13)<br />
Y-akseli = (M21, M22, M23)<br />
Z-akseli = (M31, M32, M33)<br />
Siirros XYZ = (M41, M42, M43)X-akseli = (M11, M12, M13)
Y-akseli = (M21, M22, M23)
Z-akseli = (M31, M32, M33)
Siirros XYZ = (M41, M42, M43)


Matriisi on nyt esitetty Direct3D:n käyttämässä vasenkätisessä koordinaatistossa. Yllä olevassa 4x4 matriisissa X, Y ja Z-akselit määräävät pyörityksen, ja siirros paikan missä matriisi on. Kukin akseli on johonkin suuntaan osoittava yksikkövektori (yksikkövektori on vektori, jonka pituus on tasan 1). Kun halutaan siirrellä 3-ulotteisia malleja, asetetaan siirros-vektorille uusia arvoja. Jos malleja halutaan pyöritellä, muokataan akseleita. Direct3D sisältää D3DX-kirjastossa yleisimmille pyöritys- ja siirrosoperaatioille valmiita funktioita.

Useimmille herää varmaankin tässä vaiheessa kysymys: "Miksi käyttää 4x4 matriisia, kun pyöritysosan voisi esittää erikseen ja siirroksen pelkällä vektorilla? Itseasiassa 3x3 matriisi ja siirrosvektorikin riittäisivät, eikö totta?". Aivan oikein. Homogeenisten 4x4 matriisien käyttämisen voi kuitenkin perustella matemaattisesti ja aluksi voikin olla helpompaa vain hyväksyä asia ja siirtyä tutkiskelemaan 4x4 matriisien toimintaa tarkemmin. Jos haluat tehdä juuri näin, on parempi että siirryt tässä vaiheessa kohtaan "Yksikkömatriisi". Muut voivat lukea eteenpäin ja todistua siitä että 4x4 matriisien käyttö on todellakin erittäin kätevää.

Kuvittele tilanne, jossa sinulla on 2 erilaista pyöritystä sekä kaksi siirrosta. Käyttämällä erillistä 3x3 matriisia pyörityksille ja vektoria siirrokselle saat siis eteesi kaksi erillistä 3x3 matriisia ja 2 paikkavektoria. Haluat kuitenkin yhdistää kyseiset siirrokset ja pyöritykset yhdeksi 3x3 matriisiksi sekä siirrokseksi. Yrityksestä tulee suhteellisen hankala.

Kun tilanteen siirtää homogeeniseen koordinaatistoon, siirros saadaan yhtenäiseksi pyörityksen kanssa. Toisin sanoen 4x4 matriiseja käyttämällä tapauksen saa yksinkertaistettua yhdeksi matriisituloksi, jolloin siirrosten vaikutus käsitellään automaattisesti. Tämän lisäksi 4x4 matriisien käyttö on nopeampaa kuin erillisten pyörityskulmien (tai erillisen 3x3 pyöritysmatriisin) ja siirrosvektoreiden käyttö. Alla oleva listaus selventänee asiaa (Matrix and Quaternion FAQ : Q4, vapaa suomennos):

Jos annetaan verteksi V = (x,y,z), ja X, Y ja Z-akseleiden pyörityskulmat (A,B ja C) sekä siirros (D,E,F), algoritmi määritellään seuraavasti:

//
// Sini- ja kosinifunktioiden esilaskenta (valmistelu).
// Tarvitaan tehdä vain kerran.
//
sx = sin(A) 
cx = cos(A)
sy = sin(B)
cy = cos(B)
sz = sin(C)
cz = cos(C)

//
// Jokaisen pyörityksen tekeminen verteksille.
//
x1 = x * cz + y * sz
y1 = y * cz - x * sz
z1 = z

x2 = x1 * cy + z1 * sy
y2 = z1
z2 = z1 * cy - x1 * sy

x3 = x2
y3 = y2 * cx + z1 * sx
z3 = z2 * cx - x1 * sx

//
// Verteksin siirros.
//
xr = x3 + D
yr = y3 + E
zr = z3 + F


Yhteensä algoritmi käyttää alla olevan taulukon mukaisesti laskenta-aikaa:

Valmistelu:
-------------------------
6 trigonometrista funktiota
6 asettamista (Muuttuja = luku;)
-------------------------
Verteksiä kohden:
------------------------
12 asettamista
12 kertolaskua
9  summausta
------------------------


4x4 Matriisikertolaskua käyttämällä samat operaatiot vievät alla olevan taulukon mukaisesti laskenta-aikaa:

Valmistelu:                    Muutos
--------------------------     ------
6  trigonometrisiä funktioita  0
18 asettamista                 -12
12 kertolaskua                 +12
6  vähennystä                  +6
--------------------------     -------


Verteksiä kohden         Muutos
------------------------ ------
3  asettamista           -9
9  kertolaskua           -3
6  summausta             -3
------------------------ ------


Taulukoita vertailemalla huomaa että 4x4 matriisin sisältämän pyöritysmatriisin valmisteluun menee vähintään 12 kertolaskua lisää ja 18 ylimääräistä asettamista. Vaikka tämä näyttää huimasti suuremmalta määrältä kuin erillisiä siirros ja pyöritysosia käyttämällä, on 4x4 matriisien käyttö silti nopeampaa. Koska ylimääräiset laskut tehdään valmistelussa, ne tarvitaan tehdä vain kerran. Verteksiä kohden tehdään yhteensä 15 operaatiota vähemmän, jolloin valmistelussa käytettyjen ylimääräisten laskujen aiheuttama 'lisä' saadaan korvattua jo neljän verteksin prosessoinnissa.

Nyt kun olemme vakuuttaneet itsemme 4x4 matriisien erinomaisuudesta, on aika siirtyä katsomaan miltä tyypillisimmät 3D-grafiikassa käytetyt 4x4 matriisit näyttävät.

Yksikkömatriisi on matriisi, jossa ei määritellä lainkaan pyörityksiä tai siirrosta (siirrosvektori on siis nollavektori, jossa x, y ja z ovat kaikki nollia).

X-akseli = ( 1, 0, 0 )<br />
Y-akseli = ( 0, 1, 0 )<br />
Z-akseli = ( 0, 0, 1 )<br />
Siirros XYZ = ( 0, 0, 0)X-akseli = ( 1, 0, 0 )
Y-akseli = ( 0, 1, 0 )
Z-akseli = ( 0, 0, 1 )
Siirros XYZ = ( 0, 0, 0)

Mittakaavamatriisi määrää nimensä mukaan x, y ja z -akseleille mittakaavan. Käytännössä matriisi siis pidentää tai lyhentää akseleiden pituutta.

X-akseli = ( Sx, 0, 0 )<br />
Y-akseli = ( 0, Sy, 0 )<br />
Z-akseli = ( 0, 0, Sz )<br />
Siirros XYZ = ( 0, 0, 0)X-akseli = ( Sx, 0, 0 )
Y-akseli = ( 0, Sy, 0 )
Z-akseli = ( 0, 0, Sz )
Siirros XYZ = ( 0, 0, 0)

Siirrosmatriisi on hyvin samankaltainen kuin yksikkömatriisi. Ainoana erona on siirros-osan nollasta eriävät x:n, y:n ja z:n arvot.

X-akseli = ( 1, 0, 0 )<br />
Y-akseli = ( 0, 1, 0 )<br />
Z-akseli = ( 0, 0, 1 )<br />
Siirros XYZ = ( Tx, Ty, Tz)X-akseli = ( 1, 0, 0 )
Y-akseli = ( 0, 1, 0 )
Z-akseli = ( 0, 0, 1 )
Siirros XYZ = ( Tx, Ty, Tz)


Pyöritysmatriisit muokkaavat X, Y ja Z -akseleiden vektorien arvoja. Käytännössä pyöritykset toimivat siten, että kahta akselia pyöritetään trigonometrisilla funktiolla (sin, cos, jne) yhden valitun akselin ympäri. Haluttu akseli siis asetetaan yksikkövektoriksi, ja kahta muuta akselia muokataan. Kuvailen tässä 3 pyöritysmatriisia, joilla kaikki muut pyöritykset voidaan saada aikaan.

Pyöritys X-akselin ympäri:

Nyt asetamme X-akselia kuvaavan vektorin yksikkövektoriksi ja pyöritämme Y ja Z -akseleja kuvaavia vektoreita YZ-tasossa halutun määrän.

X-akseli = ( 1, 0, 0 )<br />
Y-akseli = ( 0, cos(kulma), sin(kulma) )<br />
Z-akseli = ( 0, -sin(kulma), cos(kulma) )<br />
Siirros XYZ = ( 0, 0, 0)X-akseli = ( 1, 0, 0 )
Y-akseli = ( 0, cos(kulma), sin(kulma) )
Z-akseli = ( 0, -sin(kulma), cos(kulma) )
Siirros XYZ = ( 0, 0, 0)


Pyöritys Y-akselin ympäri:

Pyöritys Y ja Z -akseleiden ympäri toimii samoin kuten X-akselinkin kohdalla. Ainoana erona on yksikkövektorin sekä sini/kosini-funktioiden paikkojen vaihtuminen akseleiden mukaan.

X-akseli = ( cos(kulma), 0, -sin(kulma) )<br />
Y-akseli = ( 0, 1, 0 )<br />
Z-akseli = ( sin(kulma), 0, cos(kulma) )<br />
Siirros XYZ = ( 0, 0, 0)X-akseli = ( cos(kulma), 0, -sin(kulma) )
Y-akseli = ( 0, 1, 0 )
Z-akseli = ( sin(kulma), 0, cos(kulma) )
Siirros XYZ = ( 0, 0, 0)


Pyöritys Z-akselin ympäri:

X-akseli = ( cos(kulma), sin(kulma), 0 )<br />
Y-akseli = ( -sin(kulma), cos(kulma), 0 )<br />
Z-akseli = ( 0, 0, 1 )<br />
Siirros XYZ = ( 0, 0, 0)X-akseli = ( cos(kulma), sin(kulma), 0 )
Y-akseli = ( -sin(kulma), cos(kulma), 0 )
Z-akseli = ( 0, 0, 1 )
Siirros XYZ = ( 0, 0, 0)

Matriisien kertolaskua käytetään 3-ulotteisessa grafiikassa jatkuvasti. Se tarkoittaa käytännössä jonkin koordinaatiston muuntamista toiseen koordinaatistoon. Näin esimerkiksi kaksi erilaista pyöritysmatriisia, mittakaavan kasvatus sekä siirrosmatriisi saadaan yhdistettyä yhdeksi matriisiksi. Koska matriisit ovat taulukko-muodossa, niiden kertomiseen on sovittu tietty järjestys. Tämän vuoksi matriisien tulo ei ole kommutatiivinen (eli samanlainen kun kerrotaan kaksi erilaista matriisia keskenään eri järjestyksessä). Esimerkki (A, B, H ja K ovat matriiseja):

    A * B = H
    B * A = K


Yllä olevan pitäisi olla päivän selvää jo aiemmin kerrottujen asioiden mukaan. Matriisi A siirrettynä matriisin B määräämään koordinaatistoon on tietysti aivan erilainen kuin matriisi B siirrettynä matriisin A määräämään koordinaatistoon. Käytännön esimerkki selventänee asiaa:

Ota kaksi esinettä ja laita ne molemmat pöydälle. Katsele niitä hetkinen. Tässä tilanteessa sinä olet maailman keskipiste, eli origo, ja molemmat pöydällä olevista esineistä ovat siirrettyinä paikoilleen kertomalla niiden paikka niiden omilla matriiseillaan. Siirry nyt aivan vasemman puoleisen esineen taakse, ja katso siitä suoraan toiseen esineeseen. Siirryit juuri vasemman puoleisen esineen paikalliseen koordinaatistoon (kerroit paikkasi esineen matriisilla), ja näit miten toinen esineistä on suhteessa sen paikalliseen koordinaatistoon. Siirry nyt oikean puoleisen esineen taakse, ja katsele vasemman puoleista esinettä. Olet nyt oikean puoleisen esineen koordinaatistossa ja näet kuinka vasemman puoleinen esine sijoittuu sen koordinaatistoon.

Mutta miksi koordinaatistojen välillä edes halutaan siirtyä? Koordinaatistojen välillä siirtyileminen on yksi tärkeimmistä elämää helpottavista asioista 3D-grafiikassa. Esimerkiksi animoinnissa muuten hankalat esineiden väliset liikkumiset saadaan 4x4 matriisikertolaskuilla aikaiseksi todella helposti. Tämän lisäksi koordinaatistoissa siirtymistä käyttämällä esimerkiksi törmäystarkistukset saadaan hoitumaan helpommin, kun testattava vektori siirretään maailman koordinaatistosta objektin paikalliseen koordinaatistoon jolloin sen suhde itse objektiin on helppo ja nopea testata (objektin mittakaavan muutokset, pyöritykset ja siirrokset otetaan automaattisesti huomioon).

Direct3D:n matriisit #

Erilaiset matriisit määräävät kuinka verteksipuskureissa määritellyt kolmiot lopulta piirretään ruudulle. Direct3D käyttää kolmea 4x4 matriisia: World (maailma), View (näkymä) ja Projection (projektio), jotka ohjelmoija voi asettaa haluamikseen. Kun kolmioita piirretään ruudulle Direct3D kertoo matriisit keskenään, ja tuloksena tulevaa matriisia käytetään siirtämään verteksit omasta paikallisesta koordinaatistostaan näkymän (view) koordinaatistoon. Englanninkielisissä lähdemateriaaleissa eri koordinaatistoista käytetään nimikettä space (eli tila, esim view space = näkymän koordinaatisto).

World Matrix - Maailmamatriisi #

Maailma-matriisia käytetään pyörittelemään ja siirtelemään kolmioita suhteessa origoon. Se on ensimmäinen edellä mainituista matriiseista joka vaikuttaa piirrettävän kolmion paikkaan ruudulla.

Kuva 3. Maailma-matriisiKuva 3. Maailma-matriisi


Kuvassa vasemmalla oleva kolmio on omassa paikallisessa koordinaatistossaan. Se sijoittuu samaan paikkaan myös silloin, kun maailma-matriisi on yksikkömatriisina. Oikealla olevassa kuvassa kolmiota on ensin pyöräytetty, ja sen jälkeen siirretty.

Mahdollisuus siirtää kolmioita maailma-matriisilla on erittäin käytännöllistä, koska usein verteksipuskureissa määriteltyjä 3-ulotteisia malleja halutaan piirtää useaan eri paikkaan. Sen sijaan että ohjelmoijan täytyisi määritellä jokaiselle eri paikassa olevalle mallille verteksit uudelleen, hän voi yksinkertaisesti asettaa vertekseille uuden paikan maailma-matriisia muokkaamalla. Direct3D tekee verteksien siirtämisen sen jälkeen automaattisesti kun D3D-laitteen DrawPrimitive() -funktiota kutsutaan. Matriisit vaikuttavat siis aina seuraaviin DrawPrimitive()-käskyihin. Esimerkkikoodissa maailma-matriisi asetetaan yksikkömatriisiksi näin:

//--------------------------------------------
// Nimi  : AsetaMatriisit() 
// Kuvaus: Asettaa world (maailma), view
//         (kuvakulma) sekä projection
//         (projektio) matriisit.
//--------------------------------------------
void AsetaMatriisit( void ) 
{

    //
    // Asetetaan maailma-matriisi yksikkömatriisiksi.
    // Maailma-matriisi määrää piirrettävien kolmioiden
    // paikan.
    //
    D3DXMATRIX matWorld;
    D3DXMatrixIdentity( &matWorld );
    g_pD3DLaite->SetTransform( D3DTS_WORLD, &matWorld );



Koodissa esitellään ensin matriisi matWorld, joka kuvaa asetettavaa maailma-matriisia. Se asetetaan yksikkömatriisiksi D3DXMatrixIdentity()-funktiolla, ja sen jälkeen luomaamme Direct3D:n laite-osoittimen funktiota SetTransform() käytetään asettamaan laitteen nykyinen maailma-matriisi juuri luoduksi yksikkömatriisiksi. SetTransform()-funktiota käytetään myös näkymä-, sekä projektio-matriiseja asetettaessa. Asetettava matriisi määritellään funktion ensimmäisessä parametrissa, ja toisen parametrin tulee olla osoitin matriisiin.

View Matrix - Näkymämatriisi #

Näkymä-matriisi määrittelee Direct3D:lle paikan ja suunnan mistä piirrettäviä kolmioita tarkastellaan. Parhaiten asian ymmärtää ajattelemalla näkymä-matriisin kameramieheksi ja näkymän kameraksi. Näkymä-matriisi kuljettelee näkymän haluttuun paikkaan, aivan kuten kameramies juoksentelisi kameran kanssa. Maailma-matriisin tavoin myös näkymä-matriisi määritellään suhteessa origoon. Jatkossa kamera-käsitettä käytetään aina kun puhutaan näkymästä.

Kuva 4. Maailma-matriisilla siirretty sylinteri, sekä näkymämatriisin asettama kamera. Vihreä leikattu pyramidi on nk. Viewing Frustrum (alue, jonka sisällä olevat kolmiot näkyvät ruudulla). Sen määrittävät yhdessä näkymä-matriisi sekä projektio-matriisi.Kuva 4. Maailma-matriisilla siirretty sylinteri, sekä näkymämatriisin asettama kamera. Vihreä leikattu pyramidi on nk. Viewing Frustrum (alue, jonka sisällä olevat kolmiot näkyvät ruudulla). Sen määrittävät yhdessä näkymä-matriisi sekä projektio-matriisi.


Direct3D:n D3DX-kirjasto antaa käyttöön todella kätevän funktion, jolla kameran paikan voi helposti määrittää kolmella vektorilla:

    // 
    // Asetetaan näkymä-matriisi. Näkymä-matriisi määrää
    // paikan josta piirrettävää 3-ulotteista kuvaa
    // katsotaan. Käytetään D3DX-apukirjaston
    // LookAt-funktiota apuna sitä asetettaessa.
    //
    D3DXMATRIX matView;
    D3DXMatrixLookAtLH( &matView,
                        &D3DXVECTOR3( 0.0f, 0.0f, 5.0f ),
                        &D3DXVECTOR3( 0.0f, 0.0f, -5.0f ),
                        &D3DXVECTOR3( 0.0f, 1.0f, 0.0f ) );
    g_pD3DLaite->SetTransform( D3DTS_VIEW, &matView );



D3DXMatrixLookAtLH()-funktio asettaa esittelemämme näkymä-matriisin (matView) käyttäen kolmea vektoria. Sen ensimmäinen parametri määrittelee matriisin, johon tulos tallennetaan, toinen on kameran paikkavektori, kolmas vektori on paikka johon kameran halutaan osoittavan ja neljäs on ylös-vektori. Viimeinen parametri voi vaikuttaa hieman oudolta. Sitä käytetään määrittämään nimensä mukaisesti suunta, jossa kamera on pystyssä. Ylös-vektoria muokkaamalla kameran saa kallistumaan ja menemään jopa ylösalaisin. Ylös-vektori voi vaikuttaa ensialkuun turhalta, mutta sitä tarvitaan asettamaan luotavan matriisin pyöritykset. Seuraavaksi D3DXMatrixLookAtLH()-funktion tarkempi kuvaus ja matematiikka johon se perustuu. Jos ei halua tietää kuinka funktio toimii, voi tämän kohdan ohittaa.

Kuten on jo sanottu matriisien pyöritykset koostuvat käytännössä kolmesta vektorista, jotka kuvaavat kolmea eri akselia (x, y ja z). Z-akselin (muista, z-akseli menee Direct3D:ssä suoraan ruudun \'sisään\') saa vähentämällä paikasta jonne kamera katsoo kameran oman paikan ja normalisoimalla tuloksen. Nyt meillä on siis jo yksi akseli laskettuna, ja toinen (Y-akseli) on annettu ylös-vektorissa. Tarvitsemme vielä yhden akselin, jotta matriisin pyöritysosa on täydellinen. Sen saa ristiinkertomalla suuntavektorin ja ylösvektorin. Tuloksena on X-akselin vektori, eli matriisin pyöritysosa on nyt valmis. Matriisin siirros-osa saadaan suoraan kameran paikkavektorina.

Projection Matrix - Projektiomatriisi #

Projektio-matriisi määrittelee kuinka verteksit siirtyvät näkymän tilaan (view-space). Koska se siirtää verteksit yksikkökuutioon leviävät lähempänä olevat kolmiot suuremmiksi kuin kauempana olevat, jolloin piirrettyyn kuvaan saadaan perspektiivi.

Perspektiivin lisäksi projektio-matriisi asettaa myös near ja far-tasot (lähitaso, kaukotaso). Lähitaso sijaitsee kameran linssissä (käytännössä se on näyttösi kuvapinta) ja kaukotaso on taasen lähitasosta jonkin verran eteenpäin ruudun 'sisään'. Nämä tasot määrittävät kuinka läheltä ruutua kolmioita aletaan piirtämään, ja kuinka kauas niitä piirretään. Kolmiota ei piirretä, jos se on lähempänä kameraa kuin lähitaso tai kauempana kuin kaukotaso. Jos kolmio on osittain tasojen ulkopuolella, se leikkautuu (engl. clipping) ja siitä piirretään vain ne osat mitkä näkyvät.

Kuva 5. Sylinteri on osittain kaukotason ulkopuolella, joten se leikkautuu.Kuva 5. Sylinteri on osittain kaukotason ulkopuolella, joten se leikkautuu.


Esimerkkikoodissa projektiomatriisi asetetaan näin:

    //
    // Projektio-matriisi määrää kuinka verteksit asettuvat
    // 2-ulotteiselle ruudulle. Tämä matriisi aiheuttaa esimerkiksi
    // sen, että kauempana olevat objektit näyttävät pienemmiltä
    // kuin lähempänä olevat objektit.
    //
    // Nyt käytämme D3DX-kirjaston valmisfunktiota sen asettamiseen.
    // D3DX_PI / 4 asettaa näkymän leveyden 45 asteeseen.
    //
    D3DXMATRIX matProj;
    D3DXMatrixPerspectiveFovLH( &matProj,
                        D3DX_PI / 4,
                        1.0f,
                        1.0f,
                        100.0f );
    g_pD3DLaite->SetTransform( D3DTS_PROJECTION, &matProj );


} 

Miten tästä eteenpäin? #

Matriisien käytön oppii parhaiten (yllätys yllätys) käyttämällä niitä. Katso D3DX-kirjastosta erilaisia matriisien käsittelyfunktioita ja kokeile niitä AsetaMatriisit()-funktiossa. Tutki erilaisia matriisien pyörityksiä ja siirroksia ja katso miten ne vaikuttavat piirrettävään kolmioon. Tee kaksi erilaista matriisia, aseta molemmat vuorotellen maailma-matriisiksi ja piirrä aina maailma-matriisin asettamisen jälkeen kolmio. Sen jälkeen lisää koodi, jossa kerrot kaksi erilaista matriisia keskenään, pistät tuloksen maailma-matriisiksi ja piirrät kolmion. Vaihda kertomisjärjestystä, ja katso miten lopputulos eroaa edellisestä.
Tuloksena tämän koodin luoman ohjelman tulisi näyttää jotakuinkin tältä (yksityiskohdat kuten yläpalkin väri vaihtelevat tietokoneiden asetusten mukaan):

Kuva 6. Ohjelman piirtämä kolmio.Kuva 6. Ohjelman piirtämä kolmio.


Vaikka tässä vaiheessa varmaan ajattelet, että jos yhdenkin kolmion piirtämisessä oli tämän verran työtä, entäpä kokonaisen kolmiulotteisen mallin?! Siinä ei kuitenkaan ole niinkään suurta eroa tähän esimerkkiin. Lataat vain tiedon, pistät verteksipuskuriin ja piirrät kuten tässä (käytät vain enemmän kuin yhden kolmion). Tiedon lataaminen tosin on seikka erikseen, ja riippuu täysin tiedostosta josta 3d-mallin lataat. Loppujen lopuksi 3D-pelimoottorin teossa yllättävän vähän aikaa kuluu varsinaisten ohjelmointirajapinnan käskyjen käyttämisessä. Suurin osa ajasta kuluukin omien tietorakenteiden ja liittymien teossa.

Loppusanat #

Nyt kun perusasiat Direct3D:n käyttökuntoon saamisesta on käyty lävitse, on aika raottaa hieman ovea sille mitä on vielä tekemättä. Kokonainen 3-ulotteinen pelimoottori (josta useimmat varmasti jo haaveilevat) vaatii vielä koodin tekstuurien, 3D-mallien, sekä muiden olennaisten tietojen lataamiselle erilaisista tiedostoista. Kyseiset asiat käydään läpi myöhemmin, mutta sitä ennen tutustutaan muiden DirectX:n osien perusteisiin, kuten DirectSound (äänet) sekä DirectInput (peliohjaimet) -rajapintojen käyttämiseen. Todelliseen pelimoottoriin on siis vielä matkaa, mutta siihen päästään kyllä.

Kiitokset Risto Karjalaiselle sekä Jussi Huhtiniemelle avusta artikkelin teossa.