Wprowadzenie do WebGL Monika Falarz, Grzegorz Jaśko, Patryk Kiepas 9 listopad 2012 Streszczenie Dokument prezentuje podstawy i wprowadza w proces tworzenia grafiki przestrzennej przy użyciu biblioteki WebGL i języka JavaScript, renderowanej przez wiodące przeglądarki WWW. W trakcie procesu nauki będziemy tworzyć wspólnie szkielet aplikacji WebGL. 1 Wstęp Celem niniejszego dokumentu jest zapoznanie czytelnika z nową technologią WebGL w stopniu umożliwającym pisanie prostych scen przestrzennych. Zakładamy, że czytelnik posiada podstawową znajomość popularnych języków JavaScript i HTML5 oraz posiada ogólną wiedzę programistyczną (zmienne, pętle, instrukcje warunkowe itp.), matematyczną (macierze, wektory i operacje na nich) i szczątkową znajomość pojęć pochodzących z grafiki komputerowej (renderowanie, potok grafiki, translacja, rotacja, skalowania, bufor głębi, bufor wierzchołków, shader, test przycięcia itp.). WebGL to nic innego jak API języka JavaScript, które umożliwa nam tworzenie interaktywnej grafiki 3D bezpośrednio w przeglądarce. Jest to standard webowy tworzony przez grupę Khronos odpowiedzialną za stworzenie i kolejne specyfikacje OpenGL. API bazuje bezpośrednio na mobilnej wersji OpenGL ES 2.0 przez co, grafika utworzona przy pomocy WebGL może być uruchamiania nie tylko na pececie, ale i na urządzeniach mobilnych i telewizorach. W trakcie nauki zaczniemy wprowadzać coraz to kolejne elementy własnego szkieletu aplikacji WebGL. W założeniu, ma on być prosty i umożliwić jak najwygodniejsze przeskoczenie całego procesu inicjalizacji. W wyniku tego w późniejszych częściach wprowadzania, będziemy mogli w całości skupić się na tworzeniu i modyfikacji tworzonej przez nas trójwymiarowej sceny. 1
Naszą naukę rozpoczniemy od przygotowania naszej przeglądarki do obsługi technologi WebGL. Następnie przejdziemy przez proces integracji WebGL z HTML5, co umożliwi nam osadzanie kreowanych programów wprost w kodzie naszej strony WWW. Kolejnym krokiem, będzie próba przygotowania przez nas pustej sceny, a następnie narysowania na niej prostego obiektu dwuwymiarowego. Gdy to nam się uda, spróbujemy swych sił w trzecim wymiarze. Rozpoczniemy od prostych brył, by przejść do ich transformacji (obroty, translacje etc.), bufora głębi i obiektów na siebie zachodzących, nakładania tekstur, oświetlenia, shaderów, obsługi klawiatury i myszki oraz wielu innych. 2 Uruchomienie WebGL 2.1 Wstęp Nie każda przeglądarka jest w stanie uruchomić aplikacje WebGL z dwóch przyczny. Pierwsza to technologiczna bariera, gdy przeglądarka po prostu go nie obsługuje. Druga to kwestia bezpieczeństwa. Otóż WebGL ma parę błędów przez co osoby postronne jak i złośliwe oprogramowanie jest w stanie zawiesić nam przeglądarkę i nawet cały komputer. Z tego powodu WebGL jest domyślnie wyłączony. 2.2 Wybór przeglądarki Przeglądarki obsługujące WebGLa to m.in.: a) Firefox (wersja 4.0 i większa) b) Opera (wersja 12.00 i wyższa) c) Chrome (wersja 9.0 i wyższa) d) Safari (wersja 5.1 i wyższa na MacOS) Wsparcie dla żadnej z przeglądarek nie jest mocne i może ulec zmianie w zależności od wersji. Kompatybilność poszczególnych przeglądarek należy sprawdzać na stronie producenta. 2.3 Włączenie WebGL w przeglądarce Z powodu wspomnianej kwestii słabego bezpieczeństwa WebGL domyślnie jest zazwyczaj wyłączony. Włączenie go w większości przeglądarkach wygląda podobnie. W pasku adresu wpisujemy podany adres, wyszukujemy atrybut i ustawiamy odpowiednią wartość: i) Firefox - adres=about:config, atrybut=webgl.force-enabled, wartość= true 2
ii) Opera - adres=opera:config, atrybut=enable WebGL, wartość=1 iii) Chrome - adres=chrome://flags, atrybut=disable WebGL, wartość= Disable To czy udało nam się włączyć można sprawdzić wchodząc na strone: http://aleksandarrodic.com/p/jellyfish/ i zobaczyć czy aplikacja się uruchamia. Warto też wspomnieć o wymaganym sprzęcie. Potrzebna jest karta z Shaderami w wersji conjamniej 2.0. Najlepiej od NVidia, ATI. Występują problemy z kartami wbudowanymi producentów Intel, SiS. Niekiedy będziemy musieli siłowo wymusić uruchomienie WebGL, gdy nasza karta jest nie do końca zgodna ze specyfikacją. Można to zrobić poprzez ustawienie odpowiednich atrybutów bądź też rezygnując ze wsparcia sprzętowego i uruchomić renderowanie programowe (ang. software rendering, na CPU). 3 Integracja z HTML5 3.1 Po stronie HTML5 Cała magia związana z WebGL w przeglądarce dzieje się na komponencie canvas pochodzącym z nowego HTML5. Integracja jest prosta. Tworzymy podstawowy szablon strony HTML5, obiekt canvas określając przy tym jego identyfikator i wymiary oraz określamy, która funkcja JavaScript ma zostać wywołana podczas ładowania strony. Używamy do tego celu event onload: <html> <head> <title>integracja WebGL z HTML5</title> <meta http-equiv="content-type" content="text/html; charset=utf8"/> <script type="text/javascript" src="start.js"> </script> </head> <body onload="start();"> <canvas id="empty_canvas" width="500" height="500"> </canvas> </body> </html> 3.2 Po stronie WebGL Następnie w kodzie start.js odwołujemy się do elementu canvas za pomocą obiektowego modelu dokumentu DOM i funkcji getelementbyid i 3
już możemy renderować grafikę w przeglądarce: function start() { var canvas = document.getelementbyid("empty_canvas"); 4 Pusta scena 4.1 Inicjalizacja W tej części postaramy stworzyć naszą pierwszą pustą scenę. Zaczynamy od stworzenia globalnej zmiennej gl przechowującej cały kontekst WebGL oraz od wywołania funkcji inicjalizującej sam WebGL init() w naszej głównej funkcji start(): var gl; function start() { var canvas = document.getelementbyid("empty_canvas"); init(); Następnie wypełniamy funkcję init() następującym kodem inicjalizującym globalną zmieną gl, która jest wypełniana odpowiednimi danymi pochodzącymi z płótna canvas. Między innymi kontekstem WebGL, udostępniającym zestaw funkcji do rysowania: function init(canvas) { gl = canvas.getcontext("webgl") canvas.getcontext("experimental-webgl"); gl.viewportwidht = canvas.width; gl.viewportheight = canvas.height; if(!gl) { alert("nie mozna zainicjalizowac!"); 4.2 Rysowanie Teraz możemy się zając rysowaniem naszej pustej sceny dopisując do start() odpowiednie instrukcje. Zaczynamy od wyczyszczenia płótna i ustawieniu mu koloru czarnego. Następnie włączamy test bufora głębi, który 4
odpowiada za warstwowe rysowanie obiektów, tak aby odpowiednio się zasłaniały. Na koniec ustawiamy wielkość obszaru rysowania pobierając dane z płótna i wywołujemy funkcje rysującą draw(): function start() { gl.clearcolor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.depth_test); gl.viewport(0, 0, gl.viewportwidth, gl.viewportheight); draw(); Nasza funkcja rysująca będzie bardzo prosta skoro niczego nie rysuje. Jedyne co robi to czyści bufor ekranu i bufor głębi co jest wymagane przy zwykłym renderowaniu sceny. Nie możemy przecież pozwolić, by dane zapisywane do bufora ekranu i głębi nakładały się na siebie. Zawsze tworząc nową klatkę musimy zapisywać dane do pustch buforów: function draw() { gl.clear(gl.color_buffer_bit gl.depth_buffer_bit); Przykładowy kod źródłowy znajduje się w folderze 1. Pusta scena. Po uruchomieniu powinniśmy otrzymać czarny pusty widok. 5 Szkielet fruitgl (cz.1) 5.1 Wstęp Tworzony przez nas szkielet fruitgl ma być nieskomplikowany. Ma umożliwić nam jak najszybsze przejście do tworzenia grafiki, pozostawiając w niepamięć takie operacje jak inicjalizacja ekranu, buforów, shaderów, operacje na macierzach i wektorach czy przekształcenia w przestrzeni. 5.2 Kod Wszystkie funkcje szkieletu są umieszczone we wspólnej przestrzeni nazw fruitgl. Technicznie kod zawarty w szkielecie nie różni się od tego użytego wcześniej przy okazji tworzenia pustej sceny. Jest tylko inaczej zorganizowany: var fruitgl = { gl : null, 5
initialize : function(canvasname) { var canvas = document.getelementbyid(canvasname); fruitgl.gl = canvas.getcontext("webgl") canvas.getcontext("experimental-webgl"); fruitgl.gl.viewportwidht = canvas.width; fruitgl.gl.viewportheight = canvas.height; if(!fruitgl.gl) { alert("nie mozna zainicjalizowac!");, fruitgl.gl.clearcolor(0.0, 0.0, 0.0, 1.0); fruitgl.gl.enable(fruitgl.gl.depth_test); fruitgl.gl.viewport(0, 0, fruitgl.gl.viewportwidth, fruitgl.gl.viewportheight); begindraw : function() { fruitgl.gl.clear(fruitgl.gl.color_buffer_bit fruitgl.gl.depth_buffer_bit);, enddraw : function() { 5.3 Wykorzystanie fruitgl Wykorzystanie szkieletu fruitgl jest bardzo proste. Najpierw dołączamy plik z kodem fruitgl.js do kodu naszej strony (przed dołączeniem kodu start.js), a następnie wywołujemy funkcje szkieletu w funkcji start(). Nasz kod strony w html: <html> <head> <script type="text/javascript" src="fruitgl.js"> </script> <script type="text/javascript" src="start.js"> </script> </head> </html> 6
Użycie fruitgl w kodzie naszej aplikacji start.js: function start() { fruitgl.initialize("empty_canvas"); fruitgl.begindraw(); draw(); fruitgl.enddraw(); function draw() { // tu rysujemy! Szkielet fruitgl oraz przykłady jego wykorzystania znajdują się w katalogu "Kody źródłowe\fruitgl". 6 Pierwszy obiekt (trójkąt) Wyświetlając grafikę w WebGL nie możemy korzystać ze standardowego potoku karty graficznej (tzw. fixed-pipeline). Musimy utworzyć bardziej współczesny potok wymagający od nas zdefiniowania jednostek cieniujących, które w rzeczywistości są prostymi funkcjami wykonywanymi na rzecz każdego wierzchołka bądź też pixela sceny. 6.1 Vertex Shader Pomijając nieistotne etapy, przekazane na kartę graficzną wierzchołki trafiają wpierw do jednostki vertex shader odpowiedzialnej za ich transformację. Jednostka ta ustawia im m.in. odpowiednią pozycję z uwzględnieniem macierzy widoku, projekcji i świata. attribute vec3 position; void main(void){ gl_position = vec4(position, 1.0); Zaczynamy od deklaracji wektora 3-elementowego position, któremu będziemy przypisywać z programu pozycję naszych wierzchołków. Pozycja ta jest przepisywana bez zmian do odpowiedniego miejsca gl_position, z którego następny etap potoku będzie ją pobierał. Dzięki temu zabiegowi, pozycje wierzchołków będą oznaczały ich rzeczywiste położenie na ekranie w zakresie 0.0-1.0 dla każdej ze współrzędnych. 7
6.2 Pixel Shader Zwrócone wierzchołki trafiają do pixel shadera odpowiedzialnego za modyfikację pojedyńczych pixeli. Jest to jedna z ostatnich faz renderingu, która wykorzystuje już ułożoną w przestrzeni scene i skupia się tylko na widocznych fragmentach modeli. precision mediump float; void main(void){ gl_fragcolor = vec4(0.5, 0.2, 1.0, 1.0); Wpierw ustawiamy średnią precyzję typu zmiennopozycjnego, a następnie przypisujemy każdemu modyfikowanemu pixelu kolor RGB(0.5, 0.2, 1.0). Tym razem, nie przekazujemy niczego z programu. Odpowiedni pixel jest wydobywany z przesłanych od vertex shadera, wstępnie przetworzonych wierzchołków. 6.3 Ładowanie shaderów Definiujemy shadery w łańcuchach znaków shaderps i shadervs. Następnie tworzymy pusty shader o odpowiednim typie i przypisujemy mu kod shaderów oraz kompilujemy: var shaderps = "precision mediump float; void main(void) { gl_fragcolor = vec4(0.5, 0.2, 1.0, 1.0);"; var shadervs = "attribute vec3 position; void main(void) { gl_position = vec4(position, 1.0); "; var pixelshader = gl.createshader(gl.fragment_shader); gl.shadersource(pixelshader, shaderps); gl.compileshader(pixelshader); var vertexshader = gl.createshader(gl.vertex_shader); gl.shadersource(vertexshader, shadervs); gl.compileshader(vertexshader); Same wypełnione shadery nie na wiele nam się zdadzą. Musimy utworzyć podprogram reprezentujący potok, któremu przypisujemy utworzone jednostki cieniujące i go linkujemy. Następnie ustawiamy go jako domyślny sposób na tworznie grafiki oraz wskazujemy atrybut position jako ten w vertex shaderze, pod który będziemy przesyłać pozycje wierzchołków z programu: 8
shaderprogram = gl.createprogram(); gl.attachshader(shaderprogram, vertexshader); gl.attachshader(shaderprogram, pixelshader); gl.linkprogram(shaderprogram); gl.useprogram(shaderprogram); shaderprogram.position = gl.getattriblocation(shaderprogram, "position"); gl.enablevertexattribarray(shaderprogram.position); 6.4 Bufor wierzchołków Tworzone przez nas figury i bryły składają się z wierzchołków. Umieszczamy je w tzw. buforach, czyli szybkich, wydzielonych obszarach pamięci na karcie graficznej, których użycie znacząco redukuje czas odczytu. Zaczynamy od wywołania funkcji tworzącej bufor, a następnie ustawiamy go na bufor domyślny, czyli ten na którym aktualnie działamy: trianglebuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, trianglebuffer); Nie pozostaje nam nic innego jak zdefiniować trzy wierzchołki dla naszego trójkąta pamiętająć, że przy używaniu współrzędnych ekranowych poruszamy się po zakresie 0.0-1.0. Następnie funkcją bufferdata() ładujemy wierzchołki do bufora ustawiając przy tym jego rozmiar: var vertices = [ 0.0, 0.7, 0.0, -0.7, -0.7, 0.0, 0.7, -0.7, 0.0 ]; gl.bufferdata(gl.array_buffer, new Float32Array(vertices), gl.static_draw); trianglebuffer.itemsize = 3; trianglebuffer.numitems = 3; 6.5 Wyświetlenie Mając zdefiniowany potok graficzny i wczytane wierzchołki trójkąta, możemy przejść do wyświetlania sceny. Uzupełniamy funkcję draw() o następujące instrukcje: function draw() { gl.bindbuffer(gl.array_buffer, trianglebuffer); 9
gl.vertexattribpointer(shaderprogram.position, trianglebuffer.itemsize, gl.float, false, 0, 0); gl.drawarrays(gl.triangles, 0, trianglebuffer.numitems); Wpierw ustawiamy bufor z wierzchołkami trójkąta jako domyślny i na nim pracujemy. Później definiujemy rozmieszczenie wierzchołków w buforze poczynając od miejsca gdzie dane z niego będą trafiać do vertex shadera, poprzez liczbę współrzędnych (wymiar), aż po typ je przechowujący (dokładność). Rysunek 1: Pierwsza grafika (trójkąt) Cały proces wyświetlania kończymy funkcją drawarrays(), która wyświetla na ekranie aktywny bufor. W naszym przypadku rysujemy trzy wierzchołki (trianglebuffer.numitems) tworzące trójkąt (gl.triangles). Powyższy rysunek przedstawia nasze dotychczasowe dokonanie. 7 Trzeci wymiar Rysowanie w dwóch wymiarach wstępnie przetransformowanych wierzchołków jest proste. Trzeci wymiar oznacza konieczność wymnożenia wszystkich wierzchołków przez odpowiednie macierze (projekcji, widoku, świata), które ustawią ich pozycje z uwzględnieniem odpowiedniej perspektyw. Wynikiem tego jest złudzenie trzeciego wymiaru sceny. Nie dokonamy tego jednak przy użyciu samego WebGL, gdyż nie zawiera on wbudowanych operacji matematycznych na wektorach i macierzach. Użyjemy do tego gotowej biblioteki glmatrix v0.9.5 dostępnej pod adresem http://code.google.com/p/glmatrix. 10
7.1 Macierze Zaczniemy od utworzenia macierzy projekcji (stworzenie iluzji trzeciego wymiaru na płaskim ekranie) i macierzy świata (rozmieszczenie obiektów na scenie). Wpierw dołączamy bibliotekę glmatrix do strony przed naszym skryptem start.js, tworzymy globalne zmienne dla naszych macierzy i modyfikujemy vertex shader tak by z nich korzystał: var worldmat = mat4.create(); var projectionmat = mat4.create(); Modyfikacja vertex shadera: attribute vec3 position; uniform mat4 uworldmat; uniform mat4 uprojectionmat; void main(void) { gl_position = uprojectionmat * uworldmat * vec4(avertexposition, 1.0); Kolejny krok to stworzenie połączenia pomiędzy naszymi macierzami w programie i vertex shaderze: shaderprogram.worldmatuniform = gl.getuniformlocation(shaderprogram, "uworldmat"); shaderprogram.projectionmatuniform = gl.getuniformlocation(shaderprogram, "uprojectionmat"); Przy wypełnianiu macierzy projekcji i świata będziemy korzystać z funkcji bibliotecznych glmatrix. Dzięki funkcji perspective() utworzona macierz projekcji sprawi, że pozbędziemy się widoku ortogonalnego na rzecz perspektywy przestrzennej o horyzontalnym polu widzenia 45 stopni i zgodnym ze stosunkiem długości do szerokości ekranu widokiem. Macierz świata tworzymy wpierw jako macierz jednostkową (neutralna dla mnożenie, identity()), by następnie dokonywać za jej pomocą translacji obiektów sceny o wektor [0.5, 0.5, -4.0] i rotacje wzgledem osi Z o 90 stopni: mat4.perspective(45, gl.viewportwidth / gl.viewportheight, 0.1, 100.0, projectionmat); mat4.identity(worldmat); mat4.translate(worldmat, [0.5, 0.5, -4.0]); mat4.rotatez(worldmat, degtorad(90)); 11
Ostatnią rzeczą jest zaaplikowanie tak utworzonych macierzy do naszego vertex shadera, tak aby mógł z nich swobodnie korzystać. Dokonujemy tego tuż przed wywołaniem funkcji rysującej: gl.uniformmatrix4fv(shaderprogram.worldmatuniform, false, worldmat); gl.uniformmatrix4fv(shaderprogram.projectionmatuniform, false, projectionmat); gl.drawarrays(gl.triangles, 0, trianglebuffer.numitems); 7.2 Efekt końcowy Rysunek 2: Trójkąt w trzecim wymiarze po transformacjach Przykład znajduje się w katalogu "Kody źródłowe\3. Trzeci wymiar". Użyliśmy w nim pomocniczej funkcji degtorad(), która odpowiada za zamiane kąta wyrażonego w stopniach na radiany. Jest to wymagane przez wszystkie funkcje przyjmujące kąt rotacji: function degtorad(degrees) { return degrees * Math.PI / 180; 8 Trochę kolorków Dokonamy lekkiej modyfikacji poprzedniego trójkąta dodając do wierzchołków dodatkową informację w postaci koloru. Przykład z tego rozdziału znajduje się w folderze "Kody źródłowe\4. Trochę kolorków". 12
8.1 Jednostki cieniujące Ponieważ wszystkie dane sceny w potoku odbiera najpierw vertex shader, także i do niego musimy przesłać kolor wierzchołka. Również i pixel shader wymaga modyfikacji, tak aby mógł korzystać z nowej informacji o kolorze: attribute vec3 position; attribute vec4 color; varying vec4 vcolor; uniform mat4 uworldmat; uniform mat4 uprojectionmat; void main(void) { gl_position = uprojectionmat * uworldmat * vec4(avertexposition, 1.0); vcolor = color; Używamy zmiennej vcolor z modyfikatorem varying dzięki czemu jej wartość jest przekazywana pomiędzy wywołaniami jednostek cieniujących: precision mediump float; varying vec4 vcolor; void main(void){ gl_fragcolor = vcolor; Ostatni krok związany z ustawianiem shaderów to wskazanie i połączenie nowych atrybutów z programem: shaderprogram.color = gl.getattriblocation(shaderprogram, "color"); gl.enablevertexattribarray(shaderprogram.color); 8.2 Bufor kolorów Informacje o kolorach wierzchołków przechowujemy tak jak i pozycje po prostu w buforze. Tworzymy zmienną dla bufora a następnie wypełniamy go: var trianglecolorbuffer; trianglecolorbuffer = gl.createbuffer(); 13
gl.bindbuffer(gl.array_buffer, trianglecolorbuffer); var colors = [ 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0 ]; gl.bufferdata(gl.array_buffer, new Float32Array(colors), gl.static_draw); trianglecolorbuffer.itemsize = 4; trianglecolorbuffer.numitems = 3; 8.3 Wyświetlenie Jedyne co musimy zrobić by wyświetlić tak pokolorowany trójkąt to przesłać bufor kolorów do vertex shadera zaraz obok bufora pozycji. Wszystko to dokonujemy w funkcji draw(), tuż przed funkcją rysowania: gl.bindbuffer(gl.array_buffer, trianglevertexcolorbuffer); gl.vertexattribpointer(shaderprogram.vertexcolorattribute, trianglevertexcolorbuffer.itemsize, gl.float, false, 0, 0); gl.drawarrays(gl.triangles, 0, trianglebuffer.numitems); Rysunek 3: Efekt końcowy: trójkąt kolorowych 9 Animacja 9.1 Podstawy Ponieważ animacja opiera się na ciągłym tworzeniu sceny na nowo z uwzględnieniem pewnych zmian, potrzebujemy mechanizmu, który nam to umożliwi. Z pomocą przychodzi nam biblioteka od Google webgl-utils i funkcja requestanimframe() cyklicznie wywołująca podaną funkcję. 14
Ściągamy bibliotekę z http://code.google.com/p/webglsamples/ source/browse/book/extension/webgl-utils.js oraz dołączamy ją do naszej strony tak jak przy okazji glmatrix. Następnie tworzymy funkcję tick() odpowiedzalną za cykliczne wywołania animacji: function tick() { requestanimframe(tick); begindraw(); draw(); enddraw(); animate(); Wywołujemy ją w funkcji start() zamiast funkcji draw(), która jest uruchamiana teraz wraz z tick(). Następnie tworzymy globalną zmienną angle śledzącą aktualną rotacje naszego trójkąta oraz lasttime do przechowywania czasu. Wypełniamy funkcję animate() odpowiedzialną za samą animacje obiektów i ich płynną formę w zależności od liczby klatek wyświetlanego obrazu: function animate() { var timenow = new Date().getTime(); if (lasttime!= 0) { var elapsed = timenow - lasttime; angle += (90 * elapsed) / 1000.0; lasttime = timenow; 9.2 Stos macierzy Użyjemy prostego stosu dla macierzy świata, aby zachowywać jej początkowy stan i oddzielić od wszelkich modyfikacji wynikłych z transformacji na pojedyńczym obiekcie. Definiujemy pustą zmienną tablicową dla stosu. Następnie tworzym dwie funkcje obsługujące stos, które zdejmują z (pop()) oraz wkładają na niego macierz (push()): var stackmat = []; function push() { var copy = mat4.create(); mat4.set(worldmat, copy); stackmat.push(copy); 15
function pop() { if (stackmat.length == 0) { throw ParamConst.INVALID_POP_MATRIX_CALL; worldmat = stackmat.pop(); 9.3 Wyświetlenie Pozostało jeszcze zmodyfikować funkcje rysującą tak aby po ustawieniu macierzy świata odłożyć ją na stos, dokonać transformacji obiektów, wyrenderować na ekranie oraz zdjąć początkową wartość macierzy dla nowej klatki: function draw() { mat4.translate(worldmat, [0.0, 0.0, -2.0]); push(); mat4.rotate(worldmat, degtorad(angle), [0, 1, 1]); gl.drawarrays(gl.triangles, 0, trianglebuffer.numitems); pop(); Rysunek 4: Efekt końcowy: trójkąt animowany 10 Sześcian Stworzym całkowicie trójwymiarową bryłę: sześcian. Użyjemy do tego tzw. indeksowanego bufora wierzchołków. Ponieważ niektóre wierzchołki są wspólne dla wielu trójkątów/ścian dlatego też możemy pozycję takiego wierzchołka przechowywać tylko raz, a dobierać się do niego poprzez ustalony indeks. Możliwość taką daje nam właśnie indeksowany bufor. 16
10.1 Bufory danych Pozbywamy się całkiem wszystkich zmiennych związanych z trójkątem. Tworzymy nowe bufory wierzchołków, kolorów i indeksów dla naszego sześcianu: var cubevertexbuffer; var cubecolorbuffer; var cubeindexbuffer; Zaczynamy od wypełnienia bufora wierzchołków składającego się z 24 elementów jako standardowy ARRAY_BUFFER, analogicznie jak w poprzednich przykładach. Następnie bierzemy się za równie standardowy bufor kolorów. Do jego utworzenia używamy tylko 6 kolorów i pętli, która dla każdej ze ścian powiela 4-krotnie dany kolor. Bliżej przyjrzymy się tylko nowemu buforowi indeksów, w którym za pomocą indeksów do wierzchołków i grupowaniu ich po trzy wyznaczamy trójkąty do renderowania: cubeindexbuffer = gl.createbuffer(); gl.bindbuffer(gl.element_array_buffer, cubeindexbuffer); var cubeindices = [ 0, 1, 2, 0, 2, 3, // Przednia 4, 5, 6, 4, 6, 7, // Tylnia 8, 9, 10, 8, 10, 11, // Górna 12, 13, 14, 12, 14, 15, // Dolna 16, 17, 18, 16, 18, 19, // Prawa 20, 21, 22, 20, 22, 23 // Lewa ] gl.bufferdata(gl.element_array_buffer, new Uint16Array(cubeIndices), gl.static_draw); cubeindexbuffer.itemsize = 1; cubeindexbuffer.numitems = 36; 10.2 Wyświetlenie Rysowanie wszystkiego nie powinno być problematyczne. Zaczynamy od zbindowania wszystkich trzech buforów, a następnie wyświetlenia danych pochodzących z bufora indeksów: function draw() { mat4.translate(worldmat, [0.0, 0.0, -7.0]); gl.bindbuffer(gl.array_buffer, cubepositionbuffer); gl.vertexattribpointer(shaderprogram.position, 17
cubepositionbuffer.itemsize, gl.float, false, 0, 0); gl.bindbuffer(gl.array_buffer, cubecolorbuffer); gl.vertexattribpointer(shaderprogram.color, cubecolorbuffer.itemsize, gl.float, false, 0, 0); gl.bindbuffer(gl.element_array_buffer, cubeindexbuffer); gl.drawelements(gl.triangles, cubeindexbuffer.numitems, gl.unsigned_short, 0); Rysunek 5: Efekt końcowy: sześcian 11 Teksturowanie 11.1 Przygotowanie tekstury Na każdą z tworzonych powierzchni możemy nałożyć dowolną teksturę naśladującą pewien materiał. Tekstura to nic innego jak plik graficzny obrazujący np. drewno, metal. Przykład ten znajduje się w folderze "Kody źródłowe\7. Teksturowanie", a użyta do nałożenia na sześcian tekstura to wood.jpg. Zaczynam od zadeklarowania zmiennej na naszą teksture i stworzenia funkcji inicjalizującej wywoływanej po inicjalizacji buforów w funkcji start(): var texture; function initializetexture() { texture = gl.createtexture(); texture.image = new Image(); texture.image.crossorigin = "anonymous"; 18
texture.image.onload = function() { settexture(texture); texture.image.src = "wood.jpg"; Następnie tworzymy funkcje settexture() odpowiedzialną za ustawienie odpowiednich parametrów tekstury (sposób składowania tekstury, jej rodzaj, użyty system kolorów i techniki jej filtrowania): function settexture(tex) { gl.bindtexture(gl.texture_2d, tex); gl.pixelstorei(gl.unpack_flip_y_webgl, true); gl.teximage2d(gl.texture_2d, 0, gl.rgba, gl.rgba, gl.unsigned_byte, tex.image); gl.texparameteri(gl.texture_2d, gl.texture_mag_filter, gl.nearest); gl.texparameteri(gl.texture_2d, gl.texture_min_filter, gl.nearest); gl.bindtexture(gl.texture_2d, null); 11.2 Przygotowanie współrzędnych Jednak tekstura nie może współistnieć z powierzchnią bez pewnego kleju. Tym klejem są dwuwymiarowe (u, v) współrzędne tekstury określające położenie wierzchołka powierzchni na teksturze. Umożliwia to rozszerzanie, zwężanie i w ogólności dopasowanie danej tekstury do powierzchni. Zakres dla tych współrzędnych to 0.0-1.0. I tak wierzchołek o współrzędnych u: 0.0, v: 0.0 będzie tam gdzie lewy górny róg tekstury, a więc tą jej częścią zostanie przykryty. Z kolei wierzchołek ze współrzednymi u: 1.0, v: 1.0 to nic innego jak prawy dolny róg. Pomiędzy wierzchołkami tekstura jest odpowiednio dopasowana. Kod wygląda następująco: var cubetexturebuffer; cubetexturebuffer = gl.createbuffer(); gl.bindbuffer(gl.array_buffer, cubetexturebuffer); var coords = [ // Przednia 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 19
0.0, 1.0, ]; gl.bufferdata(gl.array_buffer, new Float32Array(coords), gl.static_draw); cubetexturebuffer.itemsize = 2; cubetexturebuffer.numitems = 24; 11.3 Modyfikacja shaderów Wszystko czego do tej pory dokonaliśmy w kontekście tekstur na nic się zda jeśli nie zmodyfikujemy jednostek cieniujących. Współrzędne tekstury są w tej kwestii na równi z kolorem wierzchołka (który zostanie przez teksturę zastąpiony) i są przekazywane do potoku graficznego poczynając od vertex shadera. Wymagana dlatego jest jego modyfikacja: attribute vec3 position; attribute vec2 coord; varying vec2 vcoord; uniform mat4 uworldmat; uniform mat4 uprojectionmat; void main(void) { gl_position = uprojectionmat * uworldmat * vec4(position, 1.0); vcoord = coord; Współrzędne tekstur zazwyczaj nie są wykorzystywane w vertex shaderze, a tylko przekazywane dalej do pixel shadera. To w nim dla każdego fragmentu powierzchni pobierany jest odpowiedni pixel tekstury przy pomocy tzw. obiektu próbkującego (ang. sampler), by następnie zostać nowym kolorem danego wycinka powierzchni: precision mediump float; varying vec2 vcoord; uniform sampler2d usampler; void main(void) { gl_fragcolor = texture2d(usampler, vec2(vcoord.s, vcoord.t)); 20
11.4 Wyświetlenie Przed rozpoczęciem wyświetlania musimy powiązać dane programu z danymi shaderów: shaderprogram.coord = gl.getattriblocation(shaderprogram, "coord"); gl.enablevertexattribarray(shaderprogram.coord); shaderprogram.sampleruniform = gl.getuniformlocation(shaderprogram, "usampler"); Wyświetlanie zaczynamy od przekazania przekazania danych z programu do vertex shadera, by następnie aktywować nakładaną teksturę i dokonać końcowego renderingu: function draw() { gl.bindbuffer(gl.array_buffer, cubetexturebuffer); gl.vertexattribpointer(shaderprogram.coord, cubetexturebuffer.itemsize, gl.float, false, 0, 0); gl.activetexture(gl.texture0); gl.bindtexture(gl.texture_2d, texture); gl.uniform1i(shaderprogram.sampleruniform, 0); gl.drawelements(gl.triangles, cubeindexbuffer.numitems, gl.unsigned_short, 0); Rysunek 6: Efekt końcowy: sześcian oteksturowany 21
12 Obsługa klawiatury 12.1 Wprowadzenie Obsługa klawiatury jest niezwykle łatwa i opera się o zdarzenia przeglądarki/obiektowego dokumentu wyzwalane podczas wciśnięcia bądź puszczenia klawisza. Aktualna informacja o stanie klawisza (wciśnięty / puszczony) jest przechowywana w tablicy currentkeys, której stan jest zmieniany przy pomocy funkcji keydownhandle() oraz keyuphandle() wywoływanych wraz z wywołaniem wspomnianych wcześniej zdarzeń. Wszystko to opakowujemy w funkcje inicjalizującą klawiaturę: var currentkeys = {; function initializekeyboard() { document.onkeydown = keydownhandle; document.onkeyup = keyuphandle; function keydownhandle(event) { currentkeys[event.keycode] = true; function keyuphandle(event) { currentkeys[event.keycode] = false; 12.2 Obsługa klawiszy Pozostało nam tylko obsłużyć odpowiedni klawisze. Zaczynamy od zdefiniowania zmiennej zoom przechowującej aktualne oddalenie sześcianiu od ekranu oraz od funkcji wykonującej na tej zmiennej modyfikacje w zależności od wciśniętego klawisza: function handlekeys() { if (currentkeys[38]) { // Strzałka w górę zoom -= 0.05; if (currentkeys[40]) { // Strzałka w dół zoom += 0.05; I tak strzałka w górę oddali sześcian, a strzałka w dół przybliży go do nas. Funkcję tą wywołujemy funkcji tick() tuż przed rysowaniem, tak 22
aby aktualna scena mogła pozostać aktualne w stosunku do ustawionego przybliżenia: function tick() { handlekeys(); begindraw(); draw(); Na koniec musimy zaaplikować zmienną zoom do operacji translacji sześcianu. Dokonujemy tego w funkcji draw(): draw() { mat4.translate(worldmat, [0.0, 0.0, -7.0+zoom]); Rysunek 7: Efekt końcowy: sześcian przybliżony 13 Przeźroczystość Przeźroczystość jest łatwa do osiągnięcia. Należy wyłączyć bufor głębi (wkońcu nie interesuje nas obiekt najbardziej na wierzchu ale również i tan za, gdyż z racji przeźroczystości pierwszego obiektu będzie widoczny). Następnie włączyć funkcje mieszania (ang. blending) i odpowiednio ustawić: function initialize(canvasname) { gl.disable(gl.depth_test); gl.enable(gl.blend); 23
gl.blendfunc(gl.src_alpha, gl.one); Kolejny krok to modyfikacja pixel shadera tak aby wynikowy kanał alpha (przeźroczystości) dla fragmentu tekstury był przemnożony przez odpowiedni współczynnik prześwitywania (u nas 0.5 - półprzeźroczystość): precision mediump float; varying vec2 vcoord; uniform float ualpha; uniform sampler2d usampler; void main(void) { vec4 texturecolor = texture2d(usampler, vec2(vcoord.s, vcoord.t)); gl_fragcolor = vec4(texturecolor.rgb, texturecolor.a * ualpha); Następnie musimy obsłużyć nowy parametr w shaderze. W inicjalizacji shaderów pobieramy dla niego uniform, a w funkcji rysującej ustawiamy na stałe wartość 0.5: shaderprogram.alphauniform = gl.getuniformlocation(shaderprogram, "ualpha"); Oraz: gl.uniform1f(shaderprogram.alphauniform, 0.5); Rysunek 8: Efekt końcowy: sześcian przeźroczysty 24
Spis treści 1 Wstęp 1 2 Uruchomienie WebGL 2 2.1 Wstęp............................... 2 2.2 Wybór przeglądarki....................... 2 2.3 Włączenie WebGL w przeglądarce............... 2 3 Integracja z HTML5 3 3.1 Po stronie HTML5........................ 3 3.2 Po stronie WebGL........................ 3 4 Pusta scena 4 4.1 Inicjalizacja............................ 4 4.2 Rysowanie............................. 4 5 Szkielet fruitgl (cz.1) 5 5.1 Wstęp............................... 5 5.2 Kod................................ 5 5.3 Wykorzystanie fruitgl...................... 6 6 Pierwszy obiekt (trójkąt) 7 6.1 Vertex Shader........................... 7 6.2 Pixel Shader........................... 8 6.3 Ładowanie shaderów....................... 8 6.4 Bufor wierzchołków........................ 9 6.5 Wyświetlenie........................... 9 7 Trzeci wymiar 10 7.1 Macierze.............................. 11 7.2 Efekt końcowy.......................... 12 8 Trochę kolorków 12 8.1 Jednostki cieniujące....................... 13 8.2 Bufor kolorów........................... 13 8.3 Wyświetlenie........................... 14 9 Animacja 14 9.1 Podstawy............................. 14 9.2 Stos macierzy........................... 15 9.3 Wyświetlenie........................... 16 25
10 Sześcian 16 10.1 Bufory danych.......................... 17 10.2 Wyświetlenie........................... 17 11 Teksturowanie 18 11.1 Przygotowanie tekstury..................... 18 11.2 Przygotowanie współrzędnych.................. 19 11.3 Modyfikacja shaderów...................... 20 11.4 Wyświetlenie........................... 21 12 Obsługa klawiatury 22 12.1 Wprowadzenie.......................... 22 12.2 Obsługa klawiszy......................... 22 13 Przeźroczystość 23 26