Używanie vertex i fragment (lub jak kto woli - pixel) shaderów do wykonania brudnej roboty w czasie renderingu może przynieść wiele korzyści. Najbardziej oczywiste jest przeniesienie niektórych obliczeń graficznych wykonywanych normalnie na zwykłym procesorze na procesor graficzny (GPU). Cg to prosty język do pisania bardzo potężnych shaderów.
Ta lekcja ma kilka celów. Po pierwsze - zaprezentowanie prostego vertex shadera, który robi coś konkretnego, bez wchodzenia w zagadnienia oświetlenia etc? Po drugie - przybliżyć podstawowe mechanizmy potrzebne do uruchamiania vertex shaderów, które dają widoczne wyniki, korzystając z OpenGL. Jako taka, jest ona skierowana do początkujących, zainteresowanych językiem Cg, którzy mają już pewne doświadczenie z OpenGL.
Ta lekcja jest oparta o najnowszą bazę kodów NeHeGL (ang. NeHeGL Basecode). Na stronie nVidii http://www.developer.nvidia.com znajdziesz wiele informacji o samym języku Cg, zaś na http://www.cgshaders.org sporo ciekawych shaderów.
NOTA: Lekcja ta nie ma na celu nauczenia Cię wszystkiego co powinieneś wiedzieć o pisaniu shaderów w języku Cg. Tym celem jest raczej nauka sposobu ładowania i uruchamiania programów Cg przy wykorzystaniu biblioteki OpenGL.
Pierwszym krokiem (o ile nie zrobiłeś tego wcześniej) jest ściągnięcie kompilatora Cg ze strony nVidii. Ważne jest by pobrać wersję 1.1, ponieważ nVidia poczyniła znaczne zmiany pomiędzy wersjami 1.0 i 1.1 (inne nazewnictwo zmiennych, pozamieniane funkcje, etc?), i kod kompilowalny dla jednego nie koniecznie musi działać z drugim.
Kolejnym krokiem jest dodanie ścieżek w Visual Studio do plików nagłówkowych i bibliotek (plików *.lib). Ja sam po prostu skopiowałem biblioteki...
...i pliki nagłówków (GLext.h do pod folderu GL)...
Teraz możemy przystąpić do właściwej części tej lekcji.
Informacje zawarte w tym podręczniku dotyczące Cg pochodzą głównie z instrukcji użytkownika zestawu narzędzi Cg (ang. Cg Toolkit User's Manual), którą można pobrać (w formacie *.pdf) ze strony nVidii.
Jest kilka bardzo ważnych rzeczy, o których musisz pamiętać mając do czynienia z programami wierzchołków (a później także fragmentów). Po pierwsze pamiętaj o tym, że program wierzchołków [ang. vertex program, czyli po prostu skompilowany kod vertex shadera - przyp. tłum] będzie wykonywany na KAŻDYM wierzchołku. Jedynym sposobem uruchamiania takiego programu tylko na wybranych wierzchołkach jest wcześniejsze załadowanie/zwolnienie programu dla każdego indywidualnego, bądź wybranej grupy wierzchołków, albo przesyłanie wierzchołków oddzielnie do tego potoku (ang. stream), na którym wykonuje się program wierzchołków, i do potoku gdzie nie jest on wykonywany.
Dane wyjściowe z programu wierzchołków są dalej przesyłane do programu fragmentów, jeśli taki istnieje, w przeciwnym wypadku tymi danymi zajmie się dalej standardowy potok renderingu.
I wreszcie, pamiętaj o tym, że program wierzchołków jest wykonywany przed rasteryzacją, a program fragmentów tuż po niej.
Pierwszą rzeczą jaką musimy zrobić jest utworzenie pustego pliku (nazwijmy go 'wave.cg'). Następnie musimy stworzyć strukturę zawierającą wszelkie zmienne i informacje, których będziemy używać w naszym shaderze. Dodaj ten kod do pliku wave.cg.
Każda z trzech zmiennych (position, color i wave) jest zakończona pewnym wyrazem po dwukropku (odpowiednio - POSITION, COLOR0 i COLOR1). Te predefiniowane wyrazy to tzw. semantyka (znaczenie zmiennej). W OpenGL, te wyrazy mówią kompilatorowi, które zmienne będa przypisane do których rejestrów sprzętowych. Główny program musi dostarczyć danych do każdej z tych zmiennych. Zmienna z semantyką POSITION jest WYMAGANA, ponieważ jest potrzebna w procesie rasteryzacji. Jest to jedyna wartość, która musi być dostarczona do programu wierzchołków i z niego zwrócona [oczywiście zwrócona po przetworzeniu - przyp. tłum].
Kolejnym krokiem jest stworzenie struktury zawierającej dane wyjściowe, które będą przesłane do programu fragmentów po rasteryzacji.
Jak wszystkie zmienne wejściowe, taki i te wyjściowe mają przypisane odpowiednie semantyki. Hpos reprezentuje pozycję przetransformowaną w homogenicznej przestrzeni przycięcia. Col0 reprezentuje zaś kolor wierzchołka po zmianach przeprowadzonych programie wierzchołków.
Ostatnią rzeczą jaka nam pozostała jest napisać właściwy program wierzchołków, używając obu naszych nowych struktur.
Identycznie jak w języku C, definiujemy naszą funkcję tak, aby zwracała jakąś wartość (w tym przypadku strukturę vfconn), miała nazwę (tutaj jest to 'main', ale może być cokolwiek), i argumenty. W naszym przykładzie, używamy naszej struktury appdata jako wejścia (zawiera obecną pozycję wierzchołka, kolor, i wartość wave, z której skorzystamy aby stworzyć poruszającą się falę na powierzchni siatki).
Przesyłamy także parametr z kwalifikatorem uniform, który jest aktualną macierzą model-widok (ang. modelview matrix) przesłaną z OpenGL z naszej macierzystej aplikacji. Kwalifikator uniform mówi kompilatorowi, że ta wartość nie zmienia się w czasie działania naszego programu wierzchołków. Macierz model-widok jest wymagana, aby przetworzyć wierzchołki do przestrzeni homogenicznej.
Deklarujemy zmienną przechowującą nasze zmodyfikowane wartości w vertex shaderze. Te wartości będą zwrócone, i przesłane do fragment shadera (jeśli jakiś istnieje).Teraz musimy wprowadzić te zmiany w naszych danych o wierzchołkach.
Zmieniamy pozycję na osi Y dla wierzchołka korzystając z obecnych wartości na osiach X i Z. Te wartości są dzielone odpowiednio przez 4.0 i 5.0 aby fala była bardziej gładka (aby zobaczyć co mam na myśli, zmień te wartości na 1.0).
Zmienna IN.wave zawiera ciągle rosnącą wartość dzięki czemu fala przemieszcza się wzdłuż naszej siatki. Ta wartość pochodzi z głównej aplikacji.Następnie obliczamy pozycję na osi pionowej Y z pozycji X / Y na siatce jako sinus wartości wave + obecna pozycja X albo Z. Na końcu mnożymy tą wartość przez 2.5 aby fala była bardziej zauważalna (wyższa).
Teraz zastosujemy standardowe operacje, które musimy wykonać na wierzchołku i zwrócimy wyniki naszych działań.
Najpierw transformujemy wierzchołek do homogenicznej przestrzeni przycięcia. Następnie przepisujemy kolor wejściowy do koloru wyjściowego, który został nam przysłany z aplikacji. Na końcu przesyłamy strukturę wyjścia do programu fragmentów (jeśli używamy takiego).
Teraz przejdziemy do głównego programu, który stworzy nam siatkę i uruchomi nasz vertex shader, jako efekt wyjściowy zobaczymy ciekawie wyglądającą falę.
Główne kroki jakie poczynimy by pracować z naszym shaderem Cg to wygenerowanie siatki, załadowanie i kompilacja programu Cg i uruchomienie go na początku rysowania sceny.Po pierwsze musimy przygotować kilka niezbędnych rzeczy. Trzeba dodać niezbędne nagłówki pozwalające korzystać z Cg wraz z biblioteką OpenGL. Więc po innych dyrektywach #include dodajemy jeszcze dwie swoje - nagłówki Cg i CgGL.
Teraz powinniśmy być gotowi do ustawienia nowego projektu i... zabieramy się do pracy! Zanim zaczniemy, musimy się upewnić że nasze Visual Studio skorzysta z odpowiednich biblioteki. Tak więc jeszcze dwie linijki kodu:
Następnie zdefiniujemy kilka zmiennych globalnych, pomocnych przy tworzeniu siatki i włączaniu/wyłączaniu naszego programu Cg.
Zdefiniowaliśmy rozmiar jako 64 punkty dla każdej krawędzi siatki (osie X i Z). Następnie stworzyliśmy tablicę wierzchołków dla siatki. Ostatnia zmienna jest wymagana do stworzenia efektu fali na siatce.
Teraz musimy zdefiniować kilka zmiennych specyficznych dla Cg.
Pierwszą zmienną jaką potrzebujemy jest CGcontext. Zmienna typu CGcontext jest 'kontenerem' przechowującym wszystkie nasze programy Cg. Standardowo potrzebujemy tylko jednego kontekstu dla wszystkich programów wierzchołków i fragmentów jakie mamy. Możesz wybrać dowolne programy z tego samego kontekstu używając funkcji cgGetFirstProgram i cgGetNextProgram.
Następnie zdefiniujemy zmienną typu CGprogram, przechowującą nasz program wierzchołków.
Nasza zmienna typu CGprofile bedzie przechowywała profil wierzchołków dla naszego programu.
Teraz zdefiniujemy zmienne, które połączą dane z naszego programu z ich odpowiednikami w vertex shaderze.
Każdy parametr typu CGparameter jest praktycznie uchwytem na odpowiednie parametry w programie wierzchołków.
Zajęliśmy się już zmiennymi globalnymi, możemy teraz przystąpić do generowania siatki i uruchamiania shadera.W naszej funkcji inicjalizującej, przed zwróceniem wartości "TRUE?, musimy dodać trochę nowego kodu.
Najpierw wywołujemy funkcję glPolygonMode, aby zmienić sposób wyświetlania sceny na 'druciany' (ang. wireframe) ponieważ gdybyśmy rysowali wypełnione trójkąty za pomocą standardowego cieniowania płaskiego to widok nie byłby zbyt interesujący, a tak nie ma tego problemu. Potem iterujemy po wszystkich wierzchołki naszej siatki, ustawiając wartości X i Z odpowiednio wokół środka układu współrzędnych. Wartość osi pionowej Y jest ustawiana dla każdego wierzchołka na 0.0f.Interesujące jest to, że wartości ustawiane dla siatki w tym punkcie programu nie zmieniają się w czasie jego wykonywania, oznacza to że siatka jest statyczna.
Gdy zakończyliśmy wszystkie operacje na siatce, przystępujemy do ustawień związanych z Cg.
Najpierw próbujemy utworzyć nowy kontekst CGcontext, który przechowywałby wszystkie nasze programy Cg. Jeśli wartość cgContext po próbie utworzenia bedzie wynosiła NULL, oznacza to, że ta próba nie powiodła się.Zazwyczaj występuje tylko jeden błąd związany z alokacją pamięci.
Teraz ustawiamy profil wierzchołków do użycia. Dla pobrania profilu fragmentów, musimy wywołać funkcję cgGLGetLatestProfile parametrem CG_GL_FRAGMENT. Jeśli wartością zwrócona przez funkcję jest CG_PROFILE_UNKNOWN, oznacza to że dana karta graficzna nie ma wbudowanej obsługi shaderów.
Mając profil, możemy użyć funkcji cgGLSetOptimalOptions. Ustawi ona odpowiednie argumenty dla kompilatora bazując na modelu GPU i sterowniku do karty graficznej. Te funkcje są używane za każdym razem gdy kompilowany jest program Cg (po prostu optymalizuje shader w czasie kompilacji tak, by wykonywał się najszybciej jak może na dostępnym sprzęcie i sterownikach).
Teraz możemy już utworzyć i skompilować nasz program wierzchołków z pliku. Wywołujemy funkcję cgCreateProgramFromFile, która załaduje i skompiluje program Cg z podanego pliku. Pierwszy parametr, typu CGcontext definiuje do którego kontekstu będzie dodany ten program. Drugi parametr definiuje czy funkcja załaduje i skompiluje shader z pliku z kodem źródłowym (CG_SOURCE), czy też może z pliku, który zawiera prekompilowany program Cg (CG_OBJECT). Trzeci parametr to ścieżka do pliku źródłowego. Czwarty parametr to profil, którego używamy przy kompilacji shadera (używaj profili wierzchołków dla programów wierzchołków, a profili fragmentów do programów fragmentów, w przeciwnym razie shader nie skompiluje się). Piąty parametr to nazwa głównej funkcji programu Cg. Ta główna funkcja może mieć dowolna nazwę (u nas jest to 'main'). Ostatni parametr służy do przekazywania innych parametrów do kompilatora Cg. Najczęściej jest to po prostu NULL.Jeśli cgCreateProgramFromFile z jakiegoś powodu zwróci błąd, możemy pobrać kod błędu funkcją cgGetError. Następnie możemy pobrać łańcuch znaków, który opisze nam błąd w postaci zrozumiałej dla człowieka, używamy do tego funkcji cgGetErrorString.
Już prawie skończyliśmy inicjalizację.
Następnym krokiem jest załadowanie programu, i przygotowanie go do dołączenia. Wszystkie programy muszą być wcześniej załadowane zanim będą przypisane do aktualnego stanu.
Ostatni krok inicjalizacji to przypisanie uchwytów zmiennych w naszej aplikacji do odpowiednich parametrów wewnątrz programu Cg. Jeśli parametr nie istnieje, cgGetNamedParameter zwróci NULL.Jeśli nie znamy nazw parametrów wewnątrz programu Cg, to używając funkcji cgGetFirstParameter i cgGetNextParameter możemy pobrać wszystkie parametry danego programu.
Skończyliśmy już inicjalizację programu Cg, teraz tylko zajmiemy się szybko posprzątaniem po sobie, i zostanie nam już to co najciekawsze - rysowanie.W funkcji Deinitialize, sprzątamy po naszych programach.
Po prostu wywołujemy funkcję cgDestroyContext dla wszystkich naszych zmiennych typu CGcontext (możemy mieć kilka, ale zazwyczaj jest tylko jedna). Możesz samodzielnie niszczyć każdy program korzystając z funkcji cgDestoryProgram, ale wywołanie cgDestoryContext wszystkie programy przechowywane w danym kontekście wraz z nim.
Teraz musimy dodać trochę świerzego kodu do funkcji Update. Następujący kod sprawdza czy jest wciśnięta spacja, jeśli tak, zmieniamy wartość zmiennej cg_enable na wartość przeciwną.
I jeszcze ten mały kawałek, który sprawdza czy spacja została puszczona i zmienia wartość zmiennej sp na false.
W końcu skończyliśmy z tym wszystkim, czas na coś ciekawszego - uruchomimy nasz program wierzchołków i wywołamy falę na naszej siatce.
Ostatnią funkcją jaką zmodyfikujemy jest funkcja Draw. Dodamy trochę naszego kodu pomiedzy wywołaniami glLoadIdentity i glFlush.
Po pierwsze przesuwamy nasz punkt patrzenia z dala od środka układu współrzędnych, aby móc oglądać naszą siatkę w całej okazałości, używamy do tego funkcji gluLookAt.
Następną rzeczą jaką chcemy zrobić jest ustawienie macierzy model-widok, którą mamy w programie wierzchołków na taką jaką używa OpenGL. Musimy to zrobić, aby można było przetransformować pozycję wierzchołka do przestrzeni homogenicznej, a robimy to mnożąc macierz model-widok przez pozycję wierzchołka.
Następnie musimy włączyć nasz program wierzchołków. Funkcja cgGLEnableProfile włączy za nas podany profil korzystając z odpowiednich funkcji OpenGL. Funkcja cgGLBindProgram dołączy (przypisze) nasz program do aktualnego stanu. Tym samym nakazaliśmy naszej karcie graficznej zamiast standardowego przetwarzania wierzchołków, przetwarzanie za pomocą naszego programu wierzchołków. Nasz program Cg będzie wykonywał się na każdym wierzchołku dopóki nie wyłączymy powiązanego z nim profilu.
Teraz ustawiamy kolor - jednolity dla wszystkich wierzchołków. Ta wartość może być dowolnie zmieniana w czasie działania programu (możemy to też zrobić za pomocą shadera) by otrzymać różnokolorową siatkę.
Pamiętaj, aby sprawdzić przedtem czy zmienna cg_enable jest ustawiona na true, jęsli tak nie jest to nie wykonujemy żadnych z podanych wyżej operacji.Jesteśmy już gotowi do wyrenderowania naszej siatki!
Aby wyrenderować naszą siatkę, robimy pętlę wzdłuż osi Z i X (przerabiamy wszystkie wierzchołki w kolumnach siatki). Dla każdej kolumny, rozpoczynamy rysowanie paska trójkątów.Dla każdego wierzchołka jaki wyrenderujemy, dynamicznie przydzielamy wartość parametru wave. Ponieważ ten parametr jest połączony ze zmienną wave_movement w naszej aplikacji, która jest ciągle zwiększana, nasza sinusoidalna fala wydaje się 'przepływać' przez całą siatkę.
Następnie przesyłamy wierzchołek do karty graficznej, która automatycznie wykona na nim nasz program wierzchołków. Powoli zwiększamy wartość naszego parametru wave_movement dzięki czemu uzyskujemy gładki ruch fali.Jeśli wartość zmiennej wave_movement przekroczy TWO_PI ustawiamy ją od nowa na 0.0f. Wartość TWO_PI jest zdefiniowana na początku programu.
Gdy skończymy renderować scenę, sprawdzamy czy wartość cg_enable jest wciąż ustawiona na true, jeśli nie - wyłączamy nasz profil i rysujemy już tylko to co zechcemy.
Owen Bourne (o.bourne@griffith.edu.au)