Grafika 3D
 
  Zarejestruj się
::  Newsy  ::  Pliki  ::  Twoje Konto  ::  Forum  ::
Menu
· Strona główna
· Forum
· Linki
· Lista u?ytkowników
· O nas...
· Pliki
· Statystyki
· Twoje Konto
Tutoriale
· API
· Matematyka
· Teoria
· Direct3D
· OpenGL
· Techniki
Kto Jest Online
Aktualnie jest 35 gość(ci) i 0 użytkownik(ów) online.

Jesteś anonimowym użytkownikiem. Możesz się zarejestrować za darmo klikając tutaj
Tutoriale - Techniki - Cg czę?ć trzecia

Witam w trzecim (i jak na razie ostatnim) artykule poświęconym Cg. Tak jak wspominałem poprzednio tym razem zajmiemy się kodem po stronie API graficznego, czyli omówię Cg runtime.

Cg runtime nie jest częścią języka Cg jako takiego. Jest biblioteką umożliwiającą podłączenie shaderów Cg do naszych ukochanych API, jak również zarządzanie ich danymi podczas działania programu. Najważniejsze jest jednak to, że wcale nie musimy używać tej biblioteki. Jeśli bowiem użyjemy kompilatora Cg wywoływanego z linii poleceń, to do programu będziemy mogli wczytać shadery w taki sam sposób jak zawsze (mówiłem już o tym poprzednio). Jednakże używanie tej biblioteki jest dość wygodne i posiada sporo udogodnień nie tylko w kwestii kompilacji, ale również przesyłania danych do shaderów oraz zapewnia kompatybilność (oraz optymalizację) z następnymi generacjami sprzętu.

Cg runtime składa się w zasadzie z dwóch modułów. Pierwszy z nich to rdzeń tej biblioteki. Funkcje rdzenia są zawsze takie same, bez względu na to, czy są wywoływane w aplikacji wyświetlającej obraz za pomocą OpenGL czy w aplikacji z Direct3D. Używanie rdzenia jest niezbędne (acz niewystarczające). Dołącza się go oczywiście standardowo, czyli nagłówek:
#include <CG/cg.h>
oraz bibliotekę, którą można dołączyć w opcjach projektu bądź za pomocą instrukcji pragma:
#pragma comment(lib, "cg.lib")
Drugą częścią biblioteki Cg runtime jest pewnego rodzaju nakładka na API graficzne, która dostarcza funkcje charakterystyczne dla danego API. W chwili obecnej wyróżniamy trzy takie moduły. Pierwszy z nich dołączają użytkownicy OpenGL (cgGL.h, cgGL.lib). Następne dwa przeznaczone są dla Direct3D w wersji 9 (cgD3D9.h, cgD3D9lib) oraz 8 (cgD3D8.h, cgD3D8.lib). Po dołączeniu tych modułów możemy przystąpić do działania.

W aplikacji Cg reprezentowane jest przez kontekst. Kontekst Cg służy do przechowywania wszystkich shaderów jak również ich danych. Pojęcie kontekstu bliższe jest zapewne użytkownikom OpenGL. Użytkownicy Direct3D mogą traktować kontekst Cg tak samo jak obiekt główny Direct3D, z tą różnicą, że to nie kontekst dostarcza metod do tworzenia i obsługi reszty, ale to on jest przekazywany do funkcji (tak jak kontekst urządzenia w windows). Utworzenie kontekstu jest banalnie proste. Należy gdzieś w programie umieścić zmienną typu CGcontext i wywołać funkcję cgCreateContext(). Wygląda więc to tak:
CGcontext context = cgCreateContext();
Pamiętać należy o zwolnieniu kontekstu po skończeniu pracy z Cg (najczęściej przy zamykaniu aplikacji). Do zwalniania kontekstu służy funkcja
void cgDestroyContext(CGcontext context);
Jako parametr należy oczywiście przekazać kontekst Cg, który chcemy zwolnić.

Istnieje jeszcze funkcja pozwalająca sprawdzić, czy dany kontekst jest poprawny. Funkcja ta to:
CGbool cgIsContext(CGcontext context);
Jak nietrudno się domyśleć zwróci ona wartość true jeśli, przekazany jako parametr, kontekst jest poprawny.

Ponieważ Cg to język służący do pisania shaderów, drugim najważniejszym typem danych jest typ CGprogram (jak widać składnia i nazewnictwo w Cg jest bardzo podobne do OpenGL). Zmienna ta definiuje oczywiście shader niezależnie od tego czy jest to program do obróbki wierzchołków czy pikseli. Do utworzenia programu Cg służą dwie funkcje, a przy tworzeniu program można wczytać w wersji skompilowanej bądź dopiero przeznaczonej do kompilacji:
CGprogram cgCreateProgram(CGcontext context, CGenum programType, const char *program, CGprofile profile, const char *entry, const char *args);
gdzie:
  • context - uprzednio utworzony kontekst Cg
  • programType - decyduje o tym, czy program jest już skompilowany, czy Cg ma to dopiero zrobić. Parametr ten może przyjmować dwie wartości:
    • CG_SOURCE - kiedy program to kod Cg i należy go skompilować
    • CG_OBJECT - kiedy program został już wcześniej skompilowany
  • program - wskaźnik na obszar pamięci, w którym znajduje się wczytany uprzednio kod programu
  • profile - jeden z profili Cg mówiący albo o tym, dla jakiego profilu skompilować kod, albo też o tym dla jakiej wersji został on już uprzednio skompilowany
  • entry - nazwa funkcji głównej naszego programu (tak jak main w C)
  • args - argumenty dla kompilatora (jako ciąg stringów zakończonych null'em). Można tu oczywiście przekazać NULL
Powyższa funkcja służy do tworzenia programu, jeśli jego kod został już wcześniej załadowany z jakiegoś pliku do pamięci. Jeśli jednak tak nie jest i nie chce nam się pisać funkcji ładującej kod z pliku, to można użyć drugiej funkcji jakiej dostarcza Cg:
CGprogram cgCreateProgramFromFile(CGcontext context, CGenum programType, const char *program, CGprofile profile, const char *entry, const char *args);
Jak widać lista parametrów tej funkcji jest identyczna jak poprzedniej, a ich znaczenie również jest takie samo, z jednym wyjątkiem. Parametr program oznacza tutaj nazwę pliku, z którego ma być wczytany program. Obie funkcje zwracają zmienną definiującą program Cg.

Po skończeniu używania programu należy go oczywiście zwolnić. Służy do tego funkcja
void cgDestroyProgram(CGprogram program);
do której należy przekazać program (zmienną) do usunięcia. Należy jednak zauważyć, że niszczenie programów Cg należy wykonać przed zniszczeniem kontekstu.

Aby sprawdzić, czy program jest skompilowany należy wywołać funkcję
CGbool cgIsProgramCompiled(CGprogram program);
która zwróci true jeśli program jest skompilowany, a false jeśli nie (co jest sprawą dość oczywistą). Możemy również wymusić kompilację (bądź rekompilację) programu za pomocą
cgCompileProgram(CGprogram program);
Ostatnią funkcją związaną z programami Cg, którą omówię (nie omawiam wszystkich oczywiście bo się to mija z celem - zainteresowanych odsyłam do dokumentacji) będzie bardzo przydatna w szukaniu błędów kodu skadera funkcja
const char* cgGetLastListing(CGcontext context)
Zwróci ona wyjście wygenerowane przez kompilator. Można sobie coś takiego zapisać w pliku, wyświetlić w oknie konsoli bądź oknie debugera, aby dowiedzieć się co i gdzie poszło nie tak.

W tym momencie w zasadzie wszystko już powinno być ok, ale pozostała jeszcze kwestia przekazania do programów Cg parametrów. O ile dane wierzchołków znajdą się tam automatycznie o tyle pozostaje kwestia wszelkiego rodzaju macierzy, tekstur i w ogóle parametrów zaopatrzonych w słowo kluczowe uniform. W normalnym przypadku (przy standardowych shaderach) wystarczyłoby przekazać odpowiednie parametry do odpowiedniego rejestru i koniec. Tutaj jednak nie wiemy do jakiego rejestru jest przypisana jaka zmienna. Znamy za to jej nazwę i poprzez tą nazwę możemy przekazać tam jakieś parametry. Ogólnie parametr kodu Cg w aplikacji definiuje zmienna CGparameter, a sam parametr można pobrać za pomocą poniższej funkcji (choć jest to tylko jedna z wielu metod)
CGparameter cgGetNamedParameter(CGprogram program, const char *name);
Pierwszym parametrem jest oczywiście identyfikator programu z którego chcemy pobrać parametr o nazwie przekazanej w drugim parametrze.

Wszystkie omówione powyżej funkcje pochodzą z rdzenia Cg. Jak widać za ich pomocą możemy przygotować wszystko do przesłania do API, ale do tego celu potrzebujemy czegoś więcej. Poniżej znajdują się osobne opisy dla użytkowników OpenGL i Direct3D.
  • Cg i OpenGL
Kiedy już mamy przygotowane wszystkie programy i chcemy je załadować do OpenGL należy wywołać ciąg trzech instrukcji. Pierwsza z nich to
glGLLoadProgram(CGprogram program);
funkcja ta załaduje skompilowany program Cg do OpenGL. Następnie należy włączyć profil, w którym ma działać program. Tak po prawdzie to sprowadza się to (wewnętrznie w Cg) do ustawienia odpowiednich zmiennych stanu OpenGL za pomocą funkcji glEnable (np. glEnable(GL_VERTEX_PROGRAM_ARB)). Profile włącza się za pomocą funkcji
cgGLEnableProfile(CGprofile profile);
Możliwe do przekazania wartości wymieniłem w poprzedniej części. Nietrudno się domyśleć, że do wyłączenia profilu służy funkcja
cgGLDisableProfile(CGprofile profile);
Po włączeniu odpowiedniego profilu należy jeszcze nasz program (jak wszystko w OpenGL) ustawić jako bieżący, czyli 'zbindować'. Służy do tego polecenie
cgGLBindProgram(CGprogram program);
Kiedy program jest już załadowany, możemy się zając parametrami. Mając w ręce parametr potrzebujemy jakichś funkcji do przypisania mu wartości. Cg dostarcza dość pokaźny zbiór funkcji za pomocą których możemy przypisać wartości do parametru. Przykładową funkcją jest
void cgGLSetParameter4f(CGparameter parameter, float x, float y, float z, float w);
Składnia tej funkcji znana jest w zasadzie wszystkim użytkownikom OpenGL. Nie powinien również dziwić fakt, że funkcja ta występuje w wielu wersjach, w zależności od tego ile danych chcemy przesłać oraz jakiego są one typu. Można przesyłać oczywiście tablice i modyfikacja nazwy funkcji w takim wypadku wygląda dokładnie jak modyfikacja funkcji OpenGL, czyli na końcu dodajemy literkę v.

Ponieważ jednak parametrami programów Cg mogą być również macierze, toteż Cg musi dostarczyć również jakieś funkcje do przesyłania macierzy. Służy do tego rodzina funkcji o nazwie
void cgGLSetMatrixParameterfr(GLparameter parameter, const float *matrix);
Mówię rodzina, ponieważ dwie ostatnie litery w nazwie funkcji mogą się zmieniać. O ile pierwsza litera jest oczywista (typ parametru), o tyle znaczenie drugiej może być nieco niezrozumiałe. Ostatnia litera nazwy funkcji może być jedną z dwóch - r albo c. Różnica między nimi polega na sposobie ułożenia macierzy, która chcemy przesłać. Macierz może być bowiem ułożona w pamięci kolumna po kolumnie, lub wiersz po wierszu (z angielskiego row-order oraz column-order). To jak dane są ułożone zależy tylko i wyłącznie od sposobu w jaki to zrobicie. Jedni wolą tak, inni tak. Zaczynacie się pewnie zastanawiać skąd pobrać dane macierzy w OpenGL żeby ją przekazać do Cg i jak będą one ułożone ;). Na szczęście Cg dostarcza również funkcję, która automatycznie pobierze dane jednej z predefiniowanych macierzy z OpenGL.
void cgGLSetStateMatrixParameter(CGparameter parameter, GLenum stateMatrixType, GLenum transform)
gdzie:
  • stateMatrixType - określa macierz, której dane mają być przekazane do Cg.
    • CG_GL_MODELVIEW_MATRIX - macierz widoku modelu
    • CG_GL_PROJECTION_MATRIX - macierz projekcji
    • CG_GL_TEXTURE_MATRIX - macierz tekstury
    • CG_GL_MODELVIEW_PROJECTION_MATRIX - połączona macierz widoku modelu i projekcji
  • transform - określa jak dane macierzy mają być przekazane do Cg
    • CG_GL_MATRIX_IDENTITY - macierz zostanie przekazana bez modyfikacji
    • CG_GL_MATRIX_TRANSPOSE - przed przekazaniem do Cg macierz zostanie transponowana
    • CG_GL_MATRIX_INVERSE - przed przekazaniem do Cg macierz zostanie obrócona
    • CG_GL_MATRIX_INVERSE_TRANSPOSE - przed przekazaniem do Cg macierz zostanie obrócona, a następnie transponowana
Kolejnym typem danych jaki możemy przekazać są tablice. Służy do tego kilka funkcji różniących się między sobą, jak zwykle, ostatnią literą i cyfrą
void cgGLSetParameterArray1f(CGparameter parameter, long startIndex, long numberOfElements, const float *array);
Powyższa funkcja służy do przekazywania tablic wartości wektorowych i skalarnych. Jak nietrudno się domyśleć drugi parametr oznacza indeks, od którego Cg ma zacząć pobierać dane z tablicy. Trzeci parametr to oczywiście ilość elementów, które mają zostać pobrane (nazwy mówią same za siebie), a ostatni to wskaźnik na tablicę, z której te dane mają zostać pobrane. Drugi rodzaj tej funkcji służy do przekazywania tablic macierzy:
void cgGLSetMatrixParameterArrayfr(CGparameter parameter, long startIndex, long numberOfElements, const float *array);
Jak widać parametry tej funkcji niczym nie różnią się od poprzedniej. Zmieniła się tylko nieco nazwa, tak aby można było rozróżnić sposób ułożenia danych w pamięci (tak jak uprzednio przez literki r oraz c)

Ostatnim typem parametrów jaki omówię są tekstury. Jak pamiętamy w Cg tekstury reprezentowane są poprzez samplery i odnoszą się tylko i wyłącznie do programów pikseli. Żeby ustawić parametr będący teksturą należy wykonać dwa kroki. Najpierw należy wywołać funkcję
void cgGLSetTextureParameter(CGparameter parameter, GLuint texture);
gdzie texture to dobrze wszystkim znany identyfikator tekstury. Następnym krokiem jest włączenie tejże właśnie przekazanej zmiennej (włączenie samplera). Robi się to za pomocą funkcji
void cgGLEnableTextureParameter(CGparameter parameter);
Funkcja ta, aby zadziałała, musi zostać wywołana po uprzedniej, ale oczywiście przed miejscem jej wykorzystania. Po zakończeniu pracy z daną teksturą możemy ją wyłączyć za pomocą funkcji
void cgGLDisableTextureParameter(CGparameter parameter);
I to w zasadzie wszystko co jest wymagane, aby odpalić program Cg w OpenGL. Poniżej (pod opisem dla Direct3D) znajduje się przykładowy program w Cg, a na samym końcu znajdziecie oczywiście linki do kodu przykładowej aplikacji.

  • Cg i Direct3D
Chcąc załadować programy Cg do aplikacji korzystającej z Direct3D musimy zrobić dość niewiele. Pojawiają się tutaj jednak dwie kwestie. Po pierwsze Cg dostarcza osobne funkcje dla Direct3D w wersji 8, a osobne dla wersji 9. Ja omówię tylko wersję dla Direct3D 9, bo nie ma sensu omawiać czegoś, czego się już praktycznie nie używa. Druga kwestia polega na tym, że istnieją dwie metody podłączenia Cg do D3D, niejako dwa interfejsy - interfejs podstawowy i rozszerzony. Zajmę się tylko interfejsem rozszerzonym, gdyż podstawowy nie oferuje w zasadzie nic ciekawego. Zainteresowanych interfejsem podstawowym odsyłam do dokumentacji Cg. No więc dobrze. Aby zacząć zabawę z Cg i Direct3D 9 oraz interfejsem rozszerzonym :) należy najpierw przesłać do Cg wskaźnik na obiekt urządzenia Direct3D:
HRESULT cgD3D9SetDevice(IDirect3DDevice9 *device);
Obiekt Direct3D musimy przesłać z tego prostego powodu, że interfejs rozszerzony wykonuje wewnętrznie pewne wywołania funkcji Direct3D, a do tego jak wiadomo niezbędny jest obiekt urządzenia.

Przydatna może się okazać również możliwość automatycznego wyboru dostępnej wersji vertex/pixel shadera (czyli odpowiedniego profilu Cg). Dla vertex shadera robi się to za pomocą funkcji
CGprofile cgD3D9GetLatestVertexProfile();
Dla pixel shadera istnieje odpowiednik powyższej funkcji, czyli
CGprofile glD3D9GetLatestPixelProfile();
Obie funkcje wybierają w zasadzie najwyższą możliwą wersje vertex/pixel shadera. Oczywiście nie zawsze jest to zachowanie jakiego oczekujemy od programu, ale dla naszych zastosowań jest całkiem użyteczne ;).

Po wybraniu profilu można by się zastanowić, czy nie przesłać jakichś parametrów do kompilatora Cg. Cg również tutaj przychodzi nam z pomocą i dostarcza funkcje, która automatycznie pobiera 'optymalne' opcje. Jak bardzo są one optymalnie nie wiem ;), ale ponieważ możliwość taka istnieje, toteż piszę o niej tutaj. A więc do wybrania odpowiednich opcji automatycznie przez Cg służy funkcja
const char** cgD3D9GetOptimalOptions(CGprofile profile);
do której jak widać przekazujemy wybrany przez nas profil. Wynikiem działania tej funkcji jest tablica stringów zawierająca opcje dla kompilatora (przekazujemy ją jako ostatni parametr funkcji ładującej i kompilującej shader Cg).

Kolejnym krokiem jest załadowanie programu Cg do Direct3D. Robi się to za pomocą funkcji
HRESULT cgD3D9LoadProgram(CGprogram program, CG_BOOL parameterShadowingEnabled, DWORD assembleFlags);
pierwszym parametrem jest oczywiście program, który załadowaliśmy uprzednio poprzez funkcje rdzenia Cg. Jeśli drugiemu parametrowi przypiszemy TRUE to Cg będzie przechowywać kopie wszystkich zmiennych przesyłanych do programów. Co to oznacza? Oznacza to, że jeśli zmienimy później wartość jakiegoś parametru Cg to nie zostanie on od razu przesłany do pamięci stałej karty graficznej, ale zostanie zbuforowany gdzieś wewnątrz Cg. Wszystkie parametry zostaną przekazane karcie graficznej dopiero w momencie, kiedy będą naprawdę potrzebne, czyli kiedy dany program jest ustawiany jako bieżący. Co to daje? Ano pozbywamy się niepotrzebnych odwołań do pamięci karty graficznej - później możemy przesłać wszystko hurtowo. Oczywiście powoduje to zwiększenie zapotrzebowania aplikacji na pamięć operacyjną. Buforowanie parametrów nic nie daje, jeśli nasza aplikacja robi to sama, czyli wszystkie parametry shadera ładujemy naraz tuż przed jego wykorzystaniem. Ostatnim parametrem są flagi w postaci D3DXASM.

Po załadowaniu program musi zostać aktywowany. Robi się to za pomocą funkcji
HRESULT cgD3D9BindProgram(CGprogram program);
Skoro już mamy załadowany i aktywowany program, możemy zająć się przesyłaniem do niego niezbędnych parametrów, które jest tu wręcz banalnie proste. Służy do tego funkcja:
HRESULT cgD3D9SetUniform(CGparameter parameter, const void *value);
Rozmiar przesyłanych danych zależy od parametru, a więc w zasadzie od programu Cg. Ponieważ drugim argumentem tej funkcji jest wskaźnik na void, to możemy tam przekazać w zasadzie wszystko, łącznie ze zdefiniowanymi przez nas samych typami danych, macierzami itd. Mimo, że ta funkcja pozwala przesłać praktycznie wszystko, to Cg dostarcza również funkcje do przesyłania tylko macierzy D3D (a więc 4x4)
HRESULT cgD3D9SetUniformMatrix(CGparameter parameter, const D3DMATRIX *matrix);
Należy przy tym zauważyć, że jeśli parametr nie wymaga macierzy o rozmiarach 4x4 to z podanej jako argument funkcji macierzy zostanie wycięta odpowiednia ilość danych (taka pod-macierz) zaczynając od lewego górnego rogu.

Kolejnym typem parametrów są tablice. Do ich przekazywania służy osobna funkcja, a mianowicie
HRESULT cgD3D9SetUniformArray(CGparameter parameter, DWORD startIndex, DWORD numberOfElements, const void *array);
Jak łatwo zauważyć drugi argument oznacza początkowy indeks, od którego mają zostać pobrane dane. Ilość pobranych danych jest ustalana przez argument trzeci. Ostatnim argumentem tej funkcji jest wskaźnik na tablicę, z której te dane mają zostać pobrane. Istnieje jeszcze jedna wersja tej funkcji. Służy ona do przekazywania tablic macierzy:
HRESULT cgD3D9SetUniformMatrixArray(CGparameter parameter, DWORD startIndex, DWORD numberOfElements, const D3DMATRIX *matrices);
Znaczenie poszczególnych argumentów tej funkcji jest identyczne, z ich znaczeniem w poprzedniej, z tą różnicą, że ostatni argument to tablica macierzy ;).

Ostatnim typem parametrów są samplery. Jak wiemy poprzez samplery Cg identyfikuje tekstury, a same samplery odnoszą się tylko i wyłącznie do shaderów pikseli. Aby połączyć parametr typu sampler z teksturą używamy funkcji
HRESULT cgD3D9SetTexture(CGparameter parameter, IDirect3DBaseTexture9 *texture);
Parametr drugi to oczywiście wskaźnik na teksturę. Jeśli kogoś dziwi nazwa interfejsu IDirect3DBaseTexture9 i myśli, że będzie musiał używać jakichś specjalnych tekstur, to wyjaśniam od razu, że jest to interfejs bazowy wszystkich tekstur w D3D (wszystkie z niego dziedziczą), a więc można przesyłać tekstury dowolnego typu. Cg dostarcza jeszcze dwie funkcje operujące na teksturach:
HRESULT cgD3D9SetSamplerState(CGparameter parameter, D3DSAMPLERSTATETYPE type, DWORD value);
oraz
HRESULT cgD3D9SetTextureWrapMode(CGparameter parameter, DWORD value);
możliwe do przekazania parametry są identyczne z odpowiednikami tych funkcji z Direct3D, więc nie ma co omawiać.

Istnieje jeszcze jeden tryb pracy Cg z Direct3D - tryb debug, ale o tym jak go zastosować dowiecie się już sami z SDK, jeśli będziecie go potrzebować.

  • Przykładowe shadery
No i przyszła kolej coś w końcu naskrobać w tym Cg. Napiszemy sobie dwa prościutkie shaderki do multitexturingu. Pierwszy z nich będzie shaderem wierzchołków, a drugi pixeli. Program wierzchołków wygląda tak:
struct vertex
{
  float4 position : POSITION;
  float2 tex0     : TEXCOORD0;
  float2 tex1     : TEXCOORD1;
};

struct fragment
{
  float4 position : POSITION;
  float2 tex0     : TEXCOORD0;
  float2 tex1     : TEXCOORD1;
};


fragment main( vertex IN, uniform float4x4 modelViewProj)
{
  fragment OUT;

  float4 tempPosition = float4( IN.position.x, IN.position.y, IN.position.z, 1.0f );

  OUT.position = mul( modelViewProj, tempPosition );
  OUT.tex0     = IN.tex0;
  OUT.tex1     = IN.tex1;

  return OUT;
}
Jak widać funkcja shadera jest banalnie wręcz prosta i w zasadzie od razu można się domyśleć co robi, ale i tak ją opiszę, żeby nie było już żadnych niedomówień :). A więc najpierw definiujemy sobie strukturę, która będzie przechowywać parametry wejściowe wierzchołka:
struct vertex
{
  float4 position : POSITION;
  float2 tex0     : TEXCOORD0;
  float2 tex1     : TEXCOORD1;
};
Jak widać nasze wierzchołki składają się z trzech wektorów - pozycji i dwóch ze współrzędnymi tekstur. Składowe mają przypisane oczywiście nazwy znaczeniowe, aby można je było powiązać z odpowiednimi rejestrami wejściowymi.

Kolejnym etapem jest zdefiniowanie struktury wyjściowej naszego programu obrabiającego wierzchołki. Jest ona dokładnie identyczna z powyższą, dlatego nie będę jej opisywał. Dlaczego więc zrobiłem dwie? Żeby pokazać, że nic nie stoi na przeszkodzie.

Przypatrzmy się teraz funkcji main
fragment main( vertex IN, uniform float4x4 modelViewProj)
Wygląda jak normalna funkcja z C (nie jak main z C :P). Widać od razu, że w wyniku jej działania otrzymamy obiekt typu fragment, czyli naszą strukturę wyjściową. Do funkcji przekazujemy wierzchołek oraz zmienną typu uniform, która jest połączoną macierzą świata (modelu w OpenGL), widoku i projekcji. Możemy oczywiście przekazać wszystkie macierze osobno, ale wtedy musimy wymnażać każdy wierzchołek przez poszczególne macierze, co jest marnotrawstwem czasu (w bardziej zaawansowanych shaderach oczywiście - tutaj nawet nikt tego nie zauważy ;).
(...)
fragment OUT;
(...)
Tak jak w C, tak i tutaj musimy utworzyć obiekt typu, który zwracamy, aby można to było zrobic.
(...)
float4 tempPosition = float4( IN.position.x, IN.position.y, IN.position.z, 1.0f );
(...)
Ponieważ w programie mamy tylko trzy współrzędne wierzchołka określające jego pozycję, toteż musimy tutaj zastosować pewien trik, a mianowicie współrzędną w wierzchołka ustawić na 1.0, aby poprawnie wymnożyć go przez macierz, co z kolei robi poniższa linijka
(...)
OUT.position = mul( modelViewProj, tempPosition );
(...)
Wykorzystujemy tu funkcję wbudowaną mul języka Cg, która przekształca wektor przez przekazaną macierz zwracając wynik. Wynik przypisujemy do odpowiedniego pola struktury wyjściowej.
(...)
OUT.tex0     = IN.tex0;
OUT.tex1     = IN.tex1;
(...)
Podobnie robimy ze współrzędnymi tekstur - to tylko zwykłe kopiowanie.
(...)
return OUT;
(...)
Na koniec wypełnioną strukturę wyjściową (jej obiekt w sumie) zwracamy. I to byłby w zasadzie koniec, gdyby nie jedna mała sprawa. Nie wiem czy zauważyliście, ale po nawiasie klamrowym zamykającym funkcję main nie ma średnika. Nie wiem czemu, ale tam NIE MOŻE być średnika, jeśli wszystko ma działać poprawnie :/. Nie wiem czy to jest jakiś błąd Cg, czy celowe zamierzenie twórców (tyczy się to każdego shadera - niezależnie od przeznaczenia).

Skoro już wiemy jak działa program obróbki wierzchołków, to zajmijmy się programem obróbki pikseli. Jest on w sumie jeszcze prostszy ;).
struct fragment
{
  float4 position : POSITION;
  float2 tex0     : TEXCOORD0;
  float2 tex1     : TEXCOORD1;
};

float4 main(fragment IN, uniform sampler2D Tex0, uniform sampler2D Tex1) : COLOR0
{
  float4 t0 = tex2D(Tex0, IN.tex0.xy);
  float4 t1 = tex2D(Tex1, IN.tex1.xy);

  return t0*t1;
}
Jak widać na początku zdefiniowana jest struktura identyczna do wyjściowej z vertex programu. Dzieje się tak dlatego, że wyjściowe dane vertex programu służą jako dane wejściowe dla fragment programu (pixel shadera czy jak go tam jeszcze zwał :). Nieco inaczej wygląda natomiast funkcja main
float4 main(fragment IN, uniform sampler2D Tex0, uniform sampler2D Tex1) : COLOR0
Znaczy o ile lista jej parametrów nie stanowi niczego nowego (samplery są omówione w poprzednich częściach), o tyle na jej końcu znajduje się nazwa znaczeniowa. Dlaczego? Ponieważ funkcja zwraca wektor wartości zmiennopozycyjnych float4. Kompilator nie wie co ta wartość ma znaczyć, dlatego właśnie trzeba jawnie zdefiniować, że jest to kolor wyjściowy.
(...)
float4 t0 = tex2D(Tex0, IN.tex0.xy);
(...)
Tutaj mamy przykład użycia samplerów 2D. Funkcja tex2D służy do pobrania teksela z odpowiedniego miejsca tekstury (parametr pierwszy) bazując na współrzędnych przekazanych jako drugi parametr. Ważne jest, żeby ustalić które to są współrzędne. Znaczy funkcja domyślnie potraktuje drugi parametr jako wektor 4ro elementowy, więc jeśli jawnie nie ustalimy, że jest to wektor dwu-elementowy to funkcja nie zadziała.
(...)
return t0*t1;
(...)
No koniec zwracamy kolor będący połączeniem dwóch tekseli z dwóch tekstur ;). I to chyba wszystko. Przykładowe kody (osobno dla d3d i opengl) są oczywiście w dziale download (poniżej są również linki).

Kod źródłowy (dxCg.zip)

Kod źródłowy (glCg.zip)

©Copyright by Domino   



Tutoriale - Techniki
Nasze newsy s� w RSS: backend.php
PHP-Nuke Copyright © 2005 by Francisco Burzi. This is free software, and you may redistribute it under the GPL. PHP-Nuke comes with absolutely no warranty, for details, see the license.
Tworzenie strony: 0.05 sekund

:: Layout strony został stworzony przez www.nukemods.com w oparciu o styl phpbb2 Helius, którego autorem jest Cyberalien ::