Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: C++: Freelook tunneli

Sivun loppuun

peki [03.02.2005 17:28:43]

#

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);
}

Linkku [03.02.2005 21:38:26]

#

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?

peki [03.02.2005 22:12:04]

#

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

rutkis [04.02.2005 09:53:52]

#

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.

peki [04.02.2005 13:22:20]

#

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.

FooBat [04.02.2005 16:18:12]

#

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

peki [04.02.2005 18:19:59]

#

Kiitos FooBat. Pidän nuo jatkossa mielessä. :)

arcatan [09.02.2005 08:04:04]

#

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.

http://sdldoc.csn.ul.ie/sdlsetvideomode.php

peki [10.02.2005 14:53:11]

#

Ohoh. En olekaan tuota tullut huomanneeksi. Kiitoksia huomautuksesta.

BlueByte [11.02.2005 19:44:00]

#

Hyvä peki!

wabe [13.08.2008 22:15:37]

#

Miksi ei mikään näistä grafiikka jutskista toimi Dev-C++:lla?
Aina tulee joku errori...


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta