Mahdollisimman yksinkertaisesti ja selkeästi toteutettu Freeloook tunneli. Käyttää rajapintanaan SDL:ää.
Olen algoritmin toiminnan selittänyt koodissa olevissa kommenteissa. Käyttää matriiseja pyöritykseen.
Oletan, että tiedät mitä raytracing on ja sen perusperiaatteen.
Tekstuurimappaus suoritetaan UV kartoittamalla säteen ja lieriön rajapinnan törmäyspiste sylinterimäiseen avaruuteen.
Ohjelman kanssa samassa kansiossa on oltava texture.bmp niminen 256x256 bittikartta.
Valmiin ohjelman voit ladata täältä: http://mbnet.fi/peku1/FreeTunnel.zip
Ottaisin mielelläni vastaan mahdollisia optimointivinkkejä.
Tunnen kuitenkin jo ruudukkoon raytracetuksen ja siitä interpoloimisen koko ruudun kokoiseksi.
#include <stdlib.h> #include <math.h> #include "SDL.h" enum { SCREENWIDTH = 480, SCREENHEIGHT = 320, SCREENBPP = 32, SCREENFLAGS = SDL_ANYFORMAT }; class Vector { public: float x,y,z; void Normalize() { float len = sqrt(x*x + y*y + z*z); x /= len; y /= len; z /= len; } Vector operator+ (Vector param) { Vector temp; temp.x = x + param.x; temp.y = y + param.y; temp.z = z + param.z; return temp; } Vector operator- (Vector param) { Vector temp; temp.x = x - param.x; temp.y = y - param.y; temp.z = z - param.z; return temp; } Vector operator* (float param) { Vector temp; temp.x = x * param; temp.y = y * param; temp.z = z * param; return temp; } }; Vector Origin; float time = 0.0f; int texture[256][256]; // Tämä kertoo vektorin matriisilla Vector mulMatrixVector(float a[4][4], Vector b) { Vector temp; temp.x = a[0][0]*b.x + a[0][1]*b.y + a[0][2]*b.z + a[0][3]*1; temp.y = a[1][0]*b.x + a[1][1]*b.y + a[1][2]*b.z + a[1][3]*1; temp.z = a[2][0]*b.x + a[2][1]*b.y + a[2][2]*b.z + a[2][3]*1; return temp; } // Tämä pyörittää vektoria X akselin ympäri Vector rotate3DX(Vector start, float theta) { float temp[4][4] = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}; // Luo matriisi temp[0][0] = 1; temp[1][1] = cos(theta); temp[2][2] = cos(theta); temp[3][3] = 1; temp[1][2] = -sin(theta); temp[2][1] = sin(theta); // Laske tulos start = mulMatrixVector(temp, start); return start; } // Tämä pyörittää vektoria Y akselin ympäri Vector rotate3DY(Vector start, float theta) { float temp[4][4] = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}; // Luo matriisi temp[0][0] = cos(theta); temp[1][1] = 1; temp[2][2] = cos(theta); temp[3][3] = 1; temp[2][0] = -sin(theta); temp[0][2] = sin(theta); // Laske tulos start = mulMatrixVector(temp, start); return start; } void DrawScene(SDL_Surface* surface) { if ( SDL_MUSTLOCK(surface) ) { if ( SDL_LockSurface(surface) < 0 ) { fprintf(stderr, "Can't lock the screen: %s\n", SDL_GetError()); return; } } // säteen suunta Vector Direction; // lieriön säde float r = 100; // pointteri pikseleihin Uint8 *p = (Uint8 *)surface->pixels; // tästä lasketaan sijainti time += 0.05f; // liikutaan eteenpäin (siirretään origoa) Origin.z += 10; for (int x=0;x<SCREENWIDTH;x++) for (int y=0;y<SCREENHEIGHT;y++) { // tee suuntavektori kohti jokaista pikseliä Direction.x = (float)(x-160) / 0.6; // tan (~30) = ~0.6~0.8 = FOV Direction.y = (float)(y-120) / 0.6; Direction.z = 1024; // normalisoi se Direction.Normalize(); // pyöritä sitä, jotta katse pyörisi Direction = rotate3DX(Direction, time); // ALGORITMI: // // (x-a)^2 + (y-b)^2 = r^2 // lieriö sijaitsee origossa => x^2 + y^2 = r^2 // sijoita säteen yhtälö. // => (Origin.x + t*Direction.x)^2 + (Origin.y + t*Direction.y)^2 = r^2 // // => t^2*(Direction.x^2 + Direction.y^2) + t*2*(Origin.x*Direction.x + // Origin.y*Direction.y) + Origin.x^2 + Origin.y^2 - r^2 = 0; // // a = Direction.x^2 + Direction.y^2 // b = 2*(Origin.x*Direction.x + Origin.y*Direction.y) // c = Origin.x^2 + Origin.y^2 - r^2 // a*t^2 + b*t + c = 0 // Ratkaistaan toisen asteen yhtälö: // delta = b^2 - 4*a*c // Jos delta < 0, ei reaaliratkaisuja // Muussa tapauksessa piirrä UV mapattu pikseli float a = Direction.x*Direction.x + Direction.y*Direction.y; float b = 2*(Origin.x*Direction.x + Origin.y*Direction.y); float c = Origin.x*Origin.x + Origin.y*Origin.y - r*r; // laske delta float delta = b*b - 4*a*c; float t; if (delta < 0) { // ei reaaliratkaisuja => musta pikseli *(Uint32 *)(p + y * surface->pitch + (x << 2)) = 0; } else { // laske ratkaisut float sqr = sqrt(delta); float t1 = (-b + sqr)/(2*a); float t2 = (-b - sqr)/(2*a); // lähin talteen float t = (t1 < t2) ? t1 : t2; // Törmäyspiste - Tästä voi halutessaan muodostaa vektorin kohti valoa // ja pistekertoa sen pinnan normaalin kanssa. // Kun kerrotaan tekstuuri(u,v) kohdan väri tällä arvolla ja sijoitetaan // se ruudulle, saadaan valaistus. // Tässä esimerkissä ei kuitenkaan ole valaistusta. // Törmäyspiste Vector Intersection = Origin + (Direction*t); // Tee sylinterikartoitus tekstuurille tärmäyspisteen suhteen. int u = fabs(Intersection.z)*0.2; int v = fabs(atan2(Intersection.y, Intersection.x)*256/3.14159f); // Ja viimein pikseli ruudulle *(Uint32 *)(p + y * surface->pitch + (x << 2)) = texture[u%256][v]; } } // poistetaan lukitus if ( SDL_MUSTLOCK(surface) ) { SDL_UnlockSurface(surface); } //päivitä pinta SDL_UpdateRect(surface, 0, 0, 0, 0); } int main(int argc, char* argv[]) { //Alusta SDL SDL_Init(SDL_INIT_VIDEO); //Aseta exit funktio atexit(SDL_Quit); //luo ikkuna SDL_Surface* pSurface = SDL_SetVideoMode ( SCREENWIDTH, SCREENHEIGHT, SCREENBPP, SCREENFLAGS ); // aseta origo Origin.x = 0; Origin.y = 0; Origin.z = -256; // lataa tekstuuri SDL_Surface *tex = SDL_LoadBMP("texture.bmp"); int bpp = tex->format->BytesPerPixel; for(int x = 0; x < 256; x++) for(int y = 0; y < 256; y++) { Uint8 *p = (Uint8 *)tex->pixels + y * tex->pitch + x * bpp; texture[x][y] = *(Uint32 *)p; } SDL_FreeSurface(tex); SDL_Event event; for (;;) { if ( SDL_PollEvent ( &event ) ) { if ( event.type == SDL_QUIT ) break; } DrawScene(pSurface); } SDL_Quit(); return(0); }
Onko näitä mitenkään mahdollista kääntää niin että ne toimisivat ilman msvcrt.dll-tiedostoa, koska se aiheutti pagefaultin Linuxissa enkä päässyt ihailemaan mahdollisesti hienoa efektiä. Käsittääkseni ko. tiedostoa tarvitsee vain debug-versiota ajettaessa?
Kyseinen käännös ei ole debug versio, vaan on ihan release.
Tosin päällä on muutama erikoisoptimointi kääntäjän puolelta.
En kyllä pikaisesti mitään keinoa keksi tuon kääntämiseen toimivaksi ilman sitä.
Jospa joku hieman linuxläheisempi ohjelmoija voisi tästä vaikkapa binäärin Linuxille käännellä?
Käännä ilman default-libbien linkittämistä mukaan. Toki tämän jälkeen joudut käsin lisäämään pari tarvittavaa mukaan.
Tuo taitaa olla jokin ms visual c runtime.dll tjsp joka tulee oletuksena mukaan.
Jos en linkitä defaultlibbejä, kääntäjä herjaa suuresta määrästä linkkausvirheitä.
Kyseisiä virheitä en voi korjata, sillä en tiedä, mitä kirjastoja defaulibaryyn kuuluu.
lainaus:
Ottaisin mielelläni vastaan mahdollisia optimointivinkkejä.
Tutkiskelin tuota koodia vähän tarkemmin ja onhan tuossa aika paljon optimoimisen varaa.
// tee suuntavektori kohti jokaista pikseliä Direction.x = (float)(x-160) / 0.6; // tan (~30) = ~0.6~0.8 = FOV Direction.y = (float)(y-120) / 0.6; Direction.z = 1024; // normalisoi se Direction.Normalize();
Tämä esimerkiksi tehdään muuttumattomana jokaiselle kuvalle erikseen. Normalisointi sisältää nelijuuren ja useita jakolaskuja, jotka tekevät asian varsin hitaaksia (Koodi suoritetaan kerran jokaista pikseliä kohti, joten laskutoimituksia kertyy varsin paljon). Alustavat suuntavektorit voi siis taulukoida. Mutta jos et jostain syystä halua tehdä taulukointia, voit ainakin kokeilla muuttaa jakolaskut kertolaskuiksi tyyliin:
float len = 1/sqrt(x*x + y*y + z*z); x *= len; y *= len; z *= len;
Kertolasku on yleensä huomattavasti nopeampi kuin jakolasku.
// pyöritä sitä, jotta katse pyörisi Direction = rotate3DX(Direction, time);
Tämä aiheuttaa 2 tai 4 trigonometrisen funktion kutsun jokaista pikseliä kohtaan ja aivan turhaan. Tee pyöritysmatriisi kertaalleen pääsilmukoiden ulkopuolella, joilloin pistettä kohti ei enää tarvitse tehdä yhtään cos- tai sin-funktiota.
Lisäksi atan2:n integer-parametrien arvoja voi helposti taulukoida kaksiulotteiseen taulukkoon. Jos luvut ovat liian suuria taulukoitavaksi voi ne skaalata sopiviksi. Yleensä taulukointi on skaalauksen kanssakin paljon nopeampi kuin atan2-funktiokutsu.
Kannattaa myös tarkistaa, että kääntäjä on oikeasti niin hyvä kuin kuvittelet. Esim:
float t1 = (-b + sqrt(delta))/(2*a); float t2 = (-b - sqrt(delta))/(2*a); --> float s = sqrt(delta); float a2 = 2*a; float t1 = (-b+s)/a2; float t2 = (-b-s)/a2;
Erityisesti olisin varoivain kutsuttaessa funktioita samoilla parametreille. Kääntäjä ei ihan suoraviivaisesti pysty päättelemään, että kaksi samanlaista funktiokutsua palauttaa saman arvon. Samalla tavalla kirjoitetut laskulausekkeet kääntäjän pitäisi kyllä pystyä optimoimaan, mutta asia kannattaa kuitenkin tarkistaa, jos on epäilystä.
Jos suunnitellaan jotain nopeaa demo-efektiä voidaan lisäksi laskennoissa oikaista aika paljon, jos asetaan Origin.x ja Origin.y aina nollaksi. Tällöin noin puolet laskutoimituksista voidaan jättää tekemättä.
Kiitos FooBat. Pidän nuo jatkossa mielessä. :)
Kun tässä aloittelin pienimuotoista peliä ja otin koodista mallia pohjakoodiin, huomasin, että SDL:n manuaalin mukaan tuota pSurfacea ei kuuluisi vapauttaa, vaan SDL_Quit tekee sen.
Ohoh. En olekaan tuota tullut huomanneeksi. Kiitoksia huomautuksesta.
Hyvä peki!
Miksi ei mikään näistä grafiikka jutskista toimi Dev-C++:lla?
Aina tulee joku errori...
Aihe on jo aika vanha, joten et voi enää vastata siihen.