13
Witam na kolejnej lekcji. Ostatnio nauczyliśmy się jak wyświetlić kwadrat z nałożoną teksturą, dzisiaj pójdziemy dalej. Tym razem stworzymy dwa w pełni trójwymiarowe obiekty – sześcian i piramidę, oraz wprowadzimy ruch na scenie.
Efekt końcowy będzie taki jak na rysunku poniżej, na żywo można go obejrzeć na tej stronie, jednak aby go zobaczyć trzeba mieć kompatybilną przeglądarkę. Jeśli takowej nie posiadacie, instrukcję instalacji znajdziecie w ustawieniach przeglądarki.

Kod aplikacji znajdziecie tutaj. Proponuję go pobrać i śledzić w trakcie czytania lekcji.
Kod aplikacji zawarty jest w pliku index.html. Tak jak ostatnio zaczniemy go omawiać od dołu. W sekcji body nic się nie zmienia. Jest tam umieszczone odpowiednio zwymiarowane płótno, oraz przy wystąpieniu zdarzenia onLoad, czyli po załadowaniu strony, wywoływana jest funkcja webGLStart. Przejdźmy do jej ciała. Widzimy 3 nowe linie kodu.
gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL);
Odpowiadają one za uruchomienie bufora głębokości zwanego również z-buforem. Co takiego robi ten bufor? Krótko mówiąc mierzy odległości obiektów od kamery. Uruchomiony w powyższej konfiguracji sprawia, że obiekty znajdujące się bliżej obserwatora przesłaniają obiekty będące w dalszej odległości. Gdybyśmy go nie uruchomili, obiekty przesłaniały by się wzajemnie w kolejności rysowania, niezależnie od położenia (czasem również taki efekt bywa przydatny, ale opowiem o tym przy innej okazji). W każdym razie przy tworzeniu większości scen uruchomienie tego bufora jest standardowa procedurą.
Kolejna zmiana dotyczy wywołania następującej funkcji:
setInterval(tick, 15);
Tym razem cyklicznie wywoływaną funkcją nie jest drawScene, lecz funkcja tick. A cóż takiego ona robi? Spójrzmy
function tick() {
timeTick();
animate();
drawScene();
}
W jej ciele zawarty jest zbiór wszystkich funkcji wykonywanych cyklicznie w głównej pętli programu. Tylko co umieszczamy w takiej pętli. Oczywiście funkcję drawScene, ponieważ scena rysowana jest w każdej klatce. Ale oprócz drawScene mamy również dwie nowe funkcje. Pierwsza – timeTick odpowiada za obliczanie czasu, jaki upłynął od wyświetlenia poprzedniej klatki. Czas ten potrzebny nam będzie do obliczeń fizycznych związanych z ruchem. Wszystkie te obliczenia wykonujemy w funkcji animate. Podsumowując funkcja tick jest sercem całej aplikacji, które uderza co 15 milisekund.
Przejdźmy teraz do ciała funkcji timeTick i zobaczmy, jak działa
var lastTime = 0;
var elapsed=0;
var elapsed_s=0;
function timeTick()
{
var timeNow = (new Date).getTime();
if (lastTime != 0)
{
elapsed = timeNow - lastTime;
elapsed_s=elapsed/1000;
}
lastTime = timeNow;
}
Na wstępie deklarowane są trzy zmienne. Pierwsza przechowuje ostatni odczyty zegara systemowego, druga czas, jaki minął od ostatniego wywołania funkcji w milisekundach, ostania czyli elapsed_s – ten sam czas wyrażony w sekundach. Przejdźmy do ciała funkcji. Najpierw zmienna timeNow pobiera aktualną wartość zegara systemowego. Jest to czas, jaki minął od 1 stycznia 1970 roku wyrażony w milisekundach. Jak możemy sobie wyobrazić, jest to wartość bardzo duża, ale dla nas istotna jest różnica między nią, a wartością odczytaną w poprzedniej klatce. Różnica ta przekazana jest do zmiennej elapsed. Dodatkowo obliczamy wartość zmiennej elapsed_s, która, jak już powiedziałem, przechowuje ten sam czas wyrażony w sekundach. Tworzymy ją, ponieważ w większości wzorów fizycznych wykorzystuje się czas mierzony w sekundach, a lepiej wykonać przekształcenie raz i wykorzystać we wszystkich wzorach, niż przy każdym obliczeniu fizycznym dzielić zmienną elapsed przez 1000. Pomaga to w optymalizacji skryptu. Na koniec czas obecny przekazujemy do zmiennej lastTime , która będzie wykorzystana w ten sam sposób w kolejnej klatce.
Zanim przejdziemy do opisu działania funkcji animate, omówię funkcje, które umożliwiają poruszanie obiektów na scenie. Funkcje te nie są częścią samego WebGL’a, jednak są praktycznymi narzędziami ułatwiającymi wykonywanie operacji na macierzy modelowania. Odpowiadają znanym z OpenGL’a funkcjom: glLoadIdentity, glPushMatrix, glPopMatrix, glTranslate oraz glRotate. Dla wyjaśnienia macierz modelowania, czy też macierz przekształceń należy traktować jako reprezentacja bieżącego położenia układu współrzędnych.
loadIdentity()
Funkcja ta resetuje macierz modelowania, czyli ustawia bieżące położenie układu w punkcie 0.
mvPushMatrix()
Odkłada bieżącą macierz modelowania na stos.
mvPopMatrix()
Zdejmuje macierz modelowania ze stosu i ustawia jako bieżącą.
mvTranslate( [_x , _y, _z] )
Przesuwa bieżący układ współrzędnych o wartości _x, _y, _z odpowiednio wzdłuż osi x, y, z. Należy zwrócić uwagę, iż argumentem tej funkcji jest trójelementowa tablica.
mvRotate( angle,[bit_x, bit_ y, bit_ z]);
Obraca układ współrzędnych o kąt angle podawany w stopniach dookoła osi, której bit ustawiony jest na 1. Pierwszym argumentem jest tutaj kąt obrotu, natomiast drugim trójelementowa tablica z bitami osi. Przykładowo jeśli chcemy obrócić obiekt o 90 stopni dookoła osi x, wywołujemy mvRotate(90,[1,0,0]). Bity osi y oraz z ustawione są na 0, więc obiekt obróci się tylko dookoła osi x. Można dokonywać obrotów dookoła kilku osi jednocześnie.
mvRotateRad(angle, [bit_x, bit_y, bit_z]);
W grafice najlepiej jest operować na kątach mierzonych w radianach, ponieważ uwalnia to nas od przymusu przeliczania stopni na radiany przy wykonywaniu obliczeń z użyciem funkcji trygonometrycznych. Tym samym przyspiesza to znacznie działanie programu. Dla tego do dyspozycji mamy funkcję mvRotateRad, która w odróżnieniu od funkcji mvRotate, jako pierwszy parametr przyjmuje kąt mierzony w radianach.
Są to wszystkie funkcje potrzebne nam do poruszania obiektami. Przejdźmy więc do funkcji animate.
var X_rot = 0; var Y_rot = 0; var Z_rot = 0; var Z_position = 0; var Direction_forward = false;
Na początku deklarujemy zmienne przechowujące położenie obiektów. Pierwsze trzy reprezentowały będą obrót, czwarta położenie, natomiast ostatnia kierunek ruchu piramidy.
function animate() {
X_rot += 20 * elapsed_s;
Y_rot += 40 * elapsed_s;
Z_rot += 60 * elapsed_s;
Zmienne reprezentujące obrót wokół danej osi będą zwiększały wartości z różną szybkością. W wyliczeniach wykorzystujemy omówioną wcześniej zmienną elapsed_s.
if(Direction_forward)
{
Z_position += 5*elapsed_s;
if(Z_position > 3)
Direction_forward = !Direction_forward;
}
Jeśli zmienna Direction_forward ustawiona jest na true, Z_position zwiększa swoją wartość o 5*elapsed_s w każdej klatce animacji aż do przekroczenia wartości 3. Następnie wartość zmiennej Direction_forward ustawiana jest na przeciwną i sytuacja się odwraca.
else
{
Z_position -= 5*elapsed_s;
if(Z_position < -3)
Direction_forward = !Direction_forward;
}
}
Wartość zmiennej Z_position maleje aż do osiągnięcia wartości poniżej -3, po czym zmienna Direction_forward ustawiana jest z powrotem na true. Dzięki tej prostej operacji piramida będzie wykonywała jednostajny, liniowy ruch do przodu i z powrotem w zakresie 6 jednostek.
Wszystkie wyliczone zmienne związane z ruchem wykorzystywane są w funkcji drawScene, ale zanim do niej przejdziemy, przyjrzyjmy się buforom przechowującym dane obiektów.
//cube buffers var cubeVertexPositionBuffer; var cubeVertexTextureCoordBuffer; var cubeVertexIndexBuffer; //pyramid buffers var pyramidVertexPositionBuffer; var pyramidVertexTextureCoordBuffer; var pyramidVertexIndexBuffer;
Dla każdego obiektu tworzymy tak jak poprzednio 3 bufory zawierające dane określające położenie wierzchołków, koordynaty tekstur oraz indeksy.
function initBuffers() {
//set cube buffers
cubeVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
vertices = [
// front face
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// back face
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
// top face
-1.0, 1.0, -1.0,
-1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
// bottom face
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
// right face
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
// left face
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);
cubeVertexPositionBuffer.itemSize = 3;
cubeVertexPositionBuffer.numItems = 24;
Na początku tworzymy bufor ze współrzędnymi wierzchołków sześcianu.
cubeVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer);
var textureCoords = [
// front face
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
// back face
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// top face
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
// bottom face
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
// right face
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
// left face
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(textureCoords), gl.STATIC_DRAW);
cubeVertexTextureCoordBuffer.itemSize = 2;
cubeVertexTextureCoordBuffer.numItems = 24;
Następnie tworzymy bufor z koordynatami tekstur.
cubeVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
var cubeVertexIndices = [
0, 1, 2, 0, 2, 3, // front face
4, 5, 6, 4, 6, 7, // back face
8, 9, 10, 8, 10, 11, // top face
12, 13, 14, 12, 14, 15, // bottom face
16, 17, 18, 16, 18, 19, // right face
20, 21, 22, 20, 22, 23 // left face
]
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new WebGLUnsignedShortArray(cubeVertexIndices), gl.STATIC_DRAW);
cubeVertexIndexBuffer.itemSize = 1;
cubeVertexIndexBuffer.numItems = 36;
Oraz bufor z indeksami wierzchołków sześcianu.
Teraz bufory piramidy:
//set pyramid buffers
pyramidVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
vertices = [
// front face
0.0, 1.0, 0.0,
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
// right face
0.0, 1.0, 0.0,
1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
// back face
0.0, 1.0, 0.0,
1.0, -1.0, -1.0,
-1.0, -1.0, -1.0,
// left face
0.0, 1.0, 0.0,
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(vertices), gl.STATIC_DRAW);
pyramidVertexPositionBuffer.itemSize = 3;
pyramidVertexPositionBuffer.numItems = 12;
Tak samo jak poprzednio bufor z położeniem wierzchołków
pyramidVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexTextureCoordBuffer);
var textureCoords = [
// front face
0.5, 1.0,
0.0, 0.0,
1.0, 0.0,
// right face
0.5, 1.0,
0.0, 0.0,
1.0, 0.0,
// back face
0.5, 1.0,
0.0, 0.0,
1.0, 0.0,
// left face
0.5, 1.0,
0.0, 0.0,
1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new WebGLFloatArray(textureCoords), gl.STATIC_DRAW);
pyramidVertexTextureCoordBuffer.itemSize = 2;
pyramidVertexTextureCoordBuffer.numItems = 12;
Bufor z koordynatami
pyramidVertexIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, pyramidVertexIndexBuffer);
var pyramidVertexIndices = [
0, 1, 2, // front face
3, 4, 5, // right face
6, 7, 8, // back face
9, 10, 11 // left face
]
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new WebGLUnsignedShortArray(pyramidVertexIndices), gl.STATIC_DRAW);
pyramidVertexIndexBuffer.itemSize = 1;
pyramidVertexIndexBuffer.numItems = 12;
}
I z indexami wierzchołków.
Poza zwiększoną ilością buforów nasza dzisiejsza aplikacja ma również jedną teksturę więcej, ponieważ na każdy z obiektów nakładamy inny obrazek. Tekstury wczytywane są w funkcji initTextures.
var logo1Texture; var logo2Texture;
Najpierw delklarujemy zmienne przechowujące obiekty naszych tekstur
function initTextures() {
logo1Texture = gl.createTexture();
logo1Texture.image = new Image();
logo1Texture.image.onload = function() {
handleLoadedTexture(logo1Texture)
}
logo1Texture.image.src = "logo1.jpg";
logo2Texture = gl.createTexture();
logo2Texture.image = new Image();
logo2Texture.image.onload = function() {
handleLoadedTexture(logo2Texture)
}
logo2Texture.image.src = "logo2.jpg";
}
W ciele funkcji wczytujemy najpierw pierwszą, następnie drugą teksturę z plików logo1.jpg oraz logo2.jpg.
Po przygotowaniu tekstur, buforów, oraz obliczeń fizycznych, możemy przejść do ciała funkcji drawScene. Omówimy ją dokładnie krok po kroku, tak żeby wszystko było dla nas zrozumiałe.
function drawScene() {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
perspective(45, 1.333, 0.1, 100.0);
loadIdentity();
Na początku czyścimy bufory koloru oraz głębi, ustawiamy właściwości okna widoku oraz resetujemy macierz modelowania. Operacje te omówione zostały na poprzedniej lekcji, jedyną różnicą jest tutaj czyszczenie bufora głębi. Ostatnio tego nie robiliśmy, gdyż bufor głębi nie był uruchamiany.
mvTranslate([0.0, 0.0, -10.0]);
Przesuwamy bieżący układ o 10 jednostek do przodu, aby obiekty rysowane były przed nami.
mvPushMatrix();
Odkładamy macierz na stos, dzięki czemu będziemy mogli w prosty sposób wrócić do tego miejsca.
mvTranslate([-3.0, 0.0, 0.0]);
Przesuwamy bieżący układ o 3 jednostki w lewo, ponieważ sześcian rysowany będzie po lewej stronie sceny.
mvRotate(X_rot, [1, 0, 0]); mvRotate(Y_rot, [0, 1, 0]); mvRotate(Z_rot, [0, 0, 1]);
Wykonujemy obroty bieżącego układu zgodnie z wyliczonymi wcześniej wartościami zmiennych: X_rot, Y_rot, Z_rot.
gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexTextureCoordBuffer); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, cubeVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, logo1Texture); gl.uniform1i(shaderProgram.samplerUniform, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer); setMatrixUniforms(); gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
Rysujemy sześcian zgodnie z aktualnym położeniem układu na podstawie zawartości buforów z danymi obiektu.
mvPopMatrix();
Zdejmujemy macierz ze stosu. Tym samym cofamy położenie układu do miejsca, w którym ostatnio odłożyliśmy ją na stos. Wszystkie następne przesunięcia oraz obroty będą się odnosiły do tamtego położenia, czyli w naszym przypadku do punktu położonego 10 jednostek przed nami.
mvTranslate([2.0, 0.0, Z_position]);
Przesuwamy bieżący układ o 2 jednostki w prawo, w miejsce, gdzie położona będzie piramida oraz o zmienną Z_position wzdłuż osi Z. Jej wartość, wyliczana wcześniej w funkcji animate, będzie powodowała oddalanie oraz przybliżanie się do nas piramidy w zakresie 6 jednostek.
mvRotate(Y_rot, [0, 1, 0]);
Piramida, w odróżnieniu od sześcianu, będzie się obracała tylko dookoła osi y.
gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer); gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexTextureCoordBuffer); gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, pyramidVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, logo2Texture); gl.uniform1i(shaderProgram.samplerUniform, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, pyramidVertexIndexBuffer); setMatrixUniforms(); gl.drawElements(gl.TRIANGLES, pyramidVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0); }
Rysujemy piramidę w odpowiednim miejscu na podstawie zawartości jej buforów.
Funkcja drawScene zakończona, pętla programu również. Uruchamiamy aplikację i voila! Widzimy wirujący sześcian, oraz poruszającą się piramidę, więc wszystko działa jak należy.
Nauczyliśmy się dzisiaj, jak wykonać dowolny ruch na scenie i gdzie najlepiej obliczać wartości zmiennych związanych z ruchem. Jest to doskonała baza do dalszych eksperymentów, do których gorąco zachęcam. Kod gotowej aplikacji możecie pobrać tutaj.
Na dzisiaj to wszystko. Na kolejnej lekcji stworzymy bardziej kompleksowe funkcje do wczytywania tekstur, oraz wyświetlania obiektów. Pozwoli nam to uporządkować kod oraz ułatwi wyświetlanie wielu różnych obiektów.
Odnośnie obrotu, to chyba jest błąd – drugi parametr (tablica) definiuje wektor, który jest (jedną) osią obrotu. Mówienie o tej tablicy jako tablicy bitów osi jest błędem
Przepraszam, że nie w 1 poście: wydaje mi się, że przesadziłeś z ilością wierzchołków dla sześcianu i piramidy. W sześcianie wierzchołków wystarczy 8 i 5 dla piramidy. O to właśnie chodzi w indeksach, że możesz podawać indeks wierzchołka kilkakrotnie (żeby nie mieć wielokrotnie tego samego wierzchołka).
Pierwszy wpis:
Widzę, że posiadasz sporą wiedzę z OpenGL’a. Tam faktycznie jako parametr podawany jest wektor osi obrotu. Jednak funkcja mvRotate różni się od glRotate. Zaimplementowana jest w taki sposób, że obiekt albo obraca się wokół danej osi albo nie, więc określenie ‘tablica bitów osi’ uważam za poprawne. Spróbuj wpisać inne wartości niż 0/1. Zobaczysz, że efekt będzie się różnił od tego z OpenGL’a (przy wpisaniu 2 obiekt nie będzie obracał się 2 X szybciej).
Co do drugiego wpisu:
Masz rację. W indeksach chodzi właśnie o to, aby wykorzystywać jeden wierzchołek kilka razy, co wpływa pozytywnie na optymalizację. Jednak w tym tutorialu optymalizacja nie jest najważniejsza (scena jest stosunkowo prosta). Zdecydowałem się na taki sposób przechowywania danych obiektu, aby kod był bardziej czytelny i łatwiejszy do zrozumienia. Nie każdy zetknął się z indeksowaniem, a myślę, że w ten sposób łatwiej będzie zrozumieć ten mechanizm.