Dziś chciałbym ożywić nasz teren dodając teksturowanie. Ograniczenie jest takie, że możemy wykorzystać do 3 tekstur na teren jak pisałem we wcześniejszej części.
Detail Map z programu EarthSculptor
W ogóle co to jest ta detail mapa ? Krótko mówiąc detail mapa dodaje do terenu.... detale (najprostsze rozwiązanie wy renderowania terenu to mieć 1 teksturę ze wszystkimi kolorami nałożoną na cały teren a drugą z detalami /rysunek poniżej/ nałożoną kilkakrotnie na całą mapę dodając jej poziom szczegółowości)
W programie EarthSculptor jest trochę inaczej tam detail mapa określa poziom mieszania z każdej z 3 tekstur (na co przeznaczony jest 1 zestaw texCoordów)
Popatrzmy na przykładową detail mapę z programu EarthSculptor
Jak określony jest ten poziom mieszania dla każdego wierzchołka ? To proste. Każdy piksel zawiera składowe RGB w zakresie 0 - 255 (lub 0.0 - 1.0 jak w w OpenGL) i właśnie każda z tych składowych określa stopień użycia każdej tekstury (będziemy jeszcze mówić o tym później jak policzyć kolor wynikowy dla każdego wierzchołka)
Obliczanie 2 zestawów TexCoordów
Pierwszy zestaw TexCoordów jak wspomniałem wyżej służy do nałożenia całej detail mapy na cały teren.
No dobra ale potrzebujemy jeszcze jednego zestawu aby tekstury nie były rozciągnięte. Właśnie za pomocą tych TexCoordów będziemy wyświetlać tekstury (tamten zestaw służy tylko do nadania kolorów). W programie tekstury nakładane mają wymiary 16 x 16 jednostek (liczyłem ręcznie) i tak samo ja postanowiłem zrobić u siebie.
Obliczenie tych 2 zestawów nie jest ciężkie. Spójrzmy na kod poniżej znajdujący się umnie w tej samej funkcji Load co w pierwszym rozdziale.
vector texCoord0, texCoord1;
//texCoord0 to zestaw odpowiedzialny za nakładanie kwadratowych tekstur 16 x 16 tak by mapa była szeczółowa
//texCoord1 to zestaw odpowiedzialny za nałożenie całej detail mapy na teren
for(int i = 0; i < height; ++i) //height - wysokość heightmapy
for(int j = 0; j < width; ++j){ //width szerokość heightmapy
texCoord0.PB(Vec2((float)i / 16, (float)j / 16));
texCoord1.PB(Vec2((float)i / height, (float)j / width));
}
texCoord1.PB(Vec2((float)i / height, (float)j / width));
}
Jeżeli ktoś nie wie jak nakładane są tekstury to zapraszam to krótkiego wytłumaczenia pod spodem, jeżeli ktoś wie to nie musi tego czytać.
Nakładanie tekstur w OpenGL' u
Spójrzmy na poniższy rysunek
Współrzędne pokazane na rogach należy czytać w ten sposób, że : (x', y') oznacza że wybrana tekstura zostanie nałożona na wierzchołek wzdłuż osi x w x' * 100% i w y' * 100% wzdłuż osi y, czyli na powyższym rysunku tekstura zostanie nałożona na kwadrat w całości. No dobrze a jakie współrzędne powinny mieć te wierzchołki pozostałe ? Cóż... wartości pośrednie. Jeżeli znamy wysokośc i szerokość obrazka w pikselach to każdy piksel odpowiada
jednemu wierzchołkowi więc współrzedne powinny wygladać tak : (i / height, j / width) gdzie i to odpowiedni wiersz a 'j' odpowiednia kolumna na heightmapie.
jednemu wierzchołkowi więc współrzedne powinny wygladać tak : (i / height, j / width) gdzie i to odpowiedni wiersz a 'j' odpowiednia kolumna na heightmapie.
A co będzie oznaczać taki zapis : texCoord0.PB(Vec2((float)i / 16, (float)j / 16)); ? Co 16 wierzchołków wzdłuż każdej z osi wartość będzie osiągać wartość 1 więc w całości tekstura będzie nakładana co 16 wierzchołków wzdłuż każdej z osi. No dobrze a co się dzieje z wartościami większymi od 1 ? Cóż, standardowo OpenGL powiela teksturę więc tak naprawdę tekstura będzie powtarzana co 16 wierzchołków w pionie i w poziomie.
Więcej informacji o teksturowaniu znajdziemy tutaj(również o tym jak je ładować do pamięci i jak bindować, w ogóle Nehe to ogromna skarbnica wiedzy o OpenGLu) : Nehe pl
LightMap & ColorMap
Light mapa to statyczna mapa światła, bardzo łatwo dodaje efekt światła statycznego do naszego terenu praktycznie bez żadnych obliczeń, po prostu modyfikuje wynikowy kolor.
Przykład light mapy :
Color mapa robi to samo co light mapa tzn modyfikuje wynikowy kolor ale dodaje kolory do otoczenia by wyglądał różnorodnie.
Przykład color mapy :
Teoretycznie można połączyć lightmape z colormapą odrobinę upraszczając obliczenia przesyłając tylko wynikową teksturę light i color mapy, ale ja potraktowałem je jako osobne tekstury na razie.
Wyświetlanie naszego terenu z teksturami
Czas nadszedł by wszystko wyrenderować. Na początku oczywiście trzeba 2 zestawy TexCoordów wrzucić do VBO:
glGenBuffersARB(1, &vboTexCoord0);
glBindBufferARB(GL_ARRAY_BUFFER_ARB,vboTexCoord0)
;glBufferDataARB(GL_ARRAY_BUFFER_ARB, (GLsizei)(texCoord0.size() * sizeof(Vec2)), &texCoord0.front(), GL_STATIC_DRAW_ARB);
glGenBuffersARB(1, &vboTexCoord1);
glBindBufferARB(GL_ARRAY_BUFFER_ARB, vboTexCoord1);
glBufferDataARB(GL_ARRAY_BUFFER_ARB, (GLsizei)(texCoord1.size() * sizeof(Vec2)), &texCoord1.front(), GL_STATIC_DRAW_ARB);
Teraz wystarczy zbindować odpowiednie 3 tekstury dla terenu (takie jakie sobie życzymy), detail mape, light mapę, color mapę, napisać shader do mieszania(poniżej się znajduje) i... wszystko wyrenderować.
Mieszanie kolorów w Pixel Shaderze
Kod pixel shadera do mieszania wygląda u mnie tak:
#version 120
uniform sampler2D tex0; //detail map1
uniform sampler2D tex1; //texture1 detail map1
uniform sampler2D tex2; //texture2 detail map1
uniform sampler2D tex3; //texture3 detail map1
uniform sampler2D tex4; //color map
uniform sampler2D tex5; //light map
uniform sampler2D tex0; //detail map1
uniform sampler2D tex1; //texture1 detail map1
uniform sampler2D tex2; //texture2 detail map1
uniform sampler2D tex3; //texture3 detail map1
uniform sampler2D tex4; //color map
uniform sampler2D tex5; //light map
void main(){
//kolor danego piksela, który określa intensywność danej tekstury dla danego wierzchołka
vec4 intensity = texture2D(tex0, gl_TexCoord[1].st);
vec4 color1 = texture2D(tex1, gl_TexCoord[0].st); //tekstura pierwsza terenu
vec4 color2 = texture2D(tex2, gl_TexCoord[0].st); //tekstura druga terenu
vec4 color3 = texture2D(tex3, gl_TexCoord[0].st); //tekstura trzecia terenu
vec4 colorMap = texture2D(tex4, gl_TexCoord[1].st); //color map
vec4 lightMap = texture2D(tex5, gl_TexCoord[1].st); //light map
vec4 result = mix(color1, color2, intensity.g );
result = mix(result, color3, intensity.b);
vec4 color1 = texture2D(tex1, gl_TexCoord[0].st); //tekstura pierwsza terenu
vec4 color2 = texture2D(tex2, gl_TexCoord[0].st); //tekstura druga terenu
vec4 color3 = texture2D(tex3, gl_TexCoord[0].st); //tekstura trzecia terenu
vec4 colorMap = texture2D(tex4, gl_TexCoord[1].st); //color map
vec4 lightMap = texture2D(tex5, gl_TexCoord[1].st); //light map
vec4 result = mix(color1, color2, intensity.g );
result = mix(result, color3, intensity.b);
gl_FragColor = result * colorMap * lightMap;
}
vec4 result;
ta zmienna jest rezultatem naszego mieszania
result = mix(color1, color2, intensity.g ); //mieszanie pierwszej tekstury z drugą za pomocą funkcji mix
result = mix(result, color3, intensity.b); //mieszanie poprzedniego rezultatu z teksturą trzecią
Funkcja mix(value1, value2, factor) to tak zwana interpolacja liniowa. Zwraca to wartość pośrednią pomiędzy value1 a value2. Factor określa poziom tego mieszania i przymuje wartości od 0 - 1. Zero oznacza całkowitą wartość value1,a jeden całkowitą wartość value2 (albo na odwrót nie pamiętam:)). Wartości pomiędzy tym zakresem określają odpowiedni poziom wykorzystania dwóch wartości, dzięki czemu powstaje bardzo płynne i ładne mieszanie.
DEMO DO ŚCIĄGNIĘCIA TUTAJ
DEMO DO ŚCIĄGNIĘCIA TUTAJ
To już jest koniec na koniec jako główny smaczek tego artykułu chciałbym pokazać krótki filmik z renderingu terenu :)
Pozdrawiam Norbert G.