Już naprawdę dawno tu nie pisałem. Wiem, że się powtarzam - jednak, w momencie kiedy zobaczyłem, jaką datą opatrzona jest "najnowsza" notka, coś mnie wzięło i postanowiłem - tak, dzisiaj znowu coś napiszę. Choć pracy dosyć jest.Postaram się też opisać coś w miarę ciekawego i być może niebanalnego, ale nie nieosiągalnego (proszę w tym miejscu polonistów o konsultację w sprawie dobrej pisowni słowa przed nawiasem). Niestety - także troszkę zastałego, gdyż jest to rzecz, nad którą pracowałem bodajże dwa tygodnie temu i od tamtego czasu niewiele się zmieniło. Właściwie to bardzo mało. Słowem - nic.
Mianowicie, mam na myśli generowanie lochów w oparciu o plik z bitmapą, jaki może przygotować w Paintcie każdy, kto ma choć odrobinę weny twórczej i zbyt dużo wolnego czasu. Nie jest to oczywiście postać, w jakiej ten kod zawojowałby świat - umożliwia on jedynie wydrążenie korytarza i nałożeniu wokół niego elementów ściennych. Myślę jednak, że jest całkiem niezłą trampoliną do dalszego rozwijania tego zagadnienia. Czytelnicy poprzedniego wpisu skojarzą pewnie, że wówczas miałem jedynie przygotowany kod na wczytanie mapy z pliku tekstowego. Teraz jest [tadaaam] bardziej wizualnie. Co, nie ukrywam, mnie cieszy.
W odróżnieniu od wielu innych tutoriali, już na początku podam link [ooo, właśnie tutaj], pod którym można znaleźć przygotowany projekt w Visual C++ 2008 EE wraz ze skompilowaną wersją w trybie debug. Jest to też niejako kolejny test integralności elementów mojego frameworka i sprawdzenie, jak się sprawuje. Od razu mówię, że pod Vistą chodzi tragicznie, jak wszystko co jest związane z OpenGL-em, o czym pisałem parę notek temu - i chyba faktycznie zacznę myśleć o przeprowadzce i nauce DirectX-a. Ale na pewno nie teraz - studia dają się we znaki.
Po pobraniu pliku i rozpakowaniu go, przychodzi czas na obejrzenie zawartości. Zatem co my tu mamy:
- CBMP - klasa odpowiedzialna za obsługę plików w formacie BMP (czyli za wczytanie mapki z lochami).
- CCamera - klasa odpowiedzialna za obsługę kamery z widokiem z perspektywy pierwszej osoby.
- CTextureManager - klasa zarządzająca teksturami.
- CTGA - klasa obsługująca pliki w formacie TGA (czyli tekstury).
- CVector3 - struktura wektora z trzema współrzędnymi (dla kamery).
- main.cpp - bliżej niezidentyfikowane połączenie mrożonego bażanta z ruchliwą ściółką leśną wprost z gorących rejonów Antarktydy.
Zdziwić może niektórych fakt, że publikuję podstawowe kody źródłowe frameworka. Nie widzę sensu, dlaczego nie miałbym tego robić - nie jest to żaden supernowoczesny projekt, a w dodatku czerpię algorytmy z innych udostępnionych przykładów. A przy okazji każdy może się zapoznać i coś wynieść - ci bardziej początkujący same mechanizmy, a bardziej zaawansowani troszkę dobrego humoru.
Skupmy się może już na samej generacji mapy (wreszcie...). Zacznijmy od początku - funkcja inicjalizująca.
Tex.InitTex2D(0, "wall001.tga");
Imig.LoadBMP("lochy.bmp");
width = Imig.Width();
height = Imig.Height();
GenerateMap();
// kamera ustawiona tak, żeby być na początku wylotu korytarza Camera.SetCameraPosition(1.0f, 1.5f, 16.0f, 1.0f, 1.5f, 11.0f, 0, 1, 0);
Po wczytaniu tekstury ścian oraz lochów, a także zapisaniu rozmiaru bitmapy oraz (późniejszym) ustawieniu kamery, następuje wywołanie funkcji generacji mapy. Jeśli komuś nastręcza trudności korzystanie z dobrodziejstw klas udostępnionych na potrzeby przykładu - polecam zajrzeć do źródeł lub dokumentacji. Małego wyjaśnienia może jednak wymagać fakt podania takich a nie innych wartości metodzie SetCameraPosition - w tym przypadku po prostu wiedziałem, gdzie umieścić oko kamery, ale można to oczywiście obliczyć automatycznie - w zależności od potrzeb.
Przejdźmy zatem do sedna operacji, czyli funkcji GenerateMap. Na początek jednak, deklaracja paru ważnych zmiennych.
vector
(Przy okazji - proszę zignorować fakt istnienia wskaźnika mapdata - tablica ta używana była w pierwotnej wersji programu, przy tekstowej mapie. Przepraszam za to niedopatrzenie.)
Pewnie parę osób zapyta "a po jaką cholerę wektory?". Otóż - dla ułatwienia. W odpowienim przypadku można by było użyć po prostu dynamicznej tablicy typu float (albo klasy CArrayFloat, która również jest częścią frameworka), aby z kolei dalej używać mechanizmu tablic wierzchołków i znacząco przyspieszyć renderowanie. Implementacja była jednak pisana "na szybko", zatem parę rzeczy może być nie zoptymalizowanych. W niczym to jednak nie przeszkadza, a nawet ułatwia zrozumienie mechanizmu. Zauważmy bowiem, że tak naprawdę nie wiemy, ile konkretnie wierzchołków będzie wygenerowanych - nie da się tego tak szybko stwierdzić patrząc jedynie na obrazek. Dlatego lepiej jest użyć samorozszerzalnego się kontenera. Racja - jeżeli grafik/projektant/inna-pozycja-zawodowa-osoby-wykonującej-ten-element odpowiednio dokładnie planuje mapę, zna tę liczbę i może w jakiś sposób wpłynąć na programistę (np. dobrym słowem albo gumowym kurczakiem), aby ten z góry przewidział odpowiednią wielkość tablicy i zrezygnował z użycia, szczerze mówiąc, wolnych obiektów typu std::vector.
Przejdźmy zatem do rzeczonej funkcji. Początek.
// zmienne przydatne do konstrukcji prymitywów
int whalf, hhalf;
if (width % 2 == 0) // jeśli szerokość parzysta, to dzielimy pół na pół
{
whalf = width;
}
else // jeśli szerokość nieparzysta
{
whalf = width+1;
}
// to samo dla długości mapy
if (height % 2 == 0)
{
hhalf = height;
}
else
{
hhalf = height+1;
}
int templ = 0;
int tempt = hhalf;
int i;
int j;
Jak można było wyżej wywnioskować, width oraz height to wielkości opisujące bitmapę. Następuje tutaj pierwsze poważne założenie - jedna płaszczyzna na mapie będzie miała długość 2 jednostek. Czyli każdy pojedynczy "klocek" lochów będzie sześcianem o wymiarach 2x2x2. Jest to oczywiście znaczne uproszczenie, aczkolwiek na początku sprawdza się całkiem znośnie. Na podstawie tej informacji i podanych danych, generujemy połowę szerokości i długości mapy w grze. Dlaczego połowę? Ponieważ następuje tutaj drugie poważne założenie - środek mapy będzie leżał dokładnie w punkcie (0,0) układu współrzędnych OpenGL. Będziemy zatem generować wierzchołki od -whalf do whalf i od -hhalf do hhalf. A że jeden piksel bitmapy to 2 jednostki mapy w aplikacji - zatem tak to wygląda. Pod koniec deklarujemy również zmienne, które będą nam towarzyszyć w pętli - dwie opisujące "stan" aktualnie badanych współrzędnych oraz dwie będące licznikami w pętli.
for (i = 0, j = 0; i < width * height; ++i, j += 3)
{
// jeśli sześcian (kawałek murku)
if (Imig.Image()[j] == 0x78 || Imig.Image()[j+1] == 0x78 || Imig.Image()[j+2] == 0x78)
{
// przednia ściana
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt);
// prawa ściana
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt-2);
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt-2);
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt);
// tylna ściana
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt-2);
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt-2);
// lewa ściana
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt-2);
}
//... tutaj będzie else if, które zaraz nastąpi
//... a tutaj zmiana wartości zmiennych templ i tempt
}
Będziemy teraz wesoło hasać po pętli za pomocą dwóch liczników - i oraz j. Patrząc na pętle, można już wysnuć pewne podejrzenia, które się teraz pewnie potwierdzą - i będzie służyło jedynie do kontynuowania pętli (żadnego większego, a tym bardziej enigmatycznego znaczenia nie posiada), natomiast j odpowiada za kolejne składowe pikseli bitmapy. Dlaczego co trzy? Gdyż rozmiar tablicy danych o pikselach wynosi szerokość * wysokość * 3 i każda współrzędna zapisywana jest za pomocą 3 bajtów opisujących ulubiony atrybut daltonistów, czyli kolor na danej pozycji. Trzeba także wspomnieć o dwóch rzeczach. Pierwsza jest taka, że nie zawsze można dociec, za pomocą jakich składowych (zapisanych w systemie szesnastkowym) jest opisany dany kolor. Nie używam i nie jestem specjalistą w zakresie obsługi specjalistycznych programów graficznych, choć na pewno tam te wartości można znaleźć. W opisywanym przypadku, przy niewielkiej złożoności bitmapy, skorzystał z poglądu heksadecymalnego w IrfanView i po prostu wywnioskowałem, że np. 787878 to kolor szary. Druga ważna rzecz to fakt, że należy te wartości sprawdzać od tyłu (co będzie widoczne na kolejnym fragmencie kodu). Tutaj nie ma co więcej dodawać - po prostu tak musi być. W powyższym kodzie widać, iż program interpretuje szary kolor piksela jako miejsce, gdzie ma postawić "klocek". Następuje zatem zapisywanie do wektorów odpowiednich współrzędnych wierzchołków i możemy przejść dalej. Właśnie tutaj używane są zmienne whalf, hhalf, tempt i templ.
No dobrze, są murki. Ale wypadałoby również odczytać, gdzie mają być wydrążone korytarze, prawda?
else if (Imig.Image()[j] == 0x22 || Imig.Image()[j+1] == 0xB1 || Imig.Image()[j+2] == 0x4C)
{
// podłoga
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(0); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(0); vz.push_back(tempt-2);
// sufit
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt-2);
vx.push_back(templ-whalf); vy.push_back(2); vz.push_back(tempt);
vx.push_back(templ+2-whalf); vy.push_back(2); vz.push_back(tempt);
}
Tutaj właśnie widać odczytywanie współrzędnych od tyłu - wiąże się to z konwersją pomiędzy formatami RGB i BGR. Kolor zielony, użyty w przykładzie, ma wartość 4CB122, dlatego powyżej odczytujemy to jako 22B14C. Trochę to pogmatwane i nielogiczne, ale nie wiem jak wy - ja już nauczyłem się z tym żyć. Samo ciało instrukcji warunkowej nie pozostawia złudzeń - okrutny algorytm chce zamknąć gracza pomiędzy podłogą a sufitem. Chlip.
// tymczasowa współrzędna x się przesuwa
templ += 2;
// koniec linijki, wyższy wiersz
if ((i+1) % width == 0)
{
templ = 0;
tempt -= 2;
}
Aby generowanie wierzchołków następowało zgrabnie i gibko, należy po "ifach" również odpowiednio przesuwać zmienne tymczasowe. Przy każdym obiegu pętli zmienia się współrzędna x-owa. Warto tutaj wspomnieć, iż poruszamy się od lewego dolnego wierzchołka mapy do prawego górnego, co poniekąd sugerowałem w poprzedniej notce. W przypadku, gdy skończy się wiersz, zerujemy współrzędną templ i zmniejszamy tempt (bo, jak wiadomo, wartości osi z "wgłąb" zmniejszają się).
Mamy zatem wygenerowane lochy. Cieszymy się i radujemy. Nic jednak nam po lochach, gdybyśmy ich nie wyświetlili. Jest to jednak czysta formalność.
Tex.BindTex2D(0);
for (int i = 0; i < vx.size(); i += 4)
{
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f); glVertex3f(vx[i], vy[i], vz[i]);
glTexCoord2f(1.0f, 0.0f); glVertex3f(vx[i+1], vy[i+1], vz[i+1]);
glTexCoord2f(1.0f, 1.0f); glVertex3f(vx[i+2], vy[i+2], vz[i+2]);
glTexCoord2f(0.0f, 1.0f); glVertex3f(vx[i+3], vy[i+3], vz[i+3]);
glEnd();
}
Zwykła generacja kolejnych czworokątów, w dodatku oteksturowanych.
Jak widać, pominąłem milczeniem całą resztę programu - łącznie z konstrukcją, poruszaniem się, itd. Nie jest to jednak trudne - ich wybadanie i testowanie ustawień polecam bardziej Waszej cierpliwości oraz samozaparciu.
Mam nadzieję, iż każdy, kto tutaj dotarł, poczuł się w choć minimalnym stopniu usatysfakcjonowany. Tak jak napisałem na początku - nie jest to może mistrzostwo świata, ale swoją wagę ma.
Zachęcam do ściągnięcia, testowania i komentowania.
Pozdrawiam i dziękuję - SceNtriC.
2 komentarze:
"aby ten z góry przewidział odpowiednią wielkość tablicy i zrezygnował z użycia, szczerze mówiąc, wolnych obiektów typu std::vector."
a to jest ciekawe, w kontekscie http://www.csharphelp.com/archives2/archive458.html , konkretnie:
"the C++ code uses the vector template class. I rewrote the C++ code to use the native array thinking that it would be faster. It wasn�t."
skad teza, ze std::vectory sa wolniejsze od natywnych tablic w c++? z ciekawosci pytam
Powiem uczciwie, iż empirycznie tego nie sprawdziłem. Moja "wiedza" wynikała z kryterium laryngologicznego ("gdzieś tak słyszałem"), jak i indukcyjnego ("to niemożliwe, żeby ciągła modyfikacja kontenera, dodawanie, odejmowanie, zwalnianie pamięci i jej alokowanie była szybsza aniżeli obchodzenie się z tablicą dynamiczną w normalny sposób"). Tak czy inaczej, do powyższego zadania dużo bardziej nadaje się moim zdaniem std::vector czy też CArrayFloat napisany na potrzeby Yesty, a którą to klasę zamieściłem na Chomikuj.pl (http://chomikuj.pl/SceNtriC/Kody+*c5*bar*c3*b3d*c5*82owe/CArrayFloat.exe).
Dzięki za linka, później sobie dokładniej przeanalizuję treść. Być może się mylę (nawet na pewno, bo wielkich testów nie robiłem).
Pozdrawiam
Prześlij komentarz