[powrót]
Pracownia w czwartki 16.15-18.00.
Nie ma nowego zadania. Sprawy organizacyjne (napisałem o tym w emailu do grupy, ale powtarzam tutaj) :
Podsumowanie ilości punktów jaką trzeba zdobyć na konkretne oceny znajduje się niżej na tej stronie.
Jest jeszcze sporo zadań które można oddawać:
W sumie do zdobycia jest jeszcze 7 punktów, czyli nawet jeśli ktoś nie robił nic przez cały semestr, ma jeszcze szansę to nadrobić i zaliczyć pracownię. Chociaż nie oszukujmy się: czasu jest mało — te 7 punktów jest do wzięcia, ale trzeba będzie się napracować.
Rozumiem że sesja, egzaminy itd. więc terminy oddawania zadań będziemy traktować pobłażliwie (tzn. jeszcze bardziej pobłażliwie niż dotychczas :) . Umówmy się że wszystkie ustalenia o których mowa w poprzednim punkcie (czyli wszystkie 7 punktów) obowiązują do 4 lutego (niedziela do północy).
Tylko proszę dla własnego dobra nie odkładać wszystkiego na ostatnią chwilę, na ostatni weekend, itd. Egzamin jest 7 lutego, więc 4 lutego to już naprawdę ostatni dzwonek żeby zaliczać pracownię. Pamiętajcie też że ze względu na Olimpiadę Informatyczną dostęp do komputerów na naszych pracowniach będzie utrudniony/niemożliwy po 1 lutego.
Zapowiadałem na początku że na koniec semestru każdy będzie musiał zrobić większy projekt. Ponieważ jednak zrobiliśmy sporo zadań, i część osób uczciwie pracowała robiąc co tydzień zadania, to rezygnuję z tego pomysłu. Nie będzie projektu.
Chyba że ktoś chce zarobić punkty robiąc większy ciekawy projekt (zamiast "regularnych" zadań z naszej pracowni lub dodatkowo, żeby wyciągnąć punkty na większą ocenę) — wtedy proszę o kontakt, ustalimy jaki temat i ile punktów byłby on warty.
Acha, dane 1.wolf
do zadania Wolfenstein zostały dzisiaj
poprawione — był mały błąd w linijce 3, omyłkowo znalazła się
tam dodatkowa liczba 4 co mogło powodować że odczyt danych z pliku
nie działał jak powinien. Czyli jesli ktoś właśnie pracuje nad tym
zadaniem, to zalecam ściągnąć przykładową
mapę 1.wolf jeszcze raz.
Odczyt modelu 3D z pliku.
Zadanie polega na odczycie modelu 3D z pliku.
Jest wiele formatów modeli 3D. Ja 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,
ponadto Blender pozwala zapisywać zaprojektowane w nim modele w formacie
Wavefront.
Wasz program ma odczytać dowolny model 3D w formacie Wavefront. Można ściągnąć wiele modeli w tym formacie z Internetu. Należy też przećwiczyć odczyt na modelu który zaprojektowaliście w Blenderze w ramach zadania na poprzedniej pracowni.
Eksport do formatu Wavefront w Blenderze: użyj polecenia menu Blendera
File -> Export -> Wavefront (.obj). Przy eksporcie odznacz
opcję
Selection Only (żeby na pewno eksportował cala scenę), resztę opcji
można pozostawić domyślnie. Można zaznaczyć opcję
Triangulate (żeby mieć wszystkie ściany jako trójkąty).
Blender powinien zapisać dwa pliki w rodzaju
xxx.obj
i xxx.mtl
. Jeśli nie zapisze
pliku xxx.mtl
to znaczy że macie starą wersję Blendera
(a właściwie starą wersję skryptu eksportującego do Wavefronta),
więc najlepiej uaktualnijcie Blendera do najnowszej wersji (2.42a w tym momencie).
Wygenerowany plik
xxx.obj
zawiera opis geometrii i zawiera tez odnośniki
do materiałów zdefiniowanych w pliku xxx.mtl
.
Pełna
specyfikacja formatu Wavefront (.obj) (bez obaw, nie musicie
implementować wszystkiego z tej specyfikacji :).
Specyfikacja
formatu materiałów (.mtl).
Zadanie:
0.5 punkta: wersja najprostsza: odczytaj tylko geometrię
modelu. Czyli interesują nas tylko linijki zaczynające się
od f
(faces) i v
(vertices).
+ 0.5 punkta: odczytaj tez materiały, ale bez oświetlenia.
Czyli użyj kolorów materiału w liniach zaczynających się od
Kd
(diffuse color)
jako kolor glColor
dla OpenGLa. Program powinien
renderować scenę wireframe lub z wypełnionymi powierzchniami
(ale z wyłączonym oświetleniem (GL_LIGHTING
),
skoro ustawiamy tylko glColor
dla OpenGLa).
+ 1 punkt (bonus): renderuj scenę z włączonym oświetleniem
(GL_LIGHTING
).
Odczytaj porządnie własności materiałów (co najmniej kolory ambient,
diffuse, specular z linijek zaczynających się od
Ka
, Kd
, Ks
).
Odczytaj wektory normalne z pliku (linijki vn
;
eksportuj w Blenderze z zaznaczoną opcją Normals
żeby Blender zapisał odpowiednie linijki).
W swoim programie ustaw na scenie przykładowe światła, np. ustaw światło
GL_LIGHT0
żeby świeciło od strony kamery (wystarczy
w tym celu zrobić glEnable(GL_LIGHT0)
, światło numer 0
ma tak specjalnie dobrana domyślną konfiguracje).
Innymi słowy, nie trzeba próbować odczytywać światła z pliku Wavefront
(o ile wiem, Blender w ogóle nie zapisuje tam świateł) —
zamiast tego konfigurację świateł możemy "na sztywno" zaszyć w programie.
+ 1 punkt (bonus): odczytaj także tekstury z pliku, czyli
patrz na linijki map_Kd
w pliku .mtl i na linijki
vt
w pliku .obj.
Program powinien odczytać scenę w formacie Wavefront, wyświetlać ją w 3D i pozwalać użytkownikowi poruszać się po tej scenie. Użycie obsługi kamery jaką zaimplementowaliście w poprzednich zadaniach (oświetlenie 3D, powierzchnie Beziera 3D) będzie tu przydatne.
Termin na wykonanie zadania: do końca stycznia 2007
2007-02-04.
Krótki kurs Blendera.
Blender to open-source'owy program do modelowania i renderowania scen 3D. Na pracowni zrobimy sobie krótki kurs podstaw Blendera. Najprawdopodobniej przerobimy jeden lub dwa tutoriale:
Strona Blendera zawiera linki do olbrzymiej ilości dokumentacji o Blenderze. Oficjalna dokumentacja znajduje się na stronie mediawiki.blender.org.
Zadanie zasadnicze (1 punkt): zabawowe. Zaprojektuj coś w Blenderze.
Ma to coś przypominać jakiś przedmiot/obiekt rzeczywisty, ma posiadać
kilka elementów, ma używać kilku materiałów. Zrób prosty rendering
tej sceny (będziesz musiał ustawić światło i kamerę).
Termin na wykonanie zadania: tradycyjne dwa tygodnie, czyli do
2007-01-25 2007-02-04.
Alternatywnie: jeśli ktoś już zna, i preferuje, inny modeller zamiast Blendera to może go użyć (ale zasady są takie same jak do dozwolonych języków programowania: modeller musi być albo open-source'owy, albo dostępny na pracowniach w II).
W ramach najprostszego dema ray-casting zaimplementujemy algorytm wyświetlania użyty w starej grze Wolfenstein 3D.
Patrząc na screenshoty po prawej stronie można zorientować się jak Wolfenstein został zrobiony. Zauważcie ża każda pionowa linia na obrazku gracza ("pasek") składa się z trzech segmentów: na dole jest kolor podłogi, potem ściana (w Wolfensteinie ten kawałek jest pokryty teksturą, my przyjmiemy że ściana będzie miała tylko kolor (jakis kolor, byle różny od koloru podłogi...)), potem kolor sufitu. Co więcej, środkowy element paska znajduje się zawsze dokładnie na środku (to znaczy, długości segmentów podłogi i sufitu są równe). Wszystko to jest możliwe dzięki kilku założeniom Wolfensteina: 1. cały poziom gry to tylko "jedno piętro" 2. wysokość na jakiej znajduje się kamera nie zmienia się 3. gracz zawsze patrzy w kierunku poziomym (nie można "zadzierać" lub "pochylać" głowy aby spojrzeć na sufit/podłogę). Wynika z tego także że mapę poziomu można w pełni zaprojektować w 2D (patrząc na mapę z góry), pozycję kamery można pamiętać jako proste (x, y) (nie ma potrzeby pamiętania wysokości kamery), oraz kierunek patrzenia kamery można pamiętać jako prosty kąt (odchylenie kierunku patrzenia od linii x = const na widoku z góry).
Czyli pseudokod renderowania Wolfensteina można zapisać jako
for x := 0 to szerokość_ekranu do ... oblicz długość_segmentu_w_środku oraz kolor_segmentu_w_środku na podstawie aktualnego położenia kamery gracza i kierunku patrzenia .... y_1 := wysokość_ekranu / 2 - długość_segmentu_w_środku / 2 y_2 := wysokość_ekranu / 2 + długość_segmentu_w_środku / 2 narysuj pionową kreskę od (x, 0) do (x, y_1) kolorem podłogi narysuj pionową kreskę od (x, y_1) do (x, y_2) kolorem kolor_segmentu_w_środku narysuj pionową kreskę od (x, y_2) do (x, wysokość_ekrany) kolorem sufitu
Wszystko jak dotąd trywialne, tylko jak obliczyć długość_segmentu_w_środku oraz kolor_segmentu_w_środku ? Dla każdego x (dla każdego pionowego paska), mając kierunek patrzenia gracza oraz rozpiętość kąta widzenia kamery (w poziomie), możemy obliczyć kąt promienia który należy zbadać. Następnie zbadać w jaką ścianę ten promień trafia.
Nasza scena jest zapisana jako zbiór odcinków. Najprościej można więc zrealizować zadanie po prostu obliczając punkt przecięcia promienia ze wszystkimi odcinkami na scenie. Dla każdego przecięcia mamy kolor trafionej ściany i odległość przecięcia od kamery, wybieramy oczywiście to przecięcie które jest najbliżej kamery.
Na końcu trzeba jeszcze obliczyć długość_segmentu_w_środku
na podstawie odległości danego przecięcia (danej ściany) od kamery.
długość_segmentu_w_środku = 1 / odległość
.
Czyli im dalej jest ściana tym mniejsza się wydaje, czyli naturalna
perspektywa.
długość_segmentu_w_środku = skala / odległość
Obiekty położone na górnej linii mają być wyświetlane z taką samą skalą, czyli obiekty równie wysokie w 3D maja wyjść równie wysokie na ekranie. Bez cosinusa żółta kolumna miałaby wysokość 1/d1, a niebieska 1/d0. Więc przeskalujmy przy rysowaniu wysokość żółtej tak aby też była 1/d0. Czyli pomnóż przez d1/d0. Czyli podziel przez d0/d1 = Cos(α).
Czyli ostatecznie mamy
długość_segmentu_w_środku = skala / (odległość * cos(α))
Format pliku z danymi do zadania (mapą):
kolor ziemi (red, green, blue oddzielone spacjami) kolor sufitu początkowa pozycja gracza (x, y oddzielone spacjami) ilość odcinków dla każdego odcinka: x1 y1 x2 y2 red green blue
Przykładowo zobacz dane simple.wolf. Ziemia ma kolor szary (0.5 0.5 0.5), sufit jest jasnoniebieski (0.5 0.5 1), gracz staruje z pozycji (0, 0) i plansza zawiera tylko jeden kwadratowy pokoik o żółtych ścianach.
Bardziej ambitna przykładowa mapa: 1.wolf.
Przygotowywanie danych do zadania: jeżeli ktoś chce może sam
pobawić się w przygotowywanie nowych map. Moją przykładową mapę
przygotowałem rysując odpowiedni poziom w blenderze, potem eksportując
go do VRML 1.0, potem przetwarzając VRMLa moim programikiem
wolf_vrml_to_pgk.
Archiwum powyżej zawiera binarkę programu pod Linuxa i źródła
programu (do kompilacji wymagany jest jeszcze mój
engine).
wolf_vrml_to_pgk
czyta plik VRML i wypisuje na stdout
wszystkie odcinki których oba końce są na płaszczyźnie Z = 0.
Każdy odcinek jest wypisywany w formacie naszych danych, kolor
odcinka jest brany z koloru diffuse materiału. Mając tak wygenerowaną
listę odcinków nalezy do niej dopisać ręcznie pierwsze 4 linijki
(kolor ziemi, sufitu itd.) i mamy gotowy plik mapy.
Oto wersja źródłowa (w Blenderze)
przykładowej mapy.
Będzie potrzebne obliczanie przecięcia odcinka z promieniem. Powtórka z geometrii:
Funkcyjne równanie prostej: y = a * x + b.
Wada: nie ma możliwości reprezentowania prostych X = const.
Ogólne równanie prostej (f-cja uwikłana): A * x + B * y + C = 0,
przy czym A <> 0 lub B <> 0. Wektor (A, B) na płaszczyźnie
to kierunek prostej.
Zaleta: możliwość reprezentacji każdej prostej, równanie nie faworyzuje
żadnej współrzędnej (X lub Y).
Wada: niejednoznaczność. Np.
2 * x + 2 * y + 2 = 0 x + y + 1 = 0to te same proste. Więc nie można sprawdzać równości prostych trywialnie porównując współczynniki. Ale w praktyce 1. można sprawdzać równość prostych uwzględniając skalowanie współczynników 2. rzadko kiedy jest takie porównywanie potrzebne.
Mając dwa równania prostych:
A1 * x + B1 * y + C1 = 0 A2 * x + B2 * y + C2 = 0można łatwo obliczyć x,y :
if B1 <> 0 then pomnoz 1-wsze rownanie przez B2/B1 odejmij rownania od siebie: (A1 * (B2 / B1) - A2) * x + C1 * (B2 / B1) - C2 = 0 jeśli współczynnik przy x wychodzi 0 to nie ma rozwiazania, czyli linie rownolegle. Sprawdzenie: 1. wiemy że A1,B1 to kierunek 1 prostej, A2,B2 to kierunek drugiej 2. A1 * (B2 / B1) - A2 = 0. Ale takze B1 * (B2 / B1) - B2 = 0. Czyli wektor 2D (A1, B1) to tylko przeskalowany wektor (A2, B2) — czyli proste rzeczywiście rownolegle. Jesli współczynnik przy x <> 0 to oczywiscie masz rozwiazanie. else if A1 <> 0 then ... analogiczne ... else error "nieprawidlowe rownanie prostej --- A1 lub B1 musi byc <> 0"
Umiejąc obliczać przecięcie dwóch prostych, każdy sam sobie dopowie jak obliczać przecięcia odcinka z promieniem (promień = półprosta).
Podsumowując: zadanie podstawowe (1 punkt) to napisać program 1. odczytujący mapy w zadanym formacie 2. pozwalający graczowi poruszać się po mapie (strzałkami lewo/prawo obracamy się, strzałkami góra/dół idziemy do przodu/do tyłu) 3. w każdej klatce mapa jest renderowana używając opisanego wyżej podejścia z Wolfensteina, czyli proste ray-casting. W wersji podstawowej szukanie przecięcia promienia ze sceną można zrealizować jako proste
function przecięcie_promienia_ze_sceną(Promień) for i := 0 to ilość_odcinków_na_scenie ... wykonaj proste sprawdzanie przecięcia Promienia z odcinkiem numer i sceny ... return najbilższe znalezione przecięcie
Zadanie dodatkowe (1 punkt dodatkowy): ulepsz
funkcję przecięcie_promienia_ze_sceną
wspomnianą powyżej
aby używała drzewa czwórkowego (quad-tree) zamiast naiwnie
sprawdzać przecięcia z każdym odcinkiem po kolei. Czyli na początku
programu musimy zbudować quad-tree wrzucając do niego wszystkie
odcinki naszej sceny. W każdym wywołaniu
przecięcie_promienia_ze_sceną
przechodzimy tylko te węzły
drzewa które kolidują z naszym promieniem.
Dokładniej: mamy promień P i nasze drzewo. Na początku patrzymy na korzeń drzewa. W którym spośród jego 4 dzieci znajduje się początek promienia (czyli aktualna pozycja gracza w naszej grze) ? Rekurencyjnie szukaj przecięcia promienia z odcinkami w tym dziecku. Jeżeli nie znajdziesz przecięcia, to sprawdź jak nasz promień przecina się z prostymi dzielącymi tego węzła drzewa — wywołaj się rekurencyjnie dla tych dzieci do których nasz promień wchodzi.
Dokładniejsze omówienie quad-tree było na wykładzie, jest też na wikipedii, razem z masą linków.
Zamiast quad-tree można też zaimplemetować kd-drzewo.
Czas na wykonanie zadania: tradycyjne dwa tygodnie, czyli do 2007-01-18.
Oświetlenie, renderowanie 3D powierzchni wypełnionych (zamiast wireframe)
.... czyli zadanie (1 punkt) które zapowiadałem wcześniej: przerób swój program "pierwsza animacja 3D" (ręka, układ słoneczny etc.) aby powierzchnie były wypełnione i pobaw się oświetleniem w OpenGLu.
Po pierwsze, użyjmy 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
.
Kiedy zamienicie już swoja geometrie na 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ę w punkcie trzecim.
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.
Patrz artykuł Wikipedii o Z-buforze.
Kiedy używamy Z-bufora musimy zdawać sobie sprawę że wartości
zapisywane w buforze głębokości to nie są nasze zwykłe floaty.
"Nasze zwykłe floaty" byłyby nieefektywne do obliczania i porównywania
i zajmowałyby zbyt dużo pamięci. Artykuł na Wikipedii,
referencja funkcji glDepthRange oraz OpenGL
FAQ o Z-buforze podają więcej szczegółów. 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 (tzn. są pamiętane z lepszą precyzją) bliżej kamery,
mniej gęste dalej od kamery.
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
.
Wreszcie zasadnicza treść zadania: użyć oświetlenia OpenGLa. Chciałbym żeby użyć kilku różnych materiałów i co najmniej dwóch świateł OpenGLa. Możecie naturalnie dodać do swojej sceny nowe obiekty, żeby mieć na czym testować materiały.
glEnable(GL_LIGHTING)
.
Pojedyncze światła trzeba jeszcze włączyć przez
glEnable(GL_LIGHT<numer_światła>
.
glutSolidXxx
to macie to automatycznie "załatwione", te procedury
generują odpowiednie wektory normalne.
gluQuadricNormals
).
glBegin(XXX)
to musicie sami obliczać wektory normalne i przekazywać je przez
glNormal
. Przekazane do glNormal
wektory normalne powinny być
zawsze znormalizowane (tzn. mieć długość 1), lub należy kazać
OpenGLowi je normalizować przez glEnable(GL_NORMALIZE)
.
W zależności od tego jak generujecie wektory normalne (jeden wektor normalny na każdą ścianę, czy może jeden wektor normalny na każdy vertex ?) trzeba jeszcze ustawić glShadeModel na smooth lub flat.
Po dokładniejsze omówienie jak działa oświetlenie odsyłam do rozdziału 6 "Lighting" z "OpenGL programming guide".
Wasz program powinien pozwalać pobawić się oświetleniem i zmieniać
rożne parametry materiałów, oraz 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 materiałów i oświetlenia:
Bonus (1 punkt): użyj lepszego interfejsu, 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.
Przynajmniej ruch kamery do przodu / do tyłu (na klawisze góra/dół) i obracania w lewo/prawo (na klawisze lewo/prawo), czyli najprostszy ruch w grach FPS (DOOM, Quake itd.).
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.
Wielu z Was zrobiło już obsługę kamery, w ten lub inny sposób, przy oddawaniu zadania "pierwsza animacja 3D" — świetnie, zaliczę Wam za to punkt przy tym zadaniu.
Termin na wykonanie zadania: do drugiego czwartku w 2007 roku. Czyli 2007-01-11.
Time-based animation : Napisałem mały artykulik o time-based animation. Przeczytajcie go, i chciałbym żeby animacje robione przez Was w przyszłości były robione w ten sposób.
Nie ma nowego zadania z tej pracowni. Później (kiedy na wykładzie będą omawiane metody śledzenia promieni) będzie okazja żeby zadać zadanie na implemetację drzewa czwórkowego lub kd-drzewa, które były omawiane w tym tygodniu na wykładzie.
Tymczasem termin zadania z poprzedniej pracowni "animacja krzywych/powierzchni Beziera" zostaje przesunięty o tydzień. Czyli dwa tygodnie od dzisiaj, czyli w zasadzie do końca grudnia. Głównie dlatego że przykładowe dane do zadania pojawiają się z olbrzymim opóźnieniem (w chwili gdy to piszę (sobota 2006-12-16) są już przykładowe dane do animacji krzywych, przykładowe dane do animacji powierzchni powinny być dzisiaj). Ponadto chciałbym żebyście te animacje zrobili metodą "time-based animation".
Krzywe i powierzchnie Beziera.
Czyli mamy punkty a1, a2, ... an oraz b1, b2, ... bn. W każdym kroku animacji mamy zmienną f która mówi na jakim etapie animacji jesteśmy. Gdy chcemy wyświetlać krzywą, obliczamy jej punkty kontrolne jako ci = (1 - f) * ai + f * bi. Animacja ma działać w kółko, tzn. f najpierw zmienia się od 0.0 do 1.0, potem z powrotem od 1.0 do 0.0 itd.
Przykładowe dane do zadania: anim1.txt oraz anim2.txt.
Do przygotowania przykładowych danych używałem mojego programiku bezier_curves. Możecie zobaczyć źródłowe pliki powyższych krzywych: anim1_1.wrl, anim1_2.wrl, anim2_1.wrl, anim2_2.wrl (można je otworzyć w programie bezier_curves). Szczerze mówiac ze smutkiem stwierdzam że za pomocą animacji tylko jednej, bez wag krzywej Beziera zbyt ciekawych kształtów nie da się stworzyć... No trudno; jeśli ktoś chce zobaczyc nieco ciekawsze animacje, trzeba zrobić animację powierzchni.
Uwaga: jeśli ktoś chce robić zadanie bonusowe poniżej (powierzchnie Beziera), to nie musi robić zadania z krzywymi. Chociaż i tak będzie musiał zaimplementować równania krzywych podczas obliczania powierzchni... Dlatego zadanie z krzywymi będzie "automatycznie zaliczone" jeśli dostanę rozwiązanie zadania z powierzchniami.
gluLookAt
,
tylko druga trójka podaje kierunek patrzenia (podczas gdy
gluLookAt
wymaga pozycji punktu na który patrzymy, co można
trywialnie obliczyć mając pozycje kamery i kierunek patrzenia).
Animacja powierzchni działa tak samo, to znaczy interpolujemy pary odpowiadających sobie punktów kontrolnych. Tak jak przy krzywych, animacja powierzchni ma działać w kółko, tzn. f najpierw zmienia się od 0.0 do 1.0, potem z powrotem od 1.0 do 0.0 itd.
Powierzchnie wyświetlajmy jako siatkę linii
(np. używając glBegin(GL_LINES)
, patrz poprzednie zadanie "pierwsza
animacja 3D"). Później (kiedy poznamy jak działa oświetlenie
i depth buffer w OpenGLu) przerobimy to na wyświetlanie prawdziwych
wypełnionych powierzchni.
Ponieważ chcemy widzieć fakt ze powierzchnia jest w 3D, zawsze ustawiamy kamerę na pozycji zadanej w pierwszej linii pliku. Idea jest taka ze autor danego pliku ustali dogodną pozycję kamery, z której dobrze widać calą animację.
Przykładowe dane do zadania: falka.txt, plama.txt, zagiel.txt.
Do przygotowania przykładowych danych używałem mojego programiku
design_surface.
W archiwum powyżej macie skompilowaną wersję tego programu,
pod Linuxa i pod Windowsa (wersja pod Linuxa wymaga GtkGLExt).
Są tam również pliki źródłowe (*.surface
) na których
pracowałem przygotowując przykładowe dane. W przyszłym miesiącu
ten program będzie dostępny razem ze źródłami, na razie
daję Wam jego binarkę gdybyście chcieli sami pobawić się
w projektowanie nowych powierzchni do naszego zadania.
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.
To zadanie (implementacja ruchu kamery) będzie można tez zrobić później (dla sceny zaimplementowanej na pracowni "pierwsza animacja 3D").
Jeśli ktoś opracuje własne ciekawe przykładowe dane to zachęcam do przysyłania ich mi.
Termin na wykonanie zadania: dwa tygodnie (po terminie tradycyjnie
przez następne dwa tygodnie za połowę punktów).
termin przesunięty do końca grudnia 2006.
Nie ma nowego zadania. Zadanie "Podstawy 3D" z ostatniej pracowni jest bardzo ważne (i będziemy je pewnie rozwijać na późniejszych pracowniach), więc zachęcam wszystkich do zrobienia go.
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.
Zadania zasadnicze (1 punkt): Pierwsza 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. 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, i wykorzystywać mnożenie macierzy,
przynajmniej translacji i rotacji (glTranslate
, glRotate
).
Stos macierzy będzie pomocny — trzeba zapoznać się w operacjami
glPushMatrix
, glPopMatrix
.
Należy tez ustawić kamerę (używając gluLookAt
na jakiś
dogodny punkt obserwacyjny.
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, tylko proszę skonsultować je najpierw ze mną.
Termin na wykonanie zadania: 2 tygodnie. Potem przez następne 2 tygodnie zadanie będzie za 0.5 punkta. 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.
Clipping.
Zadanie zasadnicze (1 punkt):
Idea: Zaimplementuj jeden z algorytmów obcinania odcinka do prostokąta omówionych na wykładzie (np. Cohen-Sutherland lub Liang-Barsky).
Dokładniej: program powinien pobierać sześć parametrów z linii poleceń:
Program na początku powinien wylosować zadaną ilość odcinków. Pierwszy punkt odcinka jest zupełnie losowy, tzn. ma losową współrzędną X (od 0 do szerokości okienka) i losową współrzędną Y (od 0 do wysokości okienka). Drugi punkt odcinka ma współrzędne X i Y losowane w taki sposób aby były nie dalej niż podana MaxDługość od pozycji punktu pierwszego.
Następnie program w pętli rysuje wszystkie odcinki obcięte
do zadanego prostokąta. Tzn. zanim narysujemy pewien odcinek
O1 = (x1, x2, y1, y1) najpierw obcinamy go do zadanego prostokąta,
otrzymując odcinek mniejszy i rysujemy tylko ten mniejszy odcinek.
Rysowanie mniejszego odcinka robimy już używając OpenGLa,
tzn. glVertex2i(x1, y1); glVertex2i(x2, y2);
,
i rysowanie wszystkich odcinków jest zagnieżdzone pomiędzy
glBegin(GL_LINES); ... glEnd;
.
Oczywiście czasami przy obcinaniu okaże się że odcinka w ogóle
rysować nie trzeba, tym lepiej.
W momencie gdy użytkownik przyciśnie klawisz Escape, program
wypisuje (na standardowym wyjściu) ilość klatek na sekundę.
Czyli program musi sprawdzać ile razy wykonał rysowanie klatki
(wywołanie display
) i ile czasu upłynęło od czasu
narysowania pierwszej do ostatniej klatki.
Uwaga: samo obcinanie trzeba oczywiście robić w czasie display
.
W naszym prostym programiku odcinki i prostokąt obcinający są stałe,
więc byłoby dużo mądrzej gdybyśmy obcinanie wykonali tylko raz,
na początku programu — ale wtedy nasz test szybkości byłby
bez sensu.
Wykonaj eksperymenty:
Nie zależy mi na tym aby jakoś szczególnie pięknie opisywać mi wyniki eksperymentów — wystarczy że ułożycie wyniki testów (ilość klatek na sekundę dla zadanych MaxDługość, ilości trójkątów i pola prostokąta) w kilka tabelek w pliku tekstowym i krótko stwierdzicie jak wyglądają zależności, tzn. jak szybko rośnie czas w porównaniu z tym jak zmniejszamy pole prostokąta obcinającego.
Zadanie bonusowe (0.5 punkta): zaimplementuj więcej niż jeden algorytm obcinania (np. zarówno Cohen-Sutherlanda i Liang-Barsky'ego). Wykonaj te same testy, przy okazji porównując jakość obu zaimplementowanych algorytmów.
Termin: dwa tygodnie.
Fraktale.
Niech okienko naszego programu reprezentuje podzbiór liczb zespolonych. Na przykład lewy dolny róg to liczba -3-2i, prawy górny róg to 3+2i, część rzeczywista rośnie w poziomie a część urojona w pionie. Dla każdego punktu okienka mamy więc odpowiadającą mu liczbę zespoloną, nazwijmy ją C. Dla każdego punktu okienka obliczamy kolor w następujący sposób:
MaxIlośćIteracji = 100; MaxZAbs = 1000; Z := 0 + 0i; for Iteracja := 0 to MaxIlośćIteracji do Z := Operacja(Z, C); if |Z| > MaxZAbs then break; Kolor w tym punkcie okienka oblicz na podstawie Iteracja/MaxIlośćIteracji, na przykład po prostu tak: RGB = (Iteracja/MaxIlośćIteracji, Iteracja/MaxIlośćIteracji, Iteracja/MaxIlośćIteracji);
Czyli (intuicyjnie) patrzymy jak szybko rośnie funkcja Z := Operacja(Z, C), gdzie C jest parametrem funkcji który nieznacznie się zmienia dla każdego punktu okienka. Ze stałymi MaxIlośćIteracji i MaxZAbs można oczywiście eksperymentować.
Operacje mogą być różne i dają różne fraktale.
Zadanie zasadnicze (1 punkt): napisz program wyświetlający fraktal. Pozwól użytkownikowi robić zoom in / zoom out i przesuwać się po fraktalu. Np. : kliknięcie lewym klawiszem myszy przesuwa kliknięty punkt okienka na środek i przybliża widok dwa razy. Środkowy klawisz myszy (albo lewy klawisz myszy z wciśniętym Ctrl) tylko przesuwa. Prawy klawisz myszy przesuwa i oddala dwa razy. Czyli każda operacja przesuwania/zoom polega na zmianie wartości zespolonych jakim odpowiadają rogi okienka.
(Interfejs do robienia zoom in / zoom out i przesuwania się można też zrobić inaczej, jak kto lubi/umie).
Termin: dwa tygodnie.
Termin zadania "Diablo" z pracowni drugiej zostaje przesunięty jeszcze o tydzień, czyli do 2006-11-02. Podobnie termin zadań z odcinkiem i trójkątem z pracowni trzeciej. Ponadto zadania z odcinkiem i trójkątem są za 1 punkt każde (początkowo były za 0.5 punkta).
I to tyle z tej pracowni... Nie ma nowego zadania. Zachęcam wszystkich do pracy nad zadaniami "Diablo" i rasteryzacją trójkąta.
Sprawy organizacyjne: Sala: Od 2006-10-19 zajęcia będziemy mieli w oficjalnie przydzielonej nam sali 107. Już są tam komputery — ale tylko Windowsy... kto nie ma konta pod Windowsami, proszę je sobie załatwić.
Przetestowałem kompilację wszystkich naszych programików (i przez GLUTa i przez SDLa) pod Windowsami, oto mój opis kompilacji programów z PGK pod Windowsem
Temat pracowni: rasteryzacja.
Zadanie zasadnicze (z dwóch części):
odcinek(x1, y1, x2, y2: int)która rysuje odcinek wykonując algorytm Bresenhama. Algorytm był opisany na wykładzie, jest też opisany w wielu miejscach na WWW, nawet po polsku: na Wikipedii i tutaj.
Jak zrobić rysowanie punktów w OpenGLu:
najpierw glBegin(GL_POINTS)
,
potem dowolna ilość wywołań glVertex2i(x, y)
, potem
glEnd()
. Czyli celem naszej procedury odcinek
jest w zasadzie wykonanie odpowiednich wywołań glVertex2i(x, y)
.
Procedura musi działać dla wszystkich możliwych przypadków położenia punktów (x1, y1) i (x2, y2) względem siebie, nie tylko dla np. linii idącej w górę i na prawo pod kątem <= 45 stopni. Oczywiście w praktyce wystarczy zaimplementować algorytm Bresenhama tylko dla 1 przypadku, i dla innych przypadków odpowiednio zamieniać punkty końcowe kolejnością lub zamieniać współrzędne X z Y.
(0.5 pkta 1 punkt) Mając wykonaną rasteryzację odcinka,
napisz procedurę rysującą trójkąt, z cieniowaniem
kolorów:
trojkat(x1, y1: int; r1, g1, b1: float; x2, y2: int; r2, g2, b2: float; x3, y3: int; r3, g3, b3: float);
Przykładowy rendering dla wywołania
trojkat( 0, 0, 1.0, 0.0, 0.0, 200, 400, 0.0, 1.0, 0.0, 400, 200, 0.0, 0.0, 1.0);
Idea algorytmu: uporządkuj trzy podane punkty trójkąta w/g współrzędnej Y (na początku załóżmy że wszystkie trzy punkty mają różne wartości współrzędnej Y). Następnie zmieniaj Y od punktu najwyższego do najniższego, niejako wykonując dwa algorytmy Bresenhama równocześnie. Tzn. w każdym kroku (dla każdego Y) obliczamy na jakich współrzędnych X znajduje się lewy i prawy bok trójkąta. Te obliczenia wykonuje nam właśnie algorytm Bresenhama:
Dla każdego Y rysujemy linię poziomą łączącą lewy z prawym bokiem trójkąta.
Wywołania OpenGLa jakich potrzebujemy są takie same jak
w poprzednim zadaniu: glBegin(GL_POINTS)
,
potem dowolna ilość wywołań glVertex2i(x, y)
, potem
glEnd()
. Ponadto, przed każdym wywołaniem
glVertex2i(x, y)
musimy ustawić kolor używając
glColor3f(red, green, blue)
.
Jak zrobić kolorowanie: dla każdej poziomej linii można wyznaczyć kolory jej końców jako odpowiednią kombinację odpowiednich dwóch kolorów wierzchołków trójkąta (podanych w parametrach procedury). Rysując linię poziomą zmieniamy kolor liniowo od lewej do prawej. Nie zależy nam na efektywnej implementacji interpolacji kolorów — kolorki mają być wykonane jedynie tak aby działały poprawnie, i można spokojnie używać mnożeń na floatach itd. aby je uzyskać.
Termin na wykonanie zadania: tydzień, czyli do 2006-10-26
termin zadania przedłużony o tydzień, czyli do 2006-11-02.
Obrazki, używając SDL i SDL_image.
Skompilujemy programy z zeszłej pracowni w wersji SDL.
Na pracowni nie są zainstalowane biblioteki -dev do SDLa, więc
trzeba ściągnąć je samemu z repozytorium Debiana
przez WWW, rozpakować podkatalog include
, i utworzyć symlink ~/lib/libSDL.so
do
/usr/lib/libSDL....
. Potem programy należy kompilować z opcjami jak
-I ~/include/ -L ~/lib/
Patrz: zasadnicza dokumentacja SDLa, strona główna SDLa z mnóstwem linków do innej dokumentacji.
Skompiluj, przetestuj program używający SDL_image i wyświetlający obrazek. Przykładowe obrazki w różnych formatach.
Zadanie bonusowe (0.5 punkta):
Popraw powyższy program: załadowane wiersze surface idą z góry na dół,
powinny iść z dołu do góry. Skoro znamy format danych pod koniec
funkcji init (img->pixels
to tablica h * w * 3
bajtów,
kolejne ciągi w * 3
bajtów to wiersze) to możemy
napisać funkcję poprawiającą, tzn. zamieniającą w pamięci odpowiednie wiersze.
Pamiętaj użyć SDL_LockSurface
przed
operowaniem na img->pixels
.
Termin na wykonanie zadania: tydzień, czyli do 2006-10-19.
Ostatni przykład sdl_image_draw.c
miał dwie wady:
odwrócone linie oraz problem z endianess wspomniany przez
p. Dziubka (dla format.R/G/Bmask).
Oto ulepszona wersja, która wyświetla wiersze w dobrej kolejności
i jest przenośna na procesory o dowolnym endianess:
sdl_image_draw_fixed.c.
Program używa pliku sdl_utils.c
który implementuje poręczną funkcję Img_GL_Load
.
Polecam użycie jej do zadania "Diablo" na dzisiejszą pracownię.
Uwaga: odwracanie wierszy jest tutaj robione przez alokowanie nowego surface'a SDLa. Więc ten przykładowy program nie jest rozwiązaniem zadania bonusowego z poprzedniego punktu :)
Odczyt i wyświetlanie obrazków z alpha channel (GL_ALPHA_TEST, glAlphaFunc). Skompiluj i przetestuj sdl_image_draw_alpha.c, przykładowy obrazek z alpha: aaa.png
Zadanie bonusowe (0.5 punkta):
Napisz program odczytujący i wyświetlający obrazek w formacie PPM bez pomocy
zewnętrznych bibliotek do obrazków jak SDL_image
.
Bardziej precyzyjnie, zależy mi na formatach P3 i P6, czyli wartości (Red, Green, Blue) kodowane tekstowo lub binarnie. Specyfikacja formatu PPM. Uwaga: nie bójcie się punktu 10 w powyższej specyfikacji PPM, który mówi o gamma :) Można ten punkt kompletnie zignorować.
Przykładowe obrazki:
Termin na wykonanie zadania: 2 tygodnie, czyli do 2006-10-26.
Zadanie zasadnicze (1 punkt): Zaczątek gry izometrycznej w stylu Diablo. Oczywiście nie zależy nam na zrobieniu prawdziwej gry, jedynie na odczycie mapy w zadanym formacie i wyświetleniu jej na ekranie.
Format pliku mapy:
Pierwsza linia zawiera dwie liczby oddzielone spacją: szerokość i wysokość mapy.
Druga linia zawiera początkową pozycję gracza. W wersji zasadniczej zadania (za 1 punkt) można tą linię zignorować — nie trzeba rysować gracza.
Trzecia linia zawiera liczbę obrazków ziemi (liczba ta nazywana będzie
odtąd BaseTilesCount
) oraz liczbę obrazków obiektów
(liczba ta nazywana będzie odtąd BonusTilesCount
).
Następne BaseTilesCount
linii zawiera po jednym
znaku (ale nie spacji) oraz nazwę pliku. Na przykład
r images/rock.pngNazwa pliku to oczywiście nazwa istniejącego pliku z którego należy załadować obrazek. A pierwszy znak (małe "r" w przykładzie powyżej) to kod obrazka, który będzie wykorzystany później. Wszystkie wymienione tu obrazki mają rozmiar dokładnie 70x36 pixeli oraz mają tak dobrany kanał alpha że tworzą romb z przezroczystymi rogami.
Następne BonusTilesCount
linii wygląda podobnie:
każda linia to jeden znak (kod obrazka) oraz nazwa pliku
obrazka. Podane tu obrazki mają dowolny kanał alpha, i dowolny rozmiar.
Następne linie opisują faktyczną mapę. Każdy wiersz mapy to nowa linia w pliku. Każda linia w pliku zawiera po dwa znaki dla każdego pola: kod obrazka ziemi w tym miejscu oraz kod obrazka obiektu w tym miejscu. Kod obrazka obiektu może być znakiem specjalnym "_" (znak podkreślenia) oznaczającym że w tym miejscu nie ma żadnego obiektu.
Przykład prosty: plik
4 6 1 1 1 1 g woldforge/sprites/tiles/rust/kambi_rust_big_1_dt.png t woldforge/sprites/trees/birch/kambi_birch_01_mature_autumn_thc.png g_g_g_g_ g_g_g_g_ g_g_g_g_ g_gtg_g_ g_g_g_g_ g_g_g_g_
Przykład ten opisuje mapę o szerokości 4 i wysokości 6. Mapa używa tylko jednego obrazka ziemi i na środku mapy znajduje się drzewo. Przykładowy wynik renderowania takiej mapy:
Zwracam uwagę że wiersze nieparzyste są odpowiednio przesunięte (dokładnie o 35 pixeli = połowa szerokości obrazków ziemi) w stosunku do wierszy parzystych. Dzięki temu wszystkie obrazki ziemi odpowiednio pasują do siebie.
Przygotowałem dla Was dane do zadania. To archiwum zawiera przykładowe mapy i obrazki. Obrazki pochodzą z WorldForge (niektóre nieznacznie przerobiłem, przeskalowałem itd. żeby ułatwić Wam zadanie). Oczywiście jeśli ktoś chce może użyć innych obrazków, i zachęcam do projektowania własnych map — format pliku mapy jest tekstowy właśnie po to aby można go było edytować w dowolnym edytorze.
Zadanie zasadnicze (1 punkt): napisz program odczytujący mapę w formacie powyżej, odczytujący wszystkie wymagane obrazki a następnie wyświetlający daną mapę (a przynajmniej taką jej część jaka mieści się na ekranie). Podkreślam że nie wymagam tutaj zrobienia żadnej prawdziwej gry, żadnego gracza, żadnego ruszania mapą. Chodzi tylko o wyświetlenie mapy poprzez złożenie odpowiednich obrazków.
Wszystko co jest potrzebne do wykonania zadania poznaliście
na pracowni poza funkcją do przesuwania obrazków. Aby narysować
obrazek na pozycji X, Y należy przed wywołaniem glDrawPixels
(uwaga: na początku omyłkowo napisałem tutaj glReadPixels
;
oczywiście chodziło mi o glDrawPixels
!)
wywołać instrukcje
glRasterPos2i(0, 0); glBitmap(0, 0, 0, 0, X, Y, NULL);
Termin na wykonanie zadania: 2 tygodnie, czyli do 2006-10-26
termin zadania przedłużony o tydzień, czyli do 2006-11-02.
Rozszerzenie zadania (bonusowe 0.5 punkta): użyj któregokolwiek
z obrazków w podkatalogu danych tiles/woldforge/sprites/creatures
aby rysować gracza. Pozwól graczowi się przesuwać
po mapie przynajmniej używając czterech klawiszy strzałek
(góra, dół, lewo, prawo). Nie zależy nam tu na płynnym ruchu —
jedynie na skakaniu z pola na pole. Pole na którym znajduje się gracz
musi być zawsze rysowane mniej więcej na środku ekranu, w ten sposób
gracz przesuwając się tak naprawdę przesuwa mapę — w ten
sposób możemy zwiedzić mapę.
Podstawy programów używających OpenGLa, poprzez gluta lub SDLa. Proste rysowanie figur 2D.
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-19.
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.
Podsumowując punkty możliwe do zdobycia na pracowniach 1-14:
/* gra 2D */ 1 + /* obrazki, diablo */ 1 (+ 1.5) + /* rasteryzacja */ 2 + /* fraktale */ 1 + /* obcinanie */ 1 (+ 0.5) + /* animacja 3D */ 1 + /* Bezier */ 1 (+ 2) + /* oświetlenie */ 1 (+ 2) + /* wolfenstein */ 1 (+ 1) + /* blender */ 1 + /* wavefront */ 1 (+ 2) = 12 (+ 9)
Zgodnie z zapowiedziami na ocenę 5.0 należy zdobyć 12 punktów (tyle ile było za "zadania podstawowe"), na ocenę 3.0 — połowę, czyli 6 punktów. Dokładna tabelka:
punkty | ocena |
---|---|
< 6 punktów | 2.0 |
[6, 7] | 3.0 |
[7.5, 8.5] | 3.5 |
[9, 10] | 4.0 |
[10.5, 11.5] | 4.5 |
>= 12 | 5.0 |
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
.
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 AT ii.uni.wroc.pl
.
Proszę nie dołączać samego skompilowanego programu — będę
chciał go skompilować sam.