Raycaster-pelimoottori 3: Seinien tekstuurit

Pelkät yksiväriset seinät ovat aika tylsiä, kuten jo tasohyppelypelin tapauksessa havaittiin; tuleehan iso osa pelin tunnelmasta siitä, miltä se näyttää. Yksi parannus on seinien piirtäminen bittikarttatekstuurien avulla, ikään kuin yksiväriselle pinnalle levitettäisiin tapetti. Tässä osassa lisätään siis teksturoitujen seinien piirtäminen.

Globals.h

Lisää globals.h-otsikkotiedostoon vakiot tekstuurin leveydelle (TEX_WIDTH) ja korkeudelle (TEX_HEIGHT), joiden arvoksi annetaan 64. Bittikarttatekstuurien leveys ja korkeus on siis 64 pikseliä.

Lisää myös osoitinmuuttujat kahdelle bittikartalle (*textureA ja *textureB), jotka viittaavat ladattaviin tekstuureihin.

#define TEX_WIDTH 64
#define TEX_HEIGHT 64
 
extern BITMAP *textureA;
extern BITMAP *textureB;

Main.c

Lisää main.c-lähdekooditiedostoon tekstuuriosoitinmuuttujien alustukset nollaksi (NULL), mikä tarkoittaa, ettei tekstuureja ole vielä ladattu.

BITMAP *textureA = NULL;
BITMAP *textureB = NULL;

Setup.c

Lisää setup.c-lähdekooditiedoston SetupProgram-funktion määrittelyyn arvot muuttujille textureA ja textureB, eli ladataan kaksi tekstuuria tiedostoista walla.bmp ja wallb.bmp kutsumalla funktiota load_bitmap.

int SetupProgram()
{
    if (allegro_init() != 0)
        return 1;

    install_keyboard();

    install_int_ex(TimerHandler, BPS_TO_TIMER(FRAMERATE));  
    LOCK_FUNCTION(TimerHandler);
    LOCK_VARIABLE(thCounter);

    if (set_gfx_mode(GFX_AUTODETECT, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0) != 0)
    {
        if (set_gfx_mode(GFX_SAFE, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0) != 0)
        {
            set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
            allegro_message("Unable to set any graphic mode\n%s\n", allegro_error);
            return 1;
        }
    }

    buffer = create_bitmap(SCREEN_WIDTH, SCREEN_HEIGHT);

    textureA = load_bitmap("walla.bmp", NULL);
    textureB = load_bitmap("wallb.bmp", NULL);

    return 0;
}

Miksi tekstuureja on kaksi, saatat miettiä. No, muistanet, että edellisessä osassa tiettyyn suuntaan "osoittavat" seinät olivat pimeämpiä kuin toiset. Sama tehdään tekstuureillakin, eli pohjois–eteläsuunnassa seinässä käytetään tekstuuria walla.bmp ja länsi–itäsuunnassa tekstuuria wallb.bmp, joka on pimeämpi versio walla.bmp:stä.

Walla.bmp.

Wallb.bmp

Kyseiset tekstuurit on muokattu Little Martianin Retro Texture Packin ilmaisen näytepaketin (https://little-martian.itch.io/retro-texture-pack) tekstuurista LAB_2B.PNG. Muokkauksiin kuului mm. alkuperäisen PNG-tiedoston 24-bittisen värisyvyyden muuttaminen 8-bittiseksi. Samassa yhteydessä määritettiin kuvan paletti, ja koska emme ole vielä perehtyneet oman 8-bittisen paletin määrittämiseen, paletiksi piti asettaa 8-bittisen VGA-kuvatilan oletuspaletti. Lopputulos on aika mälsä, jopa ruma, mutta pääsemmepähän varmistumaan siitä, että bittikartat piirretään pelissä oikein.






















8-bittisen VGA-kuvatilan oletuspaletti.

Ks. tämä postaus oppiaksi, mitä pitää ottaa huomioon, jotta tekstuuribittikartat piirretään pelissä oikein. 

Main.c

Lisää main.c-lähdekooditiedoston main-funktioon kutsut tekstuurien vapauttamiseksi muistista. Eli, kun painetaan ESCiä ja hypätään pelisilmukasta ulos, kutsutaan kullekin tekstuurille – textureA ja textureB funktiota destroy_bitmap ennen kuin suljetaan Allegro ja koko ohjelma.

int main(void)
{
    if (SetupProgram() != 0)
        return 1;

    SetupGame();

    while(!key[KEY_ESC])
    {
        while (thCounter > 0)
        {
            MovePlayer();
 
            RotatePlayer();

            DrawGame();

            thCounter--;
        }
    }

    destroy_bitmap(textureA);
    destroy_bitmap(textureB);

    destroy_bitmap(buffer);

    remove_timer();

    allegro_exit();

    return 0;
}
END_OF_MAIN()

Draw.h

Lisää draw.h-otsikkotiedostoon DrawWallsTextured-funktion esittely. Parametreja on aika hirvittävä määrä; koita pärjätä.

#ifndef DRAW_H
#define DRAW_H

void HandleWalls(double zBuffer[]);

void DrawWallsTextured(int x, int originalDrawStart, int originalDrawEnd, int side,
double perpWallDist, double rayDirX, double rayDirY, int lineHeight, int drawStart);

void DrawWallsAsFlatColors(int x, int mapX, int mapY, int side, int drawStart,
int drawEnd);

void DrawGame();

#endif

Draw.c

Lisää draw.c-lähdekooditiedostoon DrawWallsTextured-funktion määrittely.

Pikseli lienee jo sinulle tuttu termi; nehän ovat niitä pieniä erivärisiä neliöitä, joiden avulla kaikki graafinen sisältö tehdään näkyväksi tietokoneesi näytöllä. Mutta teksturoituja seiniä piirrettäessä vastaan tulee mahdollisesti uusi termi: tekseli eli tekstuuripikseli. Eli, kun 3D-mallin pinnalle projisoidaan bittikarttatekstuuri, ko. tekstuurin pikselit ovat nimeltään tekseleitä. Tekstuuriemme resoluutiohan on 64 × 64 pikseliä, ja kun ne piirretään seiniin, myös tekseleitä on (tietysti) saman verran.

Tässä funktiossa siis:

  • lasketaan tekstuurin koordinaatit ja skaalataan niiden avulla tekstuuri oikean kokoiseksi, minkä määrittää pelaajan etäisyys seinästä
  • haetaan ja piirretään oikea tekselisuikale stretch_blit-funktiokutsulla.
void DrawWallsTextured(int x, int originalDrawStart, int originalDrawEnd, int side,
double perpWallDist, double rayDirX, double rayDirY, int lineHeight, int drawStart)
{
    // Tekstuurikoordinaattien siirto, jos seinä menee ruudun ulkopuolelle
    double texCoordShift = 0.0;

    // Jos seinän alku menee näytön yläreunan yli, lasketaan siirtymä
    if (originalDrawStart < 0)
    {
        texCoordShift = (double)(-originalDrawStart) /
        (originalDrawEnd - originalDrawStart);
    }

    // Jos seinän loppu menee näytön alareunan yli, lasketaan siirtymä
    if (originalDrawEnd > VIEW3D_HEIGHT)
    {
        texCoordShift = (double)(originalDrawEnd - VIEW3D_HEIGHT) /
        (originalDrawEnd - originalDrawStart);
    }

    // Lasketaan alkuperäinen tekstuurikohta pystysuunnassa
    double texPos = texCoordShift * TEX_HEIGHT;

    // Lasketaan tarkka kohta, johon säde osui seinään
    double wallX;
    if (side == 0)
        wallX = player.posY + perpWallDist * rayDirY;
    else
        wallX = player.posX + perpWallDist * rayDirX;

    // Otetaan vain desimaaliosa (seinän sisäinen offset)
    wallX -= floor((wallX));

    // Lasketaan vastaava X-koordinaatti tekstuurista
    int texX = (int)(wallX * (double)TEX_WIDTH);

    // Varmistetaan, ettei mennä tekstuurin ulkopuolelle
    if (texX < 0)
        texX = 0;

    if (texX >= TEX_WIDTH)
        texX = TEX_WIDTH - 1;

    // Käännetään tekstuurikoordinaatti, jos katsotaan seinää "takaa"
    if ((side == 0 && rayDirX > 0) || (side == 1 && rayDirY < 0))
        texX = TEX_WIDTH - texX - 1;

    // Alku- ja loppukohta tekstuurista pystysuunnassa
    int texYStart = (int)(texPos);
    int texYEnd = TEX_HEIGHT - 1;

    // Vastaavat kohdat ruudulla
    int screenStartY = drawStart;
    int screenEndY = originalDrawEnd;

    // Jos piirtäminen alkaa ruudun ulkopuolelta, säädetään tekstuurialkua
    if (screenStartY < 0)
    {
        texYStart += ((-screenStartY) * (texYEnd - texYStart + 1)) /
        (screenEndY - screenStartY + 1);
        screenStartY = 0;
    }

    // Jos piirtäminen päättyy ruudun ulkopuolelle, säädetään tekstuurin loppua
    if (screenEndY >= VIEW3D_HEIGHT)
    {
        texYEnd -= ((screenEndY - VIEW3D_HEIGHT + 1) * (texYEnd - texYStart + 1)) /
        (screenEndY - screenStartY + 1);
        screenEndY = VIEW3D_HEIGHT - 1;
    }

    // Varmistetaan, ettei tekstuurikoordinaatit mene rajojen yli
    if (texYStart < 0)
        texYStart = 0;
    if (texYEnd >= TEX_HEIGHT)
        texYEnd = TEX_HEIGHT - 1;

    // Valitaan oikea tekstuuri seinän suunnan mukaan
    BITMAP* texture = (side == 0) ? textureA : textureB;

    // Jos piirtoväli on järkevä, piirretään seinän pystysuora siivu
    if (screenStartY <= screenEndY)
    {
        stretch_blit(texture, buffer, texX, texYStart, 1, texYEnd - texYStart + 1, x,
        screenStartY, 1, screenEndY - screenStartY + 1);
    }
}

Muuta lopuksi draw.c-lähdekooditiedoston Handlewalls-funktiota niin, että lopussa kutsutaan DrawWallsTextured-funktiota aiemman DrawWallsAsFlatColors-funktion sijaan.

void HandleWalls(double zBuffer[])
{
    int x;

    for (x = 0; x < SCREEN_W; x++)
    {
        double cameraX = 2 * x / (double)SCREEN_W - 1;
        double rayDirX = playerDirX + planeX * cameraX;
        double rayDirY = playerDirY + planeY * cameraX;

        int mapX = (int)(playerPosX);
        int mapY = (int)(playerPosY);

        double sideDistX, sideDistY;

        double deltaDistX = (rayDirX == 0) ? 1e30 : fabs(1 / rayDirX);
        double deltaDistY = (rayDirY == 0) ? 1e30 : fabs(1 / rayDirY);

        double perpWallDist;

        int stepX, stepY;

        int hit = 0; // Was there a wall hit?
        int side; // Was a NS or a EW wall hit?

        if (rayDirX < 0)
        {
            stepX = -1;
            sideDistX = (playerPosX - mapX) * deltaDistX;
        }
        else
        {
            stepX = 1;
            sideDistX = (mapX + 1.0 - playerPosX) * deltaDistX;
        }

        if (rayDirY < 0)
        {
            stepY = -1;
            sideDistY = (playerPosY - mapY) * deltaDistY;
        }
        else
        {
            stepY = 1;
            sideDistY = (mapY + 1.0 - playerPosY) * deltaDistY;
        }

        while (hit == 0)
        {
            if (sideDistX < sideDistY)
            {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            }
            else
            {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }

            if (map[mapX][mapY] == 1)
            {
                hit = 1;
            }
        }

        if (side == 0)
            perpWallDist = (sideDistX - deltaDistX);
        else
            perpWallDist = (sideDistY - deltaDistY);

        zBuffer[x] = perpWallDist;

        int lineHeight = (int)(VIEW3D_HEIGHT / perpWallDist);

        int drawStart = -lineHeight / 2 + VIEW3D_HEIGHT / 2;
        int drawEnd = lineHeight / 2 + VIEW3D_HEIGHT / 2;

        int originalDrawStart = drawStart;
        int originalDrawEnd = drawEnd;

        if (drawStart < 0)
            drawStart = 0;

        if (drawEnd >= VIEW3D_HEIGHT)
            drawEnd = VIEW3D_HEIGHT - 1;

        DrawWallsTextured(x, originalDrawStart, originalDrawEnd, side,
        perpWallDist, rayDirX, rayDirY, lineHeight, drawStart);
    }

Noin! Nyt kun käännät ja suoritat ohjelman, seinät ovat nätisti tapetoidut! Tai no ei nyt niin nätisti tapetoidut mutta... Tapetoidut!

Kommentit