Tutorial 3

Tym razem będziemy animować nasze figury. Zanim jednak przejdziemy do omawainia kodu powiedzmy sobie w jaki sposób sceny sąanimowane w WebGL'u.

Sprawa jest prosta. Jedną scene rysujemy wielokrotnie tylko, że za każdym razem inaczej. Nie ma tutaj żadnych magicznych funkcji które mówią silnikowi "narysuj obiekt w punkcie X, a następnie przenieś go do punktu Y". Raczej mówią coś w stylu: "Narysuj obiekt w punkcie X, następnym razem jak będziesz rysował to rysuj w punkcie Y, a następnym razem w punkcie Z itd." Wynika z tego, że wszystko co jest nam potrzebne do animacji mamy już napisane. Teraz wystarczy tylko z tego skorzystać wiele razy.

Najpierw spójrzmy na funkcję webGLStart:

    function webGLStart() {
        var canvas = document.getElementById("lesson03-canvas");
        initGL(canvas);
        initShaders();
        initTexture();

        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.enable(gl.DEPTH_TEST);

        tick();
    }
    

Jak widzimy jedyną zmianą jest wywołanie funkcji tick zamiast drawScene. Zerknijmy zatem w jej ciało:

    function tick() {
        requestAnimFrame(tick);
        drawScene();
        animate();
    }
    

Pierwsza linia określa kiedy funkcja tick ma być wywołana kolejny raz po narysowaniu sceny. requestAnimFrame jest funkcją która udostępnia niezależny sposób zapytania przeglądarki o to kiedy chce przesywać scene. Może to być na przykład podczas każdego odświeżania ekranu. Podobny efekt można uzyskać wykorzystując JavaScriptową funkcję setInterval. Ma to jednak swoje wady. Jeśli otwartych będzie kilka kart z animacjami w WebGL'u to będą one animowane nawet gdy żadna z tych kart nie będzie aktualnie wyświetlana. requestAnimFrame animuje tylko zawartość widocznej karty.

Po zaplanowaniu kolejnego wywołania, rysujemy scenę, a następnie aktualizujemy jej położenie. Zobaczmy w jaki sposób to się odbywa.

Na dobry początek zwróćmy uwagę na dwie globalne zmienne deklarowane przez funkcją drawScene

    var rTri = 0;
    var rSquare = 0;
    

Przechowują one informacje o aktualnym obrocie każdej z figur. Obie zaczynają od zera, ale z czasem będą wzrastać. Używanie zmiennych globalnych wa takim celu nie jest eleganckim rozwiązaniem, ale na potrzeby tego prostego przykładu wystarczy. Bardziej eleganckie rozwiązanie poznamy w 9 części tego kursu.

Zerknijmy teraz na zmiany jakie zaszły w funkcji rysującej.

        function drawScene() {
        gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

        mat4.identity(mvMatrix);

        mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);

        mvPushMatrix();
        mat4.rotate(mvMatrix, degToRad(rTri), [0, 1, 0]);

        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);
        mvPopMatrix();


        mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);

        mvPushMatrix();
        mat4.rotate(mvMatrix, degToRad(rSquare), [1, 0, 0]);

        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

        gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
        gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

        setMatrixUniforms();
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);

        mvPopMatrix();
    }
    

Widzimy trzy nowe funkcje. Zacznijmy od rotate, gdyż wydaje się być najbardziej oczywista. W kolejności przyjmuje jako parametr, macierz modelu, kąt obrotu (wyrażony w radianach) i wektor wokół którego obracamy.

Czym są pozostałe dwie tajemnicze funkcje? Jak pamiętamy z pierwszej lekcji rysowanie odbywa się w "aktualnej" pozycji. Wyobraźmy sobie że mamy porzestrzeń w której chcemy narysować kilka budynków. Zaczynamy od podłoża. Ostatnim elementem pierwszego budynku jest antena na dachu. Teraz przemieszczamy się obok aby zacząć budować nowy budynek. Ku naszemu zdziwieniu nie będzie on na ziemi lecz w powietrzu na wysokości anteny. Dlaczego? Dlatego, że ostatnią aktualną pozycją była ta przy malowaniu anteny. Jak temu zaradzić? Tak! Trzeba zapamiętać położenie zanim zaczeliśmy budować pierwszy budynek i względem niego sięprzesuwać. Do tego właśnie służą funkcje mvPushMatrix, mvPopMatrix. Pierwsza z nich odkłada macierz na stos a druga zdejmuje, czyli przywraca aktualną pozycję. Spróbuj usunąć te funkcje a zobaczysz co się stanie

Aby nasza scena była żywa konieczne jest oczywiście zmienianie wartości rTrii rSquare. Funkcja realizująca wydaje się być w miare prosta:

    var lastTime = 0;
    function animate() {
        var timeNow = new Date().getTime();
        if (lastTime != 0) {
          var elapsed = timeNow - lastTime;

          rTri += (90 * elapsed) / 1000.0;
          rSquare += (75 * elapsed) / 1000.0;
        }
        lastTime = timeNow;
    }
    

Zaletą tego rozwiązania jest fakt, że bez względu na to jaki kto ma mocny komputer, w danym czasie figura się zawsze obróci o ten sam kąt.

Na dobrą sprawę to już wszystko. Zobaczmy jeszcze tylko jak zaimplementowane są funkcje pomocnicze mvPushMatrix, mvPopMatrix, degToRad. Zaczniemy od najprostszej, która zamienia stopnie na radiany:

        function degToRad(degrees) {
        return degrees * Math.PI / 180;
    }
    
Uważam, że nie ma co komentować. Teraz kolej na pozostałe dwie funkcje:
    var mvMatrix = mat4.create();
    var mvMatrixStack = [];
    var pMatrix = mat4.create();

    function mvPushMatrix() {
     var copy = mat4.create();
     mat4.set(mvMatrix, copy);
     mvMatrixStack.push(copy);
     }

     function mvPopMatrix() {
     if (mvMatrixStack.length == 0) {
     throw "Invalid popMatrix!";
     }
     mvMatrix = mvMatrixStack.pop();
    }
    

I wszystko już jest gotowe. Możemy się cieszyć widokiem naszej pierwszej animacji.

Wynik końcowy