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 36 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ę?ć pierwsza

Witam. Dzisiaj będę pisał o czymś, co było nieuniknioną konsekwencją rozwoju grafiki 3D i wszelkiej maści kart graficznych. Mowa będzie bowiem o Cg (C for Graphics). Postaram się Wam przybliżyć znaczenie tych dwóch liter oraz przedstawić zalety używania tego języka (tak, to jest język, nie API). No, ale zacznijmy od początku.

Rozwój kart graficznych spowodował, że to co niedawno wydawało się niemożliwe do osiągnięcia w czasie rzeczywistym, dziś już nikogo nie dziwi. Obecny PC dorównuje mocą obliczeniową stacja graficznym sprzed kilku lat kosztując przy tym kilkakrotnie mniej. Ciągle rozwijają się technologie pozwalające na wykorzystanie tejże mocy. Dla nas, jako programistów grafiki 3D, momentem przełomowym było chyba umożliwienie programowania procesorów graficznych poprzez shadery. Dostaliśmy do ręki kolejny procesor, ale w tym wypadku zoptymalizowany do używania w grafice. Mimo, iż poczatkowo bardzo niedoskonałe i ograniczone to właśnie shadery pozwoliły na osiągnięcie poziomu stacji graficznych. Pisanie ich było jednak nieco utrudnione jako, że trzeba było to robić w asemblerze, ale ze względu na ograniczenia i niewielką ilość instrukcji nie było to strasznie skomplikowane. Shadery zawierały najwyżej troszkę ponad sto linii. Od pojawienia się pierwszych kart posiadających programowalne GPU minęło już kilka lat. Obecne procesory dysponują znacznie bogatszym zestawem instrukcji oraz kilkakrotnie większą mocą obliczeniową, a ograniczenia co do ilości wywołanych instrukcji liczone są w tysiącach, albo nie ma ich w ogóle. Pisanie setek linii shaderów stało się więc nieco problematyczne, a sam kod jest często ciężki do zrozumienia. Naturalnym następstwem takiego stanu rzeczy było więc pojawienie się języka wysokiego poziomu służącego do programowania GPU. Język taki stworzyła nVidia i o nim właśnie dzisiaj mówię. Cg to język dzięki któremu będziemy mogli zamienić setki linii asemblerowego kodu na kilka raptem linijek kodu Cg. Ponadto Cg jest niezależny od API graficznego ani platformy sprzętowej. Oznacza to, że ten sam shader będzie działał tak samo w OpenGL i Direct3D, jak również pod windows, linuxem czy innymi systemami (dla których jest przewidziane wsparcie ze strony Cg). Najważniejszą jednak sprawą jest to, że kompilator Cg sam troszczy się o to, aby otrzymany kod był optymalny.

Żeby zacząć zabawę z Cg musimy najpierw zdobyć Cg SDK. Znajduje się ono tutaj: Nvidia Cg Toolkit. Po zainstalowaniu SDK wszystko powinno być gotowe do tworzenia - instalator Cg zdaje się wspiera nasz ulubiony kompilator firmy ulubionej ;P, więc wszystkie niezbędne ścieżki powinny zostać dodane. Jeśli tak się nie stanie, to będziecie musieli zrobić to ręcznie, co nie powinno stanowić dla Was żadnego problemu (nie będę zamieszczał tutaj obrazków pokazujących co i jak nacisnąć, bo zakładam, że umiecie się już posługiwać kompilatorem ;) lub po prostu przegrać nagłówki i biblioteki Cg do odpowiednich katalogów kompilatora.

Wróćmy jednak do samego Cg. Skoro jest to język to poznajmy jego zasady. Ponieważ język ten w samej swojej nazwie nawiązuje do C, toteż skojarzenia z C są jak najbardziej na miejscu i omawiając Cg będę to często robił. Po pierwsze i najważniejsze każda linijka musi kończyć się średnikiem tak jak w C. Po drugie musimy oczywiście napisać funkcję główną naszego shadera. Takie main() z C. W Cg funkcja ta może nazywać się dowolnie, może zwracać parametry i je przyjmować. Przykładowa funkcja główna może więc wyglądać tak:
outstruct vertexfunc(instruct IN)
{
   /* ... */
};
Ponieważ jednak jest to specyficzny język, toteż należy się zastanowić jakie parametry możemy przyjmować i jakie zwracać (tak po prawdzie, to funkcja nic nie zwracająca i nic nie przyjmująca jest bez sensu). To co funkcja zwraca i przyjmuje zależy od tego, czy jest to funkcja do przetwarzania wierzchołków (vertex shader) czy też pikseli (pixel shader). Pierwsza z nich będzie zapewne pobierać współrzędne wierzchołka przed przekształceniem, macierze przekształceń, współrzędne teksturowania itp. Co będzie zwracać? Tu pojawia się pewien problem - najczęściej musimy bowiem zwrócić więcej niż jedną wartość (np. pozycje wierzchołka na ekranie, współrzędne teksturowania itp.). Rozwiązania są dwa. Możemy dane zwracane przesłać w liście parametrów formalnych funkcji (co jest normalną sprawą) lub też zwrócić w postaci jakiejś struktury (tak! możemy definiować struktury). Oba rozwiązania dają ten sam efekt, z tym, że drugie jest bardziej czytelne. Sytuacja jest dokładnie taka sama w przypadku funkcji dla pixel shadera, z tą różnicą, że po prostu dane będą inne.

Poza tym, że istnieje rozróżnienie między parametrami na wejściowe i wyjściowe istnieje też inny podział. Chodzi mianowicie o parametry 'stałe' i 'zmienne'. W tym wypadku pojęcia te mają nieco inne znaczenie niż w przypadku C (dlatego zapisałem je w cudzysłowach). Parametry 'stałe' są stałe w tym sensie, że nie zmieniają się dla każdego wierzchołka. Definiuje się je dodając słowo kluczowe uniform. Takimi parametrami są na przykład macierze widoku, świata, projekcji, czyli ogólnie rzecz ujmując chodzi o takie parametry, które są stałe dla danego wywołania shadera. Parametry zmienne zmieniają się dla każdego wierzchołka. Definiuje się je normalnie, czyli tak jak zwyczajne zmienne w C. W Cg istnieją również zwyczajne parametry stałe, które definiuje się za pomocą słowa kluczowego const i które działają tak jak te z C. Skoro jesteśmy już przy parametrach funkcji (które de facto są zmiennymi) to zobaczmy jakie mamy podstawowe typy danych.

  • Typy danych.

W Cg istnieje kilka typów danych: typy podstawowe, tablice oraz struktury. Do tego dochodzi jeszcze konwersja między typami. Zobaczmy więc czym dysponujemy:

  • Typy podstawowe

Mamy ich kilka. Należy zauważyć, że możliwość ich użycia zależy w dużej mierze od sprzętu jakim dysponujemy. Na sprzęcie obsługującym tylko pixel shader w wersji 1.0 nie będziemy zapewne dysponować 32 bitowymi wartościami zmiennopozycyjnymi. Podstawowe typy danych to:

  • float - 32 bitowa zmienna zmiennopozycyjna o formacie: 1 bit znaku, 23 bity dla mantysy i 8 bitów wykładnika.
  • half - 16 bitowa zmienna zmiennopozycyjna o formacie: 1 bit znaku, 10 bitów dla mantysy i 5 bitów wykładnika.
  • int - 32 bitowa zmienna całkowita. W niektórych przypadkach Cg może potraktować tą zmienną jak typ float.
  • fixed - 12 bitowa zmienna stałopozycyjna o formacie: 1 bit znaku, 1 bit dla mantysy i 10 bitów wykładnika
  • bool - zmienna boolowska.
  • sampler - zmienna będąca uchwytem tekstury. Może ona być jednym z poniższych typów:
    • sampler1D - dla tekstur jedno-wymiarowych.
    • sampler2D - dla tekstur dwu-wymiarowych.
    • sampler3D - dla tekstur trój-wymiarowych
    • samplerCUBE - dla map sześciennych
    • samplerRECT - dla tekstur typu RECT z OpenGL (nie obsługiwane przez Direct3D)
Ponadto z każdego z wyżej wymienionych typów (poza sampler'em) można tworzyć wektory. Taki wektor może mieć do czterech wymiarów. Przykładami wektorów są:

  • float1 - jedno-wymiarowy wektor liczb zmiennopozycyjnych.
  • float2 - dwu-wymiarowy wektor liczb zmiennopozycyjnych
  • bool3 - trój-wymiarowy wektor wartości boolowskich
  • half4 - cztero-wymiarowy wektor liczb zmiennopozycyjnych o obniżonej precyzji
Widać więc, że wektory tworzy się poprzez dodanie liczby, oznaczającej wymiar wektora, do nazwy typu. Liczba musi mieścić się w zakresie [1, 4] co nie powinno nikogo dziwić, bo takich właśnie wektorów używa się w grafice.

Kolejnym typem, bez którego nie można się obejść w grafice, są macierze. Dla nich również mamy odpowiednie typy np:

  • float1x1 - macierz liczb zmiennopozycyjnych o wymiarach 1x1
  • float2x3 - macierz liczb zmiennopozycyjnych o wymiarach 2x3
  • int3x3 - macierz liczb całkowitych o wymiarach 3x3
  • float4x4 - macierz liczb zmiennopozycyjnych o wymiarach 4x4
Znowu można zauważyć, że typy macierzowe tworzy się poprzez dodanie wymiaru do nazwy typu. Wymiary, tak jak poprzednio, muszą się mieścić w zakresie [1, 4] - połączenia są dowolne, czyli wymiary macierzy można ustalić jak się chce, byleby obie liczby nie przekroczyły zadanego zakresu.

Dostęp do składowych wektorów i macierzy jest zrealizowany na dwa sposoby. Można się do nich dostać tak jak do pól struktur (czyli poprzez kropkę), np.
float a = float2(2.0f, 3.0f).x;
Drugim sposobem jest operator []. Powyższy zapis jest równoważny temu:

float a = float2(2.0f, 3.0f)[0];
  • Struktury

Tutaj jest praktycznie tak samo jak w C, czyli używamy słowa kluczowego struct. Deklaracja przykładowej struktury wygląda tak:
struct foo
{
/* ... */
};
Definicja zmiennej typu strukturalnego też nie jest niczym nowym:
foo f;
Do pól struktury oczywiście odwołujemy się za pomocą kropki. Początkowo Cg pozwalało na budowanie struktur składających się tylko z pól (niedozwolone były więc metody). Od wersji 1.2 Cg pozwala na definiowanie metod w strukturach. Ważną rzeczą jest to, że ciało metody musi się znaleźć wewnątrz struktury (czyli w zasadzie deklaracja struktury jest najczęściej jej definicją). Do metod struktury odwołujemy się dokładnie tak samo jak do pól, czyli za pomocą kropki. Niestety w obecnej wersji istnieje jeszcze wiele ograniczeń dotyczących metod. Pierwsze z nich polega na tym, że wszystkie pola struktury muszą być zdefiniowane przed definicją metody, jeśli używa ona tych pól. Drugi problem pojawia się przy przeładowywaniu - w przypadku metod nie jest ono dozwolone.

Pola struktur definiowanych w Cg często różnią się jednak od tych z C. Chodzi mianowicie o to, że pola struktur będą najczęściej zmiennymi wejściowymi bądź wyjściowymi dla shadera. Dlatego też będzie on musiał wiedzieć jak połączyć nazwy zmiennych z rejestrami. Do tego celu służą pewne predefiniowane nazwy, które jednak nie są typami. Nazywane są one w Cg nazwami znaczeniowymi (semantic-name). Nazwy znaczeniowe omówię później, tutaj chcę jedynie pokazać jak taka definicja wygląda:
float4 Position : POSITION;
W zasadzie wszyscy mieli już do czynienia z czymś podobnym - w shaderach OpenGL'owskich też występują podobne nazwy znaczeniowe. Użytkownikom Direct3D również nie jest to obce, bo za pomocą podobnych, predefiniowanych nazw buduje się format wierzchołka (w d3d 8.0) lub strumienia (d3d 8.1, 9.0)

  • Interfejsy

Pojawiły się w wersji 1.2 Cg razem z metodami. Czym jest interfejs? Patrząc z punktu widzenia C++ interfejs jest czysto wirtualną klasą bazową, składającą się tylko z metod (a więc nie może zawierać żadnych zmiennych), dziedziczoną przez inne klasy. Interfejsy używane w Cg przypominają jednak raczej Jave niż C++ (w C++ nie istnieje pojęcie interfejsu jako takiego). Dla użytkowników Javy pojęcie interfejsu nie będzie niczym nowym, a i sama deklaracja jak i wynikające z jego użycia obowiązki niczym nie różnią się od Javy. Deklaracja interfejsu wygląda tak:
interface IFoo
{
  void foo1 (float3 a);
  void foo2 (float3 b);
};
W C++ klasy się dziedziczy. W Cg (czy Javie) interfejsu się nie dziedziczy, ale implementuje. Co to dokładnie oznacza? Oznacza to, że WSZYSTKIE metody interfejsu MUSZĄ zostać zdefiniowane w strukturze, która dany interfejs implementuje. Może się to wydawać nieco uciążliwe, ale porównując to do C++ okaże się zupełnie logiczne. Struktura implementująca nasz przykładowy interfejs wyglądać może tak:
struct SFoo : IFoo
{
  /* ... */
  void foo1 (float3 a) { /* ... */ };
  void foo2 (float3 b) { /* ... */ };
};
Struktura ta, poza swoimi własnymi danymi i metodami posiada również definicje dwóch metod z interfejsu (a więc implementuje je). Analogiczna (do powyższego przykładowego interfejsu) klasa w C++ wyglądałaby tak:
class IFoo
{
  virtual void foo1 (float3 a) =0;
  virtual void foo2 (float3 b) =0;
};
Widać więc, że obie metody tej klasy są czysto wirtualne, a więc nigdzie nie istnieje definicja takich metod. Jeśli podziedziczymy z takiej klasy, to musimy napisać ciało obu metod (czyli dokładnie jak w Cg). Należy zauważyć, że dziedziczenie między strukturami jest w Cg nadal zabronione, podobnie jak wielodziedziczenie. Nie jest więc możliwe, aby struktura implementowała więcej niż jeden interfejs. Należy również zauważyć, ze interfejs nie może dziedziczyć z innego interfejsu.

  • Tablice

Tablice w Cg, podobnie jak struktury, niewiele różnią się od tych z C. Definicja zmiennej tablicowej wygląda dokładnie tak samo, np.:
float4 arr[7];
Do elementów tablicy również odwołujemy się tak jak w C, czyli poprzez operator [].
Należy jednak uważać z tablicami, ponieważ jeśli chcemy skompilować nasz shader dla jednego ze starszych profili (czym jest profil wyjaśnię później), to może się okazać, że coś nie chce działać. Wynikać to będzie z tego, że po prostu przekroczymy ilość dostępnych rejestrów tymczasowych (czy też wejściowych).
A jak przekazać tablice do funkcji? W C najwygodniej robi się to za pomocą wskaźników. W Cg nie ma wskaźników, a tablice nie są traktowane jak typ złożony, co oznacza, że są przekazywane przez wartość, a więc cała ich zawartość jest kopiowana przed użyciem. Przykładowa funkcja pobierająca jako parametr tablice może wyglądać tak:
void foo(float4 arr[6]) { /*...*/ };
Powyższa linijka jest typowa dla wersji Cg niższych niż 1.2. W czym? W wersjach poniżej 1.2 należało podawać rozmiar tablicy przekazywanej do funkcji. Od wersji 1.2 Cg wprowadzono możliwość przekazania tablicy o nieznanym rozmiarze. Można więc napisać
void foo(float4 arr[]) { /* ... */ };
Oczywiście nic nie stoi na przeszkodzie przekazać tablicę np. dwuwymiarową (powyższa jest jednowymiarowa jakby ktoś nie wiedział ;). Robi się to tak:
void foo(float4 arr[][]) { /* ... */ };
No dobrze, ale jak teraz dowiedzieć się jaki rozmiar ma ta tablica? Tu znowu twórcy Cg podpatrzyli Jave :). Otóż możemy ową tablicę potraktować jak strukturę i użyć pseudo metody length(), czyli napisać:
arr.length();
Ta pseudo metoda zwraca rozmiar tablicy. Jeśli tablica jest wielowymiarowa (np. dwu) to do wymiaru niższego można się dostać poprzez pobranie wymiaru wyższego i wywołanie length() np.:
arr[0].length()
Wydawać by się mogło, że zniknie wiele problemów wynikających z konieczności podawania rozmiaru tablic. Niestety sprawa nie wygląda do końca tak różowo. Chodzi mianowicie o to, że na obecnym sprzęcie nie do końca da się to wszystko zrobić (zależy od sprzętu oczywiście). Wynikiem tego jest częsta konieczność definicji nieznanych rozmiarów tablic wewnątrz aplikacji.

  • Konwersja typów

Jest to standardowe zagadnienie w każdym języku programowania. Nie dziwi więc, że występuje ono i tutaj. Zasadniczo konwersja typu przebiega w taki sam sposób jak w C, a więc za pomocą operatora rzutowania, np.:
float floatvar = (float)halfval;
Gdzie halfval to zmienna typu half. Cg używa jednak innej niż C hierarchii rzutowania. Tak więc w Cg wyrażenie halfval * 2.0 zostanie zamienione na halfval * (half)2.0. W C wyrażenie takie zostałoby zastąpione wyrażeniem (double)halfval * 2.0. Należy o tym pamiętać szczególnie w przypadku pixel shaderów, ponieważ tam może się nagle okazać, że gdzieś po drodze straciliśmy kilka bitów precyzji i efekt nie jest taki jakbyśmy chcieli. Dla stałych liczbowych przewidziano sufiksy w postaci:

  • f dla liczb float (np. 2.0f)
  • h dla liczb half (np. 2.0h)
  • x dla liczb fixed (np. 0.01x)
  • Konstrukcja zmiennych

Wszelkiego rodzaju zmienne posiadają własne konstruktory. Możemy więc spokojnie napisać:
float4 aaa(1.0f, 2.0f, 4.0f, 0.5f);
To samo możemy również zrobić wewnątrz jakiegoś wyrażenia, np.
aaa*float4(0.0f, 1.0f, 0.0f, 0.0f);
  • Nazwy znaczeniowe

Wspomniałem o nich przy omawianiu struktur. Jak już powiedziałem, służą połączeniu nazw zmiennych z odpowiednimi rejestrami karty graficznej. Nie musi być jednak tak, że rejestr 0 jest zawsze powiązany z pozycją obiektu - to zależy od profilu jaki wybierzemy. W niektórych profilach nazwy znaczeniowe są ściśle powiązane z numerem rejestru, gdy tymczasem w innych mogą być inne za każdym razem. Nazwy znaczeniowe mają to do siebie, że sama nazwa określa znaczenie ;), a więc nie trzeba tutaj nic tłumaczyć. Dostępne nazwy znaczeniowe:
  • POSITION
  • NORMAL
  • BINORMAL
  • BLENDINDICES
  • BLENDWEIGHT
  • TANGENT
  • PSIZE
  • COLOR0
  • COLOR1
  • TEXCOORD0
  • TEXCOORD1
  • TEXCOORD2
  • TEXCOORD3
  • TEXCOORD4
  • TEXCOORD5
  • TEXCOORD6
  • TEXCOORD7
Tak więc przykładowa struktura wejściowa dla verex programu może wyglądać tak:
struct instruct
{
  float4 position : POSITION;
  float3 normal   : NORMAL;
  float2 tex0     : TEXCOORD0;
};
To wszystko na dzisiaj. Następnym razem omówię funkcje, instrukcje warunkowe, operatory logiczne i kupę innego śmiecia jakże ułatwiającego życie programisty ;).

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