Znowu po dwóch tygodniach i szczerze mówiąc nie wiem na jak długo – w najbliższych dniach w końcu nadchodzi sesja i chociaż jest to u mnie tylko jeden egzamin, to nie będzie tak hop-siup. Obiecałem jednak artykuł, będący swego rodzaju przewodnikiem po procesie powstawania trawy na ekranie. Z góry jednak ostrzegam, że nie będzie to okropnie zaawansowana i przebojowa roślinność jak na screenie obok, a raczej coś, co przez moją przyjaciółkę zostało określone jako “ogródek z warzywami” (pozdrawiam przyjaciółkę – ogródek zresztą też). Cóż, dobre i to. Gorzej, jakby reakcje na mój twór brzmiały “ładną teksturę znalazłeś, ale z normal mappingiem to przesadziłeś”.
Na początek przedstawię może parę linków. Scenę naturalną NVIDII oraz artykuł o budowie i implementacji trawy podawałem już wcześniej. Do tej parki dołączyłbym dwa sposobygenerowania samej trawy przez dwie konkurencyjne firmy – NVIDIA oraz ATI, a także jeden z bardziej (o ile nie najbardziej) realistycznych modeli dla scen naturalnych – badania doktorskie Kevina Boulangera. Do ostatniej strony mogę tylko dopowiedzieć, że kopie, bije i maca narządy wewnętrzne – tak realistycznych scen to nie widziałem nawet przez okno.
Wróćmy jednak z wyżyn w niziny i pokażmy sobie bardzo podstawowe generowanie trawy. Na początek załóżmy sobie parę rzeczy:
- Podstawową jednostką (obiektem) jest kępka
- Kępki są ułożone zgodnie z mapą trawy, która znajduje się w pliku zewnętrznym (plik TGA)
- Można ustawiać wysokość, rozpiętość, skalowalność oraz wartość kanału alfa dla kępek
- Oprócz kępek trawy możliwy jest dodatkowy obiekt do rozmieszczenia
Oczywiście, wykorzystywać będziemy shadery napisane w języku Cg do animacji naszych kępek. W tym miejscu pragnę zaznaczyć, że ich fragmenty (nie fragment shader, tylko fragmenty, takie zwykłe) pochodzą z pracy Adama Sawickiego, ale także z shadera NVIDII z tego programu. Na animację jednak przyjdzie jeszcze czas (choć w szkielecie już to uwzględnię) – najpierw zajmijmy się generowaniem wierzchołków trawy. Niestety, póki co nie zamieszczam kodu źródłowego na Chomiku – podczas testowania podłączałem tyle popierdółek, że sam się zacząłem w tym gubić i w chwili obecnej kod przypomina masło orzechowe, które ktoś upaćkał dżemem i musztardą. Sama słodycz. Postaram się jednak przedstawić przykładowe źródła w notce w
ten sposób, aby łatwo można było to złożyć u siebie.
Na początek przedstawię deklaracje zmiennych oraz ogólny szkielet, jaki zawarłem w klasie CGrass. Jeśli chodzi jednak o implementację metod, na razie ograniczę się do samego generowania, bez shaderów – lepiej mieć na początek trawę bez animacji, aniżeli widzieć wkurzającego się SceNtriCa, kiedy próby dołączenia shaderów zabijały scenę. Ała.
1: #ifndef _CGRASS_H_ 2: #define _CGRASS_H_
3: 4: #include "cg\cg.h" 5: #include "cg\cgGL.h" 6: #include "windows.h" 7: #include "CBillboard.h"
8: #include "CArrayFloat.h"
9: 10: // typ wyliczeniowy dla rodzaju zmiennych używanych w shaderze
11: enum PARAMETER_TYPE
12: { 13: PT_GRASS_COLOR, // kolor trawy
14: PT_WIND_COEFF_1, // pierwszy współczynnik wiatru
15: PT_WIND_COEFF_2, // drugi współczynnik wiatru
16: PT_FADE_COEFF_1, // pierwszy współczynnik zanikania
17: PT_FADE_COEFF_2, // drugi współczynnik zanikania
18: PT_DEFLECTION_SPEED, // szybkość wychylenia
19: PT_DELTA_TIME // szybkość zachodzenia zmiany przesunięcia
20: }; 21: 22: // typ wyliczeniowy dla rodzaju tekstur używanych w shaderze
23: enum TEXTURE_TYPE
24: { 25: TT_GRASS_1, // pierwsza tekstura trawy
26: TT_GRASS_2, // druga tekstura trawy
27: TT_GRASS_3, // trzecia tekstura trawy
28: TT_FLOWER_1, // pierwsza tekstura kwiatów
29: TT_FLOWER_2, // druga tekstura kwiatów
30: TT_FLOWER_3, // trzecia tekstura kwiatów
31: TT_NOISE // tekstura szumu
32: }; 33: 34: class CGrass
35: { 36: private:
37: 38: bool g_bShadersHaveBeenInitiated; // informacja, czy shadery były inicjowane
39: 40: /*
41: pola shadera
42: */
43: 44: CGcontext g_ccCgContext; // kontekst shadera
45: CGprofile g_cpCgVertexProfile; // profil vertex shadera
46: CGprofile g_cpCgFragmentProfile; // profil fragment shadera
47: CGprogram g_crCgVertexShader; // program vertex shadera
48: CGprogram g_crCgFragmentShader; // program fragment shadera
49: 50: /*
51: parametry shadera
52: */
53: 54: CGparameter g_caCgModelViewProj; // macierz widoku
55: CGparameter g_caCgGrassColor; // kolor trawy
56: CGparameter g_caCgUpVector; // wektor w górę
57: CGparameter g_caCgRightVector; // wektor prostopadły
58: CGparameter g_caCgWindCoeff1; // pierwsza współrzędna równania liniowego wiatru
59: CGparameter g_caCgWindCoeff2; // druga współrzędna równania liniowego wiatru
60: CGparameter g_caCgFadeCoeff1; // pierwsza współrzędna równania liniowego zanikania
61: CGparameter g_caCgFadeCoeff2; // druga współrzędna równania liniowego zanikania
62: CGparameter g_caCgTime; // czas trwania sceny
63: CGparameter g_caCgAlpha; // współczynnik alfy
64: CGparameter g_caCgDeflectionSpeed; // szybkość wychylenia
65: CGparameter g_caCgDecalTex1; // zwykła tekstura 1
66: CGparameter g_caCgDecalTex2; // zwykła tekstura 2
67: CGparameter g_caCgDecalTex3; // zwykła tekstura 3
68: CGparameter g_caCgNoiseTex; // tekstura z szumem Perlina
69: 70: /*
71: inne pola
72: */
73: 74: float g_fDeltaTime; // szybkość zmian przesunięcia
75: float g_fCurrentTime; // aktualny współczynnik czasowy
76: float g_fGrassColor[3]; // kolor trawy
77: 78: CBillboard g_bBillboard; // obiekt plakatujący obiekty podłoża
79: 80: float g_fGrassHeight; // wysokość trawy
81: float g_fGrassSpread; // rozpiętość trawy
82: float g_fGrassScale; // skala trawy
83: float g_fGrassAlpha; // współczynnik alfy dla trawy
84: float g_fGrassAlphaBarrier; // próg alfy
85: 86: float g_fFlowerHeight; // wysokość kwiatów
87: float g_fFlowerSpread; // rozpiętość kwiatów
88: float g_fFlowerScale; // skala kwiatów
89: float g_fFlowerAlpha; // współczynnik kwiatów
90: 91: CArrayFloat g_afGrassVertices; // współrzędne wierzchołków trawy
92: CArrayFloat g_afGrassTexCoords; // współrzędne tekstur trawy
93: CArrayFloat g_afFlowerVertices; // współrzędne wierzchołków kwiatów
94: CArrayFloat g_afFlowerTexCoords; // współrzędne tekstur kwiatów
95: 96: public:
97: 98: CGrass(); // konstruktor
99: ~CGrass(); // destruktor
100: 101: bool InitShaders(); // stworzenie shaderów danego efektu
102: bool SetParameter (PARAMETER_TYPE, float); // ustawienie nietablicowego parametru
103: bool SetParameter (PARAMETER_TYPE, float, float, float); // ustawienie tablicowego parametru
104: bool SetParameter (PARAMETER_TYPE, float*); // ustawienie tablicowego parametru
105: bool SetTexture (TEXTURE_TYPE, char*); // ustawienie tekstury
106: 107: bool GenerateGrassArea (char*); // wygenerowanie trawiastego obszaru na podstawie pliku
108: 109: bool ExportMesh (char*); // eksport siatki trawy do pliku
110: bool ImportMesh (char*); // import siatki trawy z pliku
111: 112: float SetGrassHeight (bool, float); // ustawienie wysokości trawy
113: float SetGrassSpread (bool, float); // ustawienie rozpiętości (wielkości) trawy
114: float SetGrassScale (bool, float); // ustawienie skali trawy
115: float SetGrassAlpha (bool, float); // ustawienie wartości kanału alfa dla trawy
116: float SetFlowerHeight (bool, float); // ustawienie wysokości kwiatów
117: float SetFlowerSpread (bool, float); // ustawienie rozpiętości (wielkości) kwiatów
118: float SetFlowerScale (bool, float); // ustawienie skali kwiatów
119: float SetFlowerAlpha (bool, float); // ustawienie wartości kanału alfa dla kwiatów
120: float SetGrassAlphaBarrier (bool, float); // ustawienie progu alfy, od którego jest rendering
121: 122: void RenderGrass(); // renderowanie trawy
123: void EnableShader(); // włączenie shadera
124: void DisableShader(); // wyłączenie shadera
125: void UpdateShaders(); // aktualizacja parametrów shaderów
126: }; 127: 128: #endif
Dobrze, myślę, że pola są w miarę klarownie opisane, zatem nie ma potrzeby ich “eksplanacji” (jak ja uwielbiam zapożyczone słownictwo…). Przejdźmy zatem do implementacji podstawowych metod – konstruktora i destruktora. Ich budowa nie powinna być czarną magią, ale dla spokoju ducha zamieszczam.
1: #include "CMath.h"
2: #include "CGrass.h"
3: #include "CTextureManager.h"
4: 5: CTextureManager g_ctmGrassTexes(7); // tekstury trawy i szumu
6: 7: CGrass::CGrass() // konstruktor
8: { 9: // domyślne wartości
10: g_fGrassHeight = 0.25f; 11: g_fGrassSpread = 0.1f; 12: g_fGrassScale = 0.15f; 13: g_fGrassAlpha = 0.5f; 14: g_fFlowerHeight = 0.3f; 15: g_fFlowerSpread = 0.1f; 16: g_fFlowerScale = 0.15f; 17: g_fFlowerAlpha = 0.5f; 18: g_fGrassAlphaBarrier = 0.2f; 19: 20: // początkowy stan shaderów
21: g_bShadersHaveBeenInitiated = false;
22: } 23: 24: CGrass::~CGrass() // destruktor
25: { 26: // jeśli shadery były inicjowane
27: if (g_bShadersHaveBeenInitiated)
28: { 29: cgDestroyProgram(g_crCgVertexShader); 30: cgDestroyProgram(g_crCgFragmentShader); 31: cgDestroyContext(g_ccCgContext); 32: } 33: } Teraz zacznie się cała zabawa. Mechanizm tworzenia trawy jest oparty na pliku graficznym w formacie Targa, którego nazwę
relatywnie daleko. Dlatego też algorytm umożliwia skalowanie mapy trawy, czyli podanie współczynnika, przez który przemnażane są współrzędne i dzięki temu, dla przykładowej wartości 0.15f kępki są już dosyć blisko siebie. Wyjaśni się to wszystko podczas implementacji. Na początku musimy wyczyścić istniejącą już bazę, wczytać obraz i zainicjować zmienne.1: bool CGrass::GenerateGrassArea (char* file) // wygenerowanie trawiastego obszaru na podstawie pliku
2: { 3: // jeśli już były utworzone tablice, to wymazujemy ich zawartość
4: if (g_afGrassVertices.Size() > 0 || g_afGrassTexCoords.Size() > 0)
5: { 6: g_afGrassVertices.Clear(); 7: g_afGrassTexCoords.Clear(); 8: g_afFlowerVertices.Clear(); 9: g_afFlowerTexCoords.Clear(); 10: } 11: 12: // otwarcie i wczytanie pliku TGA z obrazem
13: CTGA Area; 14: 15: if (!Area.LoadTGA(file))
16: { 17: return false;
18: } 19: 20: // zmienne pomocnicze
21: float half_w = Area.Width() / 2;
22: float half_h = Area.Height() / 2;
23: float xc = 0;
24: float zc = 0;
25: 26: // obliczenie odchylenia dla rozłożenia trzech czworokątów na kępkę
27: float devg = CMath::Sin(45.0f) * g_fGrassSpread;
28: float devf = CMath::Sin(45.0f) * g_fFlowerSpread;
29: 30: // liczniki pętli
31: int i = 0;
32: int j = 0;
Zmienne half_w i half_h to wielkość połowy mapy trawy. Wstępnie obliczamy też współczynniki odchylenia, które jest przecież liczone dla każdego wierzchołka drugiego i trzeciego czworokąta. To się właśnie nazywa optymalizacja – jak dumnie to brzmi.
Kolejna partia kodu – tym razem pokażę pętęlkę, która odpowiada za generację wierzchołków kępek.
1: for (i = 0, j = 0; i < Area.Width() * Area.Height(); ++i, j += 3)
2: { 3: // jeśli kolor czarny (kępka trawy)
4: if (Area.Image()[j] == 0x00 || Area.Image()[j+1] == 0x00 || Area.Image()[j+2] == 0x00)
5: { 6: // obliczenie współrzędnych środka kępki
7: xc = i % Area.Width() - half_w; 8: zc = (-(i / Area.Width()) % Area.Height()) + half_h; 9: 10: // zeskalowanie obrazu - chodzi o gęstość rozmieszczenia kępek trawy wobec siebie
11: // im mniejsza wartość tego parametru, tym obiekty będą bliżej siebie
12: xc *= g_fGrassScale; 13: zc *= g_fGrassScale; 14: 15: // pierwszy quad
16: g_afGrassVertices.Add(xc - g_fGrassSpread); 17: g_afGrassVertices.Add(0); 18: g_afGrassVertices.Add(zc); 19: 20: g_afGrassVertices.Add(xc + g_fGrassSpread); 21: g_afGrassVertices.Add(0); 22: g_afGrassVertices.Add(zc); 23: 24: g_afGrassVertices.Add(xc + g_fGrassSpread); 25: g_afGrassVertices.Add(g_fGrassHeight); 26: g_afGrassVertices.Add(zc); 27: 28: g_afGrassVertices.Add(xc - g_fGrassSpread); 29: g_afGrassVertices.Add(g_fGrassHeight); 30: g_afGrassVertices.Add(zc); 31: 32: // drugi quad
33: g_afGrassVertices.Add(xc - devg); 34: g_afGrassVertices.Add(0); 35: g_afGrassVertices.Add(zc + devg); 36: 37: g_afGrassVertices.Add(xc + devg); 38: g_afGrassVertices.Add(0); 39: g_afGrassVertices.Add(zc - devg); 40: 41: g_afGrassVertices.Add(xc + devg); 42: g_afGrassVertices.Add(g_fGrassHeight); 43: g_afGrassVertices.Add(zc - devg); 44: 45: g_afGrassVertices.Add(xc - devg); 46: g_afGrassVertices.Add(g_fGrassHeight); 47: g_afGrassVertices.Add(zc + devg); 48: 49: // trzeci quad
50: g_afGrassVertices.Add(xc - devg); 51: g_afGrassVertices.Add(0); 52: g_afGrassVertices.Add(zc - devg); 53: 54: g_afGrassVertices.Add(xc + devg); 55: g_afGrassVertices.Add(0); 56: g_afGrassVertices.Add(zc + devg); 57: 58: g_afGrassVertices.Add(xc + devg); 59: g_afGrassVertices.Add(g_fGrassHeight); 60: g_afGrassVertices.Add(zc + devg); 61: 62: g_afGrassVertices.Add(xc - devg); 63: g_afGrassVertices.Add(g_fGrassHeight); 64: g_afGrassVertices.Add(zc - devg); 65: } // if (Area.Image()[j] == 0x00 || Area.Image()[j+1] == 0x00 || Area.Image()[j+2] == 0x00)
66: // jeśli kolor czerwony (kwiat)
67: else if (Area.Image()[j] == 0xED || Area.Image()[j+1] == 0x1C || Area.Image()[j+2] == 0x24)
68: { 69: // obliczenie współrzędnych środka kępki
70: xc = i % Area.Width() - half_w; 71: zc = (-(i / Area.Width()) % Area.Height()) + half_h; 72: 73: // zeskalowanie obrazu - chodzi o gęstość rozmieszczenia kępek trawy wobec siebie
74: // im mniejsza wartość tego parametru, tym obiekty będą bliżej siebie
75: xc *= g_fFlowerScale; 76: zc *= g_fFlowerScale; 77: 78: // pierwszy quad
79: g_afFlowerVertices.Add(xc - g_fFlowerSpread); 80: g_afFlowerVertices.Add(0); 81: g_afFlowerVertices.Add(zc); 82: 83: g_afFlowerVertices.Add(xc + g_fFlowerSpread); 84: g_afFlowerVertices.Add(0); 85: g_afFlowerVertices.Add(zc); 86: 87: g_afFlowerVertices.Add(xc + g_fFlowerSpread); 88: g_afFlowerVertices.Add(g_fFlowerHeight); 89: g_afFlowerVertices.Add(zc); 90: 91: g_afFlowerVertices.Add(xc - g_fFlowerSpread); 92: g_afFlowerVertices.Add(g_fFlowerHeight); 93: g_afFlowerVertices.Add(zc); 94: 95: // drugi quad
96: g_afFlowerVertices.Add(xc - devf); 97: g_afFlowerVertices.Add(0); 98: g_afFlowerVertices.Add(zc + devf); 99: 100: g_afFlowerVertices.Add(xc + devf); 101: g_afFlowerVertices.Add(0); 102: g_afFlowerVertices.Add(zc - devf); 103: 104: g_afFlowerVertices.Add(xc + devf); 105: g_afFlowerVertices.Add(g_fFlowerHeight); 106: g_afFlowerVertices.Add(zc - devf); 107: 108: g_afFlowerVertices.Add(xc - devf); 109: g_afFlowerVertices.Add(g_fFlowerHeight); 110: g_afFlowerVertices.Add(zc + devf); 111: 112: // trzeci quad
113: g_afFlowerVertices.Add(xc - devf); 114: g_afFlowerVertices.Add(0); 115: g_afFlowerVertices.Add(zc - devf); 116: 117: g_afFlowerVertices.Add(xc + devf); 118: g_afFlowerVertices.Add(0); 119: g_afFlowerVertices.Add(zc + devf); 120: 121: g_afFlowerVertices.Add(xc + devf); 122: g_afFlowerVertices.Add(g_fFlowerHeight); 123: g_afFlowerVertices.Add(zc + devf); 124: 125: g_afFlowerVertices.Add(xc - devf); 126: g_afFlowerVertices.Add(g_fFlowerHeight); 127: g_afFlowerVertices.Add(zc - devf); 128: } // else if (Area.Image()[j] == 0xED || Area.Image()[j+1] == 0x1C || Area.Image()[j+2] == 0x24)
129: } // for (i = 0, j = 0; i < Area.Width() * Area.Height(); ++i; j += 3)
Kod jest długi, skomplikowany, ale jakbym wstawił to w częściach, to prawdopodobnie byłoby trudniej to zrozumieć. Potrzebujmy dwóch liczników pętli, z tym, że tylko i będzie odpowiadał za długość trwania pętli – zmienna j w ostatecznym rozrachunku stanowi trzykrotność pierwszego licznika, gdyż testuje kolejne elementy tablicy danych obrazka. Widać to zresztą przy warunkach – czarny kolor to bajty 000000, natomiast czerwony, który wykorzystałem to 241CED (czytane od tyłu). Przypominam, że sprawdzamy piksele poczynając od lewego dolnego rogu i poruszając się wierszami. Załóżmy, że jakimś cudem trafiliśmy na piksel pokolorowany na czarno (logicznie rzecz biorąc, kępki powinny być też ciemne…). Następuje obliczenie punktu środkowego obiektu (xc, zc). Wzór jest podany:
xc = (i mod width) – half_w
zc = (-(i div width) mod height) + half_h
Takie dosyć skomplikowane wzorki (można je sobie wyprowadzić metodą prób i błędów na kartce) są spodowane tym, że współrzędne obrazka z rosną “do góry”, natomiast w OpenGL współrzędna jest większa na “dole”. Dodatkowo, punkty w pliku graficznych są z zakresu [o, width (height)], natomiast w naszej aplikacji jest pełna dowolność. Po prostu kupa problemów przewagą tego pierwszego. Ktoś mi kiedyś podsunął pomysł, aby po prostu inaczej czytać obrazek – to też jest rozwiązanie, ale wtedy trzeba pomyśleć nad innym przebiegiem pętli. A skoro działa tak jak teraz – chyba nie odmówimy sobie przyjemności zastosowania szatańskich i niezrozumiałych wzorów, które przyprawiłyby o zawał niejednego matematyka? Potem można o tym referaty i artykuł pisać…
Wracając do kodu – następnym krokiem jest przemnożenie uzyskanych współrzędnych przez współczynnik skalowalności, dzięki czemu uzyskujemy bardziej gęsty rozkład trawy-która-przypomina-buraki. Definicje następnych czworokątów są widoczne dzięki dosyć dosłownym komentarzom w kodzie. Dzięki zastosowaniu CArrayFloat nie musimy się przejmować wielkością bazy (swoją drogą – z krótkim testów wyniknęło, iż moja struktura jest szybsza od STL-owskiego std::vector, choć ja sam coś w to nie wierzę patrząc na szybkość generacji obrazu [o tym będzie pod koniec {kurczę, znowu zaczynam bawić się nawiasami}]). Same współrzędne nie są problemem, gdyż panuje pełna zgodność z boskim rysunkiem poglądowym narysowanym w Paintcie, który jest gdzieś powyżej (rysunek, nie Paint). Niestety, każda kępka składa się z aż 12 wierzchołków, co daje gigantyczne ilości wierzchołków do przerobienia na całą scenę. Jeśli jednak zmiejszylibyśmy ilość czworokątów, trawa wyglądałaby dosyć groteskowo – przynajmniej bez dodatkowych zabiegów. Na tą chwilę nasuwają mi się dwa rozwiązania. Pierwsze to przeznaczenie jednego czworokąta na kępkę i zastosowanie plakatowania, które opisałem w jednej z poprzedniej notek – jest to dosyć prymitywne rozwiązanie, ale dzięki billboardowaniu sferycznemu może wyjść całkiem zadowalająco i szybko. Powstaje wtedy efekt, jakby kępki uginały się pod stopami kamery. Drugie rozwiązanie natomiast to zamiania trzech czworokątów na dwa prostopadłe. Jak to się sprawdza – szczerze mówiąc nie wiem, gdyż nie testowałem (a powinienem). Tak czy inaczej, podobny sposób jak na kępki trawy został zastosowany na “kwiaty” – z tym, że wykorzystujemy tutaj inne parametry skalowania, rozpiętości i wysokości.
1: // zwolnienie pamięci po wczytanym obrazie
2: Area.EraseTGA(); 3: 4: // dodanie informacji o współrzędnych tekstur na podstawie wielkości tablicy wierzchołków
5: for (i = 0; i < g_afGrassVertices.Size() / 12; ++i)
6: { 7: g_afGrassTexCoords.Add(0.0f); 8: g_afGrassTexCoords.Add(0.0f); 9: 10: g_afGrassTexCoords.Add(1.0f); 11: g_afGrassTexCoords.Add(0.0f); 12: 13: g_afGrassTexCoords.Add(1.0f); 14: g_afGrassTexCoords.Add(1.0f); 15: 16: g_afGrassTexCoords.Add(0.0f); 17: g_afGrassTexCoords.Add(1.0f); 18: } // for (i = 0; i < g_afGrassVertices.Size() / 12; ++i)
19: 20: for (i = 0; i < g_afFlowerVertices.Size() / 12; ++i)
21: { 22: g_afFlowerTexCoords.Add(0.0f); 23: g_afFlowerTexCoords.Add(0.0f); 24: 25: g_afFlowerTexCoords.Add(1.0f); 26: g_afFlowerTexCoords.Add(0.0f); 27: 28: g_afFlowerTexCoords.Add(1.0f); 29: g_afFlowerTexCoords.Add(1.0f); 30: 31: g_afFlowerTexCoords.Add(0.0f); 32: g_afFlowerTexCoords.Add(1.0f); 33: } // for (i = 0; i < g_afFlowerVertices.Size() / 12; ++i)
34: 35: // jeśli wszystko poszło dobrze
36: return true;
37: } // bool CGrass::GenerateGrassArea (char* file)
Zatem mamy wypełnione tablice “kępkowe” (żeby nie powiedzieć “campingowe”). Zanim przejdziemy do naszych kochanych jednostek “shadujących” oraz samego procesu renderowania, można jeszcze zapewnić jedną rzecz – eksport i import “mesha trawy” do pliku. Po co? A żeby było fajnie.
1: bool CGrass::ExportMesh (char* file) // eksport siatki trawy do pliku
2: { 3: // otwarcie pliku
4: if (!file) // nie ma nazwy
5: { 6: return false;
7: } 8: 9: FILE* pfile; // plik
10: 11: pfile = fopen(file, "wb");
12: 13: if (!pfile) // jeśli otwarcie pliku nieudane
14: { 15: return false;
16: } 17: 18: int i;
19: int j;
20: 21: // trawa
22: for (i = 0, j = 0; i < g_afGrassVertices.Size(); i += 3, j += 2)
23: { 24: fprintf(pfile, "%f %f %f %f %f\n", g_afGrassTexCoords[j], g_afGrassTexCoords[j+1], g_afGrassVertices[i], g_afGrassVertices[i+1], g_afGrassVertices[i+2]);
25: } 26: 27: // znak podziału pomiędzy trawą a kwiatami
28: // pierwsza linijka to ilość wierzchołków kwiatów, aby łatwiej importować plik
29: // ta wartość znajduje się w miejscu, gdzie są przyjmowane wartości ze zbioru {0, 1}
30: // więc nie ma konfliktu wartości
31: fprintf(pfile, "%d %f %f %f %f\n", g_afFlowerVertices.Size() / 3, 0, 0, 0, 0);
32: 33: // kwiaty
34: for (i = 0, j = 0; i < g_afFlowerVertices.Size(); i += 3, j += 2)
35: { 36: fprintf(pfile, "%f %f %f %f %f\n", g_afFlowerTexCoords[j], g_afFlowerTexCoords[j+1], g_afFlowerVertices[i], g_afFlowerVertices[i+1], g_afFlowerVertices[i+2]);
37: } 38: 39: // zamknięcie pliku
40: fclose(pfile); 41: 42: // jeśli sukces
43: return true;
44: } // bool CGrass::ExportMesh (char* file)
45: 46: bool CGrass::ImportMesh (char* file) // import siatki trawy z pliku
47: { 48: // otwarcie pliku
49: if (!file) // nie ma nazwy
50: { 51: return false;
52: } 53: 54: FILE* pfile; // plik
55: 56: pfile = fopen(file, "rb");
57: 58: if (!pfile) // jeśli otwarcie pliku nieudane
59: { 60: return false;
61: } 62: 63: // jeśli już były utworzone tablice, to wymazujemy ich zawartość
64: if (g_afGrassVertices.Size() > 0 || g_afGrassTexCoords.Size() > 0)
65: { 66: g_afGrassVertices.Clear(); 67: g_afGrassTexCoords.Clear(); 68: g_afFlowerVertices.Clear(); 69: g_afFlowerTexCoords.Clear(); 70: } 71: 72: // zmienne tymczasowe
73: float temp[5];
74: bool end = false;
75: unsigned flower_quantity; 76: 77: // zapisanie danych o trawie
78: while (!end)
79: { 80: fscanf(pfile, "%f %f %f %f %f\n", &temp[0], &temp[1], &temp[2], &temp[3], &temp[4]);
81: 82: // jeśli koniec wierzchołków trawy
83: if (temp[0] != 0 && temp[0] != 1)
84: { 85: flower_quantity = temp[0]; 86: end = true;
87: } 88: else
89: { 90: g_afGrassTexCoords.Add(temp[0]); 91: g_afGrassTexCoords.Add(temp[1]); 92: g_afGrassVertices.Add(temp[2]); 93: g_afGrassVertices.Add(temp[3]); 94: g_afGrassVertices.Add(temp[4]); 95: } 96: } // while (!end)
97: 98: // zapisanych danych o kwiatach
99: for (int i = 0; i < flower_quantity; ++i)
100: { 101: fscanf(pfile, "%f %f %f %f %f\n", &temp[0], &temp[1], &temp[2], &temp[3], &temp[4]);
102: g_afFlowerTexCoords.Add(temp[0]); 103: g_afFlowerTexCoords.Add(temp[1]); 104: g_afFlowerVertices.Add(temp[2]); 105: g_afFlowerVertices.Add(temp[3]); 106: g_afFlowerVertices.Add(temp[4]); 107: } 108: 109: // zamknięcie pliku
110: fclose(pfile); 111: 112: // jeśli sukces
113: return true;
114: } // bool CGrass::ImportMesh (char* file)
0.000000 0.000000 -2.480000 0.000000 2.400000
Pierwsze dwie wartości to współrzędne tekstury, kolejne trzy to wektor [x, y, z] wierzchołka. Pozostaje jednak problem, jak w sposób prosty oddzielić definicje trawy od kwiatów. Wykorzystałem tutaj fakt, iż współrzędne tekstury mogą przyjąć wartość 0 (i ogon za kropką) lub 1 (i ogon za kropką). Zatem przy takiej linijce oddzielającej (nazywanej dalej Dzikim i Złym Wersetem) zamiast pierwszej wartości można wstawić cokolwiek innego niż 0 czy 1. Z tym, że to cokolwiek innego powinno być w miarę rozsądnie wybrane. I tutaj dochodzimy do jeszcze jednego problemu – o ile wierzchołki kępek trawy możemy pobierać i wstawiać za pomocą pętli while (dopóki nie trafimy na Dziki i Zły Werset), to ze współrzędnymi kwiatów już tak nie zrobimy – wypada znać ilość ich wierzchołków. Zatem taką informację możemy tutaj podać – w Dzikim i Złym Wersecie pierwszą wartością będzie właśnie ilość elementów w bazie podzielona przez 3. Ważne jest też, aby tą wartość zapisać nie jako %f (typ zmiennoprzecinkowy), ale jako %d (typ całkowity), dzięki czemu unikniemy zawieszenia programu przy imporcie. A Dziki i Zły Werset będzie wyglął na przykład tak:
144 0.000000 0.000000 0.000000 0.000000
Jak samo zapisywanie przebiega widać dokładnie w kodzie. Pętla while skończy się, gdy napotka na… felerną linijkę (idiotyczną nazwę wymyśliłem, trzeba z tym skończyć…) i wtedy przejdzie z while do for.
W tej chwili możemy zająć się animacją i wyświetlaniem. Przy okazji pragnę wspomnieć, że metody, które teraz przedstawię, pochodzą tak naprawdę z prac nad klasą CLighting i zostały też wykorzystane w partiach oznaczonych jako CEffect oraz CWater (i pewnie w CTree, jeśli takową klasę utworzę). Świadczy to ponownie o złej organizacji kodu (według mnie kod zajmujący się shaderami powinien być w jednym miejscu), ale co nieco już o tym pisałem.
Dosyć tego pisania – oto funkcja inicjująca shadery trawy.
1: bool CGrass::InitShaders() // stworzenie shaderów danego efektu
2: { 3: // stworzenie kontekstu shadera
4: g_ccCgContext = cgCreateContext(); 5: 6: // inicjalizacja profilu vertex shadera
7: g_cpCgVertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX); 8: 9: // błąd, jeśli inicjalizacja profilu się nie powiodła
10: if (g_cpCgVertexProfile == CG_PROFILE_UNKNOWN || g_cpCgVertexProfile == CG_INVALID_ENUMERANT_ERROR)
11: { 12: MessageBox(NULL, "Hardware does not support vertex shaders!", "Error", MB_OK);
13: return false;
14: } 15: 16: // stworzenie vertex shadera
17: g_crCgVertexShader = cgCreateProgramFromFile(g_ccCgContext, CG_SOURCE, "shaders/gvs.cg",
18: g_cpCgVertexProfile, "main", NULL);
19: 20: // błąd, jeśli utworzenie vertex shadera nie powiodło się
21: if (!g_crCgVertexShader)
22: { 23: MessageBox(NULL, "Error creating one of vertex shaders!", "Error", MB_OK);
24: return false;
25: } 26: 27: // wczytanie vertex shadera do OpenGL
28: cgGLLoadProgram(g_crCgVertexShader); 29: 30: // inicjalizacja profilu fragment shadera
31: g_cpCgFragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT); 32: 33: // błąd, jeśli inicjalizacja profilu się nie powiodła
34: if (g_cpCgFragmentProfile == CG_PROFILE_UNKNOWN || g_cpCgFragmentProfile == CG_INVALID_ENUMERANT_ERROR)
35: { 36: MessageBox(NULL, "Hardware does not support fragment shaders!", "Error", MB_OK);
37: return false;
38: } 39: 40: // stworzenie fragment shadera
41: g_crCgFragmentShader = cgCreateProgramFromFile(g_ccCgContext, CG_SOURCE, "shaders/gfs.cg",
42: g_cpCgFragmentProfile, "main", NULL);
43: 44: // błąd, jeśli utworzenie pixel shadera nie powiodło się
45: if (!g_crCgFragmentShader)
46: { 47: MessageBox(NULL, "Error creating one of fragment shaders!", "Error", MB_OK);
48: return false;
49: } 50: 51: // wczytanie fragment shadera do OpenGL
52: cgGLLoadProgram(g_crCgFragmentShader); 53: 54: // ustawienie parametrów shadera
55: g_caCgModelViewProj = cgGetNamedParameter(g_crCgVertexShader, "ModelViewProj");
56: g_caCgUpVector = cgGetNamedParameter(g_crCgVertexShader, "UpVector");
57: g_caCgRightVector = cgGetNamedParameter(g_crCgVertexShader, "RightVector");
58: g_caCgWindCoeff1 = cgGetNamedParameter(g_crCgVertexShader, "WindCoeff1");
59: g_caCgWindCoeff2 = cgGetNamedParameter(g_crCgVertexShader, "WindCoeff2");
60: g_caCgTime = cgGetNamedParameter(g_crCgVertexShader, "Time");
61: g_caCgDeflectionSpeed = cgGetNamedParameter(g_crCgVertexShader, "DeflectionSpeed");
62: g_caCgAlpha = cgGetNamedParameter(g_crCgFragmentShader, "Alpha");
63: g_caCgGrassColor = cgGetNamedParameter(g_crCgFragmentShader, "GrassColor");
64: g_caCgFadeCoeff1 = cgGetNamedParameter(g_crCgFragmentShader, "FadeCoeff1");
65: g_caCgFadeCoeff2 = cgGetNamedParameter(g_crCgFragmentShader, "FadeCoeff2");
66: g_caCgDecalTex1 = cgGetNamedParameter(g_crCgFragmentShader, "DecalTex1");
67: g_caCgDecalTex2 = cgGetNamedParameter(g_crCgFragmentShader, "DecalTex2");
68: g_caCgDecalTex3 = cgGetNamedParameter(g_crCgFragmentShader, "DecalTex3");
69: g_caCgNoiseTex = cgGetNamedParameter(g_crCgFragmentShader, "NoiseTex");
70: 71: // ustawienie domyślnych wartości
72: g_fGrassColor[0] = 0.1f; 73: g_fGrassColor[1] = 1.0f; 74: g_fGrassColor[2] = 0.1f; 75: 76: cgSetParameter1f(g_caCgWindCoeff1, 0.10f); 77: cgSetParameter1f(g_caCgWindCoeff2, -0.09f); 78: cgSetParameter1f(g_caCgFadeCoeff1, -0.08f); 79: cgSetParameter1f(g_caCgFadeCoeff2, -0.12f); 80: cgSetParameter1f(g_caCgTime, 0); 81: cgSetParameter1f(g_caCgDeflectionSpeed, g_fGrassHeight * 0.11f); 82: cgSetParameter3fv(g_caCgGrassColor, g_fGrassColor); 83: cgSetParameter3f(g_caCgUpVector, 0.0f, 1.0f, 0.0f); 84: cgSetParameter3f(g_caCgRightVector, 1.0f, 0.0f, 1.0f); 85: 86: // początkowe wartości współczynników
87: g_fDeltaTime = 0.005f; 88: g_fCurrentTime = 0; 89: 90: // sygnalizacja, iż shadery zostały zainicjowane
91: g_bShadersHaveBeenInitiated = true;
92: 93: // inicjalizacja multiteksturingu
94: g_ctmGrassTexes.InitMultitexturing(); 95: 96: // jeśli wszystko poszło dobrze
97: return true;
98: } // bool CGrass::InitShaders()
1: bool CGrass::SetParameter (PARAMETER_TYPE type, float value) // ustawienie nietablicowego parametru
2: { 3: // w zależności od parametru
4: switch (type)
5: { 6: case PT_WIND_COEFF_1:
7: 8: cgSetParameter1f(g_caCgWindCoeff1, value);
9: break;
10: 11: case PT_WIND_COEFF_2:
12: 13: cgSetParameter1f(g_caCgWindCoeff2, value);
14: break;
15: 16: case PT_FADE_COEFF_1:
17: 18: cgSetParameter1f(g_caCgFadeCoeff1, value);
19: break;
20: 21: case PT_FADE_COEFF_2:
22: 23: cgSetParameter1f(g_caCgFadeCoeff2, value);
24: break;
25: 26: case PT_DEFLECTION_SPEED:
27: 28: cgSetParameter1f(g_caCgDeflectionSpeed, value);
29: break;
30: 31: case PT_DELTA_TIME:
32: 33: g_fDeltaTime = value;
34: break;
35: 36: default: // w każdym innym wypadku
37: 38: return false;
39: } // switch (type)
40: 41: // jeśli wszystko poszło dobrze
42: return true;
43: } // bool CGrass::SetParameter (PARAMETER_TYPE type, float value)
44: 45: bool CGrass::SetParameter (PARAMETER_TYPE type, float x, float y, float z) // ustawienie tablicowego parametru
46: { 47: // w zależności od parametru
48: switch (type)
49: { 50: case PT_GRASS_COLOR:
51: 52: g_fGrassColor[0] = x; 53: g_fGrassColor[1] = y; 54: g_fGrassColor[2] = z; 55: break;
56: 57: default: // w każdym innym wypadku
58: 59: return false;
60: } // switch (type)
61: 62: // jeśli wszystko poszło dobrze
63: return true;
64: } // bool CGrass::SetParameter (PARAMETER_TYPE type, float x, float y, float z)
65: 66: bool CGrass::SetParameter (PARAMETER_TYPE type, float* array) // ustawienie tablicowego parametru
67: { 68: // w zależności od parametru
69: switch (type)
70: { 71: case PT_GRASS_COLOR:
72: 73: g_fGrassColor[0] = array[0]; 74: g_fGrassColor[1] = array[1]; 75: g_fGrassColor[2] = array[2]; 76: break;
77: default: // w każdym innym wypadku
78: 79: return false;
80: } // switch (type)
81: 82: // jeśli wszystko poszło dobrze
83: return true;
84: } // bool CGrass::SetParameter (PARAMETER_TYPE type, float* array)
Skoro mamy zmienne shaderowe, wypadałoby jeszcze ustawić dwie rzeczy. Pierwsza to tekstury.
1: bool CGrass::SetTexture (TEXTURE_TYPE which, char* file) // ustawienie tekstury
2: { 3: if (which == TT_GRASS_1)
4: { 5: g_ctmGrassTexes.InitTex2D(0, file); 6: } 7: if (which == TT_GRASS_2)
8: { 9: g_ctmGrassTexes.InitTex2D(1, file); 10: } 11: if (which == TT_GRASS_3)
12: { 13: g_ctmGrassTexes.InitTex2D(2, file); 14: } 15: if (which == TT_FLOWER_1)
16: { 17: g_ctmGrassTexes.InitTex2D(3, file); 18: } 19: if (which == TT_FLOWER_2)
20: { 21: g_ctmGrassTexes.InitTex2D(4, file); 22: } 23: if (which == TT_FLOWER_3)
24: { 25: g_ctmGrassTexes.InitTex2D(5, file); 26: } 27: if (which == TT_NOISE)
28: { 29: g_ctmGrassTexes.InitTex2D(6, file); 30: cgGLSetTextureParameter(g_caCgNoiseTex, g_ctmGrassTexes.GetID(6)); 31: } 32: 33: // jeśli wszystko poszło dobrze
34: return true;
35: } // bool CGrass::SetTexture (TEXTURE_TYPE which, char* file)
Drugi typ ustawień, który pozostał to właśnie rozpiętość, wysokości, skalowalność oraz kanał alfa trawy i kwiatów. Napisanie takich metod jest jednak sprawą trywialną.
1: float CGrass::SetGrassHeight (bool change, float value) // ustawienie wysokości trawy
2: { 3: if (change)
4: { 5: g_fGrassHeight = CMath::Abs(value);
6: } 7: 8: return g_fGrassHeight;
9: } 10: 11: float CGrass::SetGrassSpread (bool change, float value) // ustawienie rozpiętości (wielkości) trawy
12: { 13: if (change)
14: { 15: g_fGrassSpread = value;
16: } 17: 18: return g_fGrassSpread;
19: } 20: 21: float CGrass::SetGrassScale (bool change, float value) // ustawienie skali trawy
22: { 23: if (change)
24: { 25: g_fGrassScale = value;
26: } 27: 28: return g_fGrassScale;
29: } 30: 31: float CGrass::SetGrassAlpha (bool change, float value) // ustawienie wartości kanału alfa dla trawy
32: { 33: if (change)
34: { 35: g_fGrassAlpha = CMath::Saturate(value);
36: } 37: 38: return g_fGrassAlpha;
39: } 40: 41: float CGrass::SetFlowerHeight (bool change, float value) // ustawienie wysokości kwiatów
42: { 43: if (change)
44: { 45: g_fFlowerHeight = CMath::Abs(value);
46: } 47: 48: return g_fFlowerHeight;
49: } 50: 51: float CGrass::SetFlowerSpread (bool change, float value) // ustawienie rozpiętości (wielkości) kwiatów
52: { 53: if (change)
54: { 55: g_fFlowerSpread = value;
56: } 57: 58: return g_fFlowerSpread;
59: } 60: 61: float CGrass::SetFlowerScale (bool change, float value) // ustawienie skali kwiatów
62: { 63: if (change)
64: { 65: g_fFlowerScale = value;
66: } 67: 68: return g_fFlowerScale;
69: } 70: 71: float CGrass::SetFlowerAlpha (bool change, float value) // ustawienie wartości kanału alfa dla kwiatów
72: { 73: if (change)
74: { 75: g_fFlowerAlpha = CMath::Saturate(value);
76: } 77: 78: return g_fFlowerAlpha;
79: } 80: 81: float CGrass::SetGrassAlphaBarrier (bool change, float value) // ustawienie progu alfy, od którego jest rendering
82: { 83: if (change)
84: { 85: g_fGrassAlphaBarrier = CMath::Saturate(value);
86: } 87: 88: return g_fGrassAlphaBarrier;
89: }1: Grass.SetGrassAlpha(true, Grass.SetGrassAlpha(false, 0) + 0.1f);
Tutaj muszę jeszcze wspomnieć o paru rzeczach. Po pierwsze, wysokość, rozpiętość oraz skalowalność należy ustawić przed generowaniem trawy. Jeśli zmienimy te parametry później, należy ponownie wygenerować “teren”, co trwa dobre paręnaście (-dziesiąt) sekund. Po drugie – wypada wyjaśnić parę (dosłownie parę) funkcji matematycznych, które używam w powyższych metodach. Obie należą do struktury statycznej CMath, przez co mają niezwykle profesjonalnie i żwawo wyglądający przedrostek CMath:: (a z tym już przecież blisko do std:: i chwały na wie… No dobrze, zapędziłem się). Mamy funkcję Abs, której przeznaczenia nie trzeba tłumaczyć – absolute value, czyli wartość bezwzględna. Druga funkcja, Saturate, może być nieco mniej oczywista. Jej implementacja wygląda następująco:
1: static float Saturate (float x) // zwraca wartość pomiędzy 0 a 1
2: { 3: /*
4: Jeśli x < 0 - zwraca 0
5: Jeśli x > 0 - zwraca 1
6: Jeśli 0 < x < 1 - zwraca x
7: */
8: return Max(0, Min(1, x));
9: }Same komentarze wyjaśniają chyba wszystko – chodzi o umieszczenie wartości w zakresie [0, 1], jeśli programista sam o to nie zadbał. Istnieje też funkcja Clamp, która umieszcza wartość w zakresie [a,b], gdzie a oraz b to przedziały podane jako argument. Funkcje Max oraz Min są chyba znane, ale dla spokoju ducha umieszczę jeszcze ich implementacje – a nuż (a widelec) się komuś przydadzą.
1: static float Max (float a, float b) // większa wartość
2: { 3: if (a > b)
4: { 5: return a;
6: } 7: else
8: { 9: return b;
10: } 11: } 12: 13: static float Min (float a, float b) // mniejsza wartość
14: { 15: if (a < b)16: {17: return a;18: }19: else20: {21: return b;22: }23: }
Kończymy chwilowy skok w bok w kierunku matematyki (choć tak naprawdę programowanie to sama matematyka, szczególnie to graficzne) i przechodzimy do (prawie) ostatecznej fazy kodu aplikacji – wyświetlania. Na początek trzy metody, które są konieczne z punktu widzenia uruchomienia shaderów – włączenie, wyłączenie i aktualizacja (ostatnia metoda nie jest konieczna, ale lepiej ją mieć w zanadrzu).
1: void CGrass::EnableShader() // włączenie shadera
2: { 3: if (g_bShadersHaveBeenInitiated)
4: { 5: g_fCurrentTime += g_fDeltaTime; 6: 7: if (g_fCurrentTime >= 6.28f)
8: { 9: g_fCurrentTime = 0; 10: } 11: 12: float up[3];
13: float right[3];
14: 15: g_bBillboard.GetRightVector(right); 16: g_bBillboard.GetUpVector(up); 17: 18: cgSetParameter1f(g_caCgTime, g_fCurrentTime); 19: cgSetParameter3fv(g_caCgUpVector, up); 20: cgSetParameter3fv(g_caCgRightVector, right); 21: 22: // pobranie macierzy i dołączenie do shadera
23: cgGLSetStateMatrixParameter(g_caCgModelViewProj, CG_GL_MODELVIEW_PROJECTION_MATRIX, 24: CG_GL_MATRIX_IDENTITY); 25: 26: // włączenie profili i programów shadera
27: cgGLEnableProfile(g_cpCgVertexProfile); 28: cgGLBindProgram(g_crCgVertexShader); 29: 30: cgGLEnableProfile(g_cpCgFragmentProfile); 31: cgGLBindProgram(g_crCgFragmentShader); 32: 33: cgGLEnableTextureParameter(g_caCgDecalTex1); 34: cgGLEnableTextureParameter(g_caCgDecalTex2); 35: cgGLEnableTextureParameter(g_caCgDecalTex3); 36: cgGLEnableTextureParameter(g_caCgNoiseTex); 37: 38: UpdateShaders(); 39: } 40: } 41: 42: void CGrass::DisableShader() // wyłączenie shadera
43: { 44: if (g_bShadersHaveBeenInitiated)
45: { 46: // wyłączenie profilów
47: cgGLDisableProfile(g_cpCgFragmentProfile); 48: cgGLDisableProfile(g_cpCgVertexProfile); 49: 50: cgGLDisableTextureParameter(g_caCgNoiseTex); 51: cgGLDisableTextureParameter(g_caCgDecalTex3); 52: cgGLDisableTextureParameter(g_caCgDecalTex2); 53: cgGLDisableTextureParameter(g_caCgDecalTex1); 54: } 55: } 56: 57: void CGrass::UpdateShaders() // aktualizacja parametrów shaderów
58: { 59: if (g_bShadersHaveBeenInitiated)
60: { 61: // aktualizacja danych shadera
62: cgUpdateProgramParameters(g_crCgVertexShader); 63: cgUpdateProgramParameters(g_crCgFragmentShader); 64: } 65: }A teraz samo renderowanie.
1: void CGrass::RenderGrass() // renderowanie trawy
2: { 3: // wczytanie alfy dla trawy
4: cgSetParameter1f(g_caCgAlpha, g_fGrassAlpha); 5: 6: // wczytanie koloru trawy
7: cgSetParameter3fv(g_caCgGrassColor, g_fGrassColor); 8: 9: // włączenie testu alfy
10: glEnable(GL_ALPHA_TEST); 11: glAlphaFunc(GL_GREATER, g_fGrassAlphaBarrier); 12: 13: // załączenie tekstur trawy
14: cgGLSetTextureParameter(g_caCgDecalTex1, g_ctmGrassTexes.GetID(0)); 15: cgGLSetTextureParameter(g_caCgDecalTex2, g_ctmGrassTexes.GetID(1)); 16: cgGLSetTextureParameter(g_caCgDecalTex3, g_ctmGrassTexes.GetID(2)); 17: 18: // włączenie shadera
19: EnableShader(); 20: 21: // przyporządkowanie tekstur
22: g_ctmGrassTexes.BindMultiTex2D(0, 0); 23: g_ctmGrassTexes.BindMultiTex2D(1, 1); 24: g_ctmGrassTexes.BindMultiTex2D(2, 2); 25: 26: // proces renderowania quadów
27: for (int i = 0; i < g_afGrassVertices.Size(); i += 12)
28: { 29: glBegin(GL_QUADS); 30: 31: g_ctmGrassTexes.MultiTexel2f(0, 0, 0); 32: g_ctmGrassTexes.MultiTexel2f(1, 0, 0); 33: g_ctmGrassTexes.MultiTexel2f(2, 0, 0); 34: glVertex3f(g_afGrassVertices[i], g_afGrassVertices[i+1], g_afGrassVertices[i+2]); 35: 36: g_ctmGrassTexes.MultiTexel2f(0, 1, 0); 37: g_ctmGrassTexes.MultiTexel2f(1, 1, 0); 38: g_ctmGrassTexes.MultiTexel2f(2, 1, 0); 39: glVertex3f(g_afGrassVertices[i+3], g_afGrassVertices[i+4], g_afGrassVertices[i+5]); 40: 41: g_ctmGrassTexes.MultiTexel2f(0, 1, 1); 42: g_ctmGrassTexes.MultiTexel2f(1, 1, 1); 43: g_ctmGrassTexes.MultiTexel2f(2, 1, 1); 44: glVertex3f(g_afGrassVertices[i+6], g_afGrassVertices[i+7], g_afGrassVertices[i+8]); 45: 46: g_ctmGrassTexes.MultiTexel2f(0, 0, 1); 47: g_ctmGrassTexes.MultiTexel2f(1, 0, 1); 48: g_ctmGrassTexes.MultiTexel2f(2, 0, 1); 49: glVertex3f(g_afGrassVertices[i+9], g_afGrassVertices[i+10], g_afGrassVertices[i+11]); 50: 51: glEnd(); 52: } 53: 54: // wyłączenie shaderów dla trawy
55: DisableShader(); 56: 57: // załączenie tekstur kwiatów
58: cgGLSetTextureParameter(g_caCgDecalTex1, g_ctmGrassTexes.GetID(3)); 59: cgGLSetTextureParameter(g_caCgDecalTex2, g_ctmGrassTexes.GetID(4)); 60: cgGLSetTextureParameter(g_caCgDecalTex3, g_ctmGrassTexes.GetID(5)); 61: 62: // wczytanie koloru kwiatów
63: cgSetParameter3f(g_caCgGrassColor, 1.0f, 1.0f, 1.0f); 64: 65: // wczytanie alfy dla kwiatów
66: cgSetParameter1f(g_caCgAlpha, g_fFlowerAlpha); 67: 68: // włączenie shadera
69: EnableShader(); 70: 71: // przyporządkowanie tekstur
72: g_ctmGrassTexes.BindMultiTex2D(3, 0); 73: g_ctmGrassTexes.BindMultiTex2D(4, 1); 74: g_ctmGrassTexes.BindMultiTex2D(5, 2); 75: 76: // proces renderowania quadów
77: for (int i = 0; i < g_afFlowerVertices.Size(); i += 12)
78: { 79: glBegin(GL_QUADS); 80: 81: g_ctmGrassTexes.MultiTexel2f(0, 0, 0); 82: g_ctmGrassTexes.MultiTexel2f(1, 0, 0); 83: g_ctmGrassTexes.MultiTexel2f(2, 0, 0); 84: glVertex3f(g_afFlowerVertices[i], g_afFlowerVertices[i+1], g_afFlowerVertices[i+2]); 85: 86: g_ctmGrassTexes.MultiTexel2f(0, 1, 0); 87: g_ctmGrassTexes.MultiTexel2f(1, 1, 0); 88: g_ctmGrassTexes.MultiTexel2f(2, 1, 0); 89: glVertex3f(g_afFlowerVertices[i+3], g_afFlowerVertices[i+4], g_afFlowerVertices[i+5]); 90: 91: g_ctmGrassTexes.MultiTexel2f(0, 1, 1); 92: g_ctmGrassTexes.MultiTexel2f(1, 1, 1); 93: g_ctmGrassTexes.MultiTexel2f(2, 1, 1); 94: glVertex3f(g_afFlowerVertices[i+6], g_afFlowerVertices[i+7], g_afFlowerVertices[i+8]); 95: 96: g_ctmGrassTexes.MultiTexel2f(0, 0, 1); 97: g_ctmGrassTexes.MultiTexel2f(1, 0, 1); 98: g_ctmGrassTexes.MultiTexel2f(2, 0, 1); 99: glVertex3f(g_afFlowerVertices[i+9], g_afFlowerVertices[i+10], g_afFlowerVertices[i+11]); 100: 101: glEnd(); 102: } 103: 104: // wyłączenie testu alfy, shadera i zwolnienie jednostek teksturowania
105: DisableShader(); 106: g_ctmGrassTexes.DisableMultiTexes(); 107: glDisable(GL_ALPHA_TEST); 108: } // void CGrass::RenderGrass()
Co my tu mamy… Przede wszystkim, z powodu dużej ilości parametrów innych dla trawy i kwiatów musimy niejako podzielić rendering na dwie części. Rozpoczynamy od wczytania koloru trawy i wartości alfa dla trawy, a następnie uruchamiamy OpenGL-owski test alfy na podstawie wcześniej podanej wartości g_fGrassAlphaBarrier. To sprawi, że przepuszczane będą tylko piksele, których czwarta współrzędna koloru będzie miała większa wartość od progu. Dopiero teraz podajemy tekstury do fragment shadera – na tym etapie numery 0, 1 i 2. Każda kępka składa się z trzech czworokątów, a każdy czworokąt może mieć inną teksturę – jest to o tyle ciekawe, że pozwala zasymulować prostą różnorodność, przez co kępki będą wyglądały odrobinę ciekawiej. Po włączeniu shadera rozpoczynamy normalny sposób renderowania – bindujemy tekstury do jednostek teksturujących i w pętli rysujemy kolejne czworokąty. Przy metodzie MultiTexel2f równie dobrze zamiast dwóch ostatnich argumentów możemy wykorzystać tablicę g_afGrassTexCoords. Jakoś tak się złożyło, że zrezygnowałem z tego pomysłu i przez to wyszedł mały bajzel w kodzie. Ale to nic – nie jest tak źle, żebym zaczął rzucać laptopem. Po narysowaniu wszystkich quadów, wyłączamy shadera i podmieniamy tekstury – tym razem na te z identyfikatorami 3, 4 i 5. Zmieniamy też kolor na biały (1.0f, 1.0f, 1.0f) i ustawiamy odpowiedni dla kwiatów kanał alfa. Po ponownym włączeniu shadera i zabindowaniu tekstur, rysujemy w pętli same kwiaty. Następnie wyłączamy shader, multiteksturowanie oraz test kanału alfa. Nie jest to może najwydaniejszy sposób wyświetlania trawy na ekranie (zwłaszcza bez użycia vertex arrays), ale nie jest też strasznie tragiczny – przynajmniej ja tak uważam.
No dobrze, zatem mamy przygotowaną klasę CGrass. Po wpisaniu do Visuala (czy innego IDE) mamy już zaimplementowaną klasę CGrass i w radosnej uciesze możemy już ją wywołać – na przykład w taki sposób (stosujemy tą samą teksturę na wszystkie quady trawy).
1: #include "CGrass.h"
2: 3: CGrass Grass; 4: 5: bool Init()
6: { 7: //...
8: 9: Grass.InitShaders(); 10: Grass.SetGrassHeight(true, 0.25f);
11: Grass.SetGrassSpread(true, 0.08f);
12: Grass.SetGrassScale(true, 0.15f);
13: Grass.SetTexture(TT_NOISE, "noise.tga");
14: Grass.SetTexture(TT_GRASS_1, "grasswalpha.tga");
15: Grass.SetTexture(TT_GRASS_2, "grasswalpha.tga");
16: Grass.SetTexture(TT_GRASS_3, "grasswalpha.tga");
17: Grass.SetTexture(TT_FLOWER_1, "flower.tga");
18: Grass.SetTexture(TT_FLOWER_2, "flower.tga");
19: Grass.SetTexture(TT_FLOWER_3, "flower.tga");
20: Grass.GenerateGrassArea("map.tga");
21: 22: //...
23: } 24: 25: void Render()
26: { 27: //...
28: 29: Grass.RenderGrass(); 30: 31: //...
32: }Pozostała jeszcze jedna ważna rzecz do zrobienia, z którą zawsze jest najwięcej zabawy. A raczej dwie rzeczy – vertex i fragment shader.
Według kolejności, zajmiemy się najpierw shaderem wierzchołków. Oto on:
1: struct VertexIn
2: { 3: float4 Position : POSITION; 4: float2 TexCoord : TEXCOORD0; 5: float3 Normal : NORMAL; 6: float4 Tangent : COLOR; 7: }; 8: 9: struct VertexOut
10: { 11: float4 Position : POSITION; 12: float2 TexCoord : TEXCOORD0; 13: float3 NoiseTex : TEXCOORD1; 14: }; 15: 16: VertexOut main (VertexIn In, uniform float4x4 ModelViewProj, uniform float3 UpVector, uniform float3 RightVector, 17: uniform float WindCoeff1, uniform float WindCoeff2, uniform float Time, uniform float DeflectionSpeed)
18: { 19: VertexOut Out; 20: 21: Out.TexCoord = In.TexCoord; 22: float3 pos = In.Position.xyz; 23: In.Normal = normalize(In.Normal); 24: 25: if (Out.TexCoord.y > 0.2)
26: { 27: float cosine = cos(Time) * In.Normal.z * DeflectionSpeed;
28: pos.x += cosine; 29: pos.z += cosine; 30: } 31: 32: float2 QuadBias = In.Tangent.rg * 4 - 2; 33: float2 Randoms = In.Tangent.ba * 2 - 1; 34: float2 WindFactors = float2(WindCoeff1, WindCoeff2); 35: float Wind = dot(WindFactors, Randoms);
36: float3 WorldPos = pos + (QuadBias.x + Wind) * RightVector + QuadBias.y * UpVector; 37: Out.Position = mul(ModelViewProj, float4(pos, 1)); 38: 39: Out.NoiseTex.xy = float2(WorldPos.x + WorldPos.z, WorldPos.y); 40: Out.NoiseTex.z = Out.Position.z; 41: 42: return Out;
43: }Pierwsze kilka linijek jest bardzo prostych – przekazujemy dalej wektor zawierający współrzędne tekstury oraz normalny. Następnie dokonuje się sprawa animacji – jeśli wysokość tekstury jest większa od 0.2, obliczamy wychylenie się na podstawie wartości cosinusa dla danego czasu, ostatniej współrzędne wektora normalnego oraz szybkość wychylania się, a następnie przesuwamy odpowiednie współrzędne pozycji o tą wartość. Później pobieramy odpowiednio sformatowanie wartości z rejestru COLOR, które posłużą nam jako baza do dalszych obliczeń m.in. wpływu wiatru oraz pozycji w świecie. Pozostaje tylko obliczenie końcowej pozycji wierzchołka oraz współrzędnych tekstury szumu, którą będziemy wykorzystywać we fragment shaderze. Przyznam, że nie jest kompetentny w rozkładzie tych shaderów na czynniki pierwsze, zatem moje tłumaczenia mogą być dosyć idealistyczne (“Rozumiecie? ROZUMIEMY!”).
A program do obróbki pikseli wygląda tak:
1: struct FragmentIn
2: { 3: float4 Position : POSITION; 4: float2 TexCoord : TEXCOORD0; 5: float3 NoiseTex : TEXCOORD1; 6: }; 7: 8: struct FragmentOut
9: { 10: float4 Color : COLOR; 11: }; 12: 13: FragmentOut main (FragmentIn In, uniform float3 GrassColor, uniform float FadeCoeff1, uniform float FadeCoeff2,
14: uniform sampler2D DecalTex1, uniform sampler2D DecalTex2, uniform sampler2D DecalTex3, 15: uniform sampler2D NoiseTex, uniform float Alpha)
16: { 17: FragmentOut Out; 18: 19: float4 color1 = tex2D(DecalTex1, In.TexCoord) * float4(GrassColor, Alpha); 20: float4 color2 = tex2D(DecalTex2, In.TexCoord) * float4(GrassColor, Alpha); 21: float4 color3 = tex2D(DecalTex3, In.TexCoord) * float4(GrassColor, Alpha); 22: 23: Out.Color = (color1 + color2 + color3) / 3; 24: 25: float pos = In.NoiseTex.z;
26: float MyFadeFactor = pos * FadeCoeff1 + FadeCoeff2;
27: float Fade = tex2D(NoiseTex, In.NoiseTex) + MyFadeFactor;
28: Out.Color.a = Out.Color.a - lerp(0, Fade, Alpha); 29: 30: return Out;
31: }Czy sposób, który przedstawiłem, ma wady? Ho, ho, ho – ma i to sporo. Przede wszystkim szybkość generowania się samej trawy – na moim sprzęcie jest to czas rzędu 10-20 sekund dla gęstej mapy 32x32 pikseli. Wydajność w trakcie działania programu jest już o wiele większa, choć też nie zachwyca. Oczywistą niedogodnością w przypadku tego akurat sposobu jest ograniczenie poziomu do zera – jednak bardzo łatwo można to modyfikować, zatem nie ma co o tym dalej wspominać. Inna wada to jednostajny sposób poruszania się trawy – oczywiście, da się modyfikować wartość wychylenia w sposób losowy w trakcie działania programu, ale tak czy siak całość chwieje się tak samo. Na pewno jest to bardzo prosty, ale też prymitywny sposób na trawę – myślę jednak, że na początek wystarczy i być może kogoś zainspiruje do dalszych badań nad tą kwestią.
Jak zwykle zapraszam do komentowania i dziękuję za okazane zainteresowanie.
Pozdrawiam – SceNtriC.
Niestety, nadszedł nieubłagany okres końca semestru, kiedy nagle student sobie przypomina, że beztroskie chodzenie na zajęcia i zbijanie bąków nie może trwać wiecznie – kiedyś musi przyjść weryfikacja wtłoczonej wiedzy. Dlatego też tak dużo czasu minęło od ostatniej notki – a do następnej pewnie też sporo wody w rzece upłynie. Nieważne w jakiej.
względu na renderowanie w-miarę-wyglądającej-choć-oczywiście-niezmiernie-sztampowej trawy, ale szybko uznałem, że do końca mi się to nie przyda – wystarczy gotowy obrazek z wygenerowanym obrazem przedstawiającym Nie Wiadomo Co. Właściwie, to wspominam tylko o tym zagadnieniu przy okazji – nie mam całej klasy gotowej, nawet nie mam połowy. Nie tylko dlatego, że nie mam czasu – po prostu “coś” nie działa i mnie strasznie tym denerwuje (konkretnie – kanał alfa w shaderze jakoś nie chce usuwać przezroczystych części tekstury, aczkolwiek to kwestia czasu i zabrania się za to). Sam temat został podjęty po obejrzeniu
Na szczęście w programowaniu plakaty (billboardy) nie oznaczają wałęsających się plakatów na ulicach, drzewach, słupach, murach, ścianach, sufitach, krzaczków podlewanych przed psy, które zostają na długo po wszelkich wyborach, tylko o sposobach radzenia sobie z wielką liczbą wierzchołków potrzebnych na generowanie niektórych scen. Billboardów można
także dla ruszania się nad i pod obiektem plakatowanym) oraz cylindryczną (tylko dla obchodzenia obiektów wzdłuż osi OX i OZ). Oba rodzaje billboardów mają swoją odmianę zwykłą i “oszukaną”. Ta druga jest widoczna na obrazku po lewej – obiekty billboardowane są ustawione nie tyle przodem do kamery co do płaszczyzny prostopadłej do obserwatora – czyli tak, jakby elementy sceny patrzyły na najbliższe cegiełki w murze znajdującym się za tuż za graczem.
Pomyślałem jednak, że moje tłumaczenia mogą być mętne, dlatego zamieściłem rysunki z
pewnie bym wcześniej zrezygnował z rozważań), co przy okazji wymusiło na mnie zajęcie się mapami sześciennymi (