sobota, 31 stycznia 2009

Proste generowanie trawy

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 scr004ten 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


Małe wyjaśnienie(a) – CBillboard to klasa plakatująca, CArrayFloat to samorozszerzalna tablica, natomiast wszystko pod pojęciem Flower rozumiem jako ten drugi obiekt trawiasty – nie same kępki trawy, tylko kępki czegoś innego. Uznałem, że najczęściej to będą kwiatki, zatem tak wyszło. Przepraszam metali. Wybaczcie też, jeśli kod jest dosyć brzydko sformatowany – myślę jednak, że trzeba będzie postarać się o odpowiednią organizację moich testów i umieszczenie pełnego projektu w Internecie.

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: }
Całość dzieje się już teraz w pliku CGrass.cpp (poprzednio był to CGrass.h), zatem musi nastąpić odwołanie do nagłówka. Dodatkowo, dołączamy strukturę statyczną (strukturę, której wszystkie metody są statyczne) CMath zawierającą różne (i różniste) funkcje matematyczne, a także manager tekstur. W tym miejscu chciałem się wytłumaczyć, dlaczego CMath jest strukturą, a nie po prostu przestrzenią nazw (co skutkowałoby zastosowaniem using namespace CMath; i wpisywanie stałoby się prostsze). Niestetyż, struktura frameworka okazała się już na tyle skomplikowana, że linker zaczął wypluwać bardzo, ale to bardzo dziwne i niezrozumiałe komunikaty, iż go ogólnie zdenerwowałem. Tutaj ujawnia się główna wada mojego kodu i wyjaśnienie, dlaczego będę próbował migracji na właściwy już silnik z pełnym projektem – dołączanie dalszej funkcjonalności na zasadzie “napiszę i jakoś zadziała z resztą” jest dosyć mętne i mało profesjonalne, a rodzi ogromne problemy powiązań pomiędzy poszczególnymi plikami. Tak jednak traktuję ten framework – jako naukę tworzenia dosyć skomplikowanego projektu. Co do konstruktora i destruktora, sprawa wydaje mi się oczywista – w pierwszej metodzie inicjujemy początkowe wartości, natomiast w destruktorze zwalniamy programy shadera, o ile takie zostały utworzone (przejdę do tego później).

Teraz zacznie się cała zabawa. Mechanizm tworzenia trawy jest oparty na pliku graficznym w formacie Targa, którego nazwę mapka należy podać jako argument funkcji GenerateGrassArea. Przykładowy obraz takiej mapy można obejrzeć po lewej – jest to prosty rysunek z czarnymi (kępki trawy) oraz czerwonymi (kępki kwiatów) plamkami. Każda kępka to trzy skrzyżowane czworokąty – patrząc od góry, wygląda to tak, jak na dosyć marnym (w końcu 10 minut bawienia się w Paintcie) obrazku poglądowym po prawej (albo na dużo lepszym rysunku z artykułu NVIDII [zwłaszcza lewa strona]). Widać też jak ułożone są odpowiednie wierzchołki na płaszczyźnie XZ – dla zmiennej położonej na osi OY przyjmujemy (dla potrzeb clump_rys artykułu), iż kępki rozsadzamy na poziomie 0 aż do ustalonej wysokości. Punkt (xc, zc) to tak naprawdę względne współrzędne piksela obrazka, gdzie jest czarna kropka. Trzeba jednak przyznać (ja to uczyniłem po pierwszym odpaleniu programu), że takie rozwiązanie jest dosyć kiepskie – piksele są w tablicy danych obrazka trzymane w odstępach od jeden (i także w ten sposób są liczone), a w świecie OpenGL współrzędne oddalone od siebie o jedną jednostkę są 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)


To już ostatnia część metody GenerateGrassArea i szczerze mówiąc, już czysto kosmetyczna. Wypada oczywiście zwolnić pamięć po wczytanym obrazku. Następnie uzupełniamy tablice współrzędnych tekstury w proporcji jeden wierzchołek ~ trzy współrzędne wierzchołka ~ dwie współrzędne tekstury. Nie jest to jednak konieczne działanie, o czym się przekonamy przy funkcji renderującej trawę. Kiedy zatem taka dodatkowa tablica ma zastosowanie? Przy zastosowaniu mechanizmu vertex arrays tudzież VBO. Niestety, przy próbach utworzenia czegoś takiego u nas dzieją się różne dziwne rzeczy, zatem pozostawiam to jako zadanie dla chętnych w kwestii optymalizacji. Na końcu oczywiście zwracamy wartość true (z adnotacją, iż o-dziwo-wszystko-poszło-dobrze-ale-nie-wiadomo-dlaczego).

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)
Większość jest klarowna. Każda linijka pliku, który powstał z eksportu wygląda mniej więcej tak:

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()    
Przy okazji, przepraszam za wielką ilość kodu w jednym okienku. Jak widać, pierwsza faza to załadowanie profili i podpięcie pod programy shadera pliki gvs.cg i gfs.cg – tak się nazywają kody źródłowe napisane w języku Cg, do których dojdziemy później. Po części ładującej pozostało podpięcie zmiennych pod parametry znajdujące się w plikach shadera. Z samej składni mniej więcej można wywnioskować, jak to działa – pod zmienną przypisujemy wartość fukcji nadania parametru dla odpowiedniego programu i nazwy zmiennej. Jeśli jednak komuś nie wystarcza moje tłumaczenie, to informuję, że bardzo dobre wprowadzenie do Cg i podpinania shaderów pod DirectX oraz OpenGL znajduje się na stronie Dimmension3.Spine.pl. Jest tam masa świetnych artykułów odnośnie matematyki, technik, tutoriale dwóch wiodących API graficznych, a także między innymi to, co w chwili obecnej nas interesuje, czyli kurs Cg - oto części: pierwsza, druga i trzecia. Na sam koniec metody dokonujemy ustawienia domyślnych wartości. Nie jest to konieczne, ale wtedy musimy się zdać na to, że programista nie zapomni o nadaniu wartości jakiejkolwiek zmiennej shaderowej. Poza tym, w większości wypadków domyślne ustawienia całkowicie wystarczą, dlaczego więc tego nie robić? Potrzebna jest także inicjalizacja multiteksturingu, gdyż całość renderingu właściwie się na tym opiera. Aby jednak nadawać wartości zmiennych shaderowym, wypada też dopisać metody SetParameter, które pozwalają na takie działanie. Zatem, oto one.
   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) 

Potrzebne są trzy metody tego typu, gdzie każda przyjmuje inny zestaw argumentów. Dla każdego typu parameteru możliwe są wartości jednokrotne, trzykrotne czy też tablica trójelementowa. Należy też zauważyć, że nie we wszystkich przypadkach dokonujemy od razu ustawienia parametru w shaderze – czasami wartość tą optymalniej jest nadać tuż przed rozpoczęciem procesu renderowania, zatem chwilowo jest tylko zapisywana wartość, a co z nią dalej zrobimy – wkrótce się okaże.

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)

Tutaj także nie ustawiamy wszsystkiego od razu. Można by było tak począć w przypadku, jeśli zrezygnowalibyśmy z innych obiektów aniżeli trawa. W takim beznadziejnym przypadku jak noise nasz, należy jednak najpierw zainicjować tekstury za pomocą standardowego managera tekstur. Elementy 0, 1 i 2 to tekstury przeznaczone na trawę, natomiast 3, 4 oraz 5 – na kwiaty. Ostatnia, szósta tekstura to obraz szumu, który jest także niezbędny dla obliczenia pewnych rzeczy we fragment shaderze. Jest też uniwersalny, dlatego podajemy identyfikator od razu do programu dla GPU. Przykład takiego obiektu (zaczerpnięty bodajże z silnika The Final Quest autorstwa Regedita [pozdrowienia]) przedstawiłem obok. Inne tego typu tekstury można znaleźć tutaj.
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: }

Zastosowałem tutaj mój ulubiony sposób na funkcja zmieniająco-zwracające. Oprócz faktycznej wartości, jako argument należy podać flagę, która mówi o tym, czy faktycznie chcemy wartość zmienić i potem zwrócić, czy po prostu uzyskać tą aktualną. Dzięki czemu, kod zwiększania wartości alfa o 0.1f wygląda bardzo prosto. Dla ustalonej wartości wyrażenia “bardzo prosto”.
   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:     else
  20:     {
  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: }

Jeśli shadery zostały poprawnie zainicjowane w InitShaders, to przed ich włączeniem, należy podać kilka parametrów. Są to: czas, który upłynął od momentu uruchomienia programu (g_fDeltaTime jest wartością, którą możemy modyfikować – domyślne ustawienie to 5 tysięcznych) oraz wektory góry i prostopadły (do tego właśnie potrzebny nam jest obiekt klasy CBillboard). Całość możemy podać jako parametry shadera, wraz z macierzą widoku i projekcji. Następnie włączamy profile i programy shadera oraz dołączamy aktualne tekstury. Wyłączenie jednostek “shadeujących” przebiega podobnie, z tym iż tekstury wyłączamy w odwrotnej kolejności – nie jest to chyba konieczne, ale mile widziane z punktu widzenia Dobrych Technik Programowania (DTP). Aktualizacja shaderów to tylko taka ewentualność, jeśli podczas działania shadera zostaną zmienione jakiekolwiek parametry.

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: }
Na początku próbkujemy wartość koloru z każdej tekstury, jaką podaliśmy do shadera. Jeśli będzie to ta sama – cóż, mówi się trudno, nałożyliśmy troszkę obliczeń. końcowa wartość koloru (ale jeszcze nie ta końcowa końcowa) to średnia arytmetyczna próbek. Następnie wykorzystujemy proste równania liniowe dla symulacji zanikania i po próbkowaniu tekstury szumu modyfikujemy wartość alfa piksela. Tutaj nastąpiło szalone działanie mojej inwencji twórczej, a dokładniej tego, że nie wszystko działało tak jak powinno. Wykorzystałem jedną z Zasad Pisania Shaderów i losowo dopisywałem wszystko do ostatniej linijki aż w końcu przyszedł pożądany efekt.

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.

piątek, 16 stycznia 2009

W czasie końcówki semestru

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.

Tak czy inaczej programy na zaliczenie zostały oddane (nikomu nie życzę, żeby program przestał działać przy prezentacji…), kolokwium z list napisane, indeks wypełniony – przyszedł jednak czas na zaliczenia “całosemestralne” – jedno podejście (oprócz poprawki) i koniec. To nie przelewki. Ani orzeszki.

Przy okazji jednak poznałem sposób, dzięki któremu można generować plansze sudoku (a jak niektórzy pewnie pamiętają, jakiś czas temu pisałem o tym notkę). Na próbę implementacji przyjdzie chyba jednak czekać do wakacji.

Napiszę więc co nieco o tym, co udało mi się zrobić już jakiś czas temu i zostało już (albo jest w trakcie) przenosin do frameworka, jednak jeszcze nieudokumentowane. I nie zanosi się na to tak szybko.

Szum Perlina

Jest to właśnie zjawisko widoczne na małym niebieskawym obrazku przy tytule tej notki. Ogólnie, jest to efekt pozwalający na generowanie losowych zaburzonych obrazów, które mogą znaleźć zastosowanie przy chociażby renderowaniu chmur. Póki co, funkcje obliczające szum Perlina (wraz z czymś, co się ładnie nazywa Fractional Brownian Motion) znalazły legowisko w osobnej klasie CPerlinNoise i na razie nie planują przeprowadzki – planuję napisanie od czasu do czasu takich w miarę podstawowych rzeczy i wsadzenie tego w miarę nieoblegane miejsce w strukturze klas, aby potem to wszystko połączyć i opatrzyć ramką Precompiled Headers (albo chociaż tak to przyjmować). Dlatego też systematycznie (aczkolwiek bardzo powoli, podobnie jak cały framework) rozrasta się część CMath zawierająca przydatne i mniej przydatne twory do obliczeń. Ale wracając do meritum.

Pod tym linkiem można znaleźć “przyjemny” opis zagadnienia algorytmu wprowadzonego przez Kena Perlina. Piszę z przekąsem (można pisać z przekąsem) przyjemny, gdyż jakoś nigdy nie miałem sympatii do anglojęzycznych technicznych artykułów, lecz chyba trzeba będzie się powoli przyzwyczajać. Sama zasada działania, nie jest wybitnie skomplikowana – polega na losowaniu kolejnych elementów obrazu, wygładzaniu (takim blurowaniu) okolic, interpolowaniu i voila. Niby proste, ale jak zwykle, gdy dochodzi co do czego to człowiek nie wie, gdzie ma głowę. Każdy obraz może być przetworzony wielokrotnie (ilość wyliczeń określają oktawy) na podstawie częstotliwości i amplitudy. Ale to by było za proste, żeby po prostu losować wartość dla danego piksela – należałoby jeszcze oprzeć to o interpolację wylosowanych składowanych. Ale składowe z kolei pochodzą z wygładzenia obrazu w oparciu o rogi, boki i środek. A te wartości są z kolei już wygenerowane – pod koniec artykułu jest pseudokod, gdzie można to prześledzić, a w razie wątpliwości patrzeć powyżej i czytać dokładniej o kolejnych elementach. Ja akurat w klasie CPerlinNoise dałem możliwość wyboru generacji pomiędzy obrazem 1D, 2D i 3D, którego nie ma w artykule, ale w oparciu o niego można łatwo i analogicznie to skonstruować. Jedyne problemy to większa ilość kombinacji, które należy przetworzyć przy interpolacji (liniowej lub cosinusowej) i przy wygładzaniu, gdzie mamy nie 3, nie 9, tylko 27 części do prześledzenia. Jak łatwo zauważyć, jest to trójka podniesiona do potęgi n, gdzie n to wymiar w jakim generujemy – przy czterech byłoby 81 części branych pod uwagę. Można też nieco zuniwersalizować funkcje przetwarzające szum – w pseudokodzie są funkcje w stylu InterpolatedNoise_1 (czyli działająca na sąsiednich pikselach – jeśli byłaby dwójka, to oddalonych o dwie jednostki) i jest napisane, żeby stworzyć odpowiedniki dla większych iteracji pętli. Uznałem jednak, że nie ma co się bawić w dodatkowe funkcje, zwłaszcza, że programista korzystający z tej klasy nie jest zainteresowany samym procesem. Dlatego też przykładowy nagłówek funkcji wygląda tak:

   1: float CPerlinNoise::SmoothNoise (unsigned iteration, float x, float y, float z)

Argument iteration to właśnie aktualna iteracja (ich ilość zależy od oktaw), którą właśnie przetwarza pętla, natomiast parametry x, y i z to oczywiście współrzędne obrazu, który aktualnie generujemy. A co, jeśli nie chcemy trójwymiaru tylko porządny szum dwuwymiarowy? Po prostu jako z jest podawane 0 – nie musimy się tym przejmować.

Nie uważam, że zrobiłem wszystko dobrze. Ba, jestem przekonany, że sama klasa nie działa, zwłaszcza, że jej nie testowałem. A testy jednak odgrywają ogromną rolę – szczególnie, jak się ich nie przeprowadza. Jednak dla własnych badań czy po prostu ciekawskich, zamieściłem klasę na Chomiku – wymaga to wprawdzie modułu CMath (który jest rozwinięciem skromnej biblioteki HMath, też niegdyś umieszczonej na Chomiku), ale większość potrzebnych funkcji zawiera standardowa biblioteka C++ <cmath>, natomiast takie rzeczy jak interpolacja są prawie żywcem wzięte z artykułu. Tradycyjnie czekam na komentarze i będzie mi niezmiernie miło, jeśli ktoś dzięki mnie głębiej zainteresuje się tematem. No i poprawi moje błędy. Choć niewykluczone, że kiedyś też zanurzę się głębiej w tej kwestii, także od strony czysto matematycznej.

Trawa

Początkowo zainteresowałem się szumem Perlina właśnie ze 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 tego demka firmy NVIDIA (screen po prawej) i przejrzeniu (dobra – przeglądnięciu obrazków) tego artykułu. Sama idea, jaką sobie założyłem, polega na wczytaniu czarno-białego obrazu TGA (przynajmniej na razie dwukolorowego, może potem się to zmieni) i wyszukania punktów, gdzie mają być rozstawione kępki trawy. Każda kępka to trzy skrzyżowane czworokąty, gdzie każdy ma własną teksturę (nie ma wymogu, aby były różne, ale muszą być podane wszystkie). Klasa CGrass posiada własny manager tekstur, więc wszystko dzieje się w miarę automatycznie. Wykorzystana też została struktura samorozszerzającej się tablicy typu float, czyli CArrayFloat. Zastosowałem to rozwiązanie ze względu na ogromną ilość współrzędnych wierzchołków (12 wierzchołów, czyli 36 elementów tablicy na jedną kępkę) i to akurat się sprawdza. Można też ustawić rozpiętość trawy, jej wysokość, skalowalność… Ale o tym się rozpiszę, jak uda mi się szczęśliwie dokończyć cały mechanizm.

scr001 Generalnie, problem pojawia się w procesie renderowania (jak zwykle). Na pierwszym obrazku (ale moim – nie czasem tym z NVIDII, takim specjalistą nie jestem) widać ładne (choć rozstrzelone) krzaczki. Wszystko byłoby piękne, gdyby nie to, że to wersja bez shaderów – wtedy testowałem rendering bez ich udziału. Oczywiscr002ście, po ich dołączeniu, wszystko się cudowne posypało, co akurat mnie specjalnie nie zaskoczyło – jakoś często zdarzają mi się przygody z programami dla GPU. Efekt widać na drugim screenie (tak, ten z czarnymi i wesoło machającymi tapetami) – cały problem tkwi w kanale alfa, a raczej w sposobie jego omijania. W czystym OpenGL zadziałało:


   1: glEnable(GL_ALPHA_TEST);
   2: glAlphaFunc(GL_GREATER, g_fAlphaBarrier);

Natomiast kooperacja tego kawałka kodu z shaderami przebiegła nie po mojej myśli. Dlaczego tak się dzieje – chwilowo nie wiem i szczerze mówiąc nie mam czasu teraz się tym zająć – czekają inne projekty. I sesja.

Pozdrawiam i dziękuję – SceNtriC.

sobota, 3 stycznia 2009

Mgiełka, plakaty tudzież niezidentyfikowana animowana maź

Jakiś młody człowiek zamiatał stopnie – teoretycznie. W praktyce przesuwał brud z miejsca na miejsce, dostarczając mu nowych widoków i okazji poznania nowych przyjaciół.

Terry Pratchett, “Maskarada”, Wyd. Prószyński i S-ka, s. 13, ok. 6-7 cm od górnej krawędzi strony i 5-11 cm od prawej krawędzi strony

Generalnie, powyższy cytat nie ma nic wspólnego z treścią notki. W praktyce oczywiście dałoby się wysnuć konkluzję – książkę Maskarada (należącą oczywiście do serii Świata Dysku) dostałem pod choinkę, podczas Wigilii, a Wigilia odbywała się podczas Świąt, a Święta były elementem Czasu Wolnego Od Uczelni, a Czas Wolny Od Uczelni był okresem, w czasie którego pisałem Program Zaliczeniowy, a w przerwach pomiędzy pisaniem Programu Zaliczeniowego pisałem kolejne elementy Własnego Frameworka, a z kolei w czasie, gdy próbowałem pisać Własny Framework, dostawałem polecenie wykonania czynności zwanej Sprzątaniem. Zatem można wydedukować wniosek płynący z cytatu – przyjemności wiążą się z nieprzyjemnościami…

A tak naprawdę, po prostu ten fragment mi się strasznie spodobał i chciałem się nim podzielić.

W każdym razie, faktycznie udało mi się napisać ok. 88% (no dobrze, 87%) trzeciego i ostatniego programu, który mam oddać w ramach zajęć z Podstaw Programowania. Zostały mi tylko trzy rzeczy – stworzenie systemu pomocy oraz gruntowne przetestowanie aplikacji i denerwowanie się błędami, które znajdę na 10 minut przed oddaniem. Najlepszym relaksem podczas tworzenia tego typu rzeczy jest pisanie czegoś na boku. Tym czymś okazał się oczywiście framework Yesta.

Poranna mgła w ściśle określonym obszarze

Czyli, krótko mówiąc, mgła objętościowa zwana też wolumetryczną. Oparta na lekcji Nehe, mimo początkowej niesforności związanej z rozszerzeniami OpenGL dała się stosunkowo łatwo zaimplementować do przestrzeni nazw CGems. Po oglądnięciu efektów pracy Nehe (a potem mojej) doszedłem jednakże do wniosku, że mgła odległościowa jest dużo bardziej efektywna w większości przypadków. Weźmy chociażby podróżowanie po lochach – zastosowanie mgły znajdującej się w ściśle określonym miejscu widzę jedynie w przypadku, kiedy chcemy zasłonić jakiś fragment, który nie ma być widoczny dla gracza albo jest elementem zawiłej zagadki w stylu “idź i zobacz, co znajduje się za tym czymś”. O wiele lepiej sprawują się tutaj standardowe algorytmy na renderowanie mgły. A co do użycia tego w Yeście:

   1: float fogColor[4] = {0.6f, 0.3f, 0.0f, 1.0f};        
   2: SetQuality(QL_NICEST);
   3: SetFogSettings(FT_LINEAR, 0.5f, 0.0f, 1.0f, fogColor);
   4: SetEffect(GE_FOG, true);
   5: SetFogType(FA_VOLUMETRIC); // SetFogType(FA_DEPTH_BASED);
   6:  
   7: //... (przechodzimy do funkcji renderującej)
   8:  
   9: EnableEffects();
  10:  
  11: //... (renderowanie z użyciem FogCoord(x))
  12:  
  13: DisableEffects();

Jakoś tak się złożyło, że w momencie włączania pierwszej mgły do frameworka nie znałem jeszcze shaderów, zatem jej generacja polega na skróconym zapisanie funkcji OpenGL-owych. Chociaż, czy teraz robiłbym to inaczej… Śmiem wątpić – jakoś przeraża mnie myśl o nakładaniu się paru shaderów jednocześnie na siebie – a zbytniego ich skomplikowania, aby obsługiwały parę efektów naraz, nie lubię. Taki dziwny ze mnie typ.

Plakaty – cholera, znowu walka wyborcza…

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

użyć na przykład do generowania trawy czy też drzewek. W tutorialu na Lighthouse 3D została przygotowana scenka z bałwanami i drzewami, które są niczym innym jak czworokątami z nałożoną teksturą drzewa. W momencie jednak, kiedy odpali się program, widać, że drzewa zwracają się w kierunku obserwatora. Wprawdzie nie machają gałązkami do kamery (to jeszcze nie ten poziom schizofrenii), ale starają się ukryć bycie płaskim obrazem poprzez odpowiednie ustawianie się. Pod linkiem, który podałem parę linijek powyżej, jest dokładnie opisany sposób plakatowania – mamy wersję sferyczną (która jest ustawiona 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 Lighthouse 3D. Prawdziwe plakatowanie jest przedstawione na rysunku po prawej – obiekty są zwrócone przodem do obserwatora niezależnie od jego położenia. Można wręcz rzec, że w tym przypadku elementy sceny są dużo odważniejsze – nie zawsze jest miło spoglądać na zimnych i pozbawionych wyrazu niczym surykatka po wejściu na minę bohaterów gier komputerowych. A one to robią. Znaczy, nie surykatki.

“Patrz, woda! Woda! No, to jest woda! I CO Z TEGO, ŻE NIE MA FAL?!"

water002 Całkiem niedawno zacząłem przeglądać dokładniej pracę magisterską Regedita i doszedłem do wniosku, że pewne rzeczy nie są aż tak strasznie – po prostu nie ma dobrego opisu, jak się za to zabrać. Podniesiony tą myślą na duchu, żywiołowo zabrałem się do renderowania wody. Nie będę tutaj za bardzo wnikał w szczegóły implementacji – polecam zaznajomić się z pracą Rega (pozdrawiam i gratuluję przystępnie, ale i profesjonalnie napisanej magisterki), gdzie jest to dobrze water004 opisane, a także do źródeł silnika The Final Quest oraz demka przedstawiającego możliwości engine’u. Mogę w sumie tylko nadmienić ogólną konstrukcję – cały proces polega na podaniu współczynników i wartości wektorów wiatru, wyliczeniu tego czarnomagicznymi sposobami, uwzględnienie koloru nieba, horyzontu, kaustyk i – co ciekawe – wody, a także podaniu mapy normalnych oraz kaustyk. water006 To tak z grubsza. Jako, że musiałem coś zepsuć (taka tradycja), wiele rzeczy mi nie wychodziło (co widać na screenach), ale uznałem to za warte do włożenia w szufladkę “Powinno działać przy odpowiednio dobranych parametrach. Powinno.”. Po 1,5 dnia uznałem też, że nadszedł czas na zastosowanie Pierwszej Reguły SceNtriCa dotyczącej shaderów. Brzmi ona: “Jeśli S jest shaderem, a X liczbą należącą do zbioru liczb całkowitych dodatnich, to w celu ulepszenia efektu należy dopisać X losowych i niezrozumiałych dla autora linijek kodu do jednego lub water007 obu programów shadera pod warunkiem, że całość będzie poprawna pod względem semantycznym i syntaktycznym.”. Jest jeszcze niestety Druga Reguła SceNtriCa dotycząca shaderów, która jest uzupełnieniem pierwszej. Brzmi ona: “Skuteczność stosowania Pierwszej Reguły SceNtriCa wynosi ok. 10%. W reszcie przypadków powoduje maniakalny śmiech pochodzący od autora.”. Tak mniej więcej przedstawiała się sprawa z próbą dodania parallaxa do wody (ciecz nagle przestała się ruszać – widocznie się obraziła). Jedyne co zadziałało i spowodowało, że przedstawiony powyżej drugi i trzeci screen mocno się od siebie różnią, to ten kawałek kodu zawarty w shaderze wierzchołków:

   1: float3 lightP = mul((float3x3)Matrix, LightPosition);
   2: float3 LightDir = normalize(lightP - float3(pos.x, pos.y, pos.z));
   3: float3 binormal = cross(In.Tangent, In.Normal);
   4: float3x3 tbnMatrix = float3x3(In.Tangent, binormal, In.Normal);
   5: Out.LightDir = mul(tbnMatrix, LightDir);

Konkretnie chodzi o trzy ostatnie linijki – dwie pierwsze są standardowe (wtedy LightDir jest tak naprawdę Out.LightDir). Po próbie wyliczenia tego na papierze (tak, czasami umiem się obyć bez komputera) wyszło mi, że wektor powstały po zmianie jest mocno powiększony w stosunku do tego “standardowego” (przynajmniej po wzięciu przeze mnie przykładowych wartości). W każdym razie, światło powstałe przy użyciu tego wektora wyliczone we fragment shaderze jest dużo mocniejsze – co mi się akurat optycznie podoba. Jako, że zżerała mnie też ciekawość przy próbie implementacji czegoś co się nazywa Environmental Mapping (mapowanie środowiskowe), wprowadziłem drugi rodzaj programów shadera, właśnie z użyciem “refrakcji” (bo czy to jest taka prawdziwa refrakcja, to bym się mocno zastanawiał, a i tak pewnie bym wcześniej zrezygnował z rozważań), co przy okazji wymusiło na mnie zajęcie się mapami sześciennymi (cube mapami). Czy efekt jest warty nadmienienia – widać (a raczej nie widać) na ostatnim screenie. W każdym razie, było to ciekawe doświadczenie.

Natomiast, jeśli komuś przyszłoby do głowy zajmować się wodą w dużo bardziej realistyczny sposób, polecam tę pracę. Osobiście skończyłem czytać po pierwszym akapicie. Ale nawet fajna fabuła.

Nie będzie dzisiaj mrożącego krew w żyłach i zapierającego… Nieważne – po prostu to był kolejny raport z placu boju na płaszczyźnie frameworkowej. Uff, to teraz wypada się zająć w końcu uczelnią… No i kolejną książką ze Świata Dysku.

Pozdrawiam i dziękuję – SceNtriC.