poniedziałek, 23 sierpnia 2010

Terrain Rendering part I

Praca oczywiście powolutku posuwa się do przodu. W tym pości wreszczie wyrenderujemy coś pożytecznego a mianowicie postaram się opisać jak wyrenderować teren.

Teren oczywiście będzie składać się z tak zwanej mapy wysokości (heightmapy).

W swoim silniku mam już gotowy kod importujacy kod z niezwykle prostego programu EarthSculptor.
Ograniczenie jakie nakładam narazie jest takie, że możemy dodać narazie 3 różne tekstury na teren (chodź myślę, że tyle wystarczy.

Przykładowy screen z programu : 

Dobra zacznijmy od początku.
Program dostarcza nam takie pliki jak : zwykłą heightmapę z których liczymy wysokości, lightmapę - tzw statyczną mapę cieni, detail map - zawiera informację jak mieszać tekstury dla każdego wierzchołka.


Wczytywanie heightmapy : w silniku mamy możliwość odczytu tekstur narazie w formacie BMP oraz TGA, lecz ich działanie opiszę w innym poście i pokażę jak mamy to wczytywać (z gotowym kodem).


Klasa wczytująca bitmapę zawiera sporo metod i pół a narazie nie chcę dawać kodu źródłowego ponieważ pracuję jeszcze nad geomipmappingiem, kiedy to skończę to dopiero dam cały kod.

Narazie przyjrzyjmy się jednej metodzie :

void EvoHeightMap::Load(EvoTexture* heightMap, int amountOfSectors, float xzScale = 1.0f, float yScale = 1.0f);

Pierwszy parametr to obiekt tekstury  z której będą wyznaczane wysokości wierzchołków.
Drugi parametr to liczba sektorów, które są kwadratami i MUSZĄ być potęgą liczby 2.
Trzeci parametr skaluje wierzchołki na osiach x i z.
Czwarty parametr skaluje wysokość.

Samo wyznaczanie wysokości nie jest niczym skompilowanym, na początku tworzymy odpowiednie tablice dynamicznie:

void EvoHeightMap::Load(video::EvoTexture *heightMap, int amountOfSectors, float xzScale, float yScale){
int height = heightMap->GetHeight(), width =heightMap->GetWidth();

int halfHeight = height / 2, halfWidth = width / 2, counter = -1;

Vec3** terrain, **normal;


        terrain = new Vec3*[height + 1];
        normal = new Vec3*[height + 1];
        REP(i, height + 1){
            terrain[i] = new Vec3[width + 1];
            normal[i] = new Vec3[width + 1];
        }


tutaj następuje ustawianie położenia wierzchołka w przestrzeni 3D


REP(i, width)
            REP(j, height){
                terrain[i][j] = Vec3((-halfWidth + j) * xzScale,
                                     (data[++counter] + data[++counter] + data[++counter]) / 3.0f * yScale - halfHeight * yScale,
                                     -(-halfHeight + i) * xzScale);
            }


haflWidth i halfHeight to zmienne pomocnicze by środek modelu znajdował się w punkcie (0,0,0)
REP to zwykłe makro pętli, a wygląda tak :


#define REP(x, p) for(int x = 0; x < p; ++x) czyli zwykła pętla:)

Pozostało nam jeszcze obliczyć normalne, ale żeby to zrobić musimy znać wszystkie trójkąty na mapie, by wiedzieć do których trójkątów należy dany wierzchołek. Spójrzmy na poniższy rysunek(nie dokońca podpisałem wierzchołki):



 można zauważyć pewną regularność tworzenia kolejnych trójkątów. Mianowicie trójkąty składane są z następujących wierzchołków : (0, 1, 4); (4, 1, 5); (1, 2, 5); (5, 2, 6) itd.
Mianowicie znając szerokośći wysokość naszej mapy (która z założenia jest kwadratowa i jest potęgą liczby 2).

Otrzymujemy następujący kod na otrzymanie wszystkich indeksów potrzebnych do ułożenia całej naszej siatki.

vector

REP(i, width - 1){
            REP(j, height - 1){
                //Pierwszy trójkąt
                face.PB(j + width * i);
                face.PB(j + width * i + 1);
                face.PB(j + width * i + height);
                //Drugi trójkąt
                face.PB(j + width * i + height);
                face.PB(j + width * i + 1);
                face.PB(j + width * i + 1 + height);
         }
}

Skoro mamy już wsyzstkie indeksy wierzchołków, możemy przystąpić do liczenia normalnych.
 Jak policzyć wektor normalny dla pojedyńczego trójkąta ?








Potrzebujemy teraz 2 wektorów. Musimy wykorzystać wsyzstkie 3 wierzchołki i wyznaczyć dwa różne wektory niech to będzie np:

w1 = v1 - v2;
w2 = v1 - v3;

i teraz wystarczy policzyć iloczyn wektorowych tych 2 wektorów, znormalizować.. i mamy wektor normalny powierzchni. Prawda że proste ?

Co zrobić jeżeli wierzchołek należy do kilku trójkątów na raz (tak jak u nas na heightmapie wierzchołek należy do kilkut trójkątów naraz)? Ja rozwiązałem to tak, że otrzymane wektory poprostu do siebie dodaję a następnie wszystko normalizuję i w ten sposób otrzymuję wektor normalny dla każdego wierzchołka.

Kod, który to robi przedstawia się tak (wykorzystuję zadeklarowaną wcześniej tablicę normal) :

        for(int i = 0; i < face.size(); i += 3){
            Vec3 v1 = terrain[face[i] / width][face[i] % width] - terrain[face[i + 1] / width][face[i + 1] % width];
            Vec3 v2 = terrain[face[i] / width][face[i] % width] - terrain[face[i + 2] / width][face[i + 2] % width];
            Vec3 n = Dot(v1, v2); //Wektor normalny powierzchni
            Normalize(&n);
            normal[face[i] / width][face[i] % width] += n;
            normal[face[i + 1] / width][face[i + 1] % width] += n;
            normal[face[i + 2] / width][face[i + 2] % width] += n;
        }
        REP(i, width)
            REP(j, height)
                Normalize(&normal[i][j]);

Na początku idę przez wszystkie trójkąty (każdy trójkąt składa się z 3 wierzchołków dlatego jest tam i += 3).







Vec3 v1 = terrain[face[i] / width][face[i] % width] - terrain[face[i + 1] / width][face[i + 1] % width];
Vec3 v2 = terrain[face[i] / width][face[i] % width] - terrain[face[i + 2] / width][face[i + 2] % width];

Te 2 linijki obliczają 2 wektory tak jak pisałem wyżej z tym trójkątem. Zapis może wydaje się dziwny ale zauważmy że tablica face jest 1 wymiarowa a ja mam tablicę terrain dwuwymiarową. Jeżeli zastanowisz się to napewno zrozumiesz jak to działa.
Vec3 n = Dot(v1, v2); //Wektor normalny powierzchni
Normalize(&n);

Potem obliczam  wektor normalny powierzchni funkcją Dot (jest w biblioteczce matematycznej jak również Normal i wiele funkcji, tylko wystarczy tam zajrzeć), a następnie normalizuję ten wektor.

Teraz tylko dodaję do każdego wierzchołka nowo otrzymany wektor(ta sama uwaga z tablicami co wcześniej):

normal[face[i] / width][face[i] % width] += n;
normal[face[i + 1] / width][face[i + 1] % width] += n;
normal[face[i + 2] / width][face[i + 2] % width] += n;

A na koniec wszystkie wektory normalizuję i tak mamy wektory normalne dla każdego wierzchołka:

REP(i, width)
      REP(j, height)
            Normalize(&normal[i][j]);

Teraz wystarczy tylko utworzyć VBO i wyrenderować :)
Ja utowrzyłem sobie 2 tablice pomocnicze dla VBO, bo kod jest bardziej czytelny dla mnie

vector vertex, normalCoord;

REP(i, height + 1)
      REP(j, width + 1){
            vertex.PB(terrain[i][j]);
            normalCoord.PB(normal[i][j]);
     }
a tutaj wspomniane vbo.

glGenBuffersARB(1, &vboVertex);
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboVertex);
        glBufferDataARB(GL_ARRAY_BUFFER_ARB, (GLsizei)(vertex.size() * sizeof(Vec3)), &vertex.front(), GL_STATIC_DRAW_ARB);
        vertex.erase(ALL(vertex));


glGenBuffersARB(1, &vboNormal);
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboNormal);
        glBufferDataARB(GL_ARRAY_BUFFER_ARB, (GLsizei)(normalCoord.size() * sizeof(Vec3)), &normalCoord.front(), GL_STATIC_DRAW_ARB);
        normalCoord.erase(ALL(normalCoord));

na koniec tylko wystarczy wygenerować Index Buffer (wspomniana wyżej tablica face) i możemy renderować:

glGenBuffersARB(1, &vboFace);
                glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, vboFace);
                glBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, (GLsizei)(face.size() * sizeof(unsigned int)), &face.front(), GL_STATIC_DRAW_ARB);

Teraz bindujemy nasze bufery i wykonujemy rendering:

//VBO z wierzchołkami
        glEnableClientState(GL_VERTEX_ARRAY);
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboVertex);
        glVertexPointer(3, GL_FLOAT, 0, (char*)NULL);

//VBO z normalnymi
        glEnableClientState(GL_NORMAL_ARRAY);
        glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboNormal);
        glNormalPointer(GL_FLOAT, 0, (char*)NULL);

//Index Buffer
        glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, vboFace

No i rendering :

glDrawElements(GL_TRIANGLES, face.size(), GL_UNSIGNED_INT, 0);

A o to ujrzymy taki obraz mniej więcej obraz :


Co prawda jeszcze bez żadnych tekstur ale i tak efekt nie jest zły.
To tyle w części pierwszej.

1 komentarz:

  1. dzis bede mial koszmary, przypomniala mi sie wlasnie grafika na studiach i algorytmy do obliczania swiatla na odbitego przez obiekty, dziekuje Ci bardzo :)

    ale tak poza tym to coraz ciekawiej zaczyna sie robic :)

    OdpowiedzUsuń