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ä?