Kirjoittaja: peki
Kirjoitettu: 25.10.2004 – 25.10.2004
Tagit: grafiikka, koodi näytille, vinkki
Suht alkeellinen SDL bumpmapperi. Koodissa on vielä optimoimisen varaa, kuten eräät varmasti huomaavat.. :)
Bumpmappauksen perusidea käy kuitenkin kommentoinnista ilmi suht selkeästi.
Koodi käyttää optimointikikkana SSE:tä kriittisimmän neliöjuuren laskemiseen. (taulukkohypyytystä voisi käyttää, mutta pidän tätä tapaa jotenkin "siistimpänä", sillä efekti on fysiikaltaan sillon lähempänä valon aitoa käyttäytymistä.) Kommentoi yli se kohta ja korvaa osoittamallani rivillä, jos koneesi ei tue SSE:tä.
Koodi olettaa, että ohjelman kansiosta löytyy tiedosto Bumpmap2.bmp, jonka koko on 640x480 pikseliä. Kuten koodista käy ilmi, kuvasta otetaan talteen vain ja ainoastaan punasävyt.
Valmis exe (SSE, sekä tavallinen versio) ja kokeilukuva löytyvät täältä.
Edit:
Olen päivittänyt koodin tukemaan tekstuurimappausta!
Suosittelen kokeilemaan, vaikka hitaampi nyt onkin.
Tarvitset kokoa 640x480 olevan 24 bittisen bittikartan nimeltä tex.bmp samaan hakemistoon ohjelman kanssa.
#include "SDL.h" #include <math.h> enum { SCREENWIDTH = 640, SCREENHEIGHT = 480, SCREENBPP = 32, SCREENFLAGS = SDL_HWSURFACE | SDL_DOUBLEBUF }; #define max(a, b)((a) > (b)) ? (a) : (b) // valon sijainti float lz, lx, ly; // bumpmappi int bumpmap[SCREENWIDTH][SCREENHEIGHT]; int tex[SCREENWIDTH][SCREENHEIGHT][3]; float pz[SCREENWIDTH][SCREENHEIGHT]; float px[SCREENWIDTH][SCREENHEIGHT]; float py[SCREENWIDTH][SCREENHEIGHT]; // Palauttaa pikselin (x,y) arvon Uint32 _fastcall getpixel(SDL_Surface *surface, int x, int y) { int bpp = surface->format->BytesPerPixel; // p on osoitin pikseliin, jonka haluamme kopioida Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp; switch(bpp) { case 1: return *p; case 2: return *(Uint16 *)p; case 3: if(SDL_BYTEORDER == SDL_BIG_ENDIAN) return p[0] << 16 | p[1] << 8 | p[2]; else return p[0] | p[1] << 8 | p[2] << 16; case 4: return *(Uint32 *)p; default: return 0; // ei pitäisi tapahtua. } } void DrawScene(SDL_Surface* surface, SDL_Event* event) { // Lukitse pinta, jotta voimme käsitellä pikseleitä suoraan if ( SDL_MUSTLOCK(surface) ) { if ( SDL_LockSurface(surface) < 0 ) { fprintf(stderr, "Can't lock the screen: %s\n", SDL_GetError()); return; } } lz = 30; // pointteri pikseleihin Uint8 *p = (Uint8 *)surface->pixels; // muuttujat etukäteen(hieman nopeampi näin) float Lx, Ly, Lz, f, clr; for(int y=1;y<SCREENHEIGHT-1;y++) { for(int x=1;x<SCREENWIDTH-1;x++) { // normaalidata talteen // valovektori Lx = lx-x; Ly = ly-y; Lz = lz; // SSE:tä käyttävä neliöjuuri f = Lx*Lx + Ly*Ly + Lz*Lz; __asm { movss xmm0, f rsqrtss xmm0, xmm0 rcpss xmm0, xmm0 movss f, xmm0 } // Jos tietokoneesi ei tue SSE:tä, korvaa edelliset rivit tällä: //f = sqrtf(Lx*Lx + Ly*Ly + Lz*Lz); // normalisoidaan Lx /= f; Ly /= f; Lz /= f; // clr on normalisoidun valovektorin ja normaalivektorin pistekertoma, eli niiden välisen kulman kosini clr = Lx*px[x][y] + Ly*py[x][y] + Lz*pz[x][y]; // phong kartoitus, eli dot * diff + dot^2 * spec + ambient clr = max(clr*0.6+clr*clr*0.4, 0); // Blendaa yhteen phong ja taustakuva int clrr = (tex[x][y][0]*clr + clr*255) / 2; int clrg = (tex[x][y][1]*clr + clr*255) / 2; int clrb = (tex[x][y][2]*clr + clr*255) / 2; // Pikseli oikeaan kohtaan *(Uint32 *)(p + y * surface->pitch + x * 4) = (clrr << 16) | (clrg << 8) | clrb; } } // poistetaan lukitus if ( SDL_MUSTLOCK(surface) ) { SDL_UnlockSurface(surface); } //päivitä pinta SDL_Flip(surface); } int main(int argc, char* argv[]) { //alusta systeemit SDL_Init(SDL_INIT_VIDEO); //aseta exit atexit(SDL_Quit); //luo ikkuna SDL_Surface* pSurface = SDL_SetVideoMode ( SCREENWIDTH, SCREENHEIGHT, SCREENBPP, SCREENFLAGS ); lz = ly = lz = 0; // ladataan kuvio SDL_Surface *bmpBump = SDL_LoadBMP("Bumpmap2.bmp"); for (int x=0;x<SCREENWIDTH;x++) for (int y=0;y<SCREENHEIGHT;y++) { // punaväri bumpmap[x][y] = getpixel(bmpBump, x,y) >> 16; } // ei tavita enää SDL_FreeSurface(bmpBump); // Tekstuurin data talteen, jotta saadaan nopeampi linkki dataan SDL_Surface* texture = SDL_LoadBMP("tex.bmp"); for (int x=0;x<SCREENWIDTH;x++) for (int y=0;y<SCREENHEIGHT;y++) { tex[x][y][0] = getpixel(texture, x,y) >> 16; tex[x][y][1] = (getpixel(texture, x,y) >> 8) & 0xff; tex[x][y][2] = getpixel(texture, x,y) & 0xff; } SDL_FreeSurface(texture); // precalcitaan kuvan normaalit float tx,ty; for (int x=-SCREENWIDTH/2;x<SCREENWIDTH/2;x++) for (int y=-SCREENHEIGHT/2;y<SCREENHEIGHT/2;y++) { tx=x/(SCREENWIDTH/2); ty=y/(SCREENHEIGHT/2); // lasketaan normaali float nx = bumpmap[x+SCREENWIDTH/2+1][y+SCREENHEIGHT/2] - bumpmap[x+SCREENWIDTH/2-1][y+SCREENHEIGHT/2]; float ny = bumpmap[x+SCREENWIDTH/2][y+SCREENHEIGHT/2+1] - bumpmap[x+SCREENWIDTH/2][y+SCREENHEIGHT/2-1]; float nz = max(1 - sqrtf(tx*tx + ty*ty), 0); // täytyy flipata z.. // normalisoidaan float length = sqrtf(nx*nx + ny*ny + nz*nz); pz[x+SCREENWIDTH/2][y+SCREENHEIGHT/2] = nz / length; px[x+SCREENWIDTH/2][y+SCREENHEIGHT/2] = nx / length; py[x+SCREENWIDTH/2][y+SCREENHEIGHT/2] = ny / length; } // normi sdl-looppi SDL_Event event; for (;;) { if ( SDL_PollEvent ( &event ) ) { if ( event.type == SDL_QUIT ) break; // liikutetaan valoa lx = event.motion.x; ly = event.motion.y; } DrawScene(pSurface, &event); } // vapautellaan SDL_FreeSurface(pSurface); return(0); }
Ihan hyvältähän tuo näyttää. Ei se kaikkein nopein bump map ole, mutta näyttäisi olevan jotakuinkin fysiikan mallien mukainen. Yleensähän bumpmappaus on toteutettu taulukoimalla valo taulukkoon ja sitten mapin perusteella haettu sieltä valoarvo. Tämä tietenkin rajoittaa valon vain tietylle alueella, mutta nopeus on sen verran suurempi, että 10 vuotta vanhemmallakin koneella pärjää. Vertailun vuoksi voi tutkia miten bump mappaus taulukoimalla onnistuu (appletti ja koodi):
http://users.interfriends.net/maurid/
Tuo appletti muuten näytti todella kauniilta. Pitäisi varmaan itsekin yrittää keksiä, miten tuo environment mappaus tehdään..
Tuossa FooBatin mainitsemassa hommelissa on kuitenkin paljon pienempi resoluutio, huomatkaa se :)
vaadin linux binääriä
Hankalampi homma, kun en Linuxille mitään osaa tehdä. :P
Edit:
HUOM! Olen päivittänyt koodin tukemaan tekstuurimappausta!
lainaus:
vaadin linux binääriä
Eiköhän ole parempi, että linux binääriä kaipaava kääntää sellaisen itse niin kuin kaikki muutkin ohjelmat. Ei tarvitse kuin asentaa sdl kirjasto ja sanoa pari valittua sanaa gcc:lle tai g++:lle.
Yksinkertainen ohje Makefile:n kanssa:
http://andrew.textux.com/tutorials/tut1/
Tämä ohjelma käyttää matikkakirjastoa, joten joudut vielä lisäämään linkkaukseen -lm parametrin.
Älä määrittele niitä muuttujia missä sattuu paikoissa ni tulee ihan kunnollista C:tä eikä mitään C++-paskaa. :)
x86-linuxbinääri: http://koodaa.mine.nu/a/kakkabmma
Tarkoitukseni ei ole ohjelmoida C:tä, vaan C++:aa.
Kerro ensin, mitä käytännön hyötyä C++:n kannalta on määritellä muuttujat funktion alussa. Mielestäni se tekee vain koodista sekavaa ja rumaa.
Mitä tehdään enää vanhalla C:llä, kun voidaan käyttää uutta ja selkeämpää C++:aa?
Kun pystyt kertomaan vahvat perusteet sille, miksi pitäisi käyttää C:tä, siirryn siihen ilomielin. Perusteeksi ei tietenkään kelpaa niin usein kuullut perusteet: "Se nyt vaan on niin." tai "Guru tuolla ircissä sanoi niin." En ole koskaan suunnitellut koodaavani ohjelmia mikrosiruille, joten siihenkään en C:n syntaksia tarvitse. Olen tästä samasta asiasta kuullut jo niin monet valitukset ja aina saanut samoja epämääräisiä vastauksia. Olisiko jonkun aika valaista asiaa selkeästi?
Mielestäni olisi aika jo luopua C:n turhan jäykästä tavasta koodata ja siirtyä tehokkaampaan ja selkeämpään tapaan kirjoittaa ohjelmia.
Jos koodi menisi muuttujien määrittelykohtaa vaihtamalla C:stä, niin minusta yhteensopivuutta ei kannata ehdoin tahdoin tärvellä. Kyse on aika pienestä asiasta, joka ei vaikuta koodin nopeuteen eikä juuri selkeyteenkään. Tilanne olisi toinen, jos koodi olisi pullollaan C++:n ominaisuuksia, joita C:stä ei löydy. Käytännössä tällä asialla ei tietenkään ole mainittavaa merkitystä.
// pari elintärkeää makroa #define max(a, b)((a) > (b)) ? (a) : (b) #define min(a, b)((a) < (b)) ? (a) : (b)
Taisi nyt sitten tulla noutaja, jos noi oli elintärkeitä. Kokeiles mitä 5*max(3,4) tai max(4,3)*2 palauttaa ;). Kannattaa korjata muihinkin ohjelmiisi, tuo on ilmiselvästi kopioitu jostakin muusta koodinpätkästäsi, kun et min-makroa missään käytä.
C:n ja C++:n eroista voin sanoa sen, että C-kääntäjät tuntuvat tuottavat tehokkaanpaa koodia kuin C++:n kääntäjät. Hyvä C-kääntäjä oikeailla parametreilla tuottaa paljon optimoidumpaa koodia kuin keskiverto assembler-koodari ikinä saa aikaiseksi, C++ ei puolestaan pahimmillaan pärjää edes javalle ;). Omien kokemuksieni mukaan C++ -binäärit ovat usein myös huomattavasti isompia kuin vastaavat C-binäärit.
Tuo min jäi vahingossa aikaisemmista bumpmapperin versioista. Otanpa sen pois.
Jos koodia MS VC++:lla kääntää, kääntääkö se puhtaan C -koodin erillisellä kääntäjällä? Tarvitsisiko minun hankkia erillinen C-kääntäjä, jos haluan täysin puhdasta C:tä koodata?
Foobat: Ei ne nyt ihan elintärkeitä ole, mutta pistinpä nyt vähän "rennomman" kommentin vaihteeksi.. ;)
Edit: Makro-ongelmahan korjautuu, kun laittaa makron sulkeisiin aina, kun sitä käytetään. Ainakaan tässä koodissa se ei vaikuta mihinkään, koska makron antamalle tulokselle ei suoraan tehdä mitään. Täytyy nuo muutkin koodit tarkistaa..
peki: mutta on tietenkin suositeltavampaa laittaa ne sulut jo tuohon makroon :) VC++ kääntää C:n ihan C:nä jos tiedoston pääte on .c ja sitä ei erikseen määritellä kääntämään C++:aa.
lainaus:
peki: mutta on tietenkin suositeltavampaa laittaa ne sulut jo tuohon makroon :)
Jep. Makrojen kanssa kannattaa muutenkin olla tarkkana. Itse aikoinaan etsin pitkään virhettä seuraavanlaisesta koodista.
#define X1 200 #define X2 300 #define LEVEYS 3*X1+2*X2 ... ... int n = LEVEYS / 2;
Muutaman päivän pään seinään hakkaus auttoi ainakin opettamaan jotain makroista :)
Miksi tuossa on kaksi exeä?