[powrót do strony głównej]

[powrót]

Podstawy Grafiki Komputerowej

Pracownia w czwartki 16.15-18.00.

  1. Pracownia 15 (2006-01-25)
  2. Pracownia 14 (2006-01-18)
  3. Pracownia 13 (2006-01-11)
  4. Pracownia 12 (2006-01-04)
  5. Pracownia 11 (2006-12-21)
  6. Pracownia 10 (2006-12-14)
  7. Pracownia 9 (2006-12-07)
  8. Pracownia 8 (2006-11-30)
  9. Pracownia 7 (2006-11-23)
  10. Pracownia 6 (2006-11-16)
  11. Pracownia 5 (2006-11-09)
  12. Pracownia 4 (2006-10-26)
  13. Pracownia 3 (2006-10-19)
  14. Pracownia 2 (2006-10-12)
  15. Pracownia 1 (2006-10-05)
  16. Najważniejsza dokumentacja
  17. Punktacja
  18. Zasady
    1. Zasady zaliczania
    2. Dozwolony język programowania
    3. Zasady wysyłania programów

1. Pracownia 15 (2006-01-25)

Nie ma nowego zadania. Sprawy organizacyjne (napisałem o tym w emailu do grupy, ale powtarzam tutaj) :

  1. Podsumowanie ilości punktów jaką trzeba zdobyć na konkretne oceny znajduje się niżej na tej stronie.

  2. Jest jeszcze sporo zadań które można oddawać:

    • Model w blenderze (1 punkt).
    • Odczyt modelu 3D z pliku (w sumie 3 punkty, przed tym zadaniem dobrze jest zrobić model w blenderze).
    • Zadania z pracowni 12, 11 oraz 7 (Wolfenstein, oświetlenie, animacja 3D) będę przyjmował jako spóźnione, czyli za połowę punktów (co daje 0.5 * (2 + 3 + 1) możliwych punktów).

    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ć.

  3. 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.

  4. 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.

2. Pracownia 14 (2006-01-18)

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:

  1. 0.5 punkta: wersja najprostsza: odczytaj tylko geometrię modelu. Czyli interesują nas tylko linijki zaczynające się od f (faces) i v (vertices).

  2. + 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).

  3. + 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.

  4. + 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.

3. Pracownia 13 (2006-01-11)

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).

4. Pracownia 12 (2006-01-04)

W ramach najprostszego dema ray-casting zaimplementujemy algorytm wyświetlania użyty w starej grze Wolfenstein 3D.

Screenshot 1 z Wolfenstein 3D
Screenshot 2 z Wolfenstein 3D
Screenshot 3 z 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. Kamera w Wolfenstein 3D

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.

  1. Pierwsza przymiarka: Zależność długości segmentu od odległości ściany jest w zasadzie trywialna: po prostu długość_segmentu_w_środku = 1 / odległość. Czyli im dalej jest ściana tym mniejsza się wydaje, czyli naturalna perspektywa.
  2. W praktyce mnożymy to przez jakąs ustaloną skalę, żeby "wyglądało dobrze" (inaczej dla niektórych danych wszystkie segmenty wyjdą nam bardzo króciutkie albo bardzo długie, i sufit/podłoga będa zajmowały zbyt wiele lub zbyt mało miejsca na ekranie). Czyli długość_segmentu_w_środku = skala / odległość
  3. Ponadto musimy jeszcze podzielić przez cosinus kąta pomiedzy badanym promieniem a kierunkiem patrzenia gracza (czyli kąta pomiędzy czerwonym a niebieskim promieniem na rysunku powyżej). Powód: najłatwiej to wyjasnić moim koślawym rysunkiem:
    Rysunek ilustrujący dlaczego jest potrzebny Cos

    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(α))
    

Dane do zadania

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.

Obliczanie przecięcia dwóch linii

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 = 0
to 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 = 0
moż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).

Zadanie

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.

5. Pracownia 11 (2006-12-21)

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.

  1. Po pierwsze, użyjmy powierzchni wypełnionych, czyli:

    1. 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: Różne wielokąty renderowane przez OpenGLa

    2. Mamy glutSolidCube i wszystkie inne glutSolidXxx, patrz dokumentacja GLUTa o "Geometric Object Rendering".

    3. Mamy quadrici OpenGLa (patrz omówienie quadriców) i możemy ustawić im gluQuadricDrawStyle na GLU_FILL.

  2. 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

    1. Zażądać Z-bufora przy tworzeniu kontekstu OpenGLa. Np. w GLUT'cie trzeba dodać GLUT_DEPTH przy glutInitDisplayMode. W SDL można dodać np.
      SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 16 );
      zanim zrobimy SDL_SetVideoMode.
    2. Włączyć używanie Z-bufora: glEnable(GL_DEPTH_TEST).
    3. Wyczyścić bufor głębokości na początku każdej klatki, czyli dodać GL_DEPTH_BUFFER_BIT do glClear.
  3. 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.

    • Materiały ustawiamy przez glMaterial.
    • Światła ustawiamy przez glLight Oświetlenie trzeba włączyć przez glEnable(GL_LIGHTING). Pojedyncze światła trzeba jeszcze włączyć przez glEnable(GL_LIGHT<numer_światła>.
    • Żeby oświetlenie działało dobrze trzeba jeszcze ustawić odpowiednie wektory normalne.
      1. Jeśli używacie procedur gluta glutSolidXxx to macie to automatycznie "załatwione", te procedury generują odpowiednie wektory normalne.
      2. Jeśli używacie quadriców to można nakazać aby generowały one odpowiednie wektory normalne (patrz gluQuadricNormals).
      3. Jeśli rysujecie "ręcznie" przez 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:

    1. W wersji podstawowej zadania, za 1 punkt, wystarczy zdefiniować odpowiednie skróty klawiszowe do zmniejszania / zwiększania wszystkich parametrów. Parametrów jest dużo (kilka świateł, kilka materiałów, każdy z kilkoma kolorami i ew. pozycją, każdy z tych parametrów to trzy wartości rzeczywiste...) więc 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 kolor Red materiału diffuse słońca, klawisz "Shift+s" zmienia kolor Green materiału diffuse słońca, klawisz "Ctrl+s" zmienia kolor Blue materiału diffuse słońca).
    2. 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.

  4. Bonus (1 punkt): dodaj obsługę klawiszy do przesuwania i obracania kamery. To jest to samo zadanie co przy "animacji powierzchni Beziera" z poprzedniej pracowni (ale tylko jeden punkt można uzyskać w sumie, więc nie musicie robić kamery przy "animacji powierzchni Beziera" jeśli zrobicie kamerę do tego zadania).

    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.

6. Pracownia 10 (2006-12-14)

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".

7. Pracownia 9 (2006-12-07)

Krzywe i powierzchnie Beziera.

  1. Zad podstawowe (1 punkt): Animuj krzywą Beziera. Napisz program odczytujący z zadanego pliku dwa ciągi punktów kontrolnych krzywej Beziera. Następnie narysuj animację od pierwszej do drugiej krzywej. Animacja polega po prostu na odpowiedniej interpolacji punktów kontrolnych pomiędzy pierwszą a drugą wersją, i dla każdych punktów kontrolnych obliczamy krzywą od nowa. Krzywa jest w 2D. Format pliku opisującego krzywe:
    1. W pierwszej linii, n = ilość punktów krzywej.
    2. W każdej z następnych n linii: współrzędne 2D kolejnych punktów kontrolnych pierwszej krzywej.
    3. W każdej z następnych n linii: współrzędne 2D kolejnych punktów kontrolnych drugiej krzywej.

    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.

  2. Zad bonusowe (1 punkt): Animuj powierzchnie Beziera. Tym razem powierzchnie są już w przestrzeni 3D. Format pliku opisującego powierzchnie:
    1. W pierwszej linii pozycja kamery: 9 liczb zmiennoprzecinkowych, pierwsza trójka to pozycja kamery, druga trójka to kierunek patrzenia kamery, trzecia trójka to wektor pionu kamery. Czyli parametry takie jak do 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).
    2. W drugiej linii, n = wymiar punktów kontrolnych powierzchni (tzn. każda powierzchnia ma n x n punktów kontrolnych).
    3. W każdej z następnych n linii: współrzędne punktów kontrolnych w danym wierszu pierwszej powierzchni. Czyli 3 * n liczb zmiennoprzecinkowych oddzielonych białymi znakami (każdy punkt kontrolny wyznaczony przez 3 kolejne punkty).
    4. W każdej z następnych n linii: analogicznie, współrzędne punktów kontrolnych drugiej powierzchni.

    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.

  3. Bonus (1 punkt): dodaj obsługę klawiszy do przesuwania i obracania kamery. 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.

    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.

8. Pracownia 8 (2006-11-30)

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.

9. Pracownia 7 (2006-11-23)

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:

Zadania zasadnicze (1 punkt): Pierwsza animacja w 3D.

  1. Ustaw macierz projection na perspektywę (czyli gluPerspective zamiast gluOrtho2D).

  2. 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).

  3. 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.

  4. Pomysły na sceny:

    1. 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).

    2. 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.

    3. 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.

    4. Inne pomysły ? Zapraszam do wymyślania własnych animacji, tylko proszę skonsultować je najpierw ze mną.

Rozdział 3 dokumentacji OpenGLa, który opisuje wszystkie potrzebne funkcje a nawet zawiera szkielet rozwiązań animacji robota i układu słonecznego.

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.

10. Pracownia 6 (2006-11-16)

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).

  1. Zademonstruj go.
  2. Po czym przetestuj na ile obcinanie się opłaca. O ile jest oczywiste że zyskujemy czas kiedy musimy wyświetlać odcinki tylko na części ekranu, pamiętajmy też że samo obcinanie również zajmuje chwilę czasu, ponieważ dla każdego odcinka musimy wykonać pewne obliczenia. Pytanie brzmi: jak duża jest ta "chwila czasu".

Dokładniej: program powinien pobierać sześć parametrów z linii poleceń:

  1. Ilość odcinków
  2. Max długość odcinka (długość w sensie "max rozpiętość w pionie lub poziomie", nie "tradycyjna" długość liczona Pitagorasem)
  3. X1 prostokąta obcinającego
  4. Y1 prostokąta obcinającego
  5. X2 prostokąta obcinającego
  6. Y2 prostokąta obcinającego

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:

  1. Dla zadanej ilości odcinków i dużej wartości MaxDługość (tak aby wylosowane odcinki miały szansę być rozpięte na cały ekran) sprawdź jak zmienia się ilość klatek/na sekundę gdy zmniejszamy pole prostokąta obcinającego. Np. wypróbuj prostokąty obcinające (0, Width, 0, Height), potem (0, 9/10 * Width, 0, 9/10 * Height), potem (0, 8/10 * Width, 0, 8/10 * Height) itd. i zanotuj ilość klatek/na sekundę. Jak mniej więcej wygląda zależność ?
  2. Podobnie jak wyżej sprawdź dla małej wartości MaxDługość (np. 50 — tak aby odcinki były małe).
  3. Wykonaj eksperymenty 1 i 2 zarówno dla małej jak i dla dużej ilości odcinków.

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.

11. Pracownia 5 (2006-11-09)

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.

  1. Np. Mandelbrot: Z := Z2 + C. Patrz wikipedia.
  2. Np. biomorph: Z := sin(Z) + eZ + C. Są różne wersje, np. tutaj proponują Z := sin(Z) + eZ + x + y2, gdzie C = x + yi.

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.

12. Pracownia 4 (2006-10-26)

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.

13. Pracownia 3 (2006-10-19)

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

  1. (0.5 pkta 1 punkt) Napisz procedurę
      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.

  2. (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);
    
    triangle smooth

    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:

    1. Po jednej stronie trójkąta zawsze mamy dwa boki: od najwyższego do środkowego punktu, i później od środkowego do najniższego punktu.
    2. Po drugiej stronie trójkąta zawsze mamy jeden bok: od najwyższego do najniższego punktu.

    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.

14. Pracownia 2 (2006-10-12)

Obrazki, używając SDL i SDL_image.

  1. 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.

  2. Skompiluj, przetestuj program używający SDL_image i wyświetlający obrazek. Przykładowe obrazki w różnych formatach.

  3. 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.

  4. 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 :)

  5. 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

  6. 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.

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

    1. Pierwsza linia zawiera dwie liczby oddzielone spacją: szerokość i wysokość mapy.

    2. Druga linia zawiera początkową pozycję gracza. W wersji zasadniczej zadania (za 1 punkt) można tą linię zignorować — nie trzeba rysować gracza.

    3. 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).

    4. Następne BaseTilesCount linii zawiera po jednym znaku (ale nie spacji) oraz nazwę pliku. Na przykład

        r images/rock.png
      
      Nazwa 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.
    5. 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.

    6. 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:

    screenshot from isometric

    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ę.

15. Pracownia 1 (2006-10-05)

Podstawy programów używających OpenGLa, poprzez gluta lub SDLa. Proste rysowanie figur 2D.

  1. Najprostszy program w OpenGLu: poprzez GLUTa, poprzez SDLa.

  2. Rysowanie trójkąta, reagowanie na kliknięcia myszą: poprzez GLUTa, poprzez SDLa.

  3. Animowanie, przesuwanie i obracanie w 2D: poprzez GLUTa. Nie napisałem wersji poprzez SDLa. W zamian za to jest wersja w ObjectPascalu.

  4. Proste modyfikacje mouse_triangle-glut.c: rysowanie 4-kątów przez GL_QUADS, rysowanie kół przez glutSolidSphere, rysowanie okręgów przez glutWireSphere.

  5. 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.

16. Najważniejsza dokumentacja

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.

17. Punktacja

Hidden now.

18. Zasady

18.1. Zasady zaliczania

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

18.2. Dozwolony język programowania

Chciałbym dać każdemu możliwość pisania programów w takim języku programowania jaki lubi. Dlatego generalnie dozwolone są:

  1. Dowolny język do którego istnieje open-source'owy kompilator/interpreter pod Linuxa. Na przykład C/C++, ObjectPascal, OCaml lub Python.
  2. Dowolny język do którego kompilator/interpreter (open-source'owy lub nie) jest zainstalowany na pracowniach w Instytucie.

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:

  1. Używania OpenGLa z tego języka.

  2. 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.

  3. 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.

18.3. Zasady wysyłania programów

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.