Kirjoittaja: Metabolix (2007).
Legenda kertoo, että SDL:ää (Simple DirectMedia Layer) ja OpenGL:ää on mahdollista käyttää yhdessä, jolloin syntyy monella alustalla toimivia ohjelmia, jotka käyttävät tehokasta grafiikkakirjastoa. Usein on kysytty, kuinka tämä salaperäinen poppakonsti onnistuu. Nyt on aika paljastaa tuo salaisuus. Ensin on syytä tuntea hieman SDL:n perusteita, eikä OpenGL-tuntemuskaan ole pahasta. Esimerkeissä käytetään C-kieltä, mutta koska samat funktiot ovat kuitenkin pääosassa, jokainen osannee kääntää asiat omalle ohjelmointikielelleen. Muilta osin oppaan kieli on suomi.
OpenGL on vain grafiikkakirjasto, joskin erittäin hyvä sellainen. Sen sijaan SDL on poikkeuksellisen näppärä kirjasto ikkunan luomiseen ja käsittelyyn. Miksi näitä kahta ei voisi yhdistää? Ajatuksena yhdistelmässä on, että piirtäminen tapahtuu viime kädessä täysin OpenGL:llä. SDL puolestaan hoitaa ikkunan ja syötteet sekä mahdollisesti kaikkea muutakin, mutta ei grafiikkaa. Helposti tuntuu, että SDL ei tee juuri mitään, koska OpenGL:llä tehtävä piirtokoodi yleensä vie paljon enemmän tilaa kuin ne kymmenen pikkujuttua, jotka SDL hoitaa. Tosiasiassa SDL piilottaa suuren määrän vaikeasti hallittavaa koodia ja on erittäin tervetullut apu.
Tärkein asia on kertoa SDL:lle, että OpenGL pitää alustaa. Tämä onnistuukin vaivatta yhdellä lipulla näyttötilan alustuksessa.
SDL_SetVideoMode(LEVEYS, KORKEUS, 0, SDL_OPENGL);
SDL sisältää viisi funktiota OpenGL:n käyttöä varten. SDL_GL_SwapBuffers
on kaksoispuskuroidussa piirrossa välttämätön, sillä saa taideteoksensa ilmestymään ruudulle. SDL_GL_SetAttribute
tulee tarpeeseen perusasetuksia asetettaessa, ja SDL_GL_GetAttribute
auttaa tarkistamaan, tukiko järjestelmä noita asetuksia. SDL_GL_GetProcAddress
hakee pyydetyn GL-funktion, tätä siis tarvitaan esimerkiksi OpenGL:n laajennusten käytössä. SDL_GL_LoadLibrary
antaa toisinaan tarpeellisen mahdollisuuden ladata OpenGL vasta ajon aikana, jolloin voi sen puuttuessa turvautua vaikkapa SDL:n tavallisiin piirtofunktioihin.
OpenGL toimii toki ilmankin sen ihmeempiä määreitä, mutta tämä ei välttämättä ole suotavaa, koska tällöin lopputulos on täysin kiinni käyttäjän koneella olevista oletusasetuksista. Merkittäviä tekijöitä ovat värisyvyys ja syvyyspuskuri, mutkikkaammissa ohjelmissa myös sapluunapuskuri (stencil buffer) ja kertymäpuskuri (accumulation buffer). Itse puskurien käyttöä ei tässä oppaassa selosteta, koska se ei varsinaisesti kuulu tämän oppaan aihepiiriin vaan on OpenGL:n asia. Lista eri määreistä on SDL:n otsikkotiedostossa SDL_video.h.
Asetukset täytyy säätää ennen SDL_SetVideoModen kutsumista. SDL_GL_SetAttribute ottaa parametreinaan muutettavan attribuutin tunnuksen ja arvon. Tunnukset on määritelty numeroidussa tyypissä SDL_GLattr
, arvo on kokonaisluku. Palautusarvona on nolla, jos funktio onnistui, ja -1, jos ei. Tämä siis kertoo käytännössä, onnistuiko funktio sijoittamaan annetun arvon SDL:n sisäiseen muuttujaan. Arvon kelpoisuudesta se ei kerro mitään.
// Funktion prototyyppi on siis seuraava: // int SDL_GL_SetAttribute(SDL_GLattr attr, int value); // Väreille tietty bittimäärä SDL_GL_SetAttribute(SDL_GL_RED_SIZE, VARIN_BITIT); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, VARIN_BITIT); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, VARIN_BITIT); // Syvyyspuskurille jotain... SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, SYVYYS); // Kaksoispuskurointi käyttöön SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
Koska äsken esitellyn SDL_GL_SetAttribute-funktion palautusarvo kertoo vain, onnistuiko se sijoittamaan annetun arvon jonnekin, SDL_GL_GetAttribute voi olla hyvinkin hyödyllinen. Sillä voi tarkistaa, millaiset asetukset ovatkaan käytössä. SDL_SetVideoModen jälkeen se palauttaa asetukset sen mukaan, mihin järjestelmä on päätynyt. Parametriksi se ottaa attribuutin tunnuksen ja osoittimen muuttujaan, johon arvo palautetaan. Palautusarvo toimii samoin kuin edelliselläkin funktiolla: palautetaan nolla, jos funktio onnistui, ja -1, jos ei.
SDL_SetVideoMode(LEVEYS, KORKEUS, 0, SDL_OPENGL); int syvyys, kaksoispuskurointi; SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &syvyys); SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &kaksoispuskurointi); if (syvyys != haluttu_syvyys) { printf("Syvyys ei ole oikea, haluttiin %d saatiinkin %d\n", haluttu_syvyys, syvyys); } if (!kaksoispuskurointi) { printf("Ei ole kaksoispuskuria! Qu'est-ce que c'est que cela? O_o\n"); }
Kun kaksoispuskurointi on käytössä, kuten tapaa yleensä olla, piirretyt asiat eivät siirry ruudulle omin voimin. SDL:n kanssa käytettiin funktiota SDL_Flip, mutta kun OpenGL on käytössä, korvikkeena onkin SDL_GL_SwapBuffers. Toiminta on aivan sama, mutta mitään parametria ei tarvita.
SDL_GL_SwapBuffers();
Esimerkkiohjelma ei ole järin ihmeellinen. Siinä ruudulle piirretään kolmio, jonka kulmien väriä voi muuttaa. Kulmat on numeroitu yhdestä kolmeen, kunkin kulman väristä voi vapaasti muuttaa jokaista kolmea komponenttia eli punaista (R), vihreää (G) ja sinistä (B). Muuttaminen tapahtuu painamalla samanaikaisesti kulman numeroa, värin kirjainta ja nuolta ylös tai alas. Esimerkkiohjelman voi ladata myös kokonaisena.
SDL:n ja OpenGL:n käyttöön tarvitsee luonnollisesti niiden omat otsikot. SDL on helpottanut tätäkin vielä vähän: siinä on mukana OpenGL-otsikko, joka hoitaa oikeat otsikot mukaan. Esimerkkiohjelma käyttää myös standardikirjastoa.
#include <stdlib.h> #include <stdio.h> // Näihin on ehkä lisättävä SDL/-etuliite, jos include-polkuja ei ole säädetty. #include <SDL.h> #include <SDL_opengl.h>
Jos ohjelmaan ei tule asetustiedostoa tai muuta vastaavaa, asetukset on syytä määritellä vakioiksi, jottei niiden kanssa satu vahinkoja.
// Asetukset const int VARIN_BITIT = 8; // 8 bittiä jokaiselle värin komponentille const int SYVYYS = 16; // 16-bittinen syvyyspuskuri (turha tällä kertaa) const int LEVEYS = 320; // Ikkunan mittoja voi huoletta muuttaa const int KORKEUS = 240; // Tällä kertaa kuitenkin pieni ikkuna riittää const int IKKUNALIPPU = 0; // tai SDL_FULLSCREEN // Tarkistusta varten, jotta saadaan selville, mitä asetuksista tulikaan. int syvyys, kaksoispuskurointi;
Funktioita ei kannata kaihtaa. Selvä funktiorakenne auttaa ohjelmaa huomattavasti. Tässä ovat esimerkkiohjelman funktioiden esittelyt sekä jokunen tarpeellinen makro.
// Funktiot int hoida_viestit(void); void laske_kulunut_aika(void); void alusta(void); void toimi(void); void piirra(void); void lopetusfunktio(void); // Jos a ei osu välille [min, max], palautetaan min tai max // ehto ? palautus_jos_tosi : palautus_jos_epatosi #define valille(a, min, max) (((a) < (min)) ? (min) : (((a) > (max)) ? (max) : (a)))
Ohjelmassa on vain muutama muuttuja. Kulunut aika täytyy tietenkin säilyttää olla tiedossa, samaten ohjelman virhetilanne. Toiminnallisuutta varten ohjelmassa on taulu tietueita, joihin on tallennettu kolmion kulmien värit.
// Ohjelman kierrokseen kulunut aika (millisekunteina) Uint32 kulunut_aika; // Virhemuuttuja, jotta lopetusfunktio hoitaa oikeat asiat int virhe = 0; // Kolmion kulmien värit struct rgb_t { GLfloat r, g, b; }; struct rgb_t kulman_varit[3] = { {1.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, };
Animaatioiden ja muun vastaavan on yleensä suotavaa toimia eri tietokoneilla yhtä nopeasti. Sitä varten otetaan aikaa ja tehdään muutoksia kuluneen ajan mukaan. Joskus voi olla käytännöllistä pitää laskuria esimerkiksi animaation alusta laskettuna, mutta tällä kertaa on tarpeen tietää vain, kauanko on kulunut edellisestä päivityksestä. SDL tarjoaa riittävät välineet ajanottoon.
void laske_kulunut_aika(void) { // SDL osaa näppärästi ottaa aikaa static Uint32 vanha_aika, uusi_aika; vanha_aika = uusi_aika; // Pitää olla eri aika kuin viimeksi while (vanha_aika == uusi_aika) { uusi_aika = SDL_GetTicks(); } // Kauanko on kulunut viime kutsusta? kulunut_aika = uusi_aika - vanha_aika; }
SDL:n merkittävimpiä tehtäviä on syötteiden luku ja muiden viestien hallinta. Sitä varten on hyvä tehdä erillinen funktio, joka ilmoittaa palautusarvollaan, onko kaikki kunnossa. Jos funktio palauttaa muuta kuin nollan, ohjelma lopetetaan.
int hoida_viestit(void) { // SDL hoitaa viestit - aivan kuten yleensäkin SDL_Event event; // Tutkitaan kaikki tapahtumat jonosta while (SDL_PollEvent(&event)) { // Rastin painamisesta aiheutuu yleensä tällainen if (event.type == SDL_QUIT) { return -1; } // Esc sulkee myös if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE) { return -1; } } // Nolla on onnistuminen. return 0; }
Ohjelman alustus on hyvä keskittää mahdollisimman selkeästi osa-alueittain. Tällä kertaa alustettavana ei ole kuin graafinen puoli, joten yksi funktio riittää oivallisesti. Ensin alustetaan SDL, sitten asetetaan OpenGL:lle tarvittavat attribuutit ja luodaan ikkuna. Seuraavaksi tarkistetaan, millainen ikkuna saatiin, ja tarvittaessa säädetään OpenGL:n sisäisiä asetuksia.
void alusta(void) { // Ensin pitää hoitaa SDL aluilleen if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { printf("Virhe: SDL_Init: %s\n", SDL_GetError()); exit(virhe = 1); } // Sitten asetetaan OpenGL:lle parametrit // Väreille tietty bittimäärä SDL_GL_SetAttribute(SDL_GL_RED_SIZE, VARIN_BITIT); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, VARIN_BITIT); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, VARIN_BITIT); // Syvyyspuskurille jotain... SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, SYVYYS); // Kaksoispuskurointi käyttöön SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); // Käynnistetään video. Pintaa ei tarvitse ottaa muistiin. if (SDL_SetVideoMode(LEVEYS, KORKEUS, 0, SDL_OPENGL | IKKUNALIPPU) == NULL) { printf("Virhe: SDL_SetVideoMode: %s\n", SDL_GetError()); exit(virhe = 2); } // Katsellaan, millaiset asetukset saatiin aikaan SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &syvyys); SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &kaksoispuskurointi); if (syvyys > SYVYYS) { printf("Syvyys on toivottua suurempi \\o/. Pyydettiin %d, saatiinkin %d...\n", SYVYYS, syvyys); } else if (syvyys < SYVYYS) { printf("Syvyys on toivottua pienempi :(. Haluttiin %d, saatiinkin %d...\n", SYVYYS, syvyys); } if (!kaksoispuskurointi) { printf("Ei ole kaksoispuskuria! Qu'est-ce que c'est que cela? O_o\n"); printf("Nyt voi sattua hassuja...\n"); } // Tässä voisi säätää OpenGL:n asetuksia kuntoon aivan normaalisti, // siis glEnablella ja muilla OpenGL:n säätöfunktioilla. // Asetetaan taustaväriksi tylsä harmaa glClearColor(0.5, 0.5, 0.5, 1.0); // Rullataan vielä viestit läpi ja nollataan aikalaskuri if (hoida_viestit() != 0) { exit(virhe = 0); } laske_kulunut_aika(); }
Ohjelman varsinaiset omat laskelmat voi hyvin keskittää tiettyyn paikkaan. Jos ohjelman pitää toimia yhtä nopeasti kaikilla, toiminnan voi nyt suhteuttaa kuluneeseen aikaan.
void toimi(void) { int i; GLfloat muutos; // SDL:ää voi käyttää syötteissä Uint8 *napit = SDL_GetKeyState(0); // Muutos on 1, jos painetaan vain plussaa, -1, jos vain miinusta, ja 0, jos kumpaakin. Tämä kerrotaan sitten ajalla ja jaetaan sopivalla luvulla, jotta muutosnopeudesta tulee hyvä. muutos = napit[SDLK_UP] - (GLfloat)napit[SDLK_DOWN]; muutos = muutos * kulunut_aika / 1000.0f; for (i = 0; i < 3; ++i) { // Jos painetaan oikeaa numeroa (tämä + 1) if (napit[SDLK_1 + i]) { // Kukin osaväri erikseen if (napit[SDLK_r]) kulman_varit[i].r += muutos; if (napit[SDLK_g]) kulman_varit[i].g += muutos; if (napit[SDLK_b]) kulman_varit[i].b += muutos; // Varmistetaan, ettei mene sallitun lukualueen yli kulman_varit[i].r = valille(kulman_varit[i].r, 0.0f, 1.0f); kulman_varit[i].g = valille(kulman_varit[i].g, 0.0f, 1.0f); kulman_varit[i].b = valille(kulman_varit[i].b, 0.0f, 1.0f); } } }
Piirtäminen on järkevää tehdä selkeästi yhdessä paikassa – tai ainakin aloittaa selkeästi yhdestä paikasta. Toki jos piirrettävää on paljon, on selkeämpi siirtyä piirtofunktiosta tilanteen mukaan pienempää kokonaisuutta käsittelevään funktioon.
void piirra(void) { // OpenGL:llä voi piirtää aivan normaaliin tapaan glClear(GL_COLOR_BUFFER_BIT); // Kolmio; kulmien värit annetaan näppärästi osoittimena glBegin(GL_TRIANGLES); glColor3fv((GLfloat*)&kulman_varit[0]); glVertex2f(-0.9f, -0.9f); glColor3fv((GLfloat*)&kulman_varit[1]); glVertex2f( 0.9f, -0.9f); glColor3fv((GLfloat*)&kulman_varit[2]); glVertex2f( 0.0f, 0.9f); glEnd(); // Piirrokset esille if (kaksoispuskurointi) { SDL_GL_SwapBuffers(); } }
Lopuksi täytyy luonnollisesti sulkea SDL ja mahdollisesti tehdä muitakin sulkutoimenpiteitä.
void lopetusfunktio(void) { // Nerokasta käyttöä tällekin rakenteelle... switch (virhe) { // case 0 - normaali lopetus. Kaikki sulkemistoimenpiteet on tehtävä. case 0: // case 2 - ohjelma loppui kesken ja virhekoodi on 2. case 2: SDL_Quit(); // case 1 - SDL:ää ei saatu alustettua. Alustamattomia osia ei tietenkään tarvitse sulkeakaan. case 1: break; // Muu virhekoodi - tämähän voi olla vaikka bugi, on unohtunut lisätä tänne listaan jokin virhekoodi. default: printf("Ja virhekoodikin pieleen... %d xD\n", virhe); } }
Pääfunktiosta tulee tällä tavalla lyhyt ja ytimekäs.
// SDL kaipaa toisinaan mainille parametreja, vaikkei niitä käytettäisi int main(int argc, char **argv) { // Lopetusfunktio suoritetaan automaattisesti lopuksi. atexit(lopetusfunktio); alusta(); while (hoida_viestit() == 0) { laske_kulunut_aika(); toimi(); piirra(); } return virhe = 0; }
Ohjelman linkittämiseen tarvitaan SDL ja OpenGL. SDL:n linkkeriparametrit ovat normaalisti GCC:llä -lSDL -lSDLmain
ja OpenGL:lle kelvannee Linuxissa -lGL
ja Windowsissa -lopengl32
. VC++:lla vastaavat kirjastotiedostot ovat SDL.lib, SDLmain.lib ja opengl32.lib. Esimerkkiohjelman voi tosiaan ladata myös kokonaisena. Toiminnassa tämä taideteos voisi näyttää vaikkapa tältä:
Oppaan seuraavassa osassa käytetään SDL:ää tekstuurien lataamiseen sekä ladataan OpenGL dynaamisesti.
Lauri Kenttä, 4.1.2007
eka 8D
toka :--DDD
ihkudaa opas
neljäs xo
Opas on hyvä ja seuraavaa osaa odotellaan.. :P
C-kielessä tyhjä parametrilista (esim. int hoida_viestit();
) tarkoittaa, että funktion parametreja ei ole määritelty (sille voi syöttää niin monta parametria kuin huvittaa). Oikeaoppinen tapa esitellä funktio, joka ei ota yhtään parametria, on käyttää avainsanaa void
, esimerkiksi int hoida_viestit(void);
.
Pilkunviilausta tämäkin, mutta oppaan alussa olisi ehkä voinut mainita mitä ohjelmointikieltä esimerkeissä käytetään :)
(EDIT: nämä näemmä korjattu ;-)
ehehehhee seitsemäs... Tällästä opasta oonki oottanu...
> Metabolix
Tuli se opas sittenkin (lainaus on tästä threadista):
Metabolix toiseen threadiin kirjoitti:
Tuosta SDL + OpenGL -yhdistelmästä voisin laittaa vaikka koodivinkin, ei siitä oppaaksi asti taida riittää, kun oikeastaan kyseessä on vain selvä rajaus, mitä SDL:stä käytetään, yksi oikea lippu näyttötilaa asetettaessa ja pari ylimääräistä säätöfunktiota alussa.
KingOfTheWorld kirjoitti:
Tuli se opas sittenkin
Niinpä taisi tullakin. Pääasia on kuitenkin vain tuon alusta-funktion mittainen ja olisi mahtunut hyvin pelkkään koodivinkkiinkin.
Mahtava opas! Mitä nuo SDL ja OpenGL on? En aivan ymmärtänyt sitä opasta lukiessani.
SDL luo hieanon ackunan, joka toimii vaikka millä alustoilla. SDL hoitelee myös syötteenluvun ja mahd. metelin ym. shittiä (kaiken, mikä ei liity chrafiickaan). OpenGL on hieano kraaffikakirjasto.
Ja suomeksi sama:
SDL luo ikkunan, joka toimii vaikka millä alustalla. Se lukee myös syötteitä ja tekee kaiken, mikä ei liity grafiikkaan. OpenGL luo grafiikan (2D / 3D)
OK! Kiitti Kingi! Oot ihan Ringi lol xD
*heh*
Mistä johtuu että kun dev-c++:alla aloitan uuden projektin ja kopioin esimerkki ohjelman ja laitan linkkeriin nuo asetukset ja laitan ne SDL/ jutut includeihin alussa ja sitten yritän kääntää ohjalman, niin siinnä vain vilahtaa se kääntämis ikkuna ja sitten se menee pois ekä ohjelma käynnisty eikä näy mitään virhe ilmoitusta?
Hmm. Suosittelen command promptiin kajoamista. gcc auttanee sinua. Muista nämä parametrit: "-lSDL -lSDLmain -lopengl32" sinne lophuun.
EDIT: Ubsis, muisti pätki. Kyllä se on "-lopengl32". :/
Kirjoita projektin asetuksissa olevaan linkkerin parametrilistaan oppaan lopussa mainitut parametrit.
Hyvä opas!
Voisiko ensi oppaassa sitten myös käsitellä PNG-kuvatiedostojen lataamiset? (Mikäli siihen sisältyy muutakin kuin SDL_LoadBMP-funktion IMG_Load-funktioon vaihtaminen)
Eipä siihen mitään kummempaa tarvita. SDL-pinnan siirtämiseksi OpenGL-tekstuuriksi on myös koodivinkki, siitä voi ottaa mallia (tai vaikka kopioida, ei se niin ihmeellinen ole).
PC-Master: Tutustu SDL_Imageen.
Ihan hyvä opas.
Odotan innolla jatko-osaa.
Täähän on tarpeellinen opas, toivottavasti tulee jatkoa...
hyvä opas, milloin jatkoa?
Mistähän nuo tarvittavat (OpenGL) tiedostot saa ladattua Visual C++:lle. Tuolta sivuilta kattelin niin kaikkea muuta tuntuu pystyvän lataamaan, mutta ei itse OpenGL-tiedostoja.
OpenGL tulee useimpien valtakääntäjien mukana, sitä ei luultavasti tarvitse erikseen ladata.
Itsellä heittää tuo esimerkkikoodi ainakin virheilmoitusta VC++ Express 2008:ssa.
error LNK2019: unresolved external symbol __imp__glEnd@0 referenced in function "void __cdecl piirra(void)" (?piirra@@YAXXZ)
Mistähän tuo voisi johtua? Noita erroreita tulee muutama, ja jokaisessa valitetaan tuosta unresolved external symbolista.
Googlettelin, ja tuo virheilmoitus kuulemma johtuu ettei ole linkattu opengl-tiedostoja. "You need to link against the OpenGL import library (something.lib). Add the name of this file to your linker settings."
En löydä mistään opengl.lib tiedostoja.
http://www.mrmoen.com/2008/03/30/opengl-with-visual-c-express-edition/:
OpenGL comes with implementations on most operating systems and many compilers, including Visual C++ 2008 Express Edition.
— —
Type "opengl32.lib glu32.lib" in Additional Dependencies and click OK.
Lisäsin vileä VC++:sta maininnan oppaaseen. Yleensä lippu -ljotain tarkoittaa VC++:ssa kirjastoa jotain.lib, joten -lopengl32 tarkoittaa kirjastoa opengl32.lib. Esimerkkiohjelma ei tarvitse GLUta.
Täällähän on paljon mielenkintoista juttua. Hyvä opas.
Itsellä meni kauan aikaa OpenGL:n toimintaan saamisessa. (VC++ 2008 Express Edition ja Vista)
Error: Access violation 0xC0000005
Ongelma ratkesi seuraavasti: project properties -> linker -> advanced -> data execution prevention vaihda tähän /NXCOMPAT:NO
Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.