18
Do tej pory aby wyświetlić obiekt musieliśmy stworzyć odpowiednie bufory, wypełnić je danymi o wierzchołkach, za pomocą szeregu funkcji wczytać teksturę z pliku i dopiero mogliśmy przejść do skomplikowanego rysowania w funkcji drawScene. Generalnie cały kod zajmował dużo miejsca i był niepraktyczny. Dzisiaj napiszemy klasę Model, dzięki której aby zainicjować oraz narysować dowolny obiekt na scenie wystarczą nam zaledwie dwie linijki kodu. Zapraszam do lektury!
Poza klasą Model stworzymy również prostą scenę z wieloma obiektami aby pokazać praktyczne użycie nowych narzędzi. Efekt całej aplikacji zobaczyć można na rysunku poniżej, na żywo można go obejrzeć na tej stronie.

Kod aplikacji znajdziecie tutaj. Proponuję go pobrać i śledzić w trakcie czytania lekcji. Możecie również pobrać samą klasę Model i używać jej w swoich aplikacjach.
Poziom trudności dzisiajszej lekcji jest dość zaawansowany, jednak dokładne zrozumienie działania każdej funkcji nie jest konieczne. Najważniejsze abyśmy potrafili się nimi swobodnie posługiwać, ponieważ będą wykorzystywane przy tworzeniu większości aplikacji. Zacznijmy więc.
Główny kod znajduje się oczywiście w pliku index.html, jednak narzędzia, które dzisiaj stworzymy, umieścimy w osobnym pliku. Nazwiemy go WGLtools.js. W pliku głównym umieszczamy adres do skryptu zewnetrznego:
<script type="application/x-javascript" src="WGLtools.js"></script>
Otwieramy plik i tworzymy klasę Model
Staramy się pisać obiektowo na tyle, na ile jest to możliwe w języku javascript. Choć jest on językiem obiektowym, nie pozwala na typowe programowanie oparte na hierarchicznym układzie klas. Mimo to możemy pisać funkcje, które będziemy traktowali jak klasy. Tworzyli będziemy później obiekty takich funkcji i pracowali na nich jak na obiektach typowych klas. Więcej o metodach programowania obiektowego w języku javascript powiemy sobie na jednej z kolejnych lekcji.
function Model()
{
this.Texture = null;
this.Vertices = null;
this.Tex_coords = null;
this.Indices = null;
this.init = Model_init;
this.display = Model_display;
this.load_texture = Model_load_texture;
}
Nasza klasa Model posiada 4 zmienne oraz 3 metody. Pierwsza zmienna – Texture będzie przechowywała obiekt tekstury. Druga zmienna – Vertices będzie zawierała bufor ze współrzędnymi wierzchołków. Kolejna zmienna – Tex_coords – bufor z koordynatami tekstur. Ostatnia zmienna – Indices – bufor z indeksami wierzchołków. Dodatkowo klasa jest wyposażona w 3 metody: init, display oraz load_texture.
Zacznijmy od ostatniej w kolejności – metody load_texture. Jak można się domyślić służy ona do wczytania tekstury z pliku. Spójrzmy na nią:
function Model_load_texture(adres,mag_filter,min_filter)
{
Funkcja przyjmuje 3 parametry: adres pliku z grafiką, sposób filtrowania tekstury w przypadku jej powiększania przy nakładaniu na obiekt, oraz sposób filtrowania w przypadku pomniejszania przy nakładaniu.
var Texture = gl.createTexture(); var tempImage; tempImage = new Image();
Tworzymy obiekt tekstury oraz obiekt obrazu.
tempImage.onload = function()
{
Definiujemy funkcję dla zdarzenia onload obiektu obrazu. Wywołana ona będzie, gdy cały obraz załaduje się z pliku
gl.enable(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, Texture); gl.texImage2D(gl.TEXTURE_2D, 0, tempImage);
Bindujemy nasz obiekt tekstury, czyli ustawiamy go jako bieżący oraz określamy źródło grafiki dla tekstury, w tym przypadku obiekt tempImage.
Dochodzimy do filtrowania. Zanim omówię działanie kolejnych operacji, powiem krótko na czym polega owo filtrowanie. Gdy nakładamy teksturę na obiekt mogą się zdarzyć dwa przypadki: obiekt może być mniejszy od rozdzielczyści tekstury lub większy od niej. Filtrowanie określa sposób doboru teksela (piksela tekstury) dla każdego piksela teksturowanego obiektu. Definiujemy je za pomocą funkcji gl.texParameteri, która przyjmuje 3 parametry. Pierwszy określa rodzaj stosowanych tekstur – w naszym przypadku gl.TEXTURE_2D, ponieważ używamy tekstur dwuwymiarowych. Drugi parametr określa czy definiujemy filtrowanie dla powiększania czy dla pomniejszania. W pierwszym przypadku przyjmuje on wartość gl.TEXTURE_MAG_FILTER, natomiast w drugim gl.TEXTURE_MIN_FILTER.
Jeśli obiekt jest większy od rozdzielczości tekstury, trzeba jakoś wypełnić brakujące piksele. Możliwości są dwie. Można pobrać wartość piksela tekstury położonego najbliżej pustego piksela na obiekcie lub zastosować liniową interpolację najbliższych pikseli tekstury. Metoda najbliższego piksela jest szybsza, ale mniej efektowna. Aby ją włączyć, jako ostatni parametr funkcji gl.texParameteri podajemy gl.NEAREST. Liniowa interpolacja wymaga większej ilości obliczeń, ale efekt jest bardziej przyjazny dla oka. Aktywujemy ją za pomocą parametru gl.LINEAR.
W przypadku, gdy rozdzielczość tekstury jest większa od obiektu sytuacja wygląda nieco inaczej. Pikseli obiektu jest mniej niż tekseli, więc trzeba jakoś określić jaki teksel być nałożony na dany piksel obiektu. I tak jak poprzednio możemy wybrać pierwszy najbliższy teksel podając parametr gl.NEAREST, lub zastosować interpolację najbliższych tekseli podając gl.LINEAR.
Jednak niezależnie od tego, którą opcje wybierzemy, jeśli obiekt będzie dużo mniejszy od tekstury, będą się na nim pojawiały dziwne wzorki (szczególnie podczas przybliżania i oddalania). Wynika to z wykorzystania tylko części tekseli przy teksturowaniu obiektu. Aby temu zapobiec stosuje się mipmapy.
Mipmapy są powszechnie znanym i stosowanym mechanizmem pozwalającym kontrolować poziom szczegółowości tekstur. Polega on na stworzeniu szeregu tekstur o coraz to mniejszych rozdzielczościach od nominalnej, aż do uzyskania tekstury składającej się z jednego piksela. Następnie mechanizm ten dobiera odpowiednią mipmapę zależnie od aktualnej rozdzielczości obiektu. Jeśli obiekt znajduje się dalej od obserwatora, nałożona będzie mipmapa o mniejszej szczegółowości, jeśli bliżej – o większej.
Decydując się na stosowanie mipmap mamy do wyboru cztery rodzaje filtrowania:
Przy wyborze filtra trzeba znaleźć równowagę pomiędzy wydajnością a efektem. Filtry stosujące metodę interpolacji wartości są ładniejsze, ale bardziej obciążają sprzęt.
Ok. wróćmy do funkcji ładującej tekstury. Poza adresem pliku przyjmuje ona 2 parametry określające właśnie rodzaj filtrowania. Pierwszy dotyczy opcji gl.TEXTURE_MAG_FILTER, a drugi gl.TEXTURE_MIN_FILTER.
if( !mag_filter ) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
Jeśli nie podamy parametru mag_filter, lub podamy zero jako jego wartość, dla opcji gl.TEXTURE_MAG_FILTER domyślnie zastosowany będzie filtr gl.LINEAR.
else gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, mag_filter);
W przeciwnym wypadku wartość zmiennej zostanie przekazana do funkcji gl.texParameteri.
if( !min_filter ) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
Podobnie jest w przypadku opcji gl.TEXTURE_MIN_FILTER. Jeśli nie podamy parametru min_filter lub podamy 0, domyślnie zastosowany będzie filtr gl.LINEAR.
else
{
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, min_filter);
Jeśli natomiast podamy konkretną wartość, zostanie ona przekazana do funkcji gl.texParameteri.
if( min_filter==gl.LINEAR_MIPMAP_NEAREST || min_filter==gl.LINEAR_MIPMAP_LINEAR || min_filter==gl.NEAREST_MIPMAP_NEAREST || min_filter==gl.NEAREST_MIPMAP_LINEAR )
gl.generateMipmap(gl.TEXTURE_2D);
}
Dodatkowo jeśli wybrany będzie jeden z czterech filtrów wykorzystujący mechanizm mipmap, wywołujemy funkcję gl.generateMipmap, która wygeneruje szereg mipmap o coraz to mniejszych rozdzielczościach.
gl.bindTexture(gl.TEXTURE_2D, null); }
Dla bezpieczeństwa informujemy WebGL, że żadna tekstura nie jest ustawiona jako aktywna.
tempImage.src = adres; this.Texture = Texture; }
I na koniec ładujemy grafikę z pliku oraz przekazujemy obiekt tekstury do zmiennej Texture naszej klasy Model.
Omówimy teraz kolejną metodę – init. Podpięta jest pod nią funkcja Model_init.
function Model_init(tex,mag_filter,min_filter,tab1,tab2,tab3)
{
Funkcja przyjmuje 5 parametrów charakteryzujących obiekt:
Przejdźmy do działania funkcji.
if( tex ) this.load_texture(tex,mag_filter,min_filter);
Najpierw wczytywana jest tekstura za pomocą opisanej wcześniej metody load_texture.
if( tab1 )
{
this.Vertices = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.Vertices);
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(tab1), gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
this.Vertices.itemSize = 3;
this.Vertices.numItems = tab1.length/3;
}
Jeśli parametr tab1 zawiera tablicę, na jej podstawie tworzony jest bufor ze współrzędnymi wierzchołków.
if( tab2 )
{
this.Tex_coords = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.Tex_coords);
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(tab2), gl.STATIC_DRAW);
gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
this.Tex_coords.itemSize = 2;
this.Tex_coords.numItems = tab2.length/2;
}
Jeśli parametr tab2 zawiera tablicę, na jej podstawie tworzony jest bufor z koordynatami tekstur.
if( tab3 )
{
this.Indices = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.Indices);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new WebGLUnsignedShortArray(tab3), gl.STATIC_DRAW);
this.Indices.itemSize = 1;
this.Indices.numItems = tab3.length;
}
}
I na koniec jeśli parametr tab3 zawiera tablicę, na jej podstawie tworzony jest bufor z indeksami wierzchołków.
Jak widać metoda init służy do inicjalizacji obiektu. Za jej pośrednictwem wczytywana jest tekstura oraz tworzone są bufory z danymi wierzchołków. Po jej odpowiednim wywołaniu obiekt jest gotowy do narysowania. Będziemy go rysowali za pomocą metody display, pod którą podpięta jest funkcja Model_display. Spójrzmy jak ona działa.
function Model_display(mode)
{
Zmienna mode jest opcjonalnym parametrem funkcji określającym metodę wyświetlania prymitywów. Do wyboru mamy następujące prymitywy: gl.POINTS, gl.LINES, gl.LINE_LOOP, gl.LINE_STRIP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN. Jeśli tego parametru nie podamy, obiekt rysowany będzie domyślnie za pomocą pojedynczych trójkątów, czyli gl.TRIANGLES.
gl.bindBuffer(gl.ARRAY_BUFFER, this.Vertices); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
Najpierw bindujemy bufor zawierający współrzędne wierzchołków,
gl.bindBuffer(gl.ARRAY_BUFFER, this.Tex_coords); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
następnie bufor z koordynatami tekstury
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.Texture); gl.uniform1f(gl.getUniformLocation(shaderProgram, "uSampler"), 0);
oraz samą teksturę. Przechodzimy do rysowania obiektu. Metoda rysowania zależna jest od tego, czy stworzyliśmy bufor z indexami wierzchołków poprzez przekazanie do metody init odpowiedniej tablicy, czy też nie.
Jeżeli nie stworzyliśmy bufora z indeksami, bufory z koordynatami tekstur oraz ze współrzędnymi wierzchołków muszą zawierać osobno opis wierzchołków każdego trójkąta. Jeśli przykładowo naszym obiektem jest kwadrat, a podstawowym prymitywem trójkąt, bufory muszą zawierać opis sześciu, a nie czterech wierzchołków, ponieważ składa się on z dwóch trójkątów.
if( !this.Indices )
{
setMatrixUniforms();
if( !mode )
gl.drawArrays(gl.TRIANGLES, 0, this.Vertices.numItems);
else
gl.drawArrays(mode, 0, this.Vertices.numItems);
}
Obiekt rysowany jest za pomocą funkcji gl.drawArrays na podstawie zawartości aktywnych buforów za pomocą prymitywów podanych jako pierwszy jej parametr. Jeżeli tego parametru nie podaliśmy, obiekt rysowany jest za pomocą trójkatów, czyli za pomocą gl.TRIANGLES.
Jeśli stworzyliśmy bufor z indeksami postępujemy nieco inaczej.
else
{
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.Indices);
setMatrixUniforms();
if( !mode )
gl.drawElements(gl.TRIANGLES, this.Indices.numItems, gl.UNSIGNED_SHORT, 0);
else
gl.drawElements(mode, this.Indices.numItems, gl.UNSIGNED_SHORT, 0);
}
}
Na początku ustawiamy go jako aktywny. Obiekty w tym przypadku rysowane są za pomocą funkcji gl.drawElements, która w odróżnieniu od funkcji gl.drawArrays, pozwala na dowolny porządek przetwarzania wierzchołków, określony właśnie za pomocą indeksów. I tak jak ostatnio jeśli podaliśmy parametr określający rodzaj prymitywów, to jest on przekazywany do funkcji gl.drawElements, natomiast jeśli nie – obiekt domyślnie rysowany jest za pomocą metody gl.TRIANGLES.
Funkcja do wyświetlania obiektu zakończona, cała klasa też już jest gotowa. Jej kod jest na tyle elastyczny, że bez problemu będziemy mogli wprowadzać dalsze modyfikacje w zależności od potrzeb.
Przykładowy program
Aby pokazać działanie klasy Model stworzymy prosty program, który będzie wyświetlał na płótnie 40 obracających się sześcianów. Ich położenie na osi x oraz y będzie wybierane losowo za pomocą funkcji random. Dodatkowo nałożymy na nie 10 różnych tekstur.
Szkielet aplikacji oparty jest na kodzie tworzonym na poprzednich lekcjach. Omówimy tylko funkcje związane z zastosowaniem klasy Model.
function c_Cube()
{
//position
this.Position_x = 0;
this.Position_y = 0;
//rot
this.Rot_x = 0;
this.Rot_y = 0;
this.Rot_z = 0;
//rot speed
this.X_speed;
this.Y_speed;
this.Z_speed
}
Na początku tworzymy prostą klasę c_Cube, która będzie przechowywała dane naszych obiektów związane z położeniem, kątem oraz prędkością obrotu.
c_Cube.prototype = new Model;
Podpinamy pod jej prototyp naszą klasę Model, dzięki czemu dziedziczy po niej wszystkie metody oraz zmienne.
var Objects = new Array(); var Objects_amount = 40;
Tworzymy tablicę, która będzie przechowywała obiekty oraz zmienną Objects_amount określającą ich ilość. Dla przykładu stworzymy 40 sześcianów.
Wykorzystamy dane wierzchołków sześcianu z poprzedniej lekcji i umieścimy je w tablicach Tab1, Tab2 oraz Tab3.
Dodatkowo przygotowujemy 10 tekstur. Nazywamy je kolejno od 0.jpg do 9.jpg.
W funkcji WebGLStart tworzymy następującą pętlę wykonującą operacje inicjujące każdy obiekt
for(var i=0; i<Objects_amount; i++)
{
var tex_num = i % 10;
Na początku deklarujemy zmienną tex_num, określającą numer pliku, z którego załadujemy teksturę. Nadajemy wartość z zakresy 0 – 9.
Objects[i] = new c_Cube();
Tworzymy obiekt klasy c_Cube;
Objects[i].init(tex_num+".jpg",gl.LINEAR,gl.LINEAR_MIPMAP_LINEAR,Tab1,Tab2,Tab3);
Wywołujemy dla niego metodę init z następującymi parametrami: zmienna tex_num z rozszerzeniem JPG jako adres pliku z teksturą, filtr gl.LINEAR dla gl.TEXTURE_MAG_FILTER, filtr gl.LINEAR_MIPMAP_LINEAR dla gl.TEXTURE_MIN_FILTER, oraz trzy tablice z danymi wierzchołków sześcianu.
Objects[i].Position_x = Math.random()*30-15; Objects[i].Position_y = Math.random()*30-15; Objects[i].X_speed = Math.random()*100-50; Objects[i].Y_speed = Math.random()*100-50; Objects[i].Z_speed = Math.random()*100-50; }
Na koniec określamy losowe położenie obiektu na osi x oraz y z zakresu od -15 do 15 oraz losową prędkość obrotu wokół poszczególnych osi z zakresu od -50 do 50.
function animate()
{
for(var i=0; i<Objects_amount; i++)
{
Objects[i].Rot_x += Objects[i].X_speed * elapsed_s;
Objects[i].Rot_y += Objects[i].Y_speed * elapsed_s;
Objects[i].Rot_z += Objects[i].Z_speed * elapsed_s;
}
}
W funkcji animate zwiększamy kąty obrotu obiektów o iloczyn wartości prędkości ich obrotu i zmiany czasu.
mvTranslate([0.0, 0.0, -40]);
for(var i=0; i<Objects_amount; i++)
{
mvPushMatrix();
mvTranslate([Objects[i].Position_x, Objects[i].Position_y, 0.0]);
mvRotate(Objects[i].Rot_x, [1, 0, 0]);
mvRotate(Objects[i].Rot_y, [0, 1, 0]);
mvRotate(Objects[i].Rot_z, [0, 0, 1]);
Objects[i].display();
mvPopMatrix();
}
W funkcji drawScene wyświetlamy obiekty w odpowiednim miejscu za pomocą stworzonej dzisiaj metody display.
Uruchamiamy i wszystko działa zgodnie z planem. Efekt działania nie jest może oszałamiający, ale stworzyliśmy dzisiaj potężne narzędzie i zobaczyliśmy, jak można je wykorzystać. Zapraszam do eksperymentów. Kod aplikacji możecie pobrać tutaj.
W kolejnej lekcji zrobimy porządek w kodzie. Podzielimy go na poszczególne skrypty i umieścimy je w osobnych plikach. Dzięki temu kod aplikacji będzie bardziej czytelny i łatwiejszy w modyfikacji. Zapraszam.
Witam, bardzo fajny tutorial, tylko mam jakiś dziwny problem z teksturami, niestety obiekty są nioteksturowane, w poprzednim przykładzie byłby, natomiast tutaj już nie mam kartę ATI, ciekawe czy jest jakiś związek pomiędzy tym.
Hej.
W stworzonym programie tekstury wykorzystują mechanizm mipmap. Prawdopodobnie nie masz zainstalowanych najnowszych sterowników do karty graficznej lub używasz starszej wersji systemu Windows i dlatego nie możesz korzystać z tej funkcjonalności. Miałem podobny problem, pomogło gdy przesiadłem się z XP na Windows7 i zainstalowałem najnowsze stery.
Można również zrezygnować z tego mechanizmu. Właściwie w tak prostej aplikacji nie widać efektu działania mipmap. Aby to zrobić zamień linijkę
Objects[i].init(tex_num+”.jpg”,gl.LINEAR,gl.LINEAR_MIPMAP_LINEAR,Tab1,Tab2,Tab3);
na
Objects[i].init(tex_num+”.jpg”,gl.LINEAR,gl.LINEAR,Tab1,Tab2,Tab3);
Powinno pomóc
Świetna lekcja. Chyba najlepsza jak do tej pory.
Ale widzę chyba kilka rzeczy, które możnaby poprawić:
1. zamiast warunków (zmienna == undefined || zmienna == null) wystarczy napisać (!zmienna). A zamiast (zmienna != null && zmienna != 0) można napisać po prostu (zmienna)
2.
var Texture = gl.createTexture();
var Vertices_buffer = gl.createBuffer();
var Coord_buffer = gl.createBuffer();
var Indices_buffer = gl.createBuffer();
wg mnie (testowałem i działa) niepotrzebnie używasz tych dodatkowych zmiennych, które później i tak przypisujesz do
this.Texture, this.Vertices, this.Tex_coords, this.Indices. A można przecież od razu ich używać.
3. zamiast
for (i…) {
var tex_num = i;
while(tex_num>9)
tex_num-=10;
}
mozna napisac
for (i…)
var tex_num = i % 10;
Pozdrawiam
Zgadzam się z Tobą we wszystkich trzech przypadkach. Zapis, który zaproponowałeś jest bardziej przejrzysty. Dzięki za uwagi.
W przypadku tworzenia obiektu tekstury, bez wykorzystania zmiennej pośredniej metoda nie działa prawidłowo. Pewnie dlatego, że wykorzystana jest przy definicji uchwytu zdarzenia obiektu osobnej klasy.