Tutorial 9

Witaj w 9 tutorialu. Tym razem wykorzystamy obiekty JavaScript dzięki czemu stworzymy wiele niezależnych animowanych obiektów w naszej scenie 3D. Dowiemy się jak zmieniać kolor tekstur oraz co się dzieje gdy mieszamy tekstury ze sobą.

Najlepszym sposobem na zobaczenie różnic w stosunku do poprzedniego przykładu jest rozpoczęcie przeglądania kodu od samego dołu. Oto jak wygląda funkcja webGLStart

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

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

    document.onkeydown = handleKeyDown;
    document.onkeyup = handleKeyUp;

    tick();
  }
   

Widzimy, że pojawiła się nowa funkcja. Inicjuje ona obiekty JavaScript. Zaraz do tego przejdziemy. Wcześniej jednak zwróćmy uwagę, że nie ma funkcji, która włączała bufor głębokości. Jak zapewne pamiętasz z ostatniej lekcji blending nie idzie w parze z buforem głębokości.

Kolejna większa zmiana zachodzi w animate. Wcześniej aktualizowaliśmy zmienne globalne, które określały parametry sceny. Teraz natomiast przechodzimy po wszystkich obiektach gwiazdek i każemy im się animować

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

      for (var i in stars) {
        stars[i].animate(elapsed);
      }
    }
    lastTime = timeNow;

  }
  

Następna w kolejności jest funkcja drawScene. Tutaj się zmieniło tak dużo, że nie będę podkreślał zmian tylko przejdziemy przez kod kawałek po kawałku. Zatem od początku

    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);

  

Ten fragment jest stały i od pierwszej lekcji się w ogóle nie zmienił. Dalej włączamy blending kodem:

    gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
    gl.enable(gl.BLEND);
  

Podczas rysowania będziemy używać następującej tekstury. Jej zaletą przy włączonym opcjach blendingu jest to, że wszystko co jest czarne będzie zupełnie przezroczyste. W rzeczywistości będzie tak że im mniej jasny jest fragment tekstury tym bardziej będzie przezroczysty. To jest właśnie efekt jaki chcemy osiągnąć.

Dalej ustawiamy pozycję kamery w centralnym punkcie oraz przechylamy naszą scene wokół osi X. Robimy to następującym kodem:

    mat4.identity(mvMatrix);
    mat4.translate(mvMatrix, [0.0, 0.0, zoom]);
    mat4.rotate(mvMatrix, degToRad(tilt), [1.0, 0.0, 0.0]);
  

Teraz jesteśmy gotowi do rysowania. Na dobry początek sprawdzamy czy opcja "iskrzenie" jest włączona:

      var twinkle = document.getElementById("twinkle").checked;
  

Na koniec podobnie jak przy animowaniu przechodzimy po wszystkich obiektach i karzemy im się narysować:

    for (var i in stars) {
      stars[i].draw(tilt, spin, twinkle);
      spin += 0.1;
    }

  }
  

Tyle w kwestii rysowania. Widzimy że gwiazdki są zdolne do animowania i rysowania się. Następny kod odpowiedzialny jest za ich tworzenie.

  var stars = [];
  function initWorldObjects() {
    var numStars = 50;

    for (var i=0; i < numStars; i++) {
      stars.push(new Star((i / numStars) * 5.0, i / numStars));
    }
  }
  

Każda gwiazda otrzymuje pierwszy parametr określający początkową odległość od środka sceny i drugą określającą prędkość z jaką się porusza wokół środka sceny. Oba parametry zależą od pozycji na liście. Dalej przeglądając kod napotkamy funkcję, która tworzy gwizadkę - obiekt JavaScript.

  function Star(startingDistance, rotationSpeed) {
    this.angle = 0;
    this.dist = startingDistance;
    this.rotationSpeed = rotationSpeed;

    // Set the colors to a starting value.
    this.randomiseColors();
  }
  

To co tutaj się dzieje to inicjalizacja gwiazdki i przypisanie jej wartości przekazanych w parametrach, a następnie wylosowanie jej koloru.Wyżej definiowane są funkcje prototypowe dla gwiazdek, co gwarantuje, że każda nowo tworzona gwiazdka będzie posiadała te funkcje. Oczekujemy przecież, że każda gwiazda będzie umiała się narysować, animować i wylosować kolor dla siebie. Najpierw omówimy sobie funkcję rysowania. Na początku wrzucamy macierz modelu na stos, dzięki czemu możemy bez obaw przemieszczać obiekt.

        Star.prototype.draw = function(tilt, spin, twinkle) {
            mvPushMatrix();
  

Następnie rotujemy o zdefiniowany dla danej gwiazdki kąt i przemieszczamy o jej dystans od środka. Przejdźmy dalej.

    mat4.rotate(mvMatrix, degToRad(-this.angle), [0.0, 1.0, 0.0]);
    mat4.rotate(mvMatrix, degToRad(-tilt), [1.0, 0.0, 0.0]);
  

Linie te są odpowiedzialne za to, żęby tekstura gwiazdki była zawsze zwrócona w stronę widza. W rzeczywistości są one płaskimi kwadratami, ale dzięki temu zabiegowi podczas obracania sceny wokół osi X gwiazdki ciągle wyglądają dobrze. Następne linie są od rysowania gwiazdy.

        if (twinkle) {
          gl.uniform3f(shaderProgram.colorUniform, this.twinkleR, this.twinkleG, this.twinkleB);
          drawStar();
        }

        mat4.rotate(mvMatrix, degToRad(spin), [0.0, 0.0, 1.0]);

        gl.uniform3f(shaderProgram.colorUniform, this.r, this.g, this.b);
        drawStar();
  

Do obsługi iskrzenia przejdziemy później. Niezależnie od tego czy opcja iskrzenia jest włączona zawsze obracamy gwiazdkę wokół osi Z, co sprawia że kręci się wokół własnego środka. Następnie przekazujemy w zmiennej typu uniform kolor gwiazdki i wywołujemy funkcję dawStar (o niej zaraz).

O co chodzi z tym iskrzeniem? Więc każda gwiazdka ma zdefiniowane dwa kolory - normalny i "iskrzący". Aby uzyskać fajny efekt, zanim narysujemy właściwą gwiazdkę, rysujemy w innym kolorze gwiazdkę, która się nie obraca. Oba obiekty są blendowane co ostatecznie powoduje wrażenie jakby obiekt się mienił. Na koniec po narysowaniu obiektu oczywiście zdejmujemy macierz modelu ze stosu:

         mvPopMatrix();
  };
  

Zobaczmy w jaki sposób odbywa się animacja naszych gwiazdek. Najpierw ustawiamy liczbę klatek na sekundę i zapisujemy do zmiennej globalnej:

  var effectiveFPMS = 60 / 1000;
  Star.prototype.animate = function(elapsedTime) {
  

Teraz możemy ustawić kąt:

      this.angle += this.rotationSpeed * effectiveFPMS * elapsedTime;
  

... oraz dostosować odległość od środka, wywalając go poza scenę i losując nowy kolor gdy gwiazdka osiągnie środek sceny.

    this.dist -= 0.01 * effectiveFPMS * elapsedTime;
    if (this.dist < 0.0) {
      this.dist += 5.0;
      this.randomiseColors();
    }

  };
  

Na koniec kawałek kodu, który losuje kolory:

  Star.prototype.randomiseColors = function() {
    this.r = Math.random();
    this.g = Math.random();
    this.b = Math.random();

    // When the star is twinkling, we draw it twice, once
    // in the color below (not spinning) and then once in the
    // main color defined above.
    this.twinkleR = Math.random();
    this.twinkleG = Math.random();
    this.twinkleB = Math.random();
  };
  

Już zmierzając ku końcowi definiujemy funkcję rysowania, która nie przynosi ze sobą żadnych nowości:

function drawStar() {
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, starTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, starVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, starVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

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

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, starVertexPositionBuffer.numItems);
}
  

Trochę wyżej jest inicjalizacja bufora oraz kod odpowiedzialny za obsługę klawiszy. Wszystko znane z poprzednich lekcji, więc teraz przejdźmy do shaderów. Z vertex shadera zostało usunięte wszystko co miało cokolwiek wspólnego ze światłem, także przyjął on postać taką jak w lekcji nr 5. Fragment shader jest bardziej interesujący:

  precision mediump float;

  varying vec2 vTextureCoord;

  uniform sampler2D uSampler;

  uniform vec3 uColor;

  void main(void) {
    vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    gl_FragColor = textureColor * vec4(uColor, 1.0);
  }
  

... ale nie wiele ciekawszy. Jedyne co robi to przechwyca kolor przekazany w zmiennej typu uniform i wykorzystuje do wycieniowania tekstury, która jest monochromatyczna.

To tyle. Następnym razem stworzymy nasz pierwszy świat i poznamy podstawy obsługi kamery.

Wynik końcowy



Iskrzenie
Góra/dół - obracanie wokół osi X, PageUp/PageDown - zoom