Piotr Siekański Wydział Mechatroniki PW Wprowadzenie do WebGL i Three.js Wprowadzenie do JavaScript...2 Co to jest WebGL?...3 Konfiguracja systemu...3 Debugowanie w WebGL...3 Biblioteka Three.js...3 Rysujemy pierwszą scenę...3 HTML...3 Początek pracy z biblioteką Three.js...4 Dodawanie brył...5 Dodawanie tekstur...6 Animacja...6 Światła i podłoże...7 Obsługa klawiatury...8 Obsługa kontrolek HTML...9 Zadania...10 1
Wprowadzenie do JavaScript JavaScript jest językiem skryptowym, zbliżonym do języka C, stworzonym przez firmę Netscape i stosowanym głównie do obsługi interakcji na stronach internetowych, takich jak np. reagowanie na kliknięcia, wysyłanie formularzy czy dynamiczne generowanie treści strony w zależności od decyzji użytkownika. Tworzenie grafiki 3D z wykorzystaniem WebGL wymaga korzystania z języka JavaScript do komunikacji z przeglądarką. Tabela 1. prezentuje podstawowe różnice pomiędzy językiem C/C++ a JavaScript w zakresie koniecznym do rozumienia kodu i pisania apletów WebGL. Nazwa zmiennej logicznej Nazwa zmiennej tekstowej Deklarowanie zmiennych C/C++ bool string Zawsze wymaga podania typu. int myint; JavaScript boolean String Nie wymaga podania typu zmiennej. Wszystkie zmienne można zadeklarować słowem var. Można określić typ używając dwukropka. var myint = 3; //int przez wartość var myfloat : float; var myvariable; // typ przypisywany dynamicznie wolniejsze wykonywanie kodu. Zmienna znakowa char mychar = 'a'; Nie istnieje, trzeba odwoływać się bezpośrednio. var mychar = a [0]; Słowo kluczowe new przy tworzeniu obiektów Deklaracja funkcji Konieczne Zwracany_typ nazwa_funkcji (parametry) Nieobowiązkowe Tabela 1: Podstawowe różnice pomiędzy C/C++ a JavaScript function nazwa_funkcji (parametry) Często wykorzystywaną właściwością JS jest możliwość przypisywania nowych pól i metod do obiektu bez ich wcześniejszej deklaracji np.: var osoba1 = { imię: Ania, wzrost: 175, wyswietlimie : function(){ alert(this.imie); // funkcja alert wyświetla okno z danymi podanymi // jako argument. osoba1.wyswietlimie(); //zwróci: Ania osoba1.waga = 50; //dodanie nowej właściwości osoba1.wypisz = function() {alert('wzrost: '+this.wzrost + ', waga: '+this.waga) //dodanie nowej metody osoba1.wypisz();//zwróci: Wzrost: 175, waga: 50 2
Co to jest WebGL? WebGL jest wieloplatformowym standardem niskopoziomowej grafiki 3D bazującym na API OpenGL ES 2.0 wariancie standardu OpenGL przeznaczonego dla urządzeń mobilnych. Za rozwój WebGL odpowiedzialna jest grupa Khronos (http://www.khronos.org/webgl). WebGL wymaga karty graficznej obsługującej OpenGL 2.0 i kompatybilnej przeglądarki. Obecnie wszystkie najpopularniejsze przeglądarki w najnowszych wersjach obsługują ten standard. Na potrzeby laboratorium zalecane jest korzystanie z przeglądarki Mozilla Firefox. Konfiguracja systemu Pod adresem http://get.webgl.org/ można sprawdzić, czy dana przeglądarka obsługuje WebGL. Jeśli nie, należy zainstalować najnowsze sterowniki do karty graficznej oraz włączyć obsługę WebGL w przeglądarce. Debugowanie w WebGL Można włączyć konsolę WWW (Firefox Dla twórców witryn Konsola WWW lub poleceniem Ctrl + Shift + K). Tu będą pojawiać się wszystkie informacje o błędach w wykonaniu kodu. Biblioteka Three.js Zrozumienie zasad działania WebGL wymaga wiele wysiłku, a kod pisany przy jego użyciu jest bardzo długi i skomplikowany. Żeby temu zaradzić, stworzono bibliotekę three.js (http://threejs.org), dzięki której możliwe jest znaczne przyspieszenie pisania kodu. Wspomniana biblioteka jest stale rozwijanym projektem open-source, dostępnym bezpłatnie na licencji MIT. Rysujemy pierwszą scenę Aplety WebGL można pisać w dowolnym edytorze tekstu, wystarczy nawet windowsowy Notatnik. Jednak na potrzeby laboratorium sugerowane jest korzystanie z Notepad++ lub NetBeans IDE. HTML Plik szkielet_threejs.html zawiera podstawowy schemat HTML umożliwiający rozpoczęcie pracy z three.js. Znaczenie kodu jest oczywiste: najpierw ustalany jest tytuł strony, później zdefiniowany jest zestaw znaków umożliwiający korzystanie z polskich czcionek: <!doctype html> <html> <head> <title>wprowadzenie do three.js</title> <meta charset="iso-8859-2"> </head> 3
Następnie definiowany jest styl strony bez marginesów i ładowana jest biblioteka three.js, z katalogu js. Biblioteka dodana jest jako skrypt JS, jedynym parametrem, który należy podać, jest jej lokalizacja - w tym przypadku podkatalog nazwany js. <body style="margin: 0;"> <script src="js/three.js"></script> Cały kod pisany w dalszej części, zwany skryptem, będzie znajdował się wewnątrz znaczników <script>. Obecnie skrypt jest pusty. <script> // Miejsce na kod sceny 3D </script> </body> </html> Początek pracy z biblioteką Three.js Każdy projekt budowany z wykorzystaniem three.js wymaga zdefiniowania trzech elementów: sceny kamery renderera Scena jest wirtualną przestrzenią, w której umieszcza się jej elementy składowe takie jak bryły, światła itp. Renderer jest silnikiem graficznym wykorzystywanym do pokazania sceny z perspektywy kamery. Dlatego najczęściej projekt rozpoczyna się od zdefiniowania tych trzech obiektów: var scena = new THREE.Scene(); var kamera = new THREE.PerspectiveCamera(45,window.innerWidth/window.innerHeight, 0.1, 1000); var renderer = new THREE.WebGLRenderer(); Nazwy scena, kamera, renderer są dowolnie nadanymi nazwami obiektów o podanych typach reprezentujących scenę, kamerę perspektywiczną i silnik renderujący. Parametry kamery to odpowiednio: fov pole widzenia (ang. field of view) kąt, który obejmuje oko. Najczęściej stosowana wartość mieści się w przedziale <45, 90 >. aspect współczynnik proporcji obrazu, najczęściej stosunek szerokości do wysokości płótna near obiekty bliższe niż wartość near nie będą renderowane far obiekty dalsze niż wartość far nie będą renderowane Warto zwrócić uwagę na zmienne window.innerwidth i window.innerheight, przechowujące rozmiary okna. Są one udostępniane przez przeglądarkę i nie wymagają wcześniejszej deklaracji. Three.js korzysta z następującego układu współrzędnych: oś x jest osią poziomą i skierowaną w prawą stronę ekranu, oś y jest osią pionową i skierowaną w górę ekranu, a oś z jest osią poziomą i skierowaną do obserwatora. Każdy nowy obiekt, także kamera, domyślnie znajduje się w punkcie (0,0,0) sceny. Tak ustawiona kamera nie będzie obejmować dodawanych obiektów, które będą tworzone w tym samym punkcie. Dlatego należy 4
przesunąć kamerę wzdłuż osi z: kamera.position.set(0.0, 0.0, 6.0); Każda ze współrzędnych obiektu dostępna jest także w formie pojedynczej współrzędnej ( x, y, z) dlatego identyczne przesunięcie kamery można uzyskać używając polecenia: kamera.position.z = 6.0; Następnie należy zdefiniować rozmiar renderera: renderer.setsize(window.innerwidth, window.innerheight); oraz domyślny kolor tła: renderer.setclearcolor(0x888888, 1); Drugi parametr odpowiada za przezroczystość. W tym przypadku wybrany został kolor szary (0x888888 jest to zapis szestnastkowy składowych RGB: R=88, G=88, B=88) i rysowanie nieprzezroczyste (1): Ostatnim krokiem jest dodanie renderera do obiektu body za pomocą metody renderer.domelement. DOM (Document Object Model) jest sposobem, w jaki JavaScript odwołuje się do zawartości strony, dlatego chcąc mieć dostęp do renderera, trzeba go dodać w sposób zrozumiały dla JavaScriptu. Dzięki temu zostanie utworzone płótno (ang. canvas), na którym będzie wyświetlać się renderowana scena. document.body.appendchild(renderer.domelement); Dalszą część skryptu powinny wypełnić definicje obiektów tworzących scenę i operacje dodania ich do sceny. Skrypt należy zakończyć funkcją renderującą, mającą w najprostszej wersji przykładową nazwę render i postać: var render = function () { requestanimationframe(render); renderer.render(scena, kamera); ; którą zaraz potem wywołujemy poprzez: render(); Najpierw renderer czeka, aż przeglądarka wywoła funkcję requestanimationframe(), która będzie wywoływana 60 razy na sekundę, chyba że wykonanie wszystkich instrukcji w funkcji render() będzie trwało dłużej niż 1/60 sekundy, wtedy kolejna klatka animacji będzie wyświetlona odpowiednio później, a szybkość odświeżania obrazu spadnie. Funkcja requestanimationframe() przyjmuje jako parametr odwołanie do funkcji, którą musi wywołać, dzięki czemu funkcja render() będzie wywoływana 60 razy na sekundę. Dodawanie brył Po zapisaniu pliku i uruchomieniu go w przeglądarce, zgodnie z tym, co dotychczas pokazano, wyświetli się pusta szara strona, ponieważ taki kolor tła płótna został ustawiony i nie ma żadnego elementu, który mógłby być narysowany. Każdy obiekt zdefiniowany z użyciem biblioteki three.js musi posiadać dwa elementy składowe: geometrię i materiał. Geometria odpowiada za jego kształt, a materiał za wygląd. Jako pierwszy przykładowy obiekt zostanie zdefiniowana prosta piłka do koszykówki. Do stworzenia obiektu piłki potrzebny będzie kształt sfery, którego nie trzeba definiować, ponieważ biblioteka three.js zawiera potrzebną funkcję z trzema parametrami odpowiadającym za promień tworzonej sfery oraz ilość południków i 5
równoleżników: var geometriasfery = new THREE.SphereGeometry(1, 25, 25); Istnieje wiele predefiniowanych brył, pełna lista znajduje się w dokumentacji (http://threejs.org/docs/) w zakładce Extras / Geometries. Następnie należy zdefiniować materiał, z którego będzie wykonana piłka. Najpopularniejsze materiały w three.js to: Basic, który jest niewrażliwy na światło Lambert, który implementuje cieniowanie Gourauda Phong, który implementuje cieniowanie Phonga Na razie w budowanej scenie nie ma żadnych świateł, więc wykorzystany zostanie materiał typu Basic: var pilkamaterial = new THREE.MeshBasicMaterial({color: 0xff0000); Tworzony materiał ma kolor czerwony (0xff0000). Następnie należy stworzyć siatkę piłki, która połączy jej geometrię i materiał. var pilka = new THREE.Mesh(geometriaSfery, pilkamaterial); Tak stworzony obiekt typu mesh należy dodać do sceny: scena.add(pilka); Po zapisaniu pliku i uruchomieniu go w przeglądarce powinno wyświetlić się czerwone koło będące rzutem sfery na płaszczyznę ekranu. Dodawanie tekstur Czerwona piłka nie przypomina piłki do koszykówki, dlatego należy nałożyć na nią teksturę. Plik graficzny tekstury należy najpierw zaimportować i zapisać w odpowiedniej zmiennej. Poniższe polecenie zaimportuje plik BasketballColor.jpg znajdujący się w tym samym katalogu i wskaźnik do niego zapisze w zmiennej nazwanej pilkatextura: var pilkatextura = new THREE.ImageUtils.loadTexture('BasketballColor.jpg'); Następnie należy dodać plik tekstury do materiału piłki. W tym celu można zamienić wcześniej wpisane polecenie: var pilkamaterial = new THREE.MeshBasicMaterial({color: 0xff0000); na var pilkamaterial = new THREE.MeshBasicMaterial({map: pilkatextura); Po zapisaniu i uruchomieniu pliku w przeglądarce powinna wyświetlić się piłka do koszykówki. Animacja Każdy obiekt posiada właściwości transform i rotation, które umożliwiają modyfikowanie położenia obiektu w przestrzeni. Dodanie instrukcji modyfikujących powyższe właściwości w funkcji render() umożliwia ich cykliczne wykonywanie i w efekcie tworzenie animacji, np. wpisanie instrukcji: 6
pilka.rotation.y+=0.01; pilka.rotation.x+=0.01; będzie powodować obrót piłki wokół osi x i y, a dodanie ciągu instrukcji: if (pilka.position.y<0) { kierunekruchu = 1; if (pilka.position.y>1) { kierunekruchu = -1; pilka.position.y+=0.01*kierunekruchu; doda do sceny efekt kozłowania piłki. Powyższe instrukcje korzystają ze zmiennej nielokalnej nazwanej kierunekruchu, którą należy zadeklarować wcześniej w obszarze skryptu, poza funkcją render: var kierunekruchu = 1; Światła i podłoże Ruch piłki w przestrzeni sprawia wrażenie dość chaotycznego, dlatego jako następny obiekt zostanie dodana płaszczyzna podłoża, od której piłka będzie się odbijać. W tym celu należy dodać nową geometrię, nowy materiał oraz nowy obiekt podłoża: var geometriaplaszczyzny = new THREE.PlaneGeometry(1,1); var podlogamaterial = new THREE.MeshLambertMaterial({color: 0x0000ff ); var podloga = new THREE.Mesh(geometriaPlaszczyzny, podlogamaterial); scena.add(podloga); Geometria płaszczyzny przyjmuje dwa argumenty odpowiadające za jej długość i szerokość. Po zapisaniu i uruchomieniu apletu w przeglądarce wyświetli się mały kwadrat za poruszającą się piłką. Kwadrat jest koloru czarnego, chociaż jego deklarowanym kolorem był niebieski (0x0000ff). Dzieje się tak, ponieważ płaszczyzna korzysta z materiału innego typu: basic dla piłki, lambert dla płaszczyzny podłoża. Materiał typu lambertowskiego wymaga zdefiniowania światła w scenie. Należy dodać nowe światło punktowe: var swiatlopunktowe = new THREE.PointLight( 0xffffff, 2, 15 ); scena.add(swiatlopunktowe); swiatlopunktowe.position.set(2.5, 1.0, 1.0); Parametry światła punktowego odpowiadają kolejno za jego kolor, intensywność oraz odległość, po którym jego energia spada do zera. Jeżeli wartość tego parametru wynosi 0, energia światła punktowego jest stała. Teraz płaszczyzna ma już odpowiedni, niebieski kolor. Kolejnym krokiem będzie jej ustawienie prostopadle tak, żeby stworzyć efekt odbicia piłki. W tym celu można dopisać polecenie: podloga.rotation.x=-math.pi/2; W tym ustawieniu płaszczyzna znajduje się dokładnie równolegle do osi kamery, więc jest niewidoczna, dlatego można także obrócić i oddalić kamerę: kamera.rotation.x = -Math.PI/6; kamera.position.set(0, 4, 10); Po wykonaniu tych instrukcji płaszczyzna jest za mała, dlatego przy użyciu polecenia scale zostanie powiększona dziesięciokrotnie w każdym kierunku: podloga.scale.set( 10,10,10); 7
Teraz piłka wpada w płaszczyznę, dlatego należy przesunąć płaszczyznę podłoża o wartość promienia piłki w kierunku ujemnych wartości osi y: podloga.position.y-=1; Zadeklarowane wcześniej światło punktowe znajduje się w punkcie (2.5, 1.0, 1.0), co można wywnioskować patrząc na płaszczyznę podstawy, która jest jaśniejsza z prawej strony. Chcąc jednak zobaczyć punkt, z którego pada światło, należy zdefiniować nowy obiekt o nazwie czasteczka, którego pozycja będzie odpowiadać pozycji źródła światła. Nie trzeba definiować jego geometrii, ponieważ można skorzystać z geometrii piłki i ją przeskalować: var czasteczka = new THREE.Mesh( geometriasfery, new THREE.MeshBasicMaterial({color: 0xffffff)); czasteczka.scale.set(0.05, 0.05, 0.05); scena.add(czasteczka); czasteczka.position = swiatlopunktowe.position; Warta wyjaśnienia jest ostatnia linijka, w niej zostaje przypisany do wektora pozycji tworzonej cząsteczki wektor pozycji punktowego źródła światła. Po stworzeniu widzialnej reprezentacji źródła światła, można je także animować. W tym celu w funkcji renderującej należy dodać zmienną przechowującą aktualny czas, na podstawie którego będzie wyliczana pozycja światła: var timer = Date.now() * 0.0015; czasteczka.position.x = 3*Math.sin( timer ); czasteczka.position.y = 2; czasteczka.position.z = 3*Math.cos( timer ); Prezentowany sposób animacji źródła światła z wykorzystaniem aktualnego czasu jest alternatywą dla prostej inkrementacji lub dekrementacji wartości pozycji i rotacji obiektów, daje on większą kontrolę nad ruchem obiektów. Obsługa klawiatury W prezentowanym przykładzie brakuje możliwości interakcji z użytkownikiem. Dlatego dodana zostanie obsługa klawiatury. Do kodu sceny należy dodać linijkę: document.addeventlistener("keydown", wcisnietoklawisz, false); Powyższe polecenie powoduje wywołanie funkcji nazwanej wcisnietoklawisz, co następuje po naciśnięciu dowolnego przycisku na klawiaturze. Funkcja jest na razie niezdefiniowana, dlatego należy ją zdefiniować: function wcisnietoklawisz(event){ var kodklawisza = event.which; switch (kodklawisza) { case(32): kierunekruchu *=-1; break; Najpierw następuje sprawdzenie, który klawisz został wciśnięty - służy do tego polecenie event.which, które zwraca liczbę odpowiadającą wciśniętemu przyciskowi. Pod adresem http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes znajduje się tabela zawierająca klawisze i ich kody. Po sprawdzeniu, który z nich został wciśnięty, następuje instrukcja switch, w której wpisana jest reakcja na naciśnięcie wybranych klawiszy. W tym przypadku naciśnięcie spacji powoduje odwrócenie 8
kierunku ruchu piłki. Obsługa kontrolek HTML Aplet w przeglądarce wygodniej obsługuje się za pomocą myszki niż klawiatury, dlatego kolejnym dodanym elementem będą standardowe kontrolki HTML. Do najpopularniejszych należą: pola wyboru (ang. checkbox) wykluczające się przyciski wyboru (ang. radio buttons) suwaki (ang. range) Każdy z nich dodaje się nie w obrębie skryptu JS, ale wewnątrz sekcji body HTML, który znajduje się na początku kodu. Tuż po linijce <body style="margin: 0;"> należy dodać następujące polecenia tworzące kolejno pole wyboru, suwak i dwa wykluczające się przyciski wyboru: <input type="checkbox" id="swiatlo" > światło <input type="range" id="predkoscswiatla" min ="0.1" max="2" step ="0.1"/> Prędkość ruchu światła <input type="radio" id="zgodnie" name="ruchswiatla" checked="checked"> Przeciwnie do ruchu wskazówek zegara <input type="radio" id="przeciwnie" name="ruchswiatla"> Zgodnie z ruchem wskazówek zegara Każdy z tych elementów musi mieć zdefiniowany typ (type) i identyfikator służący do pobierania wartości elementu (id). W przypadku suwaków konieczne jest tez zdefiniowanie wartości minimalnej (min), maksymalnej (max) oraz kroku (step). W przypadku wykluczających się przycisków wyboru, muszą mieć one zdefiniowaną nazwę (name), która musi być identyczna dla całej grupy wykluczających się przycisków wyboru. Do każdego pola wyboru i przycisku wyboru można dodać argument checked, który powoduje domyślne zaznaczenie tej kontrolki przy starcie apletu. W przypadku suwaków domyślną wartość można zdefiniować dodając argument value. Aby odczytać wartości kontrolek HTML, używa się polecenia document.getelementbyid("id_elementu").checked w przypadku kontrolek zawierających wartości logiczne (np. pola wyboru) lub document.getelementbyid("id_elementu").value w przypadku kontrolek zawierających wartości liczbowe (np. suwaki). Wartość każdej kontrolki można przechowywać w zmiennej. W tym celu należy w obszarze skryptu JavaScript zdefiniować cztery nowe zmienne: var swiatloswieci; var ruchzgodny; var ruchprzeciwny; var predkoscswiatla; które następnie wewnątrz funkcji renderującej będą przyjmować wartości odczytane z kontrolek: swiatloswieci = document.getelementbyid("swiatlo").checked; ruchzgodny = document.getelementbyid("zgodnie").checked; ruchprzeciwny = document.getelementbyid("przeciwnie").checked; predkoscswiatla = document.getelementbyid("predkoscswiatla").value; 9
Odczytane wartości zostaną wykorzystane do sterowania apletem. Najpierw należy dodać możliwość włączania i wyłączania światła. W funkcji renderującej po pobraniu wartości z kontrolek należy dopisać: if (swiatloswieci == true) { swiatlopunktowe.intensity = 2; else { swiatlopunktowe.intensity = 0; Działanie tego fragmentu kodu jest oczywiste: jeżeli kontrolka jest zaznaczona (ma wartość true), to światło świeci z intensywnością 2, w przeciwnym wypadku światło ma intensywność równą zero, czyli nie świeci. Następnie należy dodać obsługę prędkości ruchu światła i jego kierunku. Należy zmienić fragment kodu odpowiadającego za ruch światła z: czasteczka.position.x = 3* Math.sin( timer ); czasteczka.position.y = 2; czasteczka.position.z = 3* Math.cos( timer ); na: czasteczka.position.x = 3* Math.sin( timer * predkoscswiatla ); czasteczka.position.y = 2; if (ruchzgodny==true) { czasteczka.position.z = 3* Math.cos( timer * predkoscswiatla ); else if (ruchprzeciwny==true) { czasteczka.position.z = -3* Math.cos( timer * predkoscswiatla ); Wartość pobranej zmiennej predkoscswiatla zmienia wartość argumentu funkcji trygonometrycznych używanych do wyliczenia aktualnej pozycji cząsteczki, a dodanie instrukcji warunkowych pozwala na sterowanie jej kierunkiem ruchu. Zadania 1. Nałożyć dowolną, znalezioną w Internecie teksturę na podłoże. 1. Zamienić światło białe na trzy źródła światła odpowiednio czerwonego, zielonego i niebieskiego poruszające się niezależnie od siebie i kontrolowane za pomocą niezależnych kontrolek. 2. Wykorzystując tablicę dodać kilka nowych piłek do sceny. 3. Korzystając z funkcji Math.random() zmienić wysokość kozła każdej z piłek. 10