[powrót]

Podstawy Grafiki Komputerowej

Pracownia we wtorek 8.15-10.00.

  1. Uwagi do oddawania końcowych zadań (2007-01-26)
  2. Zadanie 5 – kilka zadań do wyboru (2007-01-19)
    1. Zad 5.1 do wyboru: Cienie metodą plane-projected shadows
    2. Zad 5.2 do wyboru: Odbicia lustrzane na płaszczyźnie
    3. Zad 5.3 do wyboru: Cienie metodą shadow volumes
    4. Zad 5.4 do wyboru: Rasteryzacja
  3. Kilka kolejnych wiadomości (2007-01-08)
  4. Kilka wiadomości (2007-12-17)
  5. Zadanie 4 (2007-12-04)
  6. Zadanie 3 (2007-11-13)
  7. Zadanie 2 (2007-10-23)
  8. Zadanie 1 (2007-10-09)
  9. Najważniejsza dokumentacja
  10. Punktacja
  11. Zasady
    1. Zasady zaliczania
    2. Oddawanie programów
    3. Dozwolony język programowania

1. Uwagi do oddawania końcowych zadań (2007-01-26)

Podsumowanie punktów podstawowych do zdobycia:

  1 (zad 1, zabawka 2d) +
  2 (zad 2, obrazek wektorowy) +
  1.5 (zad 3, animacja 3d) +
  2.5 (zad 4, Wavefront) +
  2 (zad 5, różne) =
  9

Zgodnie z tą punktacją, potrzeba 9 / 2 = 4.5 punktów żeby zaliczyć pracownię. Zdecydowałem obniżyć trochę wymagania, przyjmijmy że 8 punktów to norma, potrzeba 8 / 2 = 4 punktów żeby zaliczyć pracownię na ocenę 3. Zachęcam do rozwiązywania ostatnich zadań, Wavefronta (były za to zadanie w sumie 4 punkty do zdobycia, czasu prawie dwa miesiące) i ostatniego zadania (można zdobyć mnóstwo punktów, w sumie aż 8 jeżeli ktoś zaimplementuje wszystko; czyli gdyby ktoś chciał, mógłby zaliczyć całą pracownię na ocenę 5 tylko za implementację tego zadania). Zadanie Wavefront jest już mniej (o połowę) punktowane, wszystko z zadania 5 jest w pełni punktowane do końca sesji egzaminacyjniej (normalnej, nie poprawkowej).

Acha: w najbliższy wtorek są oficjalnie zajęcia czwartkowe. Czyli nie będzie już pracowni z PGK w najbliższy wtorek o 8 rano (chyba że komuś bardzo pasuje taki termin i chce przyjść). Proszę umawiajcie się emailem kiedy pasuje Wam oddawanie zadań.

2. Zadanie 5 – kilka zadań do wyboru (2007-01-19)

Na koniec, kilka zadań z efektami graficznymi: cienie i odbicia lustrzane. Można zaimplementować jedno lub więcej z poniższych zadań, do wyboru. Do punktacji końcowej, za "normę" przyjmujemy że w sumie należy zdobyć za poniższe zadania 2 punkty.

Jeżeli ktoś ma problem z wyborem: najłatwiej zacząć od implementacji wersji podstawowej plane-projected shadows albo odbić na płaszczyźnie (szczególnie odbicia na płaszczyźnie na początku są trywialnie...). Potem można je rozwinąć aby używać stencil bufora. Wersja podstawowa plane-projected shadows (bez stencil bufora) + wersja podstawowa odbić (ze stencil buforem, bez clipPlanes) dają już w sumie 2.5 punkta, czyli nawet więcej niż "norma".

Termin zadań: egzamin jest 11 lutego o ile dobrze wiem. Powiedzmy że termin zadań jest do 10 lutego. Acha, gdzieś pomiędzy 1-10 lutego możemy zrobić dodatkowe ćwiczenia (z racji tego że na początku semestru nie zrobiliśmy pierwszej pracowni, bo była przed wykładem). Możemy je poświęcić na przygotowanie się do egzaminu, omówienie typowych zadań etc. Czekam na propozycje jakie terminy Wam pasują, mi w zasadzie pasuje wszystko.

2.1. Zad 5.1 do wyboru: Cienie metodą plane-projected shadows

(Wersja podstawowa z glPolygonOffset za 1 punkt:)
Patrz pierwsza część artykułu "Real-time Shadowing Techniques".

  1. Możemy skonstruować macierz która wykonuje rzut dowolnego modelu 3D na zadaną płaszczyznę, z punktu widzenia zadanego światła. (Wzór tej macierzy podany jest w artykule powyżej.) Czyli możemy wyrenderować scenę z cieniem w stylu

      /* ... ustaw światło GL_LIGHT0 na pozycji light_position ... */
    
      /* ... narusuj prostokąt (quad) o równaniu płaszczyzny plane ... */
    
      rysuj_model();
    
      glPushMatrix();
        glMultMatrix(oblicz_macierz_rzutujaca_model_na_plaszczyzne(plane, light_position));
        glColor3f(0, 0, 0); /* cień jest czarny */
        rysuj_model();
      glPopMatrix();
    

    Pamiętajcie też mieć wyłączone oświetlenie (glDisable(GL_LIGHTING)) na czas rysowania cienia, żeby był należycie czarny.

    W rysuj_model można wyrenderować cokolwiek — na przykład można tam wrzucić renderowanie jakiegoś modelu w formacie Wavefront, co macie zrobione w poprzednim zadaniu. Jeżeli ktoś nie zamierza robić zadania z Wavefrontem, to można tutaj użyć dowolnego nietrywialnego modelu, np. glutSolidTeapot.

  2. Jak zobaczycie, ta metoda renderowania cienia ma trochę problemów. Przede wszystkim związanych z faktem że "rysujemy cień", co jest jakby na opak z fizyką. "Cień" normalnie polega na tym że czegoś (światła) nie ma. W normalnym algorytmie należałoby najpierw narysować scenę z wyłączonych oświetleniem, potem włączyć światło i rysować część sceny która jest oświetlona... my tego nie robimy. Zamiast tego najpierw rysujemy wszystko oświetlone, a dopiero potem "rysujemy cień".

    W rezultacie nasz "cień" np. koliduje w z-buforze z samym prostokątem na który miał rzucać cień (w końcu są rysowane na tej samej pozycji). To powoduje przykre śnieżenie pomiędzy kolorem prostokąta a czernią w miejscu gdzie jest cień. W najprostszej wersji zadania aby to obejść należy użyć glPolygonOffset oraz glEnable(GL_POLYGON_OFFSET_FILL).

  3. (+1 punkt) zamiast glPolygonOffset można postarać się bardziej. Nie tylko poprawimy w ten sposób śnieżenie, ale też naprawimy drugi problem: nasz cień "wychodzi" poza prostokąt na którym miał być. Tzn. nasz prostokąt jest jakimś skończonym quadem, ale cień pojawi się wszędzie, na całej nieskóńczonej płaszczyźnie przechodzącej przez ten prostokąt (co jest logiczne, w końcu nawet funkcja oblicz_macierz_rzutujaca_model_na_plaszczyzne dostała tylko równanie płaszczyzny, nie znała dokładnej pozycji naszego prostokąta).

    Można elegancko to rozwiązać przez bufor szablonów, stencil buffer. Przy czyszczeniu ekranu przez glClear będziemy chcieli czyścić stencil buffer (flaga GL_STENCIL_BUFFER_BIT). Po narysowaniu modelu, rysujemy płaszczyznę, w taki sposób aby "oznaczyć" sobie w stencil buforze gdzie znajduje się nasz quad. Będzie do tego potrzebna glStencilOp. Potem rysujemy nasz cień ale tylko tam gdzie znajdował się quad — do tego celu będziemy musieli testować stencil buffer, patrz glStencilFunc.

    Dzięki temu nasz cień będzie rysowany tylko tam gdzie jest rysowany quad. Ponadto, zwróćmy uwagę że już nie potrzebujemy depth testu (skoro wiemy dokładnie gdzie nasz quad jest widoczny). Możemy go wyłączyć (np. glDisable(GL_DEPTH_TEST)). Dzięki temu, nie potrzebujemy już glPolygonOffset ani glEnable(GL_POLYGON_OFFSET_FILL).

2.2. Zad 5.2 do wyboru: Odbicia lustrzane na płaszczyźnie

(Wersja podstawowa (bez clipPlanes) za 1.5 punkta:)
Świetne omówienie tej techniki, z przykładowym kodem źródłowym.

Początek jest naprawdę prosty: wyobraźmy sobie że nasza scena 3D jest zawsze ponad płaszczyną Z, tzn. ma Z > 0. Wyobraźmy sobie też że na płaszczyźnie Z = 0 mamy doskonałe, nieskończone lustro. To jak wyrenderować odbicie naszej sceny ?

  rysuj_model();

  glPushMatrix();
    glScalef(1, 1, -1);
    rysuj_model();
  glPopMatrix();

Innymi słowy, glScalef(1, 1, -1) odbija nam model względem Z. Jako rysuj_model należy użyć dowolnej nietrywialnej sceny, można wyrenderować dowolny model w Wavefroncie albo np. glutSolidTeapot.

Żeby sztuczka wyglądała lepiej, należy:

  1. Po pierwsze, niech lustro będzie prostokątem, zamiast wyobrażać sobie "nieskończoną płaszczyznę lustra". Czyli narysujmy tego quada w OpenGLu.

  2. Teraz zróbmy żeby kolor lustra był zmieszany z kolerem odbitego obrazu. Czyli użyjemy dodatkowo blending (patrz glBlendFunc, glEnable(GL_BLEND) i czwarty komponent do kolorów (dla glColor lub glMaterial / glLight)). Tzn. najpierw narysuj scenę, potem w trybie blending narysuj lustro. (Na odwrót nie byłoby dobrze — rysowanie całej sceny z włączonym blending spowoduje artefakty, ponieważ depth test nie będzie działał jak należy w sytuacji kiedy kolejny malowany kolor jest mieszany z kolorem ekranu.) To sprawi wrażenie odpowiednio zabarwionego lustra.

  3. Po drugie, teraz będzie widać że nasz odbity model jest widoczny wszędzie — nie tylko tam gdzie narysowaliśmy quad lustra. Musimy to poprawić używając stencil bufora, zupełnie podobnie jak dla plane-projected shadows. Wyznaczymy sobie gdzie jest widoczna tafla lustra, i będziemy rysować odbicie lustrzane tylko tam.

    Zauważcie że w praktyce oznacza to że narysujemy lustro co najmniej dwa razy: raz, tylko do stencil bufora, żeby wyznaczyć w stencil buforze gdzie jest lustro. Potem narysujemy odbitą scenę. I drugi raz narysujemy lustro, tym razem do bufora kolorów, żeby dodać zabarwienie lustra.

    Pamiętajcie cały czas pilnować i odpowiednio przełączać zapis / test trzech buforów: stencil, bufora głębości i bufora kolorów. Bufory można też czyścić razem albo z osobna przez glClear. Po chwili kombinowania ukaże się odpowiedni efekt... przykładowy kod źródłowy jest w artykule powyżej, w razie potrzeby.

  4. (+0.5 punkta) Wreszcie, co się stanie kiedy nasz model może przecinać taflę lustra, czyli kiedy ma dowolne Z ? (zakładając że zrobiliśmy lustro na Z = 0). Trzeba go obciąć, bo inaczej będzie widać że obiekt "wystaje" z lustra. Patrz glEnable(GL_CLIP_PLANExxx) oraz glClipPlane(GL_CLIP_PLANExxx, ...). Krótkie omówienie tego razem z przykładowym kodem jest tutaj.

2.3. Zad 5.3 do wyboru: Cienie metodą shadow volumes

(2 punkty) Cienie za pomocą shadow volumes, w najprostszej wersji (z-pass, jedno źródło światła (do wyboru: punktowe lub kierunkowe), bez żadnych optymalizacji). Google podaje mnóstwo linków na ten temat, więc nie będę się rozpisywał... podkreślam tylko że w ramach zadania nie trzeba implementować żadnych optymalizacji. Czyli z-pass (bez żadnych capping). Najpierw renderuj całą scene z wyłączonymi światłami (jedynie global ambient, ew. ambient świateł). Potem odpowiednio zainicjuj stencil bufor używając shadow volumes: wystarczy dla każdego trójkąta wyrenderować odpowiedni shadow volume (czyli 3 quady). Po czym wyrenderuj scenę z włączonym oświetleniem, tam gdzie stencil bufor mówi że nie ma cienia.

2.4. Zad 5.4 do wyboru: Rasteryzacja

(2 punkty) Samemu zaimplementować implementację macierzy + rasteryzację + z-bufor i napisac OpenGLa np. w trybie tekstowym. (Hej, OpenGL a'la aalib ?).

Zasadniczo trzeba wklepać macierze OpenGLa i zaimplementować mnożenie macierzy 4x4 oraz rasteryzację trójkątów (pełna wersja: z kolorami, niepełna: bez kolorów). Idea jest taka żeby samemu zaimplementować minimalny podzbiór OpenGLa: glOrtho, gluPerspective, glFrustum, glTranslate, glRotate, glScale (to wszystko jest glMultMatrix z odpowiednim parametrem), glLoadMatrix, glMultMatrix, glBegin(GL_TRIANGLES), glVertex, glEnd, glColor (w pełnej wersji: glColor może być pomiędzy glBegin...glEnd, czyli musimy interpolować kolory; w niepełnej: glColor musi być poza glBegin...glEnd, czyli mamy tylko trójkąty jednego koloru).

Dodatkowo, implementacja ma zawsze zachowywać się jakby test głębokości i zapis do z-bufora były włączone (opis z-bufora był w treśći poprzedniego zadania, głębokość pixela mamy po prostu w komponencie "z" wierzchołka po transformacjach macierzami).

3. Kilka kolejnych wiadomości (2007-01-08)

Nowe zadanie pojawi się za kilka dni. Na razie czekam jeszcze na więcej rozwiązań zadania 4, punktacja jest aktualna na dzisiaj wieczór więc pewnie wszyscy właśnie pracują nad zadaniem 4 :) Tym niemniej od przyszłego wtorku punktacja za zadanie spada do 3/4, potem do połowy...

Zapowiedź przyszłego zadania: już niepotrzebna, patrz sekcja powyżej.

4. Kilka wiadomości (2007-12-17)

Kilka wiadomości dzisiaj:

  1. Jak niektórzy z Was zauważyli, funkcja clock ma niekiedy zbyt małą dokładność pod Linuxem. Czyli pod Unixami najlepiej używać gettimeofday. Rozszerzyłem artykulik o time-based animation o przykłady użycia gettimeofday.

  2. Punktacja na dole strony jest w pełni uzupełniona na dzień dzisiejszy. Jeżeli czegoś brakuje, to znaczy że gdzieś mi się zawieruszyło — proszę zgłaszać.

  3. Punktacja za zadanie 4: zadanie będzie liczyło się do ogólnej punktacji jakby było warte 2.5 punktów podstawowych. Co znaczy tyle że chciałbym żeby każdy z Was zrobił coś dodatkowego, nie tylko geometrię + oświetlenie. Zadanie jest i ciekawsze i ważniejsze niż np. zadanie z grafiką wektorową, stąd taka decyzja.

    1 punkt jest za podstawową zabawę z geometrią i oświetleniem,
    +1 za tekstury (które są stosunkowo proste kiedy mamy już zrobione materiały, trzeba nauczyć się posługiwać jakąś biblioteką do odczytu obrazków),
    +1 za animację,
    +1 za interfejs GUI do bardziej elastycznej zabawy ze światłami (wcześniej było 0.5 punkta, zwiększyłem).

  4. Nieco wygładziłem treść zadania 4, dopisałem też akapicik o testach FPS na końcu.

    Dodałem też więcej przykładowych modeli do katalogu sample_wavefront_obj: prosty cube z teksturą, i humanoid. Humanoid zawiera 4 pliki OBJ o tej samej strukturze, możecie bawić się składając je w animacje. Animacja pomiędzy dwoma modelami: humanoid_walk_1 i humanoid_walk_2 spowoduje że ludzik wykona krok.

5. Zadanie 4 (2007-12-04)

Odczyt modelu 3D z pliku.

Zadanie polega na odczycie modelu 3D z pliku. Jest wiele formatów modeli 3D, proponuję Wam zaimplemetowanie odczytu formatu Wavefront (pliki xxx.obj, dodatkowo materiały mogą być zapisane w oddzielnym pliku xxx.mtl). Format Wavefront to format tekstowy, bardzo łatwy do odczytu linia-po-linii. Pełna specyfikacja formatu Wavefront (.obj) (bez obaw, nie musicie implementować wszystkiego z tej specyfikacji :). Specyfikacja formatu materiałów (.mtl). Bardzo uproszczona opis formatu jest też tutaj.

Przykładowe modele:

Wasz program ma odczytać dowolny model 3D w formacie Wavefront. Absolutnie nie wymagam pełnej implementacji tego formatu, pełna specyfikacja zawiera sporo rzeczy trudnych i/lub praktycznie bezużytecznych. Aby odczytać geometrię, wystarczy interpretować linijki v (wierzchołek) oraz f (ściana; wymienione są po kolei numery wierzchołków które należy połączyć aby otrzymać ścianę). Dla materiałów i ew. tekstur trzeba interpretować trochę więcej, czytaj dalej...

Opis renderowania 3D powierzchni wypełnionych (zamiast wireframe) w OpenGLu

  1. Po pierwsze, używamy 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.

    (Chociaż akurat do tego zadania glutSolidXxx, quadrici, Wam się nie przydadzą; wystarczy używać GL_POLYGON lub GL_TRIANGLES do tego zadania).

  2. Kiedy macie już geometrie wypełnioną i uruchomicie program, zauważycie dwa problemy. Po pierwsze, obiekty są rysowane jednym, jednolitym kolorem. Nie wygląda to zbyt realistycznie... i tutaj właśnie wkracza oświetlenie OpenGLa. Oświetleniem zajmiemy się za chwilę.

    Drugi problem: jeżeli popatrzycie na swój model z rożnych stron zauważycie że ściany zasłaniają się w dość dziwny i nieprawidłowy sposób. Dokładniej: jeżeli w danym punkcie ekranu rysowaliśmy więcej niż jedną ścianę to na ekranie zobaczymy tą ścianę która była rysowana jako ostatnia w naszym display. Tak jest oczywiście źle: my chcemy widzieć ta ścianę która jest najbliżej kamery, bo to ona zasłania pozostałe ściany. Rozwiązanie: użycie bufora głębokości, albo inaczej Z-bufora.

    Krótkie omówienie jak działa Z-bufor: Idea jest prosta: dla każdego pixela który rysujemy na ekranie zapamiętajmy pomocniczą informację: odległość rysowanego punktu (w przestrzeni 3D) od kamery. Zanim narysujemy pixel sprawdzamy czy nie ma w tym miejscu ekranu już narysowanego pixela o mniejszej odległości od kamery. Pseudokod będzie pewnie bardziej jasny od moich mętnych wyjaśnień:

          ... na początku rysowania danej klatki zainicjuj bufor_głębokości:
              for x := 0 to Width
                  for y := 0 to Height
                      bufor_głębokości[x][y] := + nieskończoność;
          ...
    
          ... za każdym razem gdy chcesz wypełnić pixel x, y kolorem k: ...
              z := oblicz odległość punktu 3D (który wywołał rysowanie
                   danego pixela 2D na pozycji x, y) od kamery;
              if z < bufor_głębokości[x][y]
                  bufor_głębokości[x][y] := z;
                  // Bufor kolorów to po prostu "ten bufor który jest widoczny na ekranie"
                  bufor_kolorów[x][y] := k;
        

    Rezultat użycia Z-bufora: obiekty 3D mogą być rysowane w dowolnej kolejności w display, na ekranie będą widoczne poprawnie. Z-bufor można efektywnie zaimplementować sprzętowo, co znaczy tyle że bufor głębokości jest pamiętany na karcie graficznej i jest testowany i zapisywany przez procesorek karty graficznej. Czyli "algorytmu" Z-bufora podanego powyżej nie macie implementować sami, on już jest zaimplementowany na karcie graficznej, my go tylko "aktywujemy".

    Kiedy używamy Z-bufora musimy zdawać sobie sprawę że wartości zapisywane/porównywane w buforze głębokości to nie są zwykłe odległości obiektu od kamery. Zamiast tego są to odległości obliczane przez macierz perspektywy. W skrócie to co trzeba wiedzieć to że wartości near i far które podajemy do naszych procedur gluPerspective, glFrustum i glOrtho maja wpływ na jakość Z-bufora. Jeżeli podamy zbyt małe near lub zbyt duże far to szybko zauważymy że precyzja naszego Z-bufora jest zła. Czyli zawsze trzeba starać się podać możliwie duże near i możliwie małe far. Ponadto wartości na których operuje Z-bufor są bardziej gęste (czyli są obliczane i porównywane z lepszą precyzją) bliżej kamery, mniej gęste dalej od kamery. Czyli jest szczególnie ważne aby near było możliwie duże. Artykuł na Wikipedii o Z-buforze, referencja funkcji glDepthRange oraz OpenGL FAQ o Z-buforze podają więcej szczegółów.

    Przechodząc do implementacji: aby użyć Z-bufora w OpenGLu trzeba

    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. Używanie oświetlenia:

    • Materiały ustawiamy przez glMaterial.

      W zadaniu należy odczytać materiały z pliku z materiałami. Należy odczytywać co najmniej kolory RGB ambient, diffuse, specular z linijek zaczynających się od Ka, Kd, Ks. Ponadto, aby odpowiednio przyporządkować materiały ścianom, Wasz program musi obsługiwać linijkę newmtl w której podajemy nazwę definiowanego materiału. W pliku OBJ należy obsługiwać linijki mtllib (wskazuje skąd odczytać materiały) oraz usemtl (nazwa materiału używanego na kolejnych ścianach).

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

      W naszym zadaniu, chciałbym żebyście włączyli jedno albo dwa światła. Parametry (kolory, pozycja) świateł nie są odczytywane z pliku OBJ — ich początkowe wartości można zaszyć jako stałe w programie, i należy dać użytkownikowi możliwość zmiany tych parametrów.

    • Żeby oświetlenie działało dobrze trzeba jeszcze ustawić odpowiednie wektory normalne.

      Odczytaj wektory normalne z pliku OBJ (linijki vn). Trzecie indeksy w linijkach opisujących ścianę wskazują który wektor normalny użyć dla danego wierzchołka. Na przykład pobrubione indeksy poniżej wskazują na pierwszy, drugi i trzeci wektor normalny.

        f 1/1/1 2/2/2 3/3/3

      Jeżeli w pliku brakuje opisu wektorów normalnych, wtedy należy obliczać dla każdej ściany wektor normalny używając vector product. Jeżeli mamy trójkąt z wierzchołkami T0, T1, T2 to wektor normalny możemy obliczyć jako

        Normalized( VectorProduct(T2 - T1,  T0 - T1) )

      To oblicza wektor wychodzący ze strony CCW ściany. Wychodzi ze strony CCW (counter-clockwise) oznacza że jeżeli patrzylibyśmy na ścianę z takiej strony że jej kolejne wierzchołki wydawałyby się nam ułożone niezgodnie z kierunkiem wskazówek zegara, to wtedy wektor wychodziłby ze ściany do nas.

      Patrz wikipedia o cross product.

      Dla ściany o więcej niż 3 punktach, możemy po prostu obliczyć wektor normalny tak samo biorąc pierwsze trzy wierzchołki ściany.

      Można wydać OpenGLowi polecenie glEnable(GL_NORMALIZE), wtedy nie trzeba samemu normalizować wektórów normalnych.

      Jeżeli odczytujecie wektory normalne z pliku, wtedy każdy wierzchołek ma potencjalnie inny wektor normalny, i mamy cieniowanie Gourauda, patrz wikipedia o Gouraud shading. W OpenGLu należy ustawić glShadeModel na GL_SMOOTH żeby kolory (wynikające z różnego oświetlenia każdego wierzchołka) były należycie interpolowane.

      Wektory normalne podajemy przez glNormal (np. przed każdym glVertex).

    • Być może będziecie chcieli żeby światło mogło oświetlać obie strony płaszczyzny, patrz GL_LIGHT_MODEL_TWO_SIDE dla wywołania glLightModel. Czasami dobrze jest też zwiększyć GL_LIGHT_MODEL_AMBIENT żeby mieć jaśniej na całej scenie.

    Po dokładniejsze omówienie jak działa oświetlenie odsyłam do rozdziału "Lighting" z "OpenGL programming guide".

    Wasz program powinien pozwalać pobawić się oświetleniem, zmieniać kolor i pozycję i/lub kierunek świateł. Pozycje świateł dobrze będzie pokazywać w jakiś sposób (np. rysując w tym miejscu punkt przez glBegin(GL_POINTS) lub mały sześcianik przez glutXxxCube).

    Interfejs do operowania wszystkimi ustawieniami oświetlenia można zrobić dowolnie. Parametrów jest dużo (należy ustawić jedno lub dwa światła, każde światło ma pozycję lub kierunek, 3 kolory (ambient, diffuse, specular) RGB). Więć będziecie musieli użyć wielu klawiszy, być może też sprawdzać np. czy wciśnięty jest klawisz Shift lub Ctrl (np. klawisz "s" zmienia Red koloru diffuse światła, klawisz "Shift+s" zmienia Green koloru diffuse światła, klawisz "Ctrl+s" zmienia Blue koloru diffuse światła).

    Można też użyć lepszego interfejsu (+ 1 punkta) z przejrzyście wyglądającymi kontrolkami, które pozwalają zmieniać parametry za pomocą myszki itd. Są biblioteki które umożliwiają rysowanie takich kontrolek bezpośrednio w kontekście OpenGLa, np.

    Większość "normalnych" bibliotek do kontrolek pozwala też zainicjować kontekst OpenGLa jako szczególny rodzaj kontrolki. Np. GTK (w połączeniu z GtkGLExt lub starszą GtkGLArea), wxWidgets. Można więc napisać "normalny" program okienkowy i jako jego część wstawić kontrolkę renderującą naszą scenę przez OpenGLa.

Tekstury

Żeby odczytać tekstury z pliku rozsądnie jest użyć jakiejś gotowej biblioteki. Polecam SDL_image.

Termin na wykonanie zadania: 8 stycznia, czyli pierwsze zajęcia w nowym roku. Zadanie będzie liczyło się do ogólnej punktacji jakby było warte 2.5 punktów podstawowych — co znaczy tyle że chciałbym żeby każdy z Was zrobił coś dodatkowego, nie tylko geometrię + oświetlenie.

Kiedy już zaimplementujecie wszystko :), i w spokoju będziecie oglądać różne sceny.... Warto przetestować różne duże sceny i zobaczyć ile FPS (frames per second) ma nasz program. Jak spada FPS kiedy liczba trójkątów sceny rośnie ? Jak spada FPS kiedy zbliżamy i oddalamy się od/do sceny (tzn. ta sama ilość trójkątów raz zajmuje mało miejsca na ekranie, raz dużo) ? Jak zmienia się FPS kiedy obracamy się kamerą tak że patrzymy w pustkę i cała scena jest "za naszymi plecami" ?

6. Zadanie 3 (2007-11-13)

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:

Zadanie: 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 w "Red Book", jest też omówienie quadriców po polsku. gluQuadricDrawStyle ustawiajcie na GLU_LINE (lub GLU_SILHOUETTE lub GLU_POINT jeśli mają sens w danym przypadku; w każdym razie nie używajcie GLU_FILL).

  3. Scena ma być animowana, wykorzystywać składanie transformacji (tzn. mnożenie macierzy przez OpenGLa), przynajmniej translacji, rotacji. Stos macierzy może być pomocny — glPushMatrix, glPopMatrix z poprzedniego zadania.

  4. Należy zaimplementować dwa sposoby nawigacji:

    1. Kamera w stylu gier FPS (DOOM, Quake itd.): ruch kamery do przodu / do tyłu (na klawisze góra/dół) i obracania w lewo/prawo (na klawisze lewo/prawo).

      Czyli Twój program powinien pamiętać aktualną pozycję kamery, aktualny kierunek patrzenia i pion kamery. Przesuwanie gracza do przodu/do tyłu do dodawanie/odejmowanie do pozycji kamery pewnej części wektora kierunku patrzenia. Obracanie kamery to obracania wektora kierunku patrzenia wokół wektora pionu kamery. Obracanie punktu 3D o zadany kąt wokół zadanego kierunku — wiemy jak to zrobić z wykładu, gdzie była podana macierz obrotu 3D ? Jeśli ktoś nie pamięta, patrz OpenGL Programming Guide, Appending G.

      Mając trzy wektory pozycji, kierunku patrzenia i pionu możemy taką kamerę ustawić w OpenGLu przez gluLookAt.

    2. Nawigacja w stylu "Examine": kamera jest umieszczona w dogodnym punkcie aby widzieć mniej-więcej całą scenę, i użytkownik operuje na scene tak jakby to było pudełko którym można obracać na różne strony.

      Bardziej precyzyjnie, gracz może obracać całą sceną w osiach x, y, z (można to zaimplementować tak aby np. przyciśnięcie strzałki w lewo obracało w poziomie w lewo, albo tak aby przyciśnięcie strzałki w lewo zwiększało prędkość obrotu w lewo — ten drugi sposób jest bardziej elastyczny dla użytkownika, chociaż trzeba wtedy też zaimplemetować klawisz który zeruje wszystkie prędkości obracania się). Ponadto można scenę skalować.

  5. Time-based animation:

    Wielu z Was po implementacji zadania 1 z PGK zauważyło że animacja w Waszych programach działa z różną szybkością na różnych komputerach. Świadomie nie wspominałem o tym na pierwszej pracowni, żeby nie mieszać Wam w głowach na samym początku :)

    Teraz jednak warto sobie wyjaśnić dlaczego tak się dzieje, i jak zrobić to lepiej: przeczytajcie co to jest i jak implementować time-based animation. Animacja w tym zadaniu (i w przyszłych zadaniach) (będzie jeszcze sporo zadań wymagających takiej lub innej animacji) powinna używać tego sposobu.

  6. I jeszcze jeden drobiazg: program powinien wyświetlać FPS, frames per second, czyli liczbę klatek na sekundę, czyli ile razy na sekundę wywoływana jest metoda display. (po zrobieniu time-based animation można to np. obliczyć znając (time_now - last_idle_time)).

    Ilość FPS można wypisywać w okienku OpenGLa (np. przez glutBitmapCharacter) albo zmieniając co chwila tytuł (caption) okienka. W tym drugim przypadku uwaga — nie należy zbyt często zmieniać tytułu okienka, pod niektórymi systemami (...obserwowalne pod Windowsem, ale chyba zależy od menedżera okien) zbyt częste zmienianie tytułu okienka może spowolnić nasz program. Więc trzeba dodać "bezpiecznik" w kodzie żeby uaktualaniać tytuł okienka np. max raz na sekundę.

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

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 3 tygodnie od 2007-11-13, czyli do 2007-12-04.

Zachęcam wszystkich do zrobienia tego zadania — poznacie tu podstawy operowania obiektami 3D w OpenGLu, a to będzie przydatne w (prawie) wszystkich późniejszych zadaniach.

7. Zadanie 2 (2007-10-23)

Treść zadania wyszła długa, przepraszam. Przyznaję że kiedy układałem zadanie w mojej głowie wyglądało na mniejsze. Wierzcie lub nie, ale to jest i tak skrócona wersja... (wersja oryginalna zawierała jeszcze animacje, które sobie darujemy). Na wykonanie zadanie są trzy tygodnie, czyli do 13 listopada. Za pełne wykonanie zadania są 2 punkty, niepełne implementacje też będą przyjmowane..

Napisać program do edytowania prostego formatu obrazków w grafice wektorowej. Dość specyficzny w tym formacie będzie fakt że obiekty można łączyć w grupy, tworząc małą hierarchię obiektów.

Omówienie formatu:

Dokładny format pliku:

Na początku każdej linii może być dowolnie wiele białych znaków (spacji, tabulatorów), w ten sposób będziemy mogli robić wcięcia zapisując takie pliki ręcznie. Cały plik jest zawsze dokładnie jednym obiektem.

Przykłady:

Przykładowy prosty plik:

{
  20 20
  0
  3 3
  P 1 0 0
}

Powyższy plik definiuje prostokąt. Czerwony (kolor 1 0 0, czyli red = 1 reszta = 0). Lewy dolny róg na pozycji (20, 20) i prawy górny na pozycji (50, 50) (bo domyślnie prostokąt jest od (0, 0) do (10, 10); więc skalowanie 3 3 oznacza że mamy trzy razy większy prostokąt, od (0, 0) do (30, 30); dodanie do tego przesunięcia 20 20 oznacza że mamy prostokąt od (20, 20) do (50, 50)).

Inny przykład:

{
  100 100
  0
  1 1
  {
    -50 0
    0
    1 1
    P 0 1 0
  }
  {
    +50 0
    0
    1 1
    K 0 0 1
  }
}

Ten plik definiuje zielony prostokąt po lewej (dokładnie, rozpięty od (50, 100) do (60, 110)) i czerwone koło po prawej (dokładnie, koło o promieniu 10 o środku w punkcie (100, 500)).

Pytanie: Czy takie coś ma być obsługiwane ?

{
  100 100
  0
  3 3
  K 1 0 0
  P 0 1 0
}

(mamy tutaj koło i prostokąt nie "opakowane" bezpośrednio w obiekt grupujący).

Odpowiedź: Nie. To nie ma (w każdym razie nie musi) być obsługiwane.

Idea jest taka że samo koło/prostokąt, tzn. linijki "K ..." albo "P ..." są zawsze zawarte w obiekcie grupującym, który w tym przypadku ma tylko jedno dziecko. Można powiedzieć że mamy trzy typy obiektów:

Wszystkie te trzy typy obiektów mają transformacje.

Sugerowana implementacja obiektowa to

(To tylko sugestia, żebyśmy mieli dobre pojęcie o co chodzi; w praktyce każdy implementuje tak jak chce, naturalnie.).

Wreszcie, zadanie:

Interfejs programu opisany powyżej (jakie klawisze co robią itd.) to tylko propozycja, można to wszystko zrobić inaczej — ważne jest aby była taka sama funkcjonalność.

Na końcu, należy zrobić jakiś rysunek własnym programikiem... Najlepiej wybrać na obrazek coś gdzie będzie można wykorzystać możliwość naszego programu: można tworzyć skomplikowany obiekt, i łatwo robić jego kopie. Na pracowni za trzy tygodnie będziemy mogli się pobawić w otwieranie naszym programem obrazków innych osób.

8. Zadanie 1 (2007-10-09)

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

Kompilacja poniższych programów:

Pracownia:

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

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

10. Punktacja

Hidden now.

11. Zasady

11.1. Zasady zaliczania

Na pracowniach będziemy robili zadania (a to niespodzianka :) ). Raz na tydzień lub dwa tygodnie, czasami rzadziej, będzie ukazywało się nowe zadanie. Za każde zadanie będzie do zdobycia przynajmniej 1 punkt za wykonanie części "podstawowej". Ponadto przy niektórych zadaniach będą "bonusowe" punkty do zdobycia za zaimplementowanie dodatków / trudniejszych wariantów zadania.

W sumie w czasie semestru będzie do zdobycia X podstawowych punktów. Na ocenę 3.0 trzeba zdobyć X / 2 punktów, na ocenę 5.0 należy zdobyć X punktów. Innymi słowy, nawet na 5.0 nie trzeba robić wszystkich zadań, można "nadrabiać" punktami bonusowymi (ale zazwyczaj będą one trudniejsze do zdobycia niż punkty podstawowe, więc nie jest to zalecana strategia).

Co do obecności: skrót treści pracowni i zadania będą pojawiać się na tej stronie. Więc obecność jest zupełnie nieobowiązkowa, ale często na pracowni mogę potłumaczyć coś więcej niż jest napisane na stronie WWW. Więc zapraszam, naturalnie.

Ale ciągle zadania trzeba mi przedstawiać osobiście, w czasie pracowni albo konsultacji — więc można nie chodzić na pracownię, ale trzeba kiedyś się pojawiać.

11.2. Oddawanie 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.kambi@proton.me. Nie trzeba dołączać samego skompilowanego programu, i tak będę chciał go skompilować sam.

I pamiętajcie że zadań nie wystarczy wysyłać — wysłany program trzeba też zaprezentować mi "na żywo", w czasie pracowni albo konsultacji. Konsultacje mam w środę 14.15-16.00.

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

Co do systemu operacyjnego: w czasie pracowni jesteśmy na Łindołsach, w czasie konsultacji możemy iść na Linuxy w moim pokoju albo w 137.

W praktyce, programy które będziecie pisali na tym przedmiocie powinny być przenośne. OpenGL, glut, SDL — wszystko to jest dostepne pod każdym rozsądnym system operacyjnym jak Linux czy Windows (czy ja właśnie nazwałem Windows rozsądnym systemem ?). Ale żeby uniknać ewentualnych trudów kompilacji (instalacji bibliotek, headerów etc.) rozumiem że możecie preferować ten czy tamten system operacyjny.