Programowanie Gier -> Wykład: OpenGL
Konspekt do wykładu (aka "Michalis-woli-wiki-od-slajdów"  ).
 ). 
- Jak działa OpenGL? - Karmiony wierzchołkami 3D (vertex), na każdym wierzchołku wykonujemy pewne operacje (lighting, fog, vertex shader). Wierzchołek jest transformowany do przestrzeni ekranu (2D, z głębokością do DEPTH_TEST), rasterizer rysuje trójkąt, interpolując kolor (Gouraud shading; w przpadku shaderów, interpoluje zadane zmienne varying). Na kązdym pixelu ("fragmencie" w terminologii OpenGLa) wykonujemy test głębokości, jeżeli Ok --- to teksturujemy (albo używamy fragment shadera) i obliczony kolor rysujemy na ekranie.
- Pipeline, stages (przetwarzanie potokowe, etapy potoku) 
- Zmiany stanów
- Rysowanie prymitywów: szybkie bo wiele wierzchołków (a potem i fragmentów (aka pixeli)) można przetwarzać równolegle
- Zmiany stanów bolesne -> bo trzeba skończyć przetwarzanie istniejących potoków (nie można trzymać danych o stanie w zbyt wielu miejscach, bo to z kolei powodowałoby kopiowanie stanu wzdłuż potoku, uniemożliwiałoby uzyskanie maksymalnej szybkości potoku) 
- Zmiany primitywu (na inny albo nawet ten sam, glEnd + glBegin(GL_TRIANGLES)) bolesne na tej samej zasadzie
- Demo: gl_primitive_performance: zmiany prymitywów (wiele GL_POLYGON, wiele GL_TRIANGLE_FAN, bardziej kosztowne na nowych GPU niż GL_TRIANGLES (mimo że GL_TRIANGLES duplikuje indeksy bardziej!))
- Demo: "thrashing" stanu (niepotrzebne komendy zmieniające stan, nawet na taki sam) zabijają efektywność. Nie róbcie glMaterial przed każdym polygonem, nie ustawiajcie tekstury przed każdym polygonem. Generalnie: grupujcie prymitywy pod kątem wspólnego użycia materiałów, tekstur etc., i starajcie się "pchać" możliwie dużo rzeczy bez zmian stanu.
- Żeby zminimalizować zmiany tekstur: texture atlas, do prostych gier można zrobić to samemu, są też http://developer.nvidia.com/object/texture_atlas_tools.html 
- Nie wszystkie zmiany stanów są bolesne. Bolesne są tylko zmiany danych które nie są przekazywane w potoku (ich zmiana wymusza "flush" potoków). Np. glNormal, glTexCoord są oczywiście bezbolesne, wiadomo że zmieniamy je ~raz na vertex. Inne ciekawsze rzeczy: glColor. Dlatego glColorMaterial (kolor materiału śledzi glColor) jest niezwykle ważne: zmiany glColor są tanie, zmiany glMaterial są kosztowne.
 
- Jak optymalizować? - Warto optymalizować tylko bottleneck. To jest ogólna prawda przy każdej optymalizacji, ale w przypadku GPU optymalizacja stages które nie są bottleneck naprawdę nie da kompletnie nic. Nawiasem mówiąc, można też patrzeć z drugiej strony: stage które nie są bottleneckiem możemy do pewnego stopnia bezkarnie obciążać.
- Jak znaleźć bottleneck? Można "ręcznie": - Application stage (CPU): Naturalnie, spróbuj wyłączyć "the usual suspects" i zobacz czy pomogło, np. wyłącz kolizje. Porównaj FPS mierzone faktycznie (ile ramek na rzeczywistą sekundę) z FPS mierzone jako "ile ramek mielibyśmy gdyby program wykonywał tylko display" (to zakłada że komendy OpenGLa nie są przesadnie cache'owane, w praktyce działa całkiem Ok). Zamień wszystkie glVertex / glNormal na glColor: to sprawi że OpenGL w zasadzie nie będzie robił nic, zobacz czy poprawił się stan.
- Geometry stage: Wyłącz światła OpenGLa, vertex shaders, fog, texture coord generation. Albo na odwrót, włącz, i zobacz czy znacząco zwolniłeś program. Generalnie, geometry stage jest najtrudniejsze --- w praktyce często działa sposób "jeżeli bottleneck nie jest w application ani rasterizer, to widocznie jest w geometry stage".
- Rasterizer stage: zmniejsz rozmiar okienka.
 
- Można też używać OpenGL debuggera/profilera. TODO: notes o gDEBugger
- See also: Kurs SIGGRAPH '97: OpenGL Performance Optimization (dość stary, większość uwag ma ciągle znaczenie, ale niekiedy mniejsze niż sugerują...). - Ćwiczenie: co jest źle, jak ulepszyć draw_cities 
 
 
- Display lists: 
 - Oryginalnie dość dość przyjemna własność OpenGLa, pozwalała GPU zoptymalizować wiele rzeczy. W praktyce, obecnie konkretne przypadki optymalizacji mają swoje własne obiekty (obiekty tekstur, obiekty wierzchołków z najczęściej używanych), ale display listy mogą być Ok w szczególnych sytuacjach. 
 - Konkretne przykłady i omówienie API. 
 Zalety:- Optymalizują (potencjalnie!) wszystko. Potencjalnie nawet eliminują niepotrzebne zmiany stanów (w praktyce --- niepewne, zależy od GPU), potencjalnie robią coś jak locked vertex array / vertex buffer object (w praktyce --- tak, ale są bardziej ograniczone). 
- Trywialne w użyciu.
 - Właściwie nie wiadomo (nie ma żadnych gwarancji) co optymalizują a czego nie.
- Nie są edytowalne. Powody oczywiste --- display lista zapamiętuję wszystkie komendy OpenGLa, mechanizm edycji ciągu komend OpenGLa byłby koszmarny i nie pozwoliłby na optymalizację danych (różnego rodzaju kompresje danych i poleceń) w środku display listy.
- Chociaż można uzyskać namiastkę "edytowalności" przez nested display lists: kiedy wywołamy display listę X w drugiej display liście Y, możemy potem przebudowywać zawartość listy X bez potrzeby uaktualniania listy Y. Innymi słowy, glCallList nie jest "rozwijane" w momencie wkładania go do innej display listy.
 - Tworzenie display listy z GL_COMPILE_AND_EXECUTE tworzy kiepsko (ale szybko) zoptymalizowaną wersję display listy. Jeżeli zamierzacie wywołać display listę naprawdę wiele razy, lepiej zbudować listę z GL_COMPILE i zaraz potem ją wywołać.
 
- Jak optymalizować rysowanie prymitywów - Intuicja: vertex sharing/caching. Vertexy typowo są współdzielone przez ~2-4 prymitywy. Optymalnie, geometry stage powinno przetwarzać tylko raz każdy vertex 3D naszej sceny. Od początku o tym wiedziano, stąd wszystkie TRIANGLE_STRIP, TRIANGLE_FAN, QUAD_STRIP etc. w OpenGLu. Gdy używamy vertex arrays lub vertex buffer objects, podajemy OpenGLowi indeksy do zadanej wcześniej tablicy danych, więc GPU może cache'ować wyniki geometry stage dla kilkuset ostatnich vertexów. W ten sposób nawet TRIANGLES mogą mieć bardzo duże vertex sharing, nie ma już potrzeby na optymalizowanie swoich modeli 3D pod kątem triangle stripów (jak to było kiedyś). Acha, pamiętajcie: optymalizacje o jakich opowiadamy sobie tutaj na wykładzie są dość nisko-poziomowe. Potrafią dać niezłego "kopa", ale nie zastąpią należytych optymalizacji wysoko-poziomowych (które znają układ sceny 3D), jak frustum culling (najlepiej przy pomocy drzew) albo portali. (Opowiemy o nich trochę na następnym wykładzie.)
- Historia: client vertex arrays - Przykłady i opis API. Vertex arrays "klasyczne" są przechowywane w normalnej pamięci. GPU nie może ich nigdzie skopiować, zoptymalizować, bo zawartość pamięci może zmienić się w każdej chwili. Vertex arrays sprawiają że komunikacja jest mniejsza, w przypadku rysowania "batchami" (glDrawElements, glDrawRangeElements) GPU jest wydajniej karmione vertexami. - Rozszerzenie EXT_compiled_vertex_array (implementowane praktycznie przez wszystkie istniejące GPU) pozwala "zalokować" vertex array, co znaczy tyle że informujemy GPU że zawartość tablicy jest stała i może swobodnie ją skopiować do bardziej odpowiedniego miejsca i robić cache wartości geometry stage. Jeżeli już używacie klasycznych vertex arrays (chociaż nie powinniście --- VBO są lepsze) to koniecznie używajcie lokowania vertex arrays. - Ukazało się jeszcze kilka innych rozszerzeń do vertex arrays, patrz np. tutaj. Ostatecznie, to czego chcemy obecnie używać to vertex buffer objects. 
 
- Teraźniejszość: vertex buffer objects - Przykłady i opis API. Zauważcie że polecenia jak glVertexPointer ciągle istnieją. Ale działają inaczej kiedy mamy zbindowany jakiś vertex buffer object: ostatni parametr (wskaźnik na dane w klasycznych vertex arrays, kiedy nie mam zbindowanego VBO) oznacza teraz tylko przesunięcie wzgledem bufora, zazwyczaj podajemy zwyczajne NULL (można dzięki temu łatwo upakować wiele tablic, każda indeksowana od zera, w jednym VBO). Zalety: - dynamic (editable)
- GPU dostaje hint jak będziemy używać VBO (GL_STATIC_DRAW_ARB, GL_DYNAMIC_DRAW_ARB, GL_STREAM_DRAW_ARB) i może skopiować dane do najlepszego rodzaju pamięci (np. GL_STATIC_DRAW_ARB zapewne spowoduje kopię danych na GPU, dając max szybkość)
- standard (od OpenGL 1.5 w specyfikacji, każdy sensowny GPU je implementuje)
 
 
 
- Testowy programik gl_primitive_performance - Przygotowałem prosty programik do testowania różnych sposobów przekazywania wierzchołków do OpenGLa. Pobawimy się nim trochę w trakcie wykładu, żeby zobaczyć że optymalizacje o których opowiadamy rzeczywiście mają drastyczny wpływ na szybkość programu. Można też ściągnąć, skompilować i pobawić się samemu: sam kod źródłowy jest tutaj, chociaż wymaga reszty moich bibliotek do odczytu modeli 3D. Żeby samemu skompilować: - $ svn checkout https://vrmlengine.svn.sourceforge.net/svnroot/vrmlengine/trunk/kambi_vrml_game_engine/ $ cd kambi_vrml_game_engine/examples/vrml/ $ ./gl_primitive_performance_compile.sh $ ./gl_primitive_performance $NAZWA_MODELU [$NUMER_SIATKI] - Wymagany jest FPC. (Jeżeli komuś nie podoba się język programowania w którym napisany jest mój programik, niech najpierw zaimplementuje sam bibliotekę od odczytu wszystkich formatów 3D jak X3D/VRML, Collada, Wavefront, 3DS, MD3 etc.  ) )
- Krótko: co to są shadery? - Od czasu implementacji T&L na GPU (GPU wykonuje wszystko począwszy od vertexów 3D) komplikacja fixed function rosła w straszliwym tempie. Praktycznie każdy element przetwarzania stawał się konfigurowalny. Texturowanie jest jednym z ekstremalnych przykładów: najpierw jedna tekstura, potem texture objects, potem multi-texturing, początkowo kombinowane prosto, potem kombinowane za pomocą GL_EXT_texture_env_combine. Nagle z prostego systemu w którym możemy tylko "nałożyć teksturę na obiekt" mamy system w którym możemy powiedzieć "weż tekstury X, Y, Z, i wykonaj na nich (X+Y)*Z". Żadne normalne API nie pozwoli zapisać tego krótko --- to co jest potrzebne to specjalny język w którym będzie można zapisać wyrażenia na teksturach. Stąd pomysł na shadery. Shader = specjalny język, rozumiany przez GPU. - Prosty język, dający mnóstwo nowych możliwości a jednocześnie znacznie krótszy zapis.
- Shadery zastępują fixed function operation. Shadery na nowych GPU mogą być nawet szybsze od fixed-function: bo shadery są dostosowane do programu, nie muszą implementować całego fixed-function, które zawiera wiele elementów rzadko używanych. 
 - Demo shaderów: trywialny shader GLSL wyświetlający negację tekstury. Nie będziemy teraz dokładnie mówić o języku shaderów, ani o tym jakie polecenia OpenGLa inicjują shader. To co jest ważne, to że nasz program zwyczajnie "podaje" treść shadera jako string do OpenGLa. (Przynajmniej dopóki mówimy o GLSL, języku shaderów wbudowanym w OpenGLa.) GPU (zazwyczaj część software'owa, bo nie ma sensu tego optymalizować) umie taki język sparsować i "skompilować" do czegokolwiek do czego będzie mu wygodnie (w praktyce, do ciągu wewnętrznych, "asemblerowych" poleceń na GPU.). Kiedy wyświetlamy nasz model 3D, możemy nakazać wykonanie shadera, wtedy OpenGL zamiast fixed-function pipeline będzie wykonywał shader (vertex and fragment, są też geometry w nowych GPU ale o tym kiedy indziej.) Optymalnie, shader jest wykonywany przez GPU w hardwarze --- czyli GPU działa już trochę jak procesor, chociaż o innej architekturze (pipeline, stages równoległość są używane przy wykonywaniu shaderów tak samo jak przy fixed-function, po prostu niektóre elementy zamieniają się teraz w mini-procesorki języka shaderów.) - Przyszłość? Już jest: Używanie GPU do bardziej ogólnych obliczeń, których wyniki muszą wrócić na CPU. Patrz http://www.nvidia.com/cuda i kilka innych nowinek NVidii. 
- OpenGL 3.0 - Potrzeba zmian: chcemy usunąć wiele rzeczy z fixed-function, uprościć implementację OpenGLa. Producenci powinni skupić się na optymalizowaniu i rozwijaniu shaderów. Wszystko co jest osiągalne shaderami wyrzucamy z fixed-function.
- Wyrzucamy proste glVertex i większość jego stanu. Trzeba używać VBO żeby renderować dane.
- Wyrzucamy przy okazji naprawdę stare rzeczy, jak tryb indeksowany (ktoś tego kiedykolwiek używał?)
- Przygotowujemy się przy okazji do przyszłych zmian: nazwy tekstur muszą być generowane przez glGenTextures etc.
- Wyrzucamy też operacje na macierzach OpenGLa (macierze można mnożyć w shaderach), alpha test (można go wykonać w shaderach), bufor akumulacji (nie sprawdził się, anti-aliasing jest wykonywany obecnie zazwyczaj przez FSAA, FBO powinien przejąć jego możliwości w sposób bardziej elastyczny?)
- Uh... i jeszcze wyrzucamy trochę fajnych rzeczy   
 
- Przypomnienie: kilka starych sztuczek z buforami OpenGLa - Stencil buffer Bufor który pozwala swobodnie maskować renderowanie kawałków ekranu (per-pixel). glStencilOp, glStencilFunc. Klasyczne łatwe zastosowanie: plane mirror. (O co najmniej jednym inne klasycznym zastosowaniu, shadow volumes, opowiemy ze szczegółami za kilka wykładów). Proste omówienie techniki. Skrót: - Aby narysować scenę w lustrze, zastosuj glScene(1, 1, -1)
- Pamiętaj że ściany CW zmieniają się na CCW --- zazwyczaj najłatwiej jest zrobić na chwilę glFrontFace(GL_CW)
- Aby obiekty nie wychodziły przed lustro, przytnij je (glClipPlane)
- Aby obiekty były widoczne tylko za lustem, narysuj je tylko tam gdzie widoczna jest powierzchnia lusta. Tu właśnie mamy prosty przykład użycia stencil buffora.
- Dla ładnego efektu (żeby lustro "barwiło" na jakiś kolor) dobrze jest narysować powierzchnię lustra z blending.
- Ładna sztuczka to także nałożenie mgły volumetrycznej (EXT_FOG_COORD, albo shaderami) na elementy widoczne w lustrze.
- Zalety techniki: Dość łatwa (przy niewielkiej ostrożności, można użyć tego samego kodu do renderowania obiektów w lustrze i normalnych; ponadto zachowujemy możliwość np. zrobienia frustum culling albo innego przycinania (np. w/g zakresu mgły) dla obiektów w lustrze). Efekt bardzo ładny.
- Wady: Dodatkowy rendering sceny na każdą płaszczyznę lustra (obciążenie na fragment stage niekoniecznie duże, ale na geometry stage w zasadzie dwukrotnie większe). Technika bezużyteczna jeżeli myślimy o bardzo dużej ilości luster widocznych jednocześnie, albo skomplikowanym lustrzanym obiekcie (do tego są lepsze metody --- environment mapping).
 
- Accumulation buffer Bufor RGB do/z którego możemy skopiować bufor kolorów. Bardzo ograniczone możliwości skalowania i dodawania buforów, użyteczny do różnego rodzaju sztuczek z rozmywaniem wybranych części obrazu. Klasyczne użycia: depth of field, motion blur. Kiedyś także anti-aliasing, chociaż bardzo kosztowny --- w praktyce wszyscy używają FSAA zamiast accumulation buffer.) 
 
Przy okazji tekstur dopowiemy sobie o Frame Buffer Object.