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.