[powrót]
Pracownia we wtorek 8.15-10.00.
Podsumowanie punktów podstawowych do zdobycia:
1 (zad 1, zabawka 2d) + 2 (zad 2, obrazek wektorowy) + 1.5 (zad 3, animacja 3d) + 2.5 (zad 4, Wavefront) + 2 (zad 5, różne) = 9
Zgodnie z tą punktacją, potrzeba 9 / 2 = 4.5 punktów żeby zaliczyć pracownię. Zdecydowałem obniżyć trochę wymagania, przyjmijmy że 8 punktów to norma, potrzeba 8 / 2 = 4 punktów żeby zaliczyć pracownię na ocenę 3. Zachęcam do rozwiązywania ostatnich zadań, Wavefronta (były za to zadanie w sumie 4 punkty do zdobycia, czasu prawie dwa miesiące) i ostatniego zadania (można zdobyć mnóstwo punktów, w sumie aż 8 jeżeli ktoś zaimplementuje wszystko; czyli gdyby ktoś chciał, mógłby zaliczyć całą pracownię na ocenę 5 tylko za implementację tego zadania). Zadanie Wavefront jest już mniej (o połowę) punktowane, wszystko z zadania 5 jest w pełni punktowane do końca sesji egzaminacyjniej (normalnej, nie poprawkowej).
Acha: w najbliższy wtorek są oficjalnie zajęcia czwartkowe. Czyli nie będzie już pracowni z PGK w najbliższy wtorek o 8 rano (chyba że komuś bardzo pasuje taki termin i chce przyjść). Proszę umawiajcie się emailem kiedy pasuje Wam oddawanie zadań.
Na koniec, kilka zadań z efektami graficznymi: cienie i odbicia lustrzane. Można zaimplementować jedno lub więcej z poniższych zadań, do wyboru. Do punktacji końcowej, za "normę" przyjmujemy że w sumie należy zdobyć za poniższe zadania 2 punkty.
Jeżeli ktoś ma problem z wyborem: najłatwiej zacząć od implementacji wersji podstawowej plane-projected shadows albo odbić na płaszczyźnie (szczególnie odbicia na płaszczyźnie na początku są trywialnie...). Potem można je rozwinąć aby używać stencil bufora. Wersja podstawowa plane-projected shadows (bez stencil bufora) + wersja podstawowa odbić (ze stencil buforem, bez clipPlanes) dają już w sumie 2.5 punkta, czyli nawet więcej niż "norma".
Termin zadań: egzamin jest 11 lutego o ile dobrze wiem. Powiedzmy że termin zadań jest do 10 lutego. Acha, gdzieś pomiędzy 1-10 lutego możemy zrobić dodatkowe ćwiczenia (z racji tego że na początku semestru nie zrobiliśmy pierwszej pracowni, bo była przed wykładem). Możemy je poświęcić na przygotowanie się do egzaminu, omówienie typowych zadań etc. Czekam na propozycje jakie terminy Wam pasują, mi w zasadzie pasuje wszystko.
(Wersja podstawowa z glPolygonOffset za 1 punkt:)
Patrz pierwsza część artykułu
"Real-time Shadowing Techniques".
Możemy skonstruować macierz która wykonuje rzut dowolnego modelu 3D na zadaną płaszczyznę, z punktu widzenia zadanego światła. (Wzór tej macierzy podany jest w artykule powyżej.) Czyli możemy wyrenderować scenę z cieniem w stylu
/* ... ustaw światło GL_LIGHT0 na pozycji light_position ... */ /* ... narusuj prostokąt (quad) o równaniu płaszczyzny plane ... */ rysuj_model(); glPushMatrix(); glMultMatrix(oblicz_macierz_rzutujaca_model_na_plaszczyzne(plane, light_position)); glColor3f(0, 0, 0); /* cień jest czarny */ rysuj_model(); glPopMatrix();
Pamiętajcie też mieć wyłączone oświetlenie (glDisable(GL_LIGHTING)
)
na czas rysowania cienia, żeby był należycie czarny.
W rysuj_model
można wyrenderować cokolwiek —
na przykład można tam wrzucić renderowanie jakiegoś modelu
w formacie Wavefront, co macie zrobione w poprzednim zadaniu.
Jeżeli ktoś nie zamierza robić zadania z Wavefrontem, to można tutaj użyć
dowolnego nietrywialnego modelu, np. glutSolidTeapot
.
Jak zobaczycie, ta metoda renderowania cienia ma trochę problemów. Przede wszystkim związanych z faktem że "rysujemy cień", co jest jakby na opak z fizyką. "Cień" normalnie polega na tym że czegoś (światła) nie ma. W normalnym algorytmie należałoby najpierw narysować scenę z wyłączonych oświetleniem, potem włączyć światło i rysować część sceny która jest oświetlona... my tego nie robimy. Zamiast tego najpierw rysujemy wszystko oświetlone, a dopiero potem "rysujemy cień".
W rezultacie nasz "cień" np. koliduje w z-buforze z samym prostokątem
na który miał rzucać cień
(w końcu są rysowane na tej samej pozycji). To powoduje przykre śnieżenie
pomiędzy kolorem prostokąta a czernią w miejscu gdzie jest cień.
W najprostszej wersji zadania aby to obejść należy
użyć glPolygonOffset
oraz glEnable(GL_POLYGON_OFFSET_FILL)
.
(+1 punkt) zamiast glPolygonOffset
można postarać
się bardziej. Nie tylko poprawimy w ten sposób śnieżenie, ale też
naprawimy drugi problem: nasz cień "wychodzi" poza prostokąt na którym miał być.
Tzn. nasz prostokąt jest jakimś skończonym quadem, ale cień
pojawi się wszędzie, na całej nieskóńczonej płaszczyźnie przechodzącej przez
ten prostokąt
(co jest logiczne, w końcu nawet funkcja oblicz_macierz_rzutujaca_model_na_plaszczyzne
dostała tylko równanie płaszczyzny, nie znała dokładnej pozycji naszego
prostokąta).
Można elegancko to rozwiązać przez bufor szablonów, stencil buffer.
Przy czyszczeniu ekranu przez glClear
będziemy chcieli czyścić
stencil buffer (flaga GL_STENCIL_BUFFER_BIT
).
Po narysowaniu modelu, rysujemy płaszczyznę, w taki sposób aby
"oznaczyć" sobie w stencil buforze gdzie znajduje się nasz quad.
Będzie do tego potrzebna glStencilOp
.
Potem rysujemy nasz cień ale tylko tam gdzie znajdował się quad
— do tego celu będziemy musieli testować stencil buffer,
patrz glStencilFunc
.
Dzięki temu nasz cień będzie rysowany tylko tam gdzie jest rysowany
quad. Ponadto, zwróćmy uwagę że już nie potrzebujemy depth testu
(skoro wiemy dokładnie gdzie nasz quad jest widoczny).
Możemy go wyłączyć (np. glDisable(GL_DEPTH_TEST)
).
Dzięki temu, nie potrzebujemy już
glPolygonOffset
ani glEnable(GL_POLYGON_OFFSET_FILL)
.
(Wersja podstawowa (bez clipPlanes) za 1.5 punkta:)
Świetne
omówienie tej techniki, z przykładowym kodem źródłowym.
Początek jest naprawdę prosty: wyobraźmy sobie że nasza scena 3D jest zawsze ponad płaszczyną Z, tzn. ma Z > 0. Wyobraźmy sobie też że na płaszczyźnie Z = 0 mamy doskonałe, nieskończone lustro. To jak wyrenderować odbicie naszej sceny ?
rysuj_model(); glPushMatrix(); glScalef(1, 1, -1); rysuj_model(); glPopMatrix();
Innymi słowy, glScalef(1, 1, -1)
odbija nam model względem Z.
Jako rysuj_model
należy użyć dowolnej nietrywialnej sceny,
można wyrenderować dowolny model w Wavefroncie albo np. glutSolidTeapot
.
Żeby sztuczka wyglądała lepiej, należy:
Po pierwsze, niech lustro będzie prostokątem, zamiast wyobrażać sobie "nieskończoną płaszczyznę lustra". Czyli narysujmy tego quada w OpenGLu.
Teraz zróbmy żeby kolor lustra był zmieszany z kolerem odbitego obrazu.
Czyli użyjemy dodatkowo blending (patrz
glBlendFunc,
glEnable(GL_BLEND)
i czwarty komponent do kolorów
(dla glColor
lub glMaterial
/ glLight
)).
Tzn. najpierw narysuj scenę, potem w trybie blending narysuj lustro.
(Na odwrót nie byłoby dobrze — rysowanie całej sceny z włączonym
blending spowoduje artefakty, ponieważ depth test nie będzie działał jak
należy w sytuacji kiedy kolejny malowany kolor jest mieszany z kolorem ekranu.)
To sprawi wrażenie odpowiednio zabarwionego lustra.
Po drugie, teraz będzie widać że nasz odbity model jest widoczny wszędzie — nie tylko tam gdzie narysowaliśmy quad lustra. Musimy to poprawić używając stencil bufora, zupełnie podobnie jak dla plane-projected shadows. Wyznaczymy sobie gdzie jest widoczna tafla lustra, i będziemy rysować odbicie lustrzane tylko tam.
Zauważcie że w praktyce oznacza to że narysujemy lustro co najmniej dwa razy: raz, tylko do stencil bufora, żeby wyznaczyć w stencil buforze gdzie jest lustro. Potem narysujemy odbitą scenę. I drugi raz narysujemy lustro, tym razem do bufora kolorów, żeby dodać zabarwienie lustra.
Pamiętajcie cały czas pilnować i odpowiednio przełączać
zapis / test trzech buforów: stencil, bufora głębości i bufora kolorów.
Bufory można też czyścić razem albo z osobna przez glClear
.
Po chwili kombinowania ukaże się odpowiedni efekt... przykładowy
kod źródłowy jest w artykule powyżej, w razie potrzeby.
(+0.5 punkta) Wreszcie, co się stanie kiedy nasz model
może przecinać taflę lustra, czyli kiedy ma dowolne Z ?
(zakładając że zrobiliśmy lustro na Z = 0).
Trzeba go obciąć, bo inaczej będzie widać że obiekt "wystaje"
z lustra. Patrz glEnable(GL_CLIP_PLANExxx)
oraz
glClipPlane(GL_CLIP_PLANExxx, ...)
. Krótkie omówienie
tego razem z przykładowym kodem jest tutaj.
(2 punkty) Cienie za pomocą shadow volumes, w najprostszej wersji (z-pass, jedno źródło światła (do wyboru: punktowe lub kierunkowe), bez żadnych optymalizacji). Google podaje mnóstwo linków na ten temat, więc nie będę się rozpisywał... podkreślam tylko że w ramach zadania nie trzeba implementować żadnych optymalizacji. Czyli z-pass (bez żadnych capping). Najpierw renderuj całą scene z wyłączonymi światłami (jedynie global ambient, ew. ambient świateł). Potem odpowiednio zainicjuj stencil bufor używając shadow volumes: wystarczy dla każdego trójkąta wyrenderować odpowiedni shadow volume (czyli 3 quady). Po czym wyrenderuj scenę z włączonym oświetleniem, tam gdzie stencil bufor mówi że nie ma cienia.
(2 punkty) Samemu zaimplementować implementację macierzy + rasteryzację + z-bufor i napisac OpenGLa np. w trybie tekstowym. (Hej, OpenGL a'la aalib ?).
Zasadniczo trzeba wklepać macierze OpenGLa i zaimplementować mnożenie macierzy 4x4 oraz rasteryzację trójkątów (pełna wersja: z kolorami, niepełna: bez kolorów). Idea jest taka żeby samemu zaimplementować minimalny podzbiór OpenGLa: glOrtho, gluPerspective, glFrustum, glTranslate, glRotate, glScale (to wszystko jest glMultMatrix z odpowiednim parametrem), glLoadMatrix, glMultMatrix, glBegin(GL_TRIANGLES), glVertex, glEnd, glColor (w pełnej wersji: glColor może być pomiędzy glBegin...glEnd, czyli musimy interpolować kolory; w niepełnej: glColor musi być poza glBegin...glEnd, czyli mamy tylko trójkąty jednego koloru).
Dodatkowo, implementacja ma zawsze zachowywać się jakby test głębokości i zapis do z-bufora były włączone (opis z-bufora był w treśći poprzedniego zadania, głębokość pixela mamy po prostu w komponencie "z" wierzchołka po transformacjach macierzami).
Nowe zadanie pojawi się za kilka dni. Na razie czekam jeszcze na więcej rozwiązań zadania 4, punktacja jest aktualna na dzisiaj wieczór więc pewnie wszyscy właśnie pracują nad zadaniem 4 :) Tym niemniej od przyszłego wtorku punktacja za zadanie spada do 3/4, potem do połowy...
Zapowiedź przyszłego zadania: już niepotrzebna, patrz sekcja powyżej.
Kilka wiadomości dzisiaj:
Jak niektórzy z Was zauważyli, funkcja clock
ma niekiedy zbyt
małą dokładność pod Linuxem. Czyli pod Unixami najlepiej używać gettimeofday
.
Rozszerzyłem artykulik o time-based
animation o przykłady użycia gettimeofday
.
Punktacja na dole strony jest w pełni uzupełniona na dzień dzisiejszy. Jeżeli czegoś brakuje, to znaczy że gdzieś mi się zawieruszyło — proszę zgłaszać.
Punktacja za zadanie 4: zadanie będzie liczyło się do ogólnej punktacji jakby było warte 2.5 punktów podstawowych. Co znaczy tyle że chciałbym żeby każdy z Was zrobił coś dodatkowego, nie tylko geometrię + oświetlenie. Zadanie jest i ciekawsze i ważniejsze niż np. zadanie z grafiką wektorową, stąd taka decyzja.
1 punkt jest za podstawową zabawę z geometrią i oświetleniem,
+1 za tekstury (które są stosunkowo proste kiedy mamy
już zrobione materiały, trzeba nauczyć się posługiwać jakąś biblioteką
do odczytu obrazków),
+1 za animację,
+1 za interfejs GUI do bardziej
elastycznej zabawy ze światłami (wcześniej było 0.5 punkta, zwiększyłem).
Dodałem też więcej przykładowych modeli do
katalogu sample_wavefront_obj:
prosty cube z teksturą, i humanoid.
Humanoid zawiera 4 pliki OBJ o tej samej strukturze, możecie bawić się
składając je w animacje. Animacja pomiędzy dwoma modelami:
humanoid_walk_1
i humanoid_walk_2
spowoduje że ludzik
wykona krok.
Odczyt modelu 3D z pliku.
Zadanie polega na odczycie modelu 3D z pliku.
Jest wiele formatów modeli 3D, proponuję Wam zaimplemetowanie
odczytu formatu Wavefront (pliki xxx.obj
,
dodatkowo materiały mogą być zapisane w oddzielnym pliku xxx.mtl
).
Format Wavefront to format tekstowy, bardzo łatwy do odczytu linia-po-linii.
Pełna
specyfikacja formatu Wavefront (.obj) (bez obaw, nie musicie
implementować wszystkiego z tej specyfikacji :).
Specyfikacja
formatu materiałów (.mtl).
Bardzo uproszczona
opis formatu jest też tutaj.
W wersji podstawowej (1 punkt) trzeba odczytać model z pliku, wyświetlić jego geometrię (wypełnioną, już nie samą siatkę jak w poprzednim zadaniu) i pobawić się materiałami i oświetleniem.
Należy też użyć zabawek zrobionych w ramach poprzedniego zadania: 1. włączyć wyświetlanie FPS (frames per second) 2. umożliwić nawigację na dwa sposoby, zaimplementowaną w poprzednim zadaniu (w stylu gier jak DOOM/Quake/etc., i w stylu "Examine").
Dodatkowo (1 punkt) zaimplementować teksturowanie — czyli poprawnie odczytywać współrzędne tekstur i sam obrazek z teksturą, i odpowiednio przekazywać je do OpenGLa, aby wyświetlany model był teksturowany.
Dodatkowo (1 punkt) zaimplementować animację poprzez odczyt
dwóch modeli o tej samej strukturze z 2 plików. "O tej samej strukturze"
oznacza tutaj że dwa modele są dokładnie takie same, poza współrzędnymi
wierzchołków. Tzn. obydwa modele mają tyle samo wierzchołków, połączonych
w dokładnie taki sam sposób (czyli linie zaczynające się od f
w Wavefront są takie same), jedynie same pozycje tych wierzchołków są inne
(czyli linie zaczynające się od v
, vn
mogą zawierać
różne wartośći w obu plikach).
Animacja polega na tym że interpolujemy liniowo pomiędzy dwoma
odczytanymi w ten sposób modelami. Na przykład mamy zmienną animacja
,
która zmienia się od 0 to 1, i z powrotem. Kiedy wyświetlamy model,
wierzchołek Vi ma pozycję
Vi = V1i * (1 - animacja) + V2i * (animacja)
Gdzie V1 i V2 to pozycje wierzchołków odpowiednio z pierwszego i drugiego pliku. Czyli dla animacja = 0 wyświetlimy po prostu model pierwszy, dla animacja = 1 wyświetlimy po prostu model drugi, dla wartości pomiędzy — wyświetlimy jakiś model pośredni, animacja będzie zmieniać pierwszy model w drugi.
Taki sposób animacji nazywany jest gdzieniegdzie "vertex deformation", albo "morphing".
Przykładowe modele:
Ponadto każdy rozsądny program do modelowania 3D (na przykład (z open-source'owych) Blender, Art of Illusion, z innych: 3ds Max (jest dostępny w pracowni 107)) pozwala na eksport stworzonych modeli do tego formatu.
Hint do eksportu w Blenderze: eksportuj z zaznaczoną
opcją Normals żeby Blender zapisał odpowiednie linijki
vn
.
W Internecie można znaleźć wiele modeli w tym formacie. W razie potrzeby, można przekonwertować modele z innych formatów (VRML, 3DS, Max, wiele innych) do formatu OBJ (chociaż taka konwersja, w zależności od użytego programu i formatu, często "zgubi" nam niektóre właściwości modelu).
Wasz program ma odczytać dowolny model 3D w formacie Wavefront.
Absolutnie nie wymagam pełnej implementacji tego formatu, pełna specyfikacja
zawiera sporo rzeczy trudnych i/lub praktycznie bezużytecznych.
Aby odczytać geometrię, wystarczy interpretować linijki v
(wierzchołek) oraz f
(ściana; wymienione są po kolei numery
wierzchołków które należy połączyć aby otrzymać ścianę). Dla materiałów
i ew. tekstur trzeba interpretować trochę więcej, czytaj dalej...
Po pierwsze, używamy powierzchni wypełnionych, czyli:
Mamy do dyspozycji trójkąty (glBegin(GL_TRIANGLES)
,
glBegin(GL_TRIANGLE_FAN)
, glBegin(GL_TRIANGLE_STRIP)
),
czworokąty (glBegin(GL_QUADS)
, glBegin(GL_QUAD_STRIP)
),
wielokąty (glBegin(GL_POLYGON)
). Patrz
"OpenGL programming guide" rozdział 2. W skrócie, poniższy rysunek
wyjaśnia wszystko:
Mamy glutSolidCube
i wszystkie inne glutSolidXxx, patrz
dokumentacja GLUTa o "Geometric Object Rendering".
Mamy quadrici OpenGLa (patrz
omówienie
quadriców) i możemy ustawić im gluQuadricDrawStyle
na GLU_FILL
.
(Chociaż akurat do tego zadania glutSolidXxx, quadrici, Wam się nie przydadzą; wystarczy używać GL_POLYGON lub GL_TRIANGLES do tego zadania).
Kiedy macie już geometrie wypełnioną i uruchomicie program, zauważycie dwa problemy. Po pierwsze, obiekty są rysowane jednym, jednolitym kolorem. Nie wygląda to zbyt realistycznie... i tutaj właśnie wkracza oświetlenie OpenGLa. Oświetleniem zajmiemy się za chwilę.
Drugi problem: jeżeli popatrzycie na swój model z rożnych stron
zauważycie że ściany zasłaniają się w dość dziwny i nieprawidłowy sposób.
Dokładniej: jeżeli w danym punkcie ekranu rysowaliśmy więcej niż jedną
ścianę to na ekranie zobaczymy tą ścianę która była rysowana jako
ostatnia w naszym display
. Tak jest oczywiście źle: my chcemy
widzieć ta ścianę która jest najbliżej kamery, bo to ona zasłania
pozostałe ściany. Rozwiązanie: użycie bufora głębokości, albo
inaczej Z-bufora.
Krótkie omówienie jak działa Z-bufor: Idea jest prosta: dla każdego pixela który rysujemy na ekranie zapamiętajmy pomocniczą informację: odległość rysowanego punktu (w przestrzeni 3D) od kamery. Zanim narysujemy pixel sprawdzamy czy nie ma w tym miejscu ekranu już narysowanego pixela o mniejszej odległości od kamery. Pseudokod będzie pewnie bardziej jasny od moich mętnych wyjaśnień:
... na początku rysowania danej klatki zainicjuj bufor_głębokości: for x := 0 to Width for y := 0 to Height bufor_głębokości[x][y] := + nieskończoność; ... ... za każdym razem gdy chcesz wypełnić pixel x, y kolorem k: ... z := oblicz odległość punktu 3D (który wywołał rysowanie danego pixela 2D na pozycji x, y) od kamery; if z < bufor_głębokości[x][y] bufor_głębokości[x][y] := z; // Bufor kolorów to po prostu "ten bufor który jest widoczny na ekranie" bufor_kolorów[x][y] := k;
Rezultat użycia Z-bufora: obiekty 3D mogą być rysowane w dowolnej
kolejności w display
, na ekranie będą widoczne poprawnie.
Z-bufor można efektywnie zaimplementować sprzętowo, co znaczy
tyle że bufor głębokości jest pamiętany na karcie graficznej i jest
testowany i zapisywany przez procesorek karty graficznej.
Czyli "algorytmu" Z-bufora podanego powyżej nie macie implementować sami,
on już jest zaimplementowany na karcie graficznej, my go tylko "aktywujemy".
Kiedy używamy Z-bufora musimy zdawać sobie sprawę że wartości
zapisywane/porównywane w buforze głębokości to nie są
zwykłe odległości obiektu od kamery.
Zamiast tego są to odległości obliczane przez macierz perspektywy.
W skrócie to co trzeba wiedzieć to że wartości
near
i far
które podajemy
do naszych procedur gluPerspective
, glFrustum
i
glOrtho
maja wpływ na jakość Z-bufora. Jeżeli podamy
zbyt małe near
lub zbyt duże far
to szybko
zauważymy że precyzja naszego Z-bufora jest zła. Czyli zawsze
trzeba starać się podać możliwie duże near
i możliwie
małe far
. Ponadto wartości na których operuje Z-bufor
są bardziej gęste (czyli są obliczane i porównywane z lepszą precyzją)
bliżej kamery,
mniej gęste dalej od kamery. Czyli jest szczególnie ważne aby
near
było możliwie duże.
Artykuł na Wikipedii
o Z-buforze,
referencja funkcji glDepthRange oraz OpenGL
FAQ o Z-buforze podają więcej szczegółów.
Przechodząc do implementacji: aby użyć Z-bufora w OpenGLu trzeba
GLUT_DEPTH
przy
glutInitDisplayMode
. W SDL można dodać np.
SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );zanim zrobimy
SDL_SetVideoMode
.
glEnable(GL_DEPTH_TEST)
.
GL_DEPTH_BUFFER_BIT
do glClear
.
Używanie oświetlenia:
glMaterial
.
W zadaniu należy odczytać materiały z pliku z materiałami.
Należy odczytywać co najmniej kolory RGB ambient, diffuse, specular z
linijek zaczynających się od Ka
, Kd
, Ks
.
Ponadto, aby odpowiednio przyporządkować materiały ścianom, Wasz
program musi obsługiwać linijkę newmtl
w której podajemy
nazwę definiowanego materiału. W pliku OBJ należy obsługiwać linijki
mtllib
(wskazuje skąd odczytać materiały) oraz usemtl
(nazwa materiału używanego na kolejnych ścianach).
glLight
Oświetlenie trzeba włączyć przez glEnable(GL_LIGHTING)
.
Pojedyncze światła trzeba jeszcze włączyć przez
glEnable(GL_LIGHT<numer_światła>)
.
W naszym zadaniu, chciałbym żebyście włączyli jedno albo dwa światła. Parametry (kolory, pozycja) świateł nie są odczytywane z pliku OBJ — ich początkowe wartości można zaszyć jako stałe w programie, i należy dać użytkownikowi możliwość zmiany tych parametrów.
Odczytaj wektory normalne z pliku OBJ (linijki vn
).
Trzecie indeksy w linijkach opisujących ścianę wskazują który
wektor normalny użyć dla danego wierzchołka. Na przykład
pobrubione indeksy poniżej wskazują na pierwszy, drugi i trzeci
wektor normalny.
f 1/1/1 2/2/2 3/3/3
Jeżeli w pliku brakuje opisu wektorów normalnych, wtedy należy obliczać dla każdej ściany wektor normalny używając vector product. Jeżeli mamy trójkąt z wierzchołkami T0, T1, T2 to wektor normalny możemy obliczyć jako
Normalized( VectorProduct(T2 - T1, T0 - T1) )
To oblicza wektor wychodzący ze strony CCW ściany. Wychodzi ze strony CCW (counter-clockwise) oznacza że jeżeli patrzylibyśmy na ścianę z takiej strony że jej kolejne wierzchołki wydawałyby się nam ułożone niezgodnie z kierunkiem wskazówek zegara, to wtedy wektor wychodziłby ze ściany do nas.
Patrz wikipedia o cross product.
Dla ściany o więcej niż 3 punktach, możemy po prostu obliczyć wektor normalny tak samo biorąc pierwsze trzy wierzchołki ściany.
Można wydać OpenGLowi polecenie glEnable(GL_NORMALIZE)
,
wtedy nie trzeba samemu normalizować wektórów normalnych.
Jeżeli odczytujecie wektory normalne z pliku, wtedy każdy
wierzchołek ma potencjalnie inny wektor normalny, i mamy
cieniowanie Gourauda, patrz
wikipedia
o Gouraud shading. W OpenGLu należy ustawić glShadeModel
na
GL_SMOOTH
żeby kolory (wynikające z różnego oświetlenia każdego wierzchołka)
były należycie interpolowane.
Wektory normalne podajemy przez glNormal
(np. przed
każdym glVertex
).
GL_LIGHT_MODEL_TWO_SIDE
dla wywołania glLightModel
. Czasami dobrze jest też
zwiększyć GL_LIGHT_MODEL_AMBIENT
żeby mieć jaśniej na
całej scenie.
Po dokładniejsze omówienie jak działa oświetlenie odsyłam do rozdziału "Lighting" z "OpenGL programming guide".
Wasz program powinien pozwalać pobawić się oświetleniem,
zmieniać kolor i pozycję i/lub kierunek świateł.
Pozycje świateł dobrze będzie pokazywać w jakiś sposób (np. rysując
w tym miejscu punkt przez glBegin(GL_POINTS)
lub mały sześcianik
przez glutXxxCube
).
Interfejs do operowania wszystkimi ustawieniami oświetlenia można zrobić dowolnie. Parametrów jest dużo (należy ustawić jedno lub dwa światła, każde światło ma pozycję lub kierunek, 3 kolory (ambient, diffuse, specular) RGB). Więć będziecie musieli użyć wielu klawiszy, być może też sprawdzać np. czy wciśnięty jest klawisz Shift lub Ctrl (np. klawisz "s" zmienia Red koloru diffuse światła, klawisz "Shift+s" zmienia Green koloru diffuse światła, klawisz "Ctrl+s" zmienia Blue koloru diffuse światła).
Można też użyć lepszego interfejsu (+ 1 punkta) z przejrzyście wyglądającymi kontrolkami, które pozwalają zmieniać parametry za pomocą myszki itd. Są biblioteki które umożliwiają rysowanie takich kontrolek bezpośrednio w kontekście OpenGLa, np.
Większość "normalnych" bibliotek do kontrolek pozwala też zainicjować kontekst OpenGLa jako szczególny rodzaj kontrolki. Np. GTK (w połączeniu z GtkGLExt lub starszą GtkGLArea), wxWidgets. Można więc napisać "normalny" program okienkowy i jako jego część wstawić kontrolkę renderującą naszą scenę przez OpenGLa.
Żeby odczytać tekstury z pliku rozsądnie jest użyć jakiejś gotowej biblioteki. Polecam SDL_image.
Skompiluj, przetestuj sdl_image_draw_fixed.c. Ten program używa SDL_image i wyświetla obrazek na ekranie.
Program używa pliku sdl_utils.c
który implementuje poręczną funkcję Img_GL_Load
.
Możecie swobodnie jej używać do Waszego zadania.
SDL_image umie poradzić sobie z wieloma popularnymi formatami obrazków. Można testować na różnych obrazkach, chociażby przykładowe obrazki w różnych formatach.
Co do kompilacji programów z SDL_image pod Windowsem, na stronie windows_compilation jest odpowiedni paragraf.
glDrawPixels(img->w, img->h, GL_RGB, GL_UNSIGNED_BYTE, img->pixels);
Chcąc użyć obrazek jako teksturę, należy zamiast tego załadować
obrazek przez glTexImage2D
. Współrzędne tekstury podajemy
przez glTexCoord
(zazwyczaj glTexCoord2f
).
Ponieważ wywoływanie glTexImage2D
za każdym razem
kiedy chcemy ładować teksturę z pliku zajmuje dużo czasu,
dobrze jest używać glGenTextures
żeby wygenerować sobie
kilka identyfikatorów tekstur, potem robić glBindTexture
żeby ustawić dany identyfikator tekstury na aktywny. Na początku
programu można załadować dane tekstur, wywołując
glBindTexture
i glTexImage2D
dla wszystkich
tekstur. Potem w trakcie wyświetlania można przełączać się
pomiędzy teksturami prostym glBindTexture
.
glEnable(GL_TEXTURE_2D)
.
vt
(współrzędne tekstury), w linijkach f
drugie indeksy przy
wierzchołkach ścian wskazują odpowiednią współrzędną tekstury.
Na przykład pogrubione indeksy poniżej wskazują na pierwszą, drugi i trzecią
współrzędną tekstury:
f 1/1/1 2/2/2 3/3/3
Ponadto z pliku materiałów trzeba obsługiwać linijki w postaci
map_Kd nazwa_pliku_z_teksturążeby wiedzieć skąd załadować tekstury.
Formaty obrazków: przykładowy model w sample_wavefront_obj/basic_castle_textured/ używa tekstur w formacie JPG i PNG. Ale nie wymagam żebyście koniecznie obsługiwali akurat te formaty obrazków (chociaż są to dwa naprawdę popularne formaty, i jeżeli użyjecie SDL_image to automatycznie będziecie je obsługiwali). Wystarczy mi że będziecie obsługiwali jakikolwiek format obrazków, na przykład PPM albo BMP.
Uwaga: nawet jeżeli nie implementujecie teksturowania, Wasz program
powinien ciągle działać z plikami w których zapisane są współrzędne
tekstury. Innymi słowy, jeżeli nie implementujecie teksturowania,
wtedy Wasz program powinien ignorować współrzędne tekstur, czyli
2 indeks w wierzchołkach linii face (f
) oraz linie vt
.
A nie "wysypywać" się na takich plikach.
Termin na wykonanie zadania: 8 stycznia, czyli pierwsze zajęcia w nowym roku. Zadanie będzie liczyło się do ogólnej punktacji jakby było warte 2.5 punktów podstawowych — co znaczy tyle że chciałbym żeby każdy z Was zrobił coś dodatkowego, nie tylko geometrię + oświetlenie.
Kiedy już zaimplementujecie wszystko :), i w spokoju będziecie oglądać różne sceny.... Warto przetestować różne duże sceny i zobaczyć ile FPS (frames per second) ma nasz program. Jak spada FPS kiedy liczba trójkątów sceny rośnie ? Jak spada FPS kiedy zbliżamy i oddalamy się od/do sceny (tzn. ta sama ilość trójkątów raz zajmuje mało miejsca na ekranie, raz dużo) ? Jak zmienia się FPS kiedy obracamy się kamerą tak że patrzymy w pustkę i cała scena jest "za naszymi plecami" ?
Podstawy 3D.
Krótkie omówienie transformacji 3D w OpenGL:
Zacznijmy od funkcji reshape
. Dotychczas zawsze
używaliśmy funkcji reshape zaimplementowanej mniej więcej tak:
void reshape(int window_width, int window_height) { glViewport(0, 0, (GLsizei) window_width, (GLsizei) window_height); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, (GLdouble) window_width, 0.0, (GLdouble) window_height); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); }
Nadszedł teraz czas na zrozumienie o co tu chodzi. OpenGL zawsze pamięta dwie macierze (tak naprawdę jest jeszcze trzecia, macierz tekstury, ale nią się na razie nie zajmujemy). Jedna to macierz przeznaczona do transformacji projection (a wiec rzut perspektywiczny lub ortogonalny), druga jest przeznaczona do "normalnych" operacji (modelview) na obiektach (jak przesunięcia, obroty, skalowania). Matematyka (dlaczego macierze, i jakie macierze) była na wykładzie, więc w to nie będziemy tu wchodzić. Poniżej krótko omawiam najważniejsze funkcje OpenGLa związane z macierzami:
Polecenie glMatrixMode(GL_PROJECTION);
stwierdza że teraz będziemy operować na macierzy projection,
polecenie glMatrixMode(GL_MODELVIEW);
stwierdza
ze teraz operujemy na macierzy modelview.
Polecenie glLoadIdentity
ustawia jedną z macierzy
(tą aktualnie wybraną przez glMatrixMode
)
na macierz identyczności.
Polecenia gluOrtho2D
, gluPerspective
mnożą aktualną macierz przez odpowiednią macierz rzutowania.
Prawie zawsze używamy ich na macierzy projection. Czyli sekwencja
glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluOrtho2D(0.0, (GLdouble) window_width, 0.0, (GLdouble) window_height);
służy ustawieniu macierzy na rzut ortogonalny.
Jeśli zmienimy gluOrtho2D
na gluPerspective
(odpowiednio dobierając parametry gluPerspective
, oczywiście,
patrz dokumentacja gluPerspective
) to mamy rzut perspektywiczny.
Późniejsze
glMatrixMode(GL_MODELVIEW); glLoadIdentity();
przywraca stan domyślny: operujemy na macierzy modelview i zaczynamy z macierzą identycznościową.
Polecenia glTranslate
, glRotate
,
glScale
mnożą aktualną macierz (wybraną przez
glMatrixMode
) przez odpowiednią macierz przesunięcia,
obrotu lub skalowania. Prawie zawsze chcemy ich używać na
macierzy modelview.
Zwracam uwagę że ponieważ macierze są mnożone to transformacje się akumulują — można o nich myśleć jak o przesuwaniu obiektów albo o zmienianiu układu współrzędnych, w pierwszym przypadku kolejność operacji jest odwrotna od "intuicyjnej". Po dokładniejsze wyjaśnienie patrz rozdział 3 w "Red Book".
Zwracam uwagę ze tych poleceń (glTranslate
, glRotate
,
glScale
) już używaliśmy w
zadaniu na 1 pracowni, gdzie rysowaliśmy 2D.
Teraz okazuje się że wystarczy podawać odpowiednie przesunięcia/wektory
obrotu w 3D i te same polecenia pozwalają nam operować w 3D.
Dla OpenGLa grafika 2D jest po prostu "specyficznym przypadkiem"
grafiki 3D.
Punkt w 3D rysujemy naturalnie przez wywołanie
glVertex3f(x, y, z)
, które jest uogólnieniem znanego nam
glVertex2f(x, y)
.
Tak naprawdę glVertex2f(x, y)
to to samo co zwyczajne glVertex3f(x, y, 0)
. Czyli w
glVertex2f
trzecia współrzędna jest po prostu implicite
przyjmowana za zero. W połączeniu z rzutowaniem ortogonalnym
(domyślnie ustawionym tak że kamera patrzy wzdłuż osi Z) pozwala
to myśleć programiście o ekranie 2D, mimo że OpenGL "myśli" zawsze
w kategoriach 3D.
Polecenie gluLookAt
ustawia kamerę
(pozycję, kierunek patrzenia, pion). Prawie zawsze chcemy go użyć na
macierzy modelview.
De facto polecenie to działa na zasadzie przesuwania i obracania sceny, tak ze zarówno użytkownikowi jak i programiście wydaje się ze "przesunięto/obrócono kamerę". A tak naprawdę OpenGL nie zna pojęcia "kamery" — on tylko odpowiednio przesunął/obrócił scenę.
Co się dzieje wewnątrz ? Każdy punkt (podawany np.
przez glVertex
) jest transformowany macierzą
modelview i projection, tzn.
punkt_rysowany_na_ekranie = Macierz_projection * macierz_modelview * punkt_podany_przez_glVertex
Jak widać, do niektórych zastosowań w OpenGLu wystarczyłaby jedna macierz, zawierająca skumulowane macierze modelview i projection. Ale niektóre obliczenia (np. mgły) wymagają transformacji przez tylko jedną z tych macierzy, dlatego macierze projection i modelview są w OpenGLu rozdzielone.
Zadanie: Animacja w 3D.
Ustaw macierz projection na perspektywę (czyli gluPerspective
zamiast gluOrtho2D
).
Scena ma przedstawiać animowane obiekty 3D. Zaczniemy tylko od obiektów wireframe (sama siatka), żeby nie wchodzić w szczegóły materiałów i świateł OpenGLa. Czyli możecie używać np.
glutWireSphere
, glutWireCube
i różnych
innych procedur Wire
GLUTa. Patrz
omówienie procedur rysujących obiekty geometryczne GLUTa.
Bonus za szczególnie kreatywne użycie procedury glutWireTeapot
(rysującej czajnik) !! :)
Obiekty można rysować przez glBegin(GL_LINES)
/
glBegin(GL_LINE_LOOP)
/
glBegin(GL_LINE_STRIP)
/ glBegin(GL_POINTS)
.
Każdy punkt przez glVertex3f
.
Można też używac quadriców OpenGLa. Patrz
omówienie
quadriców w "Red Book", jest też
omówienie
quadriców po polsku.
gluQuadricDrawStyle
ustawiajcie na GLU_LINE
(lub GLU_SILHOUETTE
lub GLU_POINT
jeśli mają sens
w danym przypadku; w każdym razie nie używajcie GLU_FILL
).
Scena ma być animowana, wykorzystywać składanie transformacji
(tzn. mnożenie macierzy przez OpenGLa), przynajmniej translacji, rotacji.
Stos macierzy może być pomocny — glPushMatrix
,
glPopMatrix
z poprzedniego zadania.
Należy zaimplementować dwa sposoby nawigacji:
Kamera w stylu gier FPS (DOOM, Quake itd.): ruch kamery do przodu / do tyłu (na klawisze góra/dół) i obracania w lewo/prawo (na klawisze lewo/prawo).
Czyli Twój program powinien pamiętać aktualną pozycję kamery, aktualny kierunek patrzenia i pion kamery. Przesuwanie gracza do przodu/do tyłu do dodawanie/odejmowanie do pozycji kamery pewnej części wektora kierunku patrzenia. Obracanie kamery to obracania wektora kierunku patrzenia wokół wektora pionu kamery. Obracanie punktu 3D o zadany kąt wokół zadanego kierunku — wiemy jak to zrobić z wykładu, gdzie była podana macierz obrotu 3D ? Jeśli ktoś nie pamięta, patrz OpenGL Programming Guide, Appending G.
Mając trzy wektory pozycji, kierunku patrzenia i pionu możemy
taką kamerę ustawić w OpenGLu przez gluLookAt
.
Nawigacja w stylu "Examine": kamera jest umieszczona w dogodnym punkcie aby widzieć mniej-więcej całą scenę, i użytkownik operuje na scene tak jakby to było pudełko którym można obracać na różne strony.
Bardziej precyzyjnie, gracz może obracać całą sceną w osiach x, y, z (można to zaimplementować tak aby np. przyciśnięcie strzałki w lewo obracało w poziomie w lewo, albo tak aby przyciśnięcie strzałki w lewo zwiększało prędkość obrotu w lewo — ten drugi sposób jest bardziej elastyczny dla użytkownika, chociaż trzeba wtedy też zaimplemetować klawisz który zeruje wszystkie prędkości obracania się). Ponadto można scenę skalować.
Time-based animation:
Wielu z Was po implementacji zadania 1 z PGK zauważyło że animacja w Waszych programach działa z różną szybkością na różnych komputerach. Świadomie nie wspominałem o tym na pierwszej pracowni, żeby nie mieszać Wam w głowach na samym początku :)
Teraz jednak warto sobie wyjaśnić dlaczego tak się dzieje, i jak zrobić to lepiej: przeczytajcie co to jest i jak implementować time-based animation. Animacja w tym zadaniu (i w przyszłych zadaniach) (będzie jeszcze sporo zadań wymagających takiej lub innej animacji) powinna używać tego sposobu.
I jeszcze jeden drobiazg: program powinien wyświetlać
FPS, frames per second, czyli liczbę klatek na sekundę, czyli
ile razy na sekundę wywoływana jest metoda display.
(po zrobieniu time-based animation można to np. obliczyć znając
(time_now - last_idle_time)
).
Ilość FPS można wypisywać w okienku OpenGLa (np. przez
glutBitmapCharacter
) albo zmieniając co chwila tytuł
(caption) okienka. W tym drugim przypadku uwaga — nie należy
zbyt często zmieniać tytułu okienka, pod niektórymi systemami
(...obserwowalne pod Windowsem, ale chyba zależy od menedżera okien)
zbyt częste zmienianie tytułu okienka może
spowolnić nasz program. Więc trzeba dodać "bezpiecznik" w kodzie
żeby uaktualaniać tytuł okienka np. max raz na sekundę.
Pomysły na sceny:
Układ słoneczny. Mamy słońce, kilka planet, wokół niektórych planet są 1-3 księżyce. Orbity planet powinny być ustawione tak aby nie były wszystkie w jednej płaszczyźnie. Planety krążą wokół słońca po swoich orbitach, księżyce krążą wokół swoich planet.
Elementy (planety, księżyce) można reprezentować jako
glutWireSphere. Orbity można rysować jako ciąg punktów w
glBegin(GL_LINE_LOOP)
(tzn. okrąg/elipsę można reprezentować jako linię
łamana złożoną np. z 1000 segmentów, będzie wyglądać dobrze).
Można też użyc odpowiedniego quadrica ay narysować orbitę
bez używania wprost glBegin(GL_LINE_LOOP)
.
Ręka robota — dłoń, kilka palców, każdy palec ma dwa segmenty. Animacja powinna wykonywać ruch dłoni, np. zaciskanie dłoni w pięść (chodzi o to aby dalszy segment palca obracał się względem bliższego segmentu palca, a bliższy segment obracał się względem dłoni).
Elementy (segmenty palców, dłoń) można reprezentować jako
glutWireCube
. Zwracam uwagę że glutWireCube
wprawdzie rysuje sześcian, ale za pomocą glScale
zawsze można go zmienić w dowolny prostopadłościan.
Idący ludzik zbudowany z prostopadłościanów. Tułów (jeden prostopadłościan), 2 ręce (każda złożona z 2 prostopadłościanów — ramię i przedramię), 2 nogi (znowu każda z 2 prostopadłościanów). Czyli pokazujemy łokcie i kolana.
W animacji ruchu ramię obraca się względem tułowia,
a przedramię względem ramienia. Analogicznie z nogami.
Czyli znowu (jak we wszystkich pomysłach na animację 3D powyżej)
klasyczna sytuacja gdzie można wykorzystać mnożenie macierzy,
oraz glPush/PopMatrix
.
Prostopadłościany można rysować tak jak powyżej:
glutWireCube
odpowiednio przeskalowane przez glScale
.
Inne pomysły ? Zapraszam do wymyślania własnych animacji.
Termin na wykonanie zadania: 2 tygodnie
3 tygodnie od 2007-11-13, czyli do 2007-12-04.
Zachęcam wszystkich do zrobienia tego zadania — poznacie tu podstawy operowania obiektami 3D w OpenGLu, a to będzie przydatne w (prawie) wszystkich późniejszych zadaniach.
Treść zadania wyszła długa, przepraszam. Przyznaję że kiedy układałem zadanie w mojej głowie wyglądało na mniejsze. Wierzcie lub nie, ale to jest i tak skrócona wersja... (wersja oryginalna zawierała jeszcze animacje, które sobie darujemy). Na wykonanie zadanie są trzy tygodnie, czyli do 13 listopada. Za pełne wykonanie zadania są 2 punkty, niepełne implementacje też będą przyjmowane..
Napisać program do edytowania prostego formatu obrazków w grafice wektorowej. Dość specyficzny w tym formacie będzie fakt że obiekty można łączyć w grupy, tworząc małą hierarchię obiektów.
Jest to grafika wektorowa, co znaczy tyle że w obrazku są zapisane współrzędne konturów geometrycznych na obrazie. Np. "koło o środku w punkcie (20, 20) i promieniu 10", albo "prostokąt którego lewy dolny róg jest w punkcie (30, 30) a prawy górny w (100, 200)". Współrzędne punktów przechowujemy jako wartości zmiennoprzecinkowe (float). Obrazek zapisany w takim formacie, jako zestaw figur geometrycznych, jest łatwy do skalowania i edycji.
Dla prostoty, są tylko dwie figury geometryczne w naszym formacie obrazka: koło (wypełnione zadanym kolorem, o promieniu 10, o środku w punkcie (0, 0)) i prostokąt (też wypełniony zadanym kolorem, z lewym-dolnym rogiem w punkcie (0, 0) i prawym górym w (10, 10)). Bez obaw, obiekty opisane w następnym punkcie pozwolą nam konstruować koła o środku w dowolnym punkcie, dowolnym promieniu (a nawet koła "jajkowate"), i prostokąty o dowolnych rozmiarach, położeniu i nawet nachyleniu.
W naszym formacie obiekty można grupować. Podstawowym obiektem jest koło lub prostokąt, jak opisane wyżej. Można teraz stworzyć nowy obiekt, który jest grupą 1 lub więcej obiektów. Obiekty w środku grupy mogą być proste (koło, prostokąt) lub same mogą być grupami. De facto, można koło/prostokąt zawsze traktować jako grupy 1-elementowe. Mamy w ten sposób hierarchię obiektów.
Każdy obiekt ma swoją transformację: przesunięcie, obrót, skala. W naturalny sposób do ich realizacji można użyć funkcji OpenGLa glTranslate, glRotate, glScale. Kolejność transformacji jest tu ważna (widzieliśmy w poprzednim zadaniu że zamiana kolejnością
glTranslatef(start_x, start_y, 0); glRotatef(angle, 0, 0, 1);ma znaczenie). W naszym formacie, ustalamy że obiekt powinien być najpierw przesuwany, następnie obracany, następnie skalowany, tzn. odpowiednie polecenia OpenGLa należy wykonać w tej kolejności.
Ponieważ każdy obiekt ma swoją transformację, w szczególności obiekty będące tylko grupą innych obiektów też mają swoją transformację. Transformacja obiektu-grupy wpływa na wszystkie obiekty z tej grupy. Na przykład mamy grupę z przesunięciem (10, 10), która zawiera dwa obiekty: koło (które ma "własne" przesunięcie (5, 5)) i prostokąt (który ma "własne" przesunięcie (20, 20)). Rezultat: na ekranie, środek koła jest w punkcie (15, 15), a lewy dolny róg prostokąta w punkcie (30, 30). Jak widać, transformacje się odpowiednio sumują. Naturalnie, w przypadku kiedy w róznych miejscach hierarchii mamy także skalowania i obroty, to "sumowanie" robi się nietrywialne. Nie ma problemu, OpenGL zrobi to "sumowanie" transformacji za nas: kolejne wywołania glTranslate, glRotate, glScale kumulują się (wewnętrznie, te operacje powodują odpowiednie mnożenia macierzy).
Do implementacji grup obiektów należy wykorzystać glPushMatrix, glPopMatrix — w naturalny sposób sprawią one że transformacje w obrębie danej grupy nie będa wpływały na inne, niezwiązane grupy.
Dla prostoty implementacji (i interfejsu programu do edycji) uznajemy że grupa składa się z maksymalnie 10 elementów, które bedzięmy numerować od 0 do 9.
Dokładny format pliku:
K
:
K r g bgdzie r, g, b to liczby zmiennoprzecinkowe w zakresie 0..1 opisujące kolor (red, green, blue).
P
:
P r g b
{
. Zaraz za nim jest linia zawierająca
dwie wartośći zmiennoprzecinkowe x, y, czyli przesunięcie.
Potem linia opisująca obrót: jedna liczna zmiennoprzecinkowa,
obrót (wyrażony w stopniach, zgodnie z ruchem wskazówek zegara).
Potem linia opisujaca skalowanie: dwie liczby zmiennprzecinkowe, x, y,
skalowanie wzdłuż odpowiednich osi.
Następnie jest ciąg obiektów grupie.
Obiekt kończy się linijką }
.
Na początku każdej linii może być dowolnie wiele białych znaków (spacji, tabulatorów), w ten sposób będziemy mogli robić wcięcia zapisując takie pliki ręcznie. Cały plik jest zawsze dokładnie jednym obiektem.
Przykładowy prosty plik:
{ 20 20 0 3 3 P 1 0 0 }
Powyższy plik definiuje prostokąt. Czerwony (kolor 1 0 0, czyli red = 1 reszta = 0). Lewy dolny róg na pozycji (20, 20) i prawy górny na pozycji (50, 50) (bo domyślnie prostokąt jest od (0, 0) do (10, 10); więc skalowanie 3 3 oznacza że mamy trzy razy większy prostokąt, od (0, 0) do (30, 30); dodanie do tego przesunięcia 20 20 oznacza że mamy prostokąt od (20, 20) do (50, 50)).
Inny przykład:
{ 100 100 0 1 1 { -50 0 0 1 1 P 0 1 0 } { +50 0 0 1 1 K 0 0 1 } }
Ten plik definiuje zielony prostokąt po lewej (dokładnie, rozpięty od (50, 100) do (60, 110)) i czerwone koło po prawej (dokładnie, koło o promieniu 10 o środku w punkcie (100, 500)).
Pytanie: Czy takie coś ma być obsługiwane ?
{ 100 100 0 3 3 K 1 0 0 P 0 1 0 }
(mamy tutaj koło i prostokąt nie "opakowane" bezpośrednio w obiekt grupujący).
Odpowiedź: Nie. To nie ma (w każdym razie nie musi) być obsługiwane.
Idea jest taka że samo koło/prostokąt, tzn. linijki "K ..." albo "P ..." są zawsze zawarte w obiekcie grupującym, który w tym przypadku ma tylko jedno dziecko. Można powiedzieć że mamy trzy typy obiektów:
Wszystkie te trzy typy obiektów mają transformacje.
Sugerowana implementacja obiektowa to
(To tylko sugestia, żebyśmy mieli dobre pojęcie o co chodzi; w praktyce każdy implementuje tak jak chce, naturalnie.).
Należy w jakiś sposób pokazać na ekranie który obiekt jest wybrany — np. rysując wszystkie zawarte w nim kształty (prostokąty, koła) z obwódką innego koloru.
Podobnie należy pokazać na ekranie które dzieci w obiekcie są zaznaczone. Na przykład można rysować z zieloną obwódką kształty w obrębie zaznaczonych dzieci, i z niebieską obwódką pozostałe kształty w wybranym obiekcie (innymi słowy, niezaznaczone dzieci w wybranym obiekcie).
Jak rysować obwódkę ? Najprościej, można przed narysowaniem faktycznego prostokąta/koła, najpierw narysować w dokładnie tym samym miejscu nieco większy prostokąt/koło o kolorze obwódki.
Uwaga: czasami jest sensowne aby przy zaznaczonym nawet tylko jednym dziecku, pozwolić zamienić go na grupę: jest to sensowne kiedy tym dzieckiem jest pojedyncze koło lub prostokąt.
Nowa utworzona kopia staje się nowym wybranym obiektem (tak jakbyśmy ją wybrali spacją).
To będzie nasze podstawowe narzędzie edycyjne. Tworzymy kształty, grupujemy je, po czym możemy kopiować całe złożone obiekty. Np. mając jeden samochodzik, możemy go skopiować i przesunąć, w ten sposób możemy łatwo rysować wiele samochodzików.
Przy zapamietywaniu, wcięć (białych znaków na początku linii) nie trzeba zapamiętywać tak jak były w oryginale, ani nie trzeba próbować jakoś ładnie formatować plik.
Interfejs programu opisany powyżej (jakie klawisze co robią itd.) to tylko propozycja, można to wszystko zrobić inaczej — ważne jest aby była taka sama funkcjonalność.
Na końcu, należy zrobić jakiś rysunek własnym programikiem... Najlepiej wybrać na obrazek coś gdzie będzie można wykorzystać możliwość naszego programu: można tworzyć skomplikowany obiekt, i łatwo robić jego kopie. Na pracowni za trzy tygodnie będziemy mogli się pobawić w otwieranie naszym programem obrazków innych osób.
Podstawy programów używających OpenGLa, poprzez gluta lub SDLa. Proste rysowanie figur 2D.
Kompilacja poniższych programów:
Pod Linuxami: zainstaluj co trzeba z pakietów
(gcc, OpenGL, glut / SDL — zapewne gcc i OpenGL już są zainstalowane).
Proste polecenie kompilacji jest zapisane na początku każdego z poniższych
plików źródłowych w pierwszej linijce compile-command
.
Ścieżki do bibliotek i headerów (opcje -I
i -L
dla gcc)
mogą wymagać dostosowania.
Pod Windowsami: opisałem wszystko na osobnej stronie opis kompilacji programów z PGK pod Windowsem. Na komputerach (przynajmniej niektórych) w naszej pracowni Windowsowej 107 część z potrzebnych rzeczy jest już zainstalowana.
Pracownia:
Najprostszy program w OpenGLu: poprzez GLUTa, poprzez SDLa.
Rysowanie trójkąta, reagowanie na kliknięcia myszą: poprzez GLUTa, poprzez SDLa.
Animowanie, przesuwanie i obracanie w 2D: poprzez GLUTa. Nie napisałem wersji poprzez SDLa. W zamian za to jest wersja w ObjectPascalu.
Proste modyfikacje mouse_triangle-glut.c
:
rysowanie 4-kątów przez GL_QUADS, rysowanie kół przez glutSolidSphere,
rysowanie okręgów przez glutWireSphere.
Zadanie zasadnicze (1 punkt): skoro umiemy obsłużyć klawiaturę i myszkę i umiemy rysować proste kolorowe kształty 2D — zróbmy prostą "gro-podobną" zabawkę. Na początku na dole ekranu umieszczone są 4 niebieskie prostokąty. Gracz klika myszką wskazując pozycję w poziomie gdzie ma pojawić się bomba. Bomba rysowana jako kółko. Bomba spada w dół, jeśli zetknie się z niebieskim blokiem — blok znika.
Dla chętnych: można wymyślić inną zabawkę, byle były ruszające się kształty 2D i obsługa myszki i/lub klawiatury. Np. ping-pong (rzut z góry na stół).
Termin na wykonanie zadania: 2 tygodnie, czyli do 2006-10-23.
Dokumentacja OpenGLa:
Dokumentacja GLUTa: The OpenGL Utility Toolkit (GLUT) Programming Interface API
Zasadnicza dokumentacja SDLa, strona główna SDLa z mnóstwem linków do innej dokumentacji.
Hidden now.
Na pracowniach będziemy robili zadania (a to niespodzianka :) ). Raz na tydzień lub dwa tygodnie, czasami rzadziej, będzie ukazywało się nowe zadanie. Za każde zadanie będzie do zdobycia przynajmniej 1 punkt za wykonanie części "podstawowej". Ponadto przy niektórych zadaniach będą "bonusowe" punkty do zdobycia za zaimplementowanie dodatków / trudniejszych wariantów zadania.
W sumie w czasie semestru będzie do zdobycia X podstawowych punktów. Na ocenę 3.0 trzeba zdobyć X / 2 punktów, na ocenę 5.0 należy zdobyć X punktów. Innymi słowy, nawet na 5.0 nie trzeba robić wszystkich zadań, można "nadrabiać" punktami bonusowymi (ale zazwyczaj będą one trudniejsze do zdobycia niż punkty podstawowe, więc nie jest to zalecana strategia).
Co do obecności: skrót treści pracowni i zadania będą pojawiać się na tej stronie. Więc obecność jest zupełnie nieobowiązkowa, ale często na pracowni mogę potłumaczyć coś więcej niż jest napisane na stronie WWW. Więc zapraszam, naturalnie.
Ale ciągle zadania trzeba mi przedstawiać osobiście, w czasie pracowni albo konsultacji — więc można nie chodzić na pracownię, ale trzeba kiedyś się pojawiać.
Kod źródłowy programu (razem z ewentualnymi danymi,
plikami Makefile
itd.) należy spakować (tar.gz lub zip)
i wysłać na adres michalis.kambi@proton.me.
Nie trzeba dołączać samego skompilowanego programu, i tak będę
chciał go skompilować sam.
I pamiętajcie że zadań nie wystarczy wysyłać — wysłany program trzeba też zaprezentować mi "na żywo", w czasie pracowni albo konsultacji. Konsultacje mam w środę 14.15-16.00.
Chciałbym dać każdemu możliwość pisania programów w takim języku programowania jaki lubi. Dlatego generalnie dozwolone są:
W praktyce, proszę pamiętać że jeśli wybierzecie język którego nie znam, to będę oczekiwał że sami sobie poradzicie z ewentualnymi problemami specyficznymi dla tego języka. Upewnijcie się że są biblioteki do:
Używania OpenGLa z tego języka.
Wygodnego tworzenia okienka z kontekstem OpenGLa. Jak np. biblioteki
Co najmniej obie wersje GLUTa oraz SDL powinny być dostępne z każdego sensownego języka programowania.
Dobrze jest też móc używać różnych kontrolek (przycisków, pól edycyjnych itd.) razem z kontekstem OpenGLa. Są biblioteki które umożliwiają rysowanie takich kontrolek bezpośrednio w kontekście OpenGLa, np.
Większość "normalnych" bibliotek do kontrolek pozwala też zainicjować
kontekst OpenGLa jako szczególny rodzaj kontrolki.
Np. GTK
(w połączeniu z GtkGLExt
lub starszą GtkGLArea), wxWidgets.
Użytkownicy Lazarusa
(open-source'owe środowisko i biblioteka GUI na bazie FreePascala)
mają komponent TOpenGLControl
.
Co do systemu operacyjnego: w czasie pracowni jesteśmy na Łindołsach, w czasie konsultacji możemy iść na Linuxy w moim pokoju albo w 137.
W praktyce, programy które będziecie pisali na tym przedmiocie powinny być przenośne. OpenGL, glut, SDL — wszystko to jest dostepne pod każdym rozsądnym system operacyjnym jak Linux czy Windows (czy ja właśnie nazwałem Windows rozsądnym systemem ?). Ale żeby uniknać ewentualnych trudów kompilacji (instalacji bibliotek, headerów etc.) rozumiem że możecie preferować ten czy tamten system operacyjny.