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 45 gość(ci) i 0 użytkownik(ów) online.

Jesteś anonimowym użytkownikiem. Możesz się zarejestrować za darmo klikając tutaj
Tutoriale - OpenGL - ?wiatła i listy wy?wietlania

Uszanowanko. Tak sobie siedzę i zastanawiam się jaki by tu wstęp napisać? Jakoś moją głowę ostatnio wypełnia matma i mikroekonomia. No cóż, w końcu sesja się zbliża w związku z czym same kolosy ostatnio - nawet artykułów nie ma kiedy pisać ;-(. Cóż to mamy dziś w planie? Otóż rzucimy trochę światła na to jak w OpenGL-u robi się światła :-D. Tak, tak, dziś oświetlimy nasze bryły, a to po to, żeby jeszcze bardziej zbliżyć ich wygląd do rzeczywistego (a swoją drogą - nie wydaje się Wam dziwne, że ludzie tworząc grafikę 3D dążą do tego, żeby w jak największym stopniu przypominała ona świat realny? Obecnie w grach możemy obserwować drzewa, które wyglądają jak prawdziwe, trawę, która się rusza, itp. - istny Matrix powstaje na naszych oczach :-P). Dodałem również listy wyświetlania (Display List). Co to jest i po co to nam, wyjaśnię później. Na razie powiem tylko, że przyspieszają one w znacznym stopniu renderowanie.

No dobra. Bierzmy się do roboty, bo znowu jest jej trochę. Pojawiła się nam kolejna funkcja globalna i sporo zmiennych na początku, ale ich zastosowanie omówię później. Zaczniemy od spraw najbardziej związanych z renderowaniem, czyli od list wyświetlania.
void BuildList( void )
{
    glNewList( CYLINDER, GL_COMPILE );
    glBegin( GL_TRIANGLE_STRIP );
    for( int i=0; i<=360; i++ )
    {
        glNormal3f( 3*sinf(DEG2RAD(i)), 0, 3*cosf(DEG2RAD(i)) );
        glVertex3f( 3*sinf(DEG2RAD(i)), 5, 3*cosf(DEG2RAD(i)) );
        glNormal3f( 3*sinf(DEG2RAD(i)), 0, 3*cosf(DEG2RAD(i)) );
        glVertex3f( 3*sinf(DEG2RAD(i)), -5, 3*cosf(DEG2RAD(i)) );
    };
    glEnd();
    glEndList();
};
Mamy więc naszą funkcję BuildList(). Pewnie już domyśliliście się już, że służy do budowania listy. Zaglądnijmy więc do środka:

glNewList( CYLINDER, GL_COMPILE );
To polecenie służy do zainicjowania listy wyświetlania. Używa się go tak jak glBegin() tzn. po wywołaniu glNewList(...) należy wywołać glEndList(), która kończy budowę danej listy.

Do czego w ogóle używa się list i co one dają? Jak już wspomniałem na początku, listy poprawiają wydajność. Jak? No cóż, wszyscy zauważyli pewnie wywołania sinusa i cosinusa między glBegin() i glEnd(). Do tego te wywołania powtarzane są 360 razy, jak wynika z naszej pętli. Wszyscy wiedzą, (jak nie to już wiedzą :-) że wywołania funkcji trygonometrycznych są stosunkowo drogie (w sensie czasu wykonania i mocy obliczeniowej na nie potrzebnej). Może nie uświadczycie tego na prostym kodzie, ale kiedyś, za czasów DOOM-a, użycie czegoś takiego jak sinf() podczas rysowania było wręcz niedopuszczalne. Stosowano wiele sztuczek np. budowano tablice zawierające wartości dla poszczególnych kątów, żeby tylko przyspieszyć działanie programu. Teraz oczywiście nie jest to już stosowane, ale nadal wszyscy szanujący się programiści gier unikają wielu wywołań tych funkcji, jeśli tylko mogą. Wyobraźcie sobie na przykład, że mamy 100 takich cylindrów (każdy renderowany osobno). Przy założeniu, że chcemy mieć jakieś 90 fps, to musimy wywołać funkcje sinf() 4 x 360 x 100 x 90 = 12.960.000 razy na sekundę i tyleż samo razy cosf(). To trochę dużo prawda?!

Możemy jednak ograniczyć te wywołania dzięki OpenGL i listom wyświetlania (tak po prawdzie, to możemy zrobić to i bez OpenGL, ale chciałem zademonstrować, jaki możemy uzyskać zysk). Otóż w listach wartości cos i sin obliczane są tylko raz i zostają zachowane do późniejszego użytku. Czyli w prostych słowach nasz cylinder jest konstruowany tylko raz. Potem to już tylko procedura renderowania. Jaki zysk daje użycie list? Szczerze mówiąc, to nigdy nie sprawdzałem, ale jak pamiętam z jakiegoś źródła, to ok. 20%. To naprawdę dużo jak dla grafiki. Wszyscy pewnie teraz z entuzjazmem krzykniecie, że nic tylko listy. No to niestety muszę Was trochę zmartwić. Jak zawsze, nie ma róży bez kolców ;-(. Listy są owszem dobre, ale tylko do obiektów, które nigdy nie zmieniają swojego kształtu, koloru itp. Przy czym mówię oczywiście o sytuacji, kiedy kolor chcielibyśmy zmienić dla poszczególnych wierzchołków, czy wielokątów bryły. Jeśli natomiast dla całej bryły, to nie ma problemu, zobaczymy to w funkcji renderujacej. Dlaczego tylko do nich? Właśnie dlatego, że wartości są zapamiętywane po utworzeniu listy i aby je zmienić, trzeba by ją utworzyć od nowa.

Parametrami glNewList() są numer danej listy (u nas jest to 1, bo przecież CYLINDER zastąpiony zostanie właśnie przez 1) i sposób budowy listy. Dla drugiego parametru dostępne są dwie wartości:
  • GL_COMPILE - czyli lista jest budowana do późniejszego użytku,
  • GL_COMPILE_AND_EXECUTE - czyli lista jest budowana i od razu wykorzystywana w procesie rederowania.
glBegin( GL_TRIANGLE_STRIP );
for( int i=0; i<=360; i++)
{
    glNormal3f( 3*sinf(DEG2RAD(i)), 0, 3*cosf(DEG2RAD(i)) );
    glVertex3f( 3*sinf(DEG2RAD(i)), 5, 3*cosf(DEG2RAD(i)) );
    glNormal3f( 3*sinf(DEG2RAD(i)), 0, 3*cosf(DEG2RAD(i)) );
    glVertex3f( 3*sinf(DEG2RAD(i)), -5, 3*cosf(DEG2RAD(i)) );
};
glEnd();
Zapytacie pewnie: co on zgłupiał?! Rysuje gdzie popadnie, zamiast w funkcji renderujacej? No cóż, mielibyście całkowitą rację, gdyby nie fakt, że wszystko znajduje się pomiędzy glNewList() i glEndList(). Jak widzicie, rzeczywiście wygląda to jak renderowanie. Tak właśnie budujemy nasze listy - jakbyśmy renderowali. A co renderujemy?
glBegin( GL_TRIANGLE_STRIP );
Po parametrze widzimy, że pasy trójkątów. Należy jeszcze pamiętać, że o ile nie robimy złożeń kilku obiektów na naszej liście, to nie powinniśmy przesuwać i obracać naszym obiektem między wywołaniami glBeginList() i glEndList(), ponieważ może to nam potem bardzo utrudnić rotacje i translacje podczas wykonywania programu.

Po pętli widzimy, że rysujemy sobie cylinder (dlaczego właśnie cylinder powiem później) o wysokości 10 i promieniu 3. I pewnie wszystko byłoby jasne, gdyby nie funkcja glNormal3f(). Jej zadaniem jest wyznaczenie wektora normalnego dla każdego punktu naszej bryły. Co to jest wektor normalny? No cóż, aby zrozumieć jego zadanie, musimy się przypatrzeć trochę otaczającemu nas światu. Jesteśmy przyzwyczajeni do tego, że jeśli np. coś piszemy, a jedynym źródłem światła jest lampa nad naszą głową, to kiedy się pochylimy, na naszej kartce będzie cień. Jak on powstał? Otóż nasze włosy i głowa pochłonęły i częściowo odbiły światło, które dawała lampa. Tak więc to, że widzimy tak jak widzimy, że nasze oczy rozróżniają kolory (" ... niebieski kwiat i kolce, niebieski kwiat i kolce. Kurde, byłoby łatwiej, gdybym nie był daltonistą ..." - Shrek :-) i odcienie spowodowane jest tym, że promień światła nim dotrze do naszego oka, odbija się miliony, a nawet miliardy (a może jeszcze więcej :-) razy od różnych przedmiotów. Za każdym razem jest częściowo pochłaniany i dopiero wtedy wpada do naszego oka (lub zostaje całkowicie pochłonięty). W grafice niemożliwe jest, niestety, aby osiągnąć taki efekt w czasie rzeczywistym. Przynajmniej nie przy obecnej mocy obliczeniowej naszych komputerów. Dlatego stosuje się pewne sztuczki. Zależne są one od sposobu cieniowania. Przy cieniowaniu płaskim (Flat Shading) natężenie światła obliczane jest dla całego wielokąta. Jak nietrudno się domyślić, efekt nie jest zbyt oszałamiający, ponieważ wszystko wygląda strasznie ostro - np. kula wygląda na okrągłą, ale widać wszystkie trójkąty, z której jest zbudowana. My jednak zajmiemy się cieniowaniem Gourauda (Gouraud Shading). W tym przypadku natężenie światła obliczane jest dla każdego wierzchołka z osobna, a następnie kolor jest interpolowany (przewidywane średnie natężenie) na reszcie wielokąta. Kolor ten zależny jest od tego, jaką składową (i jakiego rodzaju) światła obiekt odbija i w jakim stopniu. Np. aby uzyskać czerwony obiekt możemy albo oświetlić go białym światłem, jednocześnie ustawiając parametry światła rozproszonego na tylko czerwony, albo zrobić biały obiekt (odbija całość światła rozproszonego) i oświetlić go czerwonym światłem. Aby jednak obliczyć natężenie światła nasz program musi wiedzieć, gdzie jest góra wielokąta i pod jakim kątem promień świata pada na nasz wierzchołek. Skąd o tym wie? Właśnie dzięki wektorowi normalnemu, który jest wektorem skierowanym prostopadle do płaszczyzny wyznaczonej przez wektory, które przecinają się w miejscu, gdzie znajduje się nasz wierzchołek. Co prawda są dwa takie wektory (dwa wektory normalne) dla każdej takiej płaszczyzny, ale my nie mamy z tym żadnego problemu. Nasze wektory normalne zwrócone są tak, że wyznaczają przód wielokąta, a jak wiadomo, zależy to od ułożenia wierzchołków (tutorial o bryłach - część poświęcona ukrywaniu niewidocznych powierzchni). Na wyznaczanie wektorów normalnych są specjalne metody (iloczyn kartezjański tych dwóch wektorów rozpinających naszą płaszczyznę). My nie korzystamy z nich, ponieważ użyliśmy figury, której wektory normalne bardzo łatwo wyliczyć. Należy jeszcze zwrócić uwagę na dwie rzeczy. Po pierwsze nie ważne, jest gdzie nasz wektor jest zaczepiony - nas interesuje tylko kąt, jaki tworzy z promieniem światła. Po drugie powinniśmy używać tzw. normalnych jednostkowych, czyli takich, których długość wynosi 1. Jest to ważne jeśli chcemy zobaczyć poprawne efekty.

Teraz już wiemy jak działa światło, (wiemy prawda? wszystko jasne? :-) nie wiemy jednak jeszcze dwóch rzeczy. Po pierwsze jakie mamy rodzaje świateł, a po drugie jakie są ich składowe - mówiłem przecież, że możemy ustalić w jakim stopniu obiekt ma odbijać światło rozproszone. Jeśli idzie o rodzaje świateł, to wyróżniamy ich trzy:

  • światła kierunkowe (Directional Light) - to takie, które umieszczone są jakby w nieskończonej odległości od sceny. Posiadają tylko przybliżoną pozycję w jakiej się znajdują i kolor. Ich promienie są równoległe i mają zawsze takie samo natężenie w każdym punkcie sceny. Działają analogicznie jak słońce - oświetlają wszystko, bez względu na fakt, jak daleko obiekt znajduje się od ich teoretycznego źródła (współrzędnych).
  • światła punktowe (Point Light) - to takie, które nie posiadają kierunku w jakim świecą. Światło punktowe jest jak żarówka, która umieszczona na środku pokoju - rozświetla całe pomieszczenie. Stopień natężenia światła zależy od odległości od jego źródła.
  • reflektory (Spot Light) - czyli takie, które świecą w danym kierunku, mają ściśle określoną pozycję i dodatkowo posiadają kąt rozwarcia stożka (snopu) światła. Działają jak latarki. Na oświetlonym obiekcie pojawia się plama światła, a jej natężenie zależnie jest od odległości od źródła, a także od stopnia zanikania światła. Stopień zanikania światła można oczywiście modyfikować.
My użyjemy reflektorów, ponieważ jest to chyba najbardziej złożony rodzaj światła (ale powiem też jak zrobić inne). Jeśli natomiast idzie o składowe świateł, to muszę powiedzieć, że każde źródło światła składa się z trzech takich jak gdyby pod-świateł. Dla każdego z nich możemy wyznaczyć intensywność i kolor (4 składowe RGBA). Oto one:

  • Światło otaczające (Ambient) - jest to światło, które nie pochodzi z żadnego ściśle określonego kierunku. Oświetla ono całą scenę równomiernie - jego promienie padają wszędzie bez względu na kąt patrzenia i obrót. Stosując tylko tą składową nie zobaczymy efektu cieniowania obiektów, czyli ich głębi. Ogólnie rzecz biorąc służy ono do wstępnego pokolorowania sceny, do zakreślenia kształtów tak, aby obiekty nie znikały całkowicie, gdy główne źródło światła przestanie je oświetlać.
  • Światło rozproszone (Diffuse) - to główny składnik światła. Jego promienie pochodzą z konkretnego kierunku i są obijane od powierzchni równomiernie, dzięki czemu możemy dostrzec głębię obiektów.
  • Światło odbłysków (Specular) - jest podobne do światła rozproszonego, z tym, że jego promienie nie są odbijane równomiernie, ale ostro w jedną stronę. Jego działanie widzieliśmy niejednokrotnie np. kiedy słońce odbija się od tafli wody i razi nas w oczy. W OpenGL-u takie odbłyski są przedstawione jako jaśniejsza plama na powierzchni.
Pora zobaczyć jak włączyć nasze światła. Robimy to oczywiście w funkcji inicjalizacyjnej. Zajrzyjmy więc do niej:
glLightfv( GL_LIGHT0, GL_DIFFUSE, DiffuseLight1 );
glLightfv( GL_LIGHT0, GL_POSITION, LightPosition1 );
Funkcja glLightfv() służy do ustalania parametrów dla danego światła. Jako pierwszy argument przyjmuje numer światła. W OpenGL-u światła są bowiem, tak jak macierze, wbudowane. Maksymalna ilość świateł, jakie można włączyć jednocześnie wynosi 8. Ktoś może powiedzieć, że to mało, ale tak naprawdę, to świateł tych się praktycznie nie używa. W grach używamy lightmap, a w profesjonalnych programach graficznych głównie tzw. Ray Tracingu (śledzenie światła).

Kolejny argument decyduje, jaką składową chcemy zmieniać dla naszego światła. GL_DIFFUSE mówi OpenGL, że chcemy zmienić kolor i natężenie światła rozpraszanego (światło otaczające i odbłysków to odpowiednio GL_AMBIENT i GL_SPECULAR). Jako trzeci parametr podajemy tablicę zawierającą składowe RGBA. To właśnie po to potrzebne nam były te tablice na początku programu. Określają one jakim kolorem i o jakim natężeniu będą oświetlane obiekty przez to światło. GL_POSITION to parametr, dzięki któremu możemy zmienić pozycję światła. Znów jako trzeci parametr podajemy tablicę, tylko tym razem inną. Jej pierwsze trzy wartości to punkt określający położenie naszego światła. Ostatni parametr decyduje, czy światło ma być w nieskończonej odległości od sceny (wartość 0.0f), czy w scenie (wartość 1.0f). Ponieważ my chcemy mieć reflektor, to podajemy tam 1. To samo robimy dla drugiego światła, zmieniając tylko tablicę.
glLightf( GL_LIGHT0, GL_SPOT_CUTOFF, 20.0f );
glLightf( GL_LIGHT0, GL_SPOT_EXPONENT, 100.0f );
Pierwsze wywołanie określa nam kąt rozwarcia stożka naszego reflektora - u nas 20 stopni. Drugi określa stopień skupienia światła. W dokumentacji jest napisane, że wartość ta (trzeci argument) jest obcinana do najbliższej liczby całkowitej z zakresu [0, 128]. Logicznie patrząc powinno więc to działać w ten sposób, że wartość 0 oznacza minimalne, a wartość 128 maksymalne skupienie światła. Niestety nie wiedzieć czemu jest na odwrót, a co więcej wartości są większe niż z zakresu [0, 128] - gdzieś tak od [0, 400].
Z reflektorem związane są jeszcze trzy wartości, które można przekazać do funkcji glLightf(). Co prawda dotyczą one tego samego, czyli ustalenia sposobu i stopnia zanikania światła, ale wspomnę o wszystkich:
  • GL_CONSTANT_ATTENUATION - określa stałe zanikanie światła,
  • GL_LINEAR_ATTENUATION - określa liniowe zanikanie światła,
  • GL_QUADRIC_ATTENUATION - określa nieliniowe (kwadratowe) zaniknie światła.
Pierwsza wartość ustala, jak nie trudno się domyślić, stałe zanikanie, czyli takie, które nie jest związane z odległością. Ustalając drugą wartość mówimy OpenGL, o ile wraz z odległością ma zanikać natężenie naszego światła. Trzecia wartość mówi OpenGL, że zanikanie światła nie jest liniowe, czyli stopień zaniku światła nie jest proporcjonalny do odległości. Aby zrobić światło kierunkowe, wystarczy nie dodawać powyższych dwóch linii kodu (wywołań glLight() z parametrami GL_SPOT_CUTOFF i GL_SPOT_EXPONENT), a na końcu tablicy pozycji światła wpisać 1. Żeby dostać światło punktowe, należy po prostu nie dodawać tych dwóch linii i tyle.
glEnable( GL_LIGHT0 );
Czyli włączenie naszego światła. Nic tu wyjaśniać raczej nie trzeba :-).
glEnable( GL_LIGHTING );
No i stała się światłość. Polecenie to bowiem włącza oświetlenie jako takie. Bez wywołania funkcji glEnable() z parametrem GL_LIGHTING nic się nie stanie, mimo że wcześniej włączyliśmy światła. No i na końcu widzimy jeszcze wywołanie naszej funkcji budującej listę wyświetlania.
Przy oświetleniu należy wspomnieć jeszcze o jednej dość ważnej funkcji, której my nie używamy w naszym kodzie, a która może się czasami przydać. Mowa o funkcji:
gLightModel[f,i][v](GLenum, float* v);
Funkcja ta zmienia model oświetlenia. Jako pierwszy parametr może przyjmować jedną z trzech wartości:
  • GL_LIGHT_MODEL_AMBIENT - definiuje ogólne natężenie światła otaczającego w scenie. Do tego parametru używamy odmian funkcji, do których możemy przesłać tablice - cztery elementy RGBA.
  • GL_LIGHT_MODEL_TWO_SIDE - określa, czy oświetlane mają być obie strony wielokątów (drugi parametr to cokolwiek innego niż 0), czy tylko jedna (drugi parametr to 0). Domyślnie jest to tylko przednia strona.
  • GL_LIGHT_MODEL_LOCAL_VIEWER - określa, czy odbicia mają być kierowane do środka okładu współrzędnych (każda wartość poza 0), czy w kierunku ujemnych wartości Z (wartość 0).
Ostatnią z omawianych funkcji jest funkcja renderująca. W jej ciele widzimy coś takiego:
glMaterialfv( GL_FRONT, GL_DIFFUSE, mat );
Mówiłem na początku, że możemy zmieniać kolor obiektów znajdujących się na listach wyświetlania, ale tylko całych obiektów. Możemy to robić za pomocą funkcji glColor() jeśli oświetlenie jest wyłączone, albo mamy włączone śledzenie koloru. Albo za pomocą właśnie powyższej funkcji. Powoduje ona ustawienie właściwości materiału dla obiektu. U nas ustawia składową światła rozproszonego. Jako drugi (pierwszego nie muszę chyba już omawiać - jak ktoś nie pamięta, to niech zaglądnie do poprzedniego arta :-) argument funkcja może przyjmować jedną z poniższych wartości.
  • GL_AMBIENT - definiuje kolor i natężenie odbicia światła otaczającego.
  • GL_DIFFUSE - definiuje kolor i natężenie odbicia światła rozproszonego.
  • GL_SPECULAR - definiuje kolor i natężenie światła odbłysków.
  • GL_EMISSION - definiuje stopień i kolor emisji światła. Wbrew pozorom nie tworzymy w ten sposób kolejnego światła. Parametr ten służy jedynie nadaniu obiektowi takiego koloru, jakby sam był źródłem światła.
  • GL_SHININESS - definiuje stopień połysku materiału, czyli jak duża ma być plama połysku - stopień jej połyskliwości i kolor definiuje składowa światła odbłysków. Do tej wartości używamy funkcji glMateriali(). Jako trzeci parametr podajemy wartość z zakresu [0, 128].
A co to takiego materiał? Jest to określenie jaki kolor i rodzaj światła ma odbijać obiekt, a jaki pochłaniać i w jakim stopniu. Jest to zupełnie coś innego niż kolor obiektu w takim pojęciu, jakiego używaliśmy do tej pory. Gdybyśmy chcieli określać kolor tak jak poprzednio, to kolorując nasz cylinder np. na czerwono zobaczylibyśmy nie cylinder, ale prostokąt z trochę zakrzywionymi krawędziami górnymi. Brakowało by mu głębi. To samo osiągnęlibyśmy ustawiając tylko składową światła otaczającego. Aby pokazać o co mi chodzi, zilustruję to na rysunku (pożyczę rysunki od Robala, więc będzie kula, a nie cylinder, ale to nawet lepiej - widać dokładniej o co biega :-)

+
=
obiekt
światło
rezultat

Jak widać kula nie wygląda jak kula, ale jak koło. Brakuje jej właśnie tej głębi, która występuje w realnym życiu. Wiadomo przecież, że część przednia jest lepiej oświetlona niż tylna. Tak więc jeśli poprawnie określimy składową dla światła rozproszonego, to efekt powinien wyglądać mniej więcej tak:

+
=
obiekt
światło
rezultat

Musicie przyznać, że wygląda znacznie lepiej. To właśnie o to nam przecież chodziło, prawda? Następną funkcją jaką widzimy jest:
glCallList( CYLINDER );
Jest to nic innego, jak wywołanie naszej listy. Wywołanie to powoduje jednocześnie narysowanie naszego cylindra. Funkcja przyjmuje tylko jeden parametr - numer listy, którą chcemy wywołać. I to wszystko.

Jest jeszcze kilka funkcji związanych z listami wyświetlania, które mogą ułatwić nam życie, ale nie będę ich dziś omawiać. Jeśli jesteście ciekawi co to za funkcje i jak działają, to odsyłam do dokumentacji. Na końcu widzimy jeszcze wywołanie funkcji glTranslate(), której zadaniem jest obracanie światłami i funkcji glLightf() z parametrem dla pozycji i kierunku światła (GL_SPOT_DIRECTION). Kierunek ten to wektor.

To w zasadzie wszystko na dzisiaj. Następnym razem będą tekstury i jak zwykle coś jeszcze (mam nadzieję, ale może się okazać, że nie będzie nic więcej :-). Tutaj, jak zwykle zresztą, możecie popatrzeć jak to mniej więcej wygląda:



Kod źródłowy

©Copyright by Domino   



Tutoriale - OpenGL
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.08 sekund

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