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 37 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ę?ć druga

Witam w kolejnej części tutorialu poświęconego Cg. Dzisiaj powiem coś niecoś o funkcjach, if-ach i innych przydatnych w programowaniu rzeczach. Zaczynamy wiec.
  • Funkcje
Każdy programista wie jak wyglądają funkcje. Cg nie wprowadza praktycznie nic nowego jeśli chodzi o ich definiowanie. Przykładowa funkcja wygląda tak:
void func (float4 a)
{
  /* ... */
};
Są dwie różnice między funkcjami w C i Cg. Po pierwsze Cg nie pozwala na rekurencję. Po drugie w Cg możemy przeciążać nazwy funkcji (jak wiadomo w C tego robić nie można i dopiero C++ na to pozwala). Zasady przeciążania są takie jak w C++, a więc przeciążenie może nastąpić poprzez różną ilość parametrów lub poprzez inny typ przyjmowanych parametrów np.
void foo (float a);
void foo (int a);
void foo (float a, float b);
Jeśli funkcja coś zwraca to do zwrócenia danej wartości używamy, tak jak w C, słowa kluczowego return, np.:
float foo (float a)
{
  float b;
  /* ... */
  return b;
};
  • Zmienne wejściowe i wyjściowe
Kolejną innowacją, w porównaniu z C, którą wprowadza Cg, jest rozróżnienie zmiennych na wejściowe i wyjściowe. Z rozróżnieniem takim mamy do czynienia tylko i wyłącznie wymieniając zmienne na liście parametrów funkcji - możemy to traktować jak przypisanie zmiennym praw dostępu. Zmienne, które przekazujemy do funkcji jako wejściowe mają dodane słowo kluczowe in np.
void foo1(in float4 a);
Zmienne takie są tylko do odczytu i nie możemy do nich nic zapisywać. Dla zmiennych wyjściowych mamy słowo kluczowe out np.:
void foo2(out half a);
Ten typ zmiennych służy z kolei tylko do zapisu i nie możemy czytać z takich zmiennych. Istnieje również trzecia możliwość - zmienne, które są zarówno wejściowe jak i wyjściowe. Takie zmienne definiuje się za pomocą słowa kluczowego inout np.:
void foo3(inout bool a);
Ten rodzaj zmiennych pozwala na ich odczyt i równocześnie na zapis do nich. Domyślnie, jeśli przekazujemy jakąś zmienną bez określenia praw dostępu do niej, będzie ona traktowana jak zmienna tylko do odczytu (pamiętajcie, że cały czas mówię tylko i wyłącznie o przekazywaniu zmiennych do funkcji!).
Po co to wszystko? Wynika to z budowy procesora graficznego. W C jeśli chcemy w funkcji zmodyfikować zmienną, to posługujemy się wskaźnikiem. W C++ możemy to dodatkowo zrobić poprzez referencję, która jednakże bazuje na wskaźnikach. Ponieważ jednak procesory graficzne nie posiadają obsługi wskaźników, toteż twórcy Cg musieli poradzić sobie w inny sposób. No i poradzili sobie ;). Mniejsza o to, jak zrobione jest to technicznie - dla nas najważniejsze jest, że to działa ;).
  • Instrukcje sterujące
W chwili obecnej Cg wspiera następujące instrukcje sterujące:
  • if/else
  • while
  • for
Na liście słów kluczowych znajdują się również switch, case i default, ale jak do tej pory żaden z profili ich nie obsługuje.
Wszystkie one mają taką samą składnię jak instrukcje w C, więc nie ma się za bardzo nad czym rozpisywać. Cg dostarcza zbiór standardowych operatorów porównania i operatorów boolowskich, czyli:
  • < (mniejszy niż)
  • <= (mniejszy bądź równy)
  • != (nierówność)
  • == (równość)
  • >= (większy bądź równy)
  • > (większy)
  • && (boolowskie AND)
  • || (boolowskie OR)
  • ! (boolowska negacja)
Dodatkowo w Cg występuje również wyrażenie warunkowe. Dla wszystkich, którzy nie za bardzo pamiętają co to :) wyrażeniem warunkowym nazywamy wyrażenie typu:
wyrażenie ? wyrażenie1 : wyrażenie2 Jest ono równoznaczne zapisowi:
if (wyrażenie) wyrażenie1
else wyrażenie2
z tym, że można go stosować w np. przypisaniach. Można więc napisać
float a = (b>3) ? 1 : 0
Wynikiem takiego działania będzie przypisanie do a 1 jeśli b jest większe od 3 lub 0 jeśli b jest mniejsze od 3.

Należy szczególnie ostrożnie posługiwać się pętlami w słabszych profilach. Pętle bowiem pojawiły się dopiero w wersji 2.0 Extended shaderów i dopiero w tych wersjach zostaną wykonane zawsze. W niższych wersjach pętle są dozwolone tylko wtedy, kiedy ilość wywołań jest znana w czasie kompilacji. Takie pętle zostaną wtedy rozwinięte w miejscu wywołania (czyli ich kod zostanie powielony w kodzie shadera tyle razy ile jest powtórzeń).
  • Operatory arytmetyczne
Operatory arytmetyczne w Cg to -, + ,*, /. Można ich używać w arytmetyce skalarnej i wektorowej. Czyli można napisać 2.0*2.0 i w wyniku otrzymać oczywiście 4.0 ;). Można również napisać
float2(2.0f, 4.0f) * float2(4.0f, 5.0f)
wynikiem takiej operacji nie będzie bynajmniej iloczyn skalarny wektorów. W wyniku dostaniemy wektor, którego składowe będą iloczynem składowych poszczególnych wektorów, czyli
float2(2.0f*4.0f, 4.0f*5.0f)
Można również mieszać arytmetykę skalarną i wektorową. Przykładowy kod mógłby więc wyglądać tak:
5.0f * float3(4.0f, 2.0f, 1.0f)
Wynikiem takie operacji będzie oczywiście wektor, którego wszystkie składowe zostaną wymnożone przez skalar, czyli:
float3(5.0f*4.0f, 5.0f*2.0f, 5.0f*1.0f)
Należy jednak pamiętać o tym, iż operatory arytmetyczne nie mają zaimplementowanej obsługi macierzy. Do mnożenia macierzy stosuje się wbudowane w Cg funkcje matematyczne, a w zasadzie jedną funkcję mul. Występuje ona w trzech odmianach. Można mnożyć wektor przez macierz, macierz przez wektor oraz dwie macierze przez siebie, ale o tym za chwilę.
  • Operator zamiany składowych
Ponieważ Cg służy do programowania GPU nie powinien dziwić fakt, że został tu zaimplementowany operator zamiany składowych. Znamy go już przecież z shaderów i wiemy, że jest dość przydatny. Operator zamiany składowych zachowuje się dokładnie tak samo jak jego odpowiednik z asemblerowych shaderów, czyli zapisując
float4(a, b, c, d).xxxz
otrzymamy
float4(a, a, a, c)
Oczywiście możliwości łączenia jest bardzo wiele. Ten operator można odnieść również do skalara celem skonstruowania wektora. Można więc zapisać
a.xxxx
i w wyniku dostac
float4(a, a, a, a)
W tym przykładzie a jest oczywiście skalarem. Na potrzeby tego działania jest on jednak traktowany jak jednoelementowy wektor i dlatego można się odnieść do jedynej jego składowej, czyli do x. Do składowych możemy się również odnosić poprzez nazwy kolorów, czyli r, g, b, a.
  • Profile Cg
Ta kwestia związana jest już z konkretnym API graficznym, dla którego kompilujemy shader. Profil Cg określa jakiego typu będzie kod wynikowy. Możemy bowiem skompilować shader dla Direct3D 9.0 i pixel shadera w wersji 2.0. Może się jednak okazać, że nasz sprzęt dysponuje tylko pixel shaderem w wersji 1.1, a więc kod wynikowy będzie musiał być inny. Dostępne profile dla Direct3D:
  • CG_PROFILE_VS_1_1
  • CG_PROFILE_VS_2_0
  • CG_PROFILE_VS_2_X
  • CG_PROFILE_PS_1_1
  • CG_PROFILE_PS_1_2
  • CG_PROFILE_PS_1_3
  • CG_PROFILE_PS_2_0
  • CG_PROFILE_PS_2_X
Dostępne profile dla OpenGL:
  • CG_PROFILE_ARBVP1
  • CG_PROFILE_VP20
  • CG_PROFILE_VP30
  • CG_PROFILE_ARBFP1
  • CG_PROFILE_FP20
  • CG_PROFILE_FP30
Są to profile dla kodu uruchomieniowego, a więc służą do przeprowadzenia kompilacji shadera podczas uruchamiania programu. Kompilację można bowiem przeprowadzić jeszcze w inny sposób, a mianowicie za pomocą oddzielnego kompilatora Cg wywoływanego z linii poleceń. Obie metody mają wady i zalety (jak zwykle ;). Pierwsza z nich jest lepsza, jeśli chcemy dostarczyć jeden plik z shaderem (w formacie Cg), który będzie działał na wszystkich kartach graficznych na których to możliwe tylko. Wadą jest jednak to, że proces kompilacji wydłuża oczywiście czas potrzebny na załadowanie programu. Nie widać tego w przypadku małych shaderów, ale w momencie kiedy mamy grę i 2000 różnej maści shaderów, proces ten jest zapewne dość łatwo zauważalny. Kolejną wadą jest to, że musimy wtedy dodawać biblioteki Cg do naszego programu. Druga metoda ma tą zaletę, że kod shadera jest już skompilowany do wersji jakiej potrzebuje dane API. Powoduje to, że po pierwsze nie trzeba czekać na kompilację bo wystarczy tylko załadować vertex program/shader (i tym podobne wynalazki) poprzez najnormalniejsze funkcje z OpenGL i Direct3D. Dodatkowo nie trzeba dodawać bibliotek Cg do projektu. Wadą jest jednak to, że jeśli chcemy dostarczyć wersje dla większości kart graficznych to przy 2000 shaderów napisanych w Cg, liczba plików z shaderami różnych wersji będzie około 5-6 razy większa. Wybór danej metody zależy oczywiście od oczekiwań. Ja będę używał metody pierwszej bo po pierwsze ładowanie shaderów do OpenGL/Direct3D znamy już bardzo dobrze (więc nauczymy się korzystać z Cg runtime), a po drugie szczerze mi się nie chce kompilować 10 wersji jednego shadera ;).
  • Funkcje wbudowane
Cg jak każdy język ma pewien zbiór wbudowanych, bibliotecznych funkcji i predefiniowanych wartości. Nie trzeba więc koniecznie pisać własnej struktury wyjściowej dla funkcji obróbki wierzchołków. Takie struktury bowiem istnieją (w różnych wersjach - jak rodzaje wierzchołków T&L w Direct3D 7) jednak przedstawienie ich tutaj mija się z celem. Mamy nauczyć się korzystać z Cg, więc wszystko napiszemy sobie sami. Kiedy już będziecie znać Cg, wtedy zaglądniecie sobie do dokumentacji SDK jeśli będzie wam to potrzebne (choć możecie zrobić to nawet teraz :). Drugą sprawą są funkcje biblioteczne. W zasadzie powinienem je tu wymienić i opisać co mniej więcej robią, bo inaczej nie będziemy mogli zrobić zbyt wiele. Problem jednak polega na tym, że w dokumentacji SDK są one wszystkie wymienione wraz z opisem w ładnej tabelce, a tabelka ta ma chyba 4 albo 5 stron. Z oczywistych powodów nie będę jej tutaj przepisywał i wszystkich zainteresowanych odsyłam do dokumentacji. Tutaj wymienię tylko te kilka, które będą nam niezbędne w dalszej pracy.

Funkcje matematyczne
Nazwa funkcji
Opis funkcji
cos(x) oblicza kosinus x
cross(a, b) iloczyn wektorowy wektorów a i b. Oba wektory muszą być trzy elementowe
dot(a, b) iloczyn skalarny wektorów a i b
lerp(a, b, f) Przeprowadza liniową interpolację (1-f)*a + b*f;
a i b to wektory lub skalary, f jest skalarem lub wektorem tego samego typu co a i b
max(a, b) maximum z a i b
min(a, b) minimum z a i b
mul(M, N) mnoży macierze M i N (M*N)
mul(M, v) monoży macierz przez wektor (traktowany jako kolumna)
mul(v, M) mnoży wektor (traktowny jako wiersz) przez macierz
pow(x, y) x do potęgi y
rsqrt(x) oblicza odwrotny pierwiastek kwadratowy z x
sin(x) oblicza sinus x
sincos(float x, out s, out c) oblicza naraz sinus i kosinus kąta x.
Funkcja ta jest szybsza niż liczenie sin i cos osobno.
sqrt(x) oblicza pierwiastek kwadratowy z x
transpose(M) transponuje macierz M

Funkcje geometryczne
Nazwa funkcji
Opis funkcji
distance(pt1, pt2) oblicza odległość euklidesową między punktami pt1 i pt2
length(v) oblicza długość wektora v
normalize(v) normalizuje wektor v
reflect(i, n) oblicza wektor odbicia z kierunku i dla plaszczyzny o wektorze normalnym n;
wektory i oraz n musza byc 3 elementowe

Funkcje operujące na teksturach
Nazwa funkcji
Opis funkcji
tex1D(sampler1D tex, float s) samluje teksture 1D używając współrzędnej s
tex2D(sampler2D tex, float2 uv) samluje teksturę 2D używając współrzędnych u i v
tex3D(sampler3D tex, float3 uvw) samluje teksturę 3D używając współrzędnych u, v, w
texCUBE(samplerCUBE tex, float3 uvw) samluje teksturę sześcienną na podstawie współrzędnych w, v, w
  • Samplery
Z pojęciem samplerów spotkaliśmy się już wcześniej. Jest to pojęcie typowe dla pixel shadera czy też fragment programu. Jak zapewne pamiętacie sampler służył do samplowania :)) tekstury, a więc do pobrania jej piksela (teksela) na podstawie podanych mu współrzędnych teksturowania (które z kolei były interpolowane wzdłuż wierzchołków trójkąta). Tutaj sprawa ma się nieco inaczej. W Cg sampler nie jest funkcją, ale typem wbudowanym. Dość dziwne by więc było, gdyby coś obliczał sam sobie. Skoro jest typem i można tworzyć zmienne tegoż typu, to pewnie coś one mają oznaczać :). No i oznaczają. W Cg obiekt samplera identyfikuje teksturę. Jeśli więc chcemy przesłać do shadera dwie tekstury 2D to w parametrach funkcji przekazujemy dwa samplery typu sampler2D. Do pobrania odpowiedniego teksela służą wbudowane funkcje Cg (samplowanie samplera :P). Niektóre z nich zostały wymienione w tabeli powyżej.
  • Kod
No więc dobrze. Skoro znamy już podstawową składnię Cg możemy zająć się kodem. Przedstawię tutaj bardzo prosty shader napisany w Cg. Nie uruchomimy go jeszcze, bo do tego celu potrzebne jest podłączenie Cg do danego API graficznego. To jednak będzie omówione dopiero w następnej części.
struct vertin
{
  float4 iPos   : POSITION;
  float2 iTex0  : TEXCOORD0;
  float4 iColor : COLOR0;
};

struct vertout
{
  float4 oPos   : POSITION;
  float2 oTex0  : TEXCOORD0;
  float4 oColor : COLOR;
};

vertout vertex_main(vertin IN, const uniform float4x4 WorldViewProj)
{
  vertout OUT;

  OUT.oPos   = mul(WorldViewProj, IN.iPos);
  OUT.oTex0 = IN.iTex0;
  OUT.oColor = IN.iColor;

  return OUT;
};
Mówiłem, że będzie bardzo prosty :). Prześledźmy kolejno co mniej więcej robi ten shader (nic w zasadzie ;). Na samym początku definiujemy sobie dwie struktury. Pierwsza z nich jest strukturą wejściową dla naszego shadera. Zostanie ona wypełniona oczywiście danymi pochodzącymi z aplikacji. Jak widać użyłem nazw znaczeniowych, aby kolejnym polom przypisać role jaką pełnią w shaderze (i aby Cg mogło sobie poradzić z przesłaniem tam danych). Dwa pola są typu float4 ponieważ z natury są 4-elementowymi wektorami liczb float. Tylko pole iTex0 jest wektorem 2-elementowym. Wynika to oczywiście stąd, iż korzystam z tekstur 2D (i ze współrzędnych UV). Struktura wyjściowa zawiera dokładnie takie same pola. Czy zatem moglibyśmy użyć tej samej struktury? Dlaczego nie. Skoro można tak zrobić w C to dlaczego nie w Cg? W końcu nigdzie w tej strukturze nie ma informacji mówiących, że może być użyta tylko do wejścia czy wyjścia (takie dane jednakże istnieją, ale dla obiektów, o czym przekonamy się za chwilę). Ja jednak wole napisać dwie struktury, ponieważ uważam, że tak jest łatwiej to wszystko zrozumieć. Idźmy dalej. Mamy naszą funkcję do obróbki wierzchołków (vertex program/shader). Zanim będziemy badać co jest w jej ciele, skupmy się na samej deklaracji:
vertout vertex_main(vertin IN, const uniform float4x4 WorldViewProj)
Są tutaj dwie bardzo ważne rzeczy, których nie widać na pierwszy rzut oka. Nie widać bowiem żadnych informacji mówiących, które parametry służą za wejście, a które za wyjście. Dzieje się tak dlatego, że domyślnie do wszystkich parametrów dodawane jest słowo kluczowe in. Zatem obiekt IN ma przypisaną informację, że jest obiektem wejściowym, a ponieważ jego pola są połączone z nazwami znaczeniowymi, toteż Cg wie gdzie umieścić dane wejściowe. Podobnie sprawa ma się ze strukturą wyjściową. Pisząc return OUT Cg łączy obiekt OUT ze słowem kluczowym out, a ponieważ pola struktury vertout mają przypisane nazwy znaczeniowe, toteż Cg wie skąd pobrać przekształcone dane do dalszej obróbki. Ostatnim ważnym zagadnieniem jest macierz. Na liście parametrów funkcji pojawia się ze słowem kluczowym uniform, które jak pamiętamy oznacza parametry przekazywane do shadera raz na jego wywołanie. Dodatkowo jest tam słowo kluczowe const, które umieściłem tylko po to, żeby pokazać, że można je połączyć z uniform ;).

Zaglądanie do ciała shadera w zasadzie nie jest już istotne, bo nie ma tam nic interesującego. Jedyną ciekawostką może być przykład użycia funkcji bibliotecznej mul w celu otrzymania współrzędnych ekranowych.

To wszystko na dzisiaj. Następna, trzecia część, będzie poświęcona używaniu Cg z OpenGL oraz Direct3D.

©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 ::